覆盖题库四.36(表达式注入如何审计)、四.37(Python 危险函数之 SSTI 部分)、四.38(Fastjson autotype),以及 Python 沙箱逃逸专题。 本篇默认你第一次接触代码审计,每个概念都会从”是什么、为什么会有、怎么找”三步讲起。

先修知识

读这篇之前,你只需要知道下面这些词是什么意思,每个一句话:

  • 表达式(Expression):一小段”能算出结果的代码”,比如 1+1、user.age > 18。它不是完整的程序,但引擎能把它算出来。
  • 表达式引擎:专门负责”算表达式”的组件。Java 里有 SpEL、OGNL、EL,Python 里 Jinja2 模板的 {{ }} 也算半个。类比:表达式引擎就是计算器,你给它式子,它给你结果——问题是它这个计算器能按的键太多了,连”删除文件”这种键都有。
  • 模板引擎 / 模板:做网页时用的”填空信纸”。模板里写 <h1>你好,{{name}}</h1>,引擎把 {{name}} 这个位置替换成真实数据。模板本体是信纸本身,渲染变量是填进去的字——这个区分是整篇的命门。
  • Sink(汇点):代码里”危险动作真正发生”的那一行,比如 getValue()、render_template_string()。类比:水流的入海口。
  • Source(源头):用户输入进入程序的那一行,比如 request.getParameter(...)。审计就是在找”从 Source 到 Sink 有没有通路”。
  • RCE(远程命令执行):攻击者让服务器执行任意系统命令(比如 id、cat /flag),是 Web 漏洞里的最高危害。
  • 上下文(Context)/ 沙箱:引擎求值时的”活动范围”。受限上下文 = 只准读数据;不受限上下文 = 能调任意类、任意方法。沙箱就是给攻击者准备的笼子——本篇会告诉你这笼子基本关不住人。
  • 魔术属性(__class__ 等):Python 里每个对象自带的隐藏属性,双下划线开头,能顺藤摸瓜摸到整个对象体系。
  • 反序列化:把一段文本/字节还原成内存里的对象。还原成”哪个类”如果能被攻击者指定,就可能出事(Fastjson 一节专门讲)。
  • getter / setter:Java 类里读/写字段的标准方法(getName() / setName())。fastjson 反序列化时会自动调用它们,某些类的 setter 里藏着危险逻辑。
  • 黑名单 / 白名单:黑名单 = “列出一堆不准用的”;白名单 = “只准用列出来的”。安全界铁律:黑名单永远列不全,白名单才可靠。
  • payload:攻击者精心构造的输入串,交给漏洞点执行。
  • 回显:服务器把执行结果显示在响应里,攻击者据此判断漏洞是否存在(比如输入 {{7*7}} 看到 49)。

0. 跨语言通用原理

0.1 一句话说清这两类漏洞

表达式注入与模板注入(SSTI,Server-Side Template Injection)的本质是同一件事:

攻击者可控的字符串,被当作”代码/表达式”而不是”数据”去求值。

先用生活类比建立直觉:

  • 你让前台帮忙打印工牌,正常流程是:你在姓名栏写”张三”,前台把”张三”印上去。
  • 出事的流程是:姓名栏的设计图纸上有一句”此处写姓名”,你趁前台不注意,把图纸本身改了,改成”此处写姓名,顺便把保险柜打开”。前台照着图纸干活,保险柜就开了。
  • “图纸”就是模板/表达式,“姓名栏”就是渲染变量。输入只能进姓名栏,绝不能改图纸——这就是全部防御思想的源头。

拆开看:

  • 表达式注入:业务代码或框架把用户输入交给表达式引擎(SpEL、OGNL、EL)求值。表达式语言在设计上就能访问对象、调用方法、甚至加载类(因为它们的正当用途就是”在配置里写逻辑”),一旦拼接进求值上下文,危害直接等同于代码执行。
  • 模板注入(SSTI):开发者把用户输入拼进模板字符串本身(而不是作为渲染变量传入),模板引擎(Jinja2/Twig/Thymeleaf/Freemarker)把输入当作模板语法解析执行,从而获得服务端代码执行。

0.2 为什么会出现这种漏洞

别觉得开发者蠢,这三个动机你将来写代码也会遇到:

  1. 图省事:要做一个”动态搜索条件”功能,产品经理说”让用户自己输条件”。开发者一想:SpEL 现成的,一行 parseExpression(condition) 搞定,比写参数解析器省两小时。——他不知道这等于给用户开了一个服务器终端。
  2. 不知道危险:render_template_string('<h1>Hello ' + name + '</h1>') 和 render_template_string('<h1>Hello {{ name }}</h1>', name=name) 看起来只差一个写法,教程里两种都见过,新手根本意识不到前者是 RCE。
  3. 误信输入:“这个参数只有内部系统会传""前端做了校验”——后端信了,攻击者绕过前端直接发请求,防线形同虚设。

0.3 统一判定公式与审计套路

统一判定公式(全篇总纲)

求值点(Sink)存在 + 表达式/模板字符串可拼接用户输入(Source)+ 求值上下文未受限(沙箱缺失或可被绕过)= 高危。

判别口诀:输入进”模板本体/表达式字符串” → 注入;输入只作”渲染变量” → 安全(引擎会自动转义,输入被当纯文本)。

审计任何一类表达式/模板注入,都是同一套三步:

  1. 搜 Sink 关键字:parseExpression、getValue、render_template_string、createTemplate、evaluate、new Template(……(各语言的具体清单后面章节给全)。
  2. 沿数据流往回找 Source:这个 Sink 的参数是哪来的?追变量赋值链,看能不能追到 request.getParameter / request.args / @RequestParam。
  3. 看上下文是否设防:有没有用受限上下文?有没有过滤?过滤是黑名单还是白名单?(黑名单直接判不合格,理由见各章。)

0.4 利用升级的统一思路

攻击者拿到”能执行表达式”之后,接下来干什么?跨语言答案惊人地一致:沿语言的对象模型漫游到危险能力。

  • Java:走 T(全限定类名) 类型引用 / 反射链,摸到 java.lang.Runtime 执行命令。
  • Python:走 __class__ → __mro__ → __subclasses__() → __globals__ 魔术属性链,摸到 os 模块执行命令。
  • PHP:走模板引擎的危险 filter/函数(比如 Twig 的 filter 可以把字符串当 PHP 函数回调,直接调 system)。

也就是说:表达式注入只是”进门”,进门后大家找的都是同一个东西——执行系统命令的能力。审计时你只要确认”门是开的”,危害上限就按 RCE 报。

0.5 新手审计时最常见的三个误判

这三个坑几乎每个初学者都会踩一次,提前打预防针:

  1. 把”模板文件里的 {{ }}”当漏洞。看到 .html / .j2 模板文件里一堆 {{ user.name }} 就紧张——别急,这是模板的正常用法,变量由服务端 assign,用户摸不到。漏洞永远发生在”模板字符串的来源”那一行,而不是模板文件本身。审计顺序:先搜 Sink 函数(render_template_string、createTemplate、parseExpression),再看参数来源,而不是打开模板文件逐行读。
  2. 看到过滤就放过。“开发已经过滤了 Runtime 关键字”——黑名单过滤不等于安全。本指南各章反复强调:字符串拼接、编码、反射间接调用,总有一款能绕过。审计结论里写清楚:“存在黑名单过滤,但黑名单可被绕过(附绕过示例),上下文未受限”,比”有过滤,低风险”专业得多。
  3. 混淆”参数进模板”和”参数进模板变量”。同一行 render_template_string('<h1>Hello {{ name }}</h1>', name=name),安全与否取决于 name 出现在**第一个参数(模板本体)还是关键字参数(渲染变量)**里。审计时眼神要落在这个细节上——这是全篇唯一的判别口诀:进图纸 = 注入,进姓名栏 = 安全。

带这三条去实战,第一轮审计的产出质量会明显不一样。


1. Java

Java 侧表达式/模板技术一览(先混个眼熟,后面逐个讲):

表达式/模板技术典型使用方注入面(触发点)危害上限
Java EL(JSP EL,${})JSP、JSP 2.x 之后的 EL 解析用户输入二次写入 JSP 属性后渲染;EL 2.2+ 支持方法调用RCE(Runtime.getRuntime().exec)
SpELSpring 全家桶SpelExpressionParser.parseExpression(...).getValue()、@Value("${...}") 拼接、Spring Cloud Gateway 路由谓词RCE
OGNLStruts2URL 参数/请求体被框架二次解析为 OGNL;_memberAccess 操纵RCE
ThymeleafSpring Boot 视图返回值拼接进视图名,命中 __${...}__ 预处理SpEL RCE
Velocity老 Java 项目模板用户输入作为模板内容 evaluate/mergeRCE(ClassTool 等)
Freemarker老 Java 项目模板用户输入作为模板内容;freemarker.template.utility.Execute / ObjectConstructorRCE

1.1 SpEL 原理与触发

是什么

SpEL(Spring Expression Language)是 Spring 自带的表达式语言。它的正当用途是让开发者在注解和配置里写动态逻辑,比如 @Cacheable(key = "#user.id")——缓存 key 不是写死的,是运行时用表达式算出来的。

生活类比:SpEL 就像 Excel 单元格里以 = 开头的公式。正常员工填 =B2*1.13 算税;但 Excel 的公式语言如果能调 cmd.exe,你把单元格内容交给外部客户随便填,会发生什么?——SpEL 就是这个”能调 cmd 的 Excel”。

数据流模型

// ❌ 危险写法:用户输入直接拼进 SpEL
ExpressionParser parser = new SpelExpressionParser();
String input = request.getParameter("name");   // ① Source:用户输入从这进来
Expression exp = parser.parseExpression(input); // ② 把输入"编译"成表达式对象
String result = exp.getValue(String.class);     // ③ Sink:求值——表达式里写什么就执行什么

数据流四步拆解:

  1. request.getParameter("name") 拿到用户完全可控的字符串,没有任何过滤。
  2. parseExpression(input) 只是编译,还不执行,但它已经承认”这串字符是代码”。
  3. getValue() 真正求值。关键在:这里没传上下文对象,SpEL 会new一个默认的 StandardEvaluationContext——不受限上下文,什么都能调。
  4. 防线分析:整段代码没有任何一道防线。输入没过滤、上下文没限制,公式三项全中 → 高危。

SpEL 的关键能力(也正是攻击面)

语法本来干什么攻击者拿来干什么
T(全限定类名)引用 Java 类型、调静态方法(比如 T(java.lang.Math).random())T(java.lang.Runtime).getRuntime().exec("id") 执行系统命令
new 包名.类(...)在表达式里new对象new java.lang.ProcessBuilder("id").start() 起进程
对象.方法(...) / 对象.属性方法调用与属性导航从任意可达对象一路反射到 Class.forName
#变量引用上下文变量(如 #user.id)访问框架注入的内部对象,扩大战果

三类典型触发场景

审计时按这三类找,基本不会漏:

  1. 显式求值:业务代码自己 new SpelExpressionParser().parseExpression(可控参数).getValue()。最直白,全文搜 parseExpression 一搜一个准。
  2. @Value / 注解拼接:@Value("${" + param + "}"),或把用户输入注入到 @Cacheable(key = "#root.getProperty('" + x + "')") 这类支持 SpEL 的注解属性中。审计时注意:注解参数里的字符串拼接,新手最容易漏。
  3. 框架内置求值点:有些框架自己会把配置当 SpEL 解析,根本轮不到业务代码出手。最出名的是 Spring Cloud Gateway:它的路由谓词参数会被当 SpEL 解析,3.1.1 / 3.0.7 之前的版本(即 3.1.x 早于 3.1.1、3.0.x 早于 3.0.7)存在通过 actuator 端点注入路由导致的 SpEL RCE(CVE-2022-22947)。这类审计依赖版本即可定性,不用自己找代码——看 pom.xml 里 spring-cloud-gateway 的版本号落在受影响区间,直接出报告。

沙箱与绕过

Spring 官方是意识到危险的,从 Spring 4.3.15 / 5.0.5 起提供了 SimpleEvaluationContext:默认禁用类型引用(T())、构造器调用、bean 引用,只允许读写数据。

// ✅ 安全写法:受限上下文,T() 与 new 直接抛异常
SimpleEvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().build();
parser.parseExpression(input).getValue(ctx, String.class);

两种上下文对比(这张表要背):

上下文类型引用 T()构造器 new结论
StandardEvaluationContext(getValue() 不传上下文时的默认)✅ 允许✅ 允许等价 RCE
SimpleEvaluationContext❌ 禁止❌ 禁止有效缓解

关于”民间防护”,记住一个结论就够了:

  • Spring 没有官方黑名单机制。项目里常见的土办法是求值前用正则过滤 T(、Runtime、ProcessBuilder 等关键字。
  • 黑名单必可被绕过:字符串拼接变形(T(java.lang.Run+time))、反射间接加载(T(java.lang.Class).forName('java.lang.Runtime') 再取方法)、利用 #this/#root 可达对象链摸到 ClassLoader……办法总比黑名单多。
  • 所以审计时看到 “黑名单过滤 + StandardEvaluationContext” 的组合,直接判定不合格,不用费劲验证绕过。

修复的正解只有两个:

  1. 换 SimpleEvaluationContext(表达式功能必须保留时);
  2. 不让用户输入进入表达式——白名单映射:用户传个 key,服务端查表拿到预定义表达式(最佳实践,案例一末尾有完整代码)。

1.2 OGNL 原理与 Struts2 触发点

是什么

OGNL(Object-Graph Navigation Language,对象图导航语言)是 Struts2 的默认表达式语言。“导航”的意思:给它一个起点对象,它能沿着属性、方法一路走到任何关联对象——user.address.city.name 这种链式访问就是它的日常。

生活类比:OGNL 像公司的内部通讯录系统,本来设计是让你按”部门→小组→员工”查电话。但如果通讯录还能帮你”拨打任意分机并让对方执行指令”,而登录口令又是门卫随口告诉任何人的,这栋楼就归攻击者管了。

为什么说 Struts2 本身就是 Sink

这是 OGNL 和 SpEL 最大的不同,也是面试高频点:

  • Struts2 的请求参数在进入 Action 前后,可能被框架二次解析为 OGNL——比如参数名本身、某些标签属性、错误回显。
  • 所以攻击者不需要应用代码显式调用 Ognl.getValue,框架自己就是 Sink。这就是为什么 Struts2 历史上爆出一连串 OGNL RCE(S2-045、S2-057……),全是框架层的锅。

防护机关:_memberAccess

OGNL 求值依赖上下文对象 OgnlContext,里面有个 _memberAccess(MemberAccess 接口的实现),相当于门卫,控制”是否允许调静态方法、访问私有成员”:

  • 正常情况下 Struts2 用 SecurityMemberAccess,把 allowStaticMethodAccess 设为 false——静态方法调用被禁,@java.lang.Runtime@getRuntime() 这种调用会被门卫拦下。
  • 攻击链的核心动作就是操纵 _memberAccess:OGNL 表达式能访问上下文中的对象并修改其属性,攻击者用表达式把 _memberAccess 替换/重置成宽松的 DefaultMemberAccess(门卫换成自己人),或清掉黑名单集合,然后 @java.lang.Runtime@getRuntime().exec(...) 就畅通无阻了。
// OGNL 恶意表达式的通用骨架(机制示意,理解结构即可,不用背)
(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(   // 第一步:换掉门卫
  @java.lang.Runtime@getRuntime().exec("id")                  // 第二步:调静态方法执行命令
)

骨架拆解:@类名@静态方法() 是 OGNL 调静态方法的语法(两个 @);(...).(...) 是逗号表达式的分组,保证先换门卫再执行。

Struts2 侧的防护与失效循环

Struts2 的防护演进是典型的”黑名单 ↔ 绕过”拉锯战,理解机制即可,不必背 CVE 号:

  1. 初版思路:过滤请求参数中的 #、\u0023、( 等字符 → 被编码变形、变量间接赋值绕过。
  2. 加强思路:在 SecurityMemberAccess / ExcludedClasses / ExcludedPackageNames 里维护类与包的黑名单(java.lang.Runtime、ProcessBuilder、ognl.OgnlContext 等)→ 攻击者不断找不在黑名单里的等价类(ClassLoader、ProcessImpl、实例方法链)绕过。
  3. 入口翻新:参数名、Content-Type 等层面的 OGNL 触发点(如 Content-Type: %{...} 被当表达式解析)多次开出新入口。

审计结论(可直接套用的模板)

Struts2 项目的 OGNL 注入审计 ≈ 版本审计: ① 确认 struts2-core 版本是否落在任一 OGNL RCE 漏洞影响区间、是否打了对应补丁; ② 业务代码层面只需确认没有自己把用户输入交给 Ognl.parseExpression / Ognl.getValue,或拼接进 <s:...> 标签的动态属性。 两件事都干净,OGNL 这章就可以翻篇。

1.3 JSP EL

  • 是什么:JSP 页面里的 ${...},本来用来在页面上读个变量,比如 ${user.name}。EL 2.2 之后升级成”还能调方法”,危险等级直接拉满——${Runtime.getRuntime()} 这种写法在旧版 EL 里根本不支持,2.2 之后合法了。
  • 注入面有两类:
    1. 二次渲染:用户输入第一次当数据存下(比如存进 request 属性),第二次被另一个 JSP/标签当 EL 表达式解析。这种最隐蔽,因为单独看每一跳都像正常代码。
    2. 显式求值:业务代码主动把输入交给 javax.el.ExpressionFactory.createValueExpression 或 JDK 自带的 ELProcessor 求值。
// ❌ 危险写法示例:用 JDK 自带的 ELProcessor 做"动态规则"
import javax.el.ELProcessor;
 
@GetMapping("/rule")
public String rule(@RequestParam String expr) {
    ELProcessor el = new ELProcessor();            // JDK 内置的 EL 求值器
    Object result = el.eval(expr);                 // 用户输入直接求值,无任何限制
    return "result: " + result;                    // expr=${"".getClass().forName("java.lang.Runtime")...} 即 RCE
}
  • 为什么会出现:不少教程把 ELProcessor 当”轻量级表达式计算器”推荐,开发者压根没意识到它默认不加任何限制。
  • 防御:用户输入只做”值”,不二次渲染为 EL;JSP 输出统一用 c:out(自动转义);见到 ELProcessor.eval(可控输入) 直接报高危。

1.4 模板引擎:Thymeleaf / Velocity / Freemarker

Thymeleaf __${...}__ 预处理

先理解一个正常功能:预处理。Thymeleaf 里 __${expression}__ 的意思是:先求值内层表达式,再把结果拼回外层继续解析。设计用途是”动态拼接片段路径”,比如 ~{fragments/__${menu}__::body}——先算出 menu 的值是 admin,再去找 fragments/admin::body 这个片段。

看出来了吗?这个”先算一次、结果再算一次”的机制,天生就是二次求值:如果内层算出来的结果里又包含 ${...},会被再解析一轮 → 注入。

触发条件(三要素缺一不可,审计时逐条核对):

  1. 控制器返回的视图名/片段选择器拼接了用户输入(经典写法:return "path/" + param; 或 return "welcome :: " + param;)。
  2. 拼接位置会经过模板解析(视图名本质是 ~{...} 片段表达式的一部分时)。
  3. 用户输入形似 __${payload}__::.x,使预处理先吃掉 payload。
// ❌ 危险写法:视图名拼接可控参数
@GetMapping("/path")
public String path(@RequestParam String lang) {
    return "user/" + lang + "/welcome";   // lang 可控 → 可构造 __${...}__ 注入
}

payload 形如:__${T(java.lang.Runtime).getRuntime().exec("id")}__::.x。逐段拆:__ 和 __ 是预处理的边界标记;中间是标准 SpEL 命令执行;::.x 是片段选择器语法,作用是让整体语法合法不报错(没有它模板解析可能先崩,payload 到不了求值那步)。

审计要点

Thymeleaf 注入的 Sink 不在模板文件里,而在 Controller 的返回语句。很多新手审计时死磕 .html 模板文件,方向就错了。 关键字:Controller 里 return + 字符串拼接 + @RequestParam/@PathVariable,以及 :: 片段选择器用法。 修复:视图名走白名单映射,或把参数放进 Model 由模板正常渲染(当数据,不当图纸)。

Velocity 注入

// ❌ 危险写法:用户输入作为模板内容
VelocityEngine ve = new VelocityEngine();
ve.init();
StringWriter w = new StringWriter();
ve.evaluate(new VelocityContext(), w, "tpl", userInput);  // userInput 被当模板源码解析

逐行看:evaluate(上下文, 输出, 模板名, 模板内容)——第四个参数 userInput 是模板本体。用户在里面写 #set($x=...)、写 $class.forName(...),引擎照单执行。

  • Velocity 2.x 起默认禁用了 ClassTool、Runtime 等危险工具的暴露,但老项目常用 1.x,或手动配置缺失 SecureUberspector(安全属性检查器),利用路径是 $class.forName(...) 或从上下文对象出发的 $e.getClass().forName("java.lang.Runtime")...。
  • 审计关键字:Velocity.evaluate、Velocity.mergeTemplate、VelocityEngine、Uberspect 配置。

Freemarker 注入

// ❌ 危险写法:模板内容来自用户输入
Template t = new Template("name", userInput, cfg);   // 第二个参数就是模板源码
t.process(dataModel, out);                           // 渲染 = 编译执行
  • Freemarker 的危险点在于两个内建工具类:freemarker.template.utility.Execute(执行系统命令)与 ObjectConstructor(实例化任意类)。配合 ?new 内建函数(按类名构造对象),一条就拿下:${"freemarker.template.utility.Execute"?new()("id")}。
    • 逐段拆:"freemarker.template.utility.Execute" 是危险类的全限定名字符串 → ?new() 把它变成实例 → ("id") 调用它的 exec 逻辑执行命令。
  • 官方缓解是 Configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER):限制 ?new 只能构造白名单类(Execute、ObjectConstructor 都不在白名单里)。
  • 审计关键字:new Template((看第二个参数来源)、setTemplateLoader 加载用户内容、?new、Execute、TemplateClassResolver 配置。

1.5 审计关键字表与判定流程

Sink / Source 关键字表(审计时的”搜索清单”,拿到项目先全文搜一遍)

类别关键字
SpELSpelExpressionParser、parseExpression、getValue、StandardEvaluationContext、@Value(拼接)、ShortcutConfigurable(SCG)
OGNLOgnl.parseExpression、Ognl.getValue / Ognl.setValue、_memberAccess、SecurityMemberAccess、struts2-core 版本号
JSP ELjavax.el.ExpressionFactory、createValueExpression、ELProcessor(JDK 自带 javax.el 求值器)
ThymeleafController return "..."+param、:: 片段、__${
VelocityVelocity.evaluate、mergeTemplate、VelocityEngine
Freemarkernew Template( 字符串来源、TemplateLoader、?new、Execute

Source 表:@RequestParam、@RequestBody、@PathVariable、request.getParameter/getHeader/getCookies、URL 路径片段、JSON 反序列化字段。

判定流程(背下来,面试可以直接复述)

发现求值点(Sink)
   │
   ├─ ① 表达式字符串是否拼接用户输入?── 否 → 安全(常量表达式,翻篇)
   │
   ├─ ② 求值上下文是否受限?
   │      SpEL:是否 SimpleEvaluationContext?
   │      OGNL:SecurityMemberAccess 是否被加固 / 版本是否打补丁?
   │      Freemarker:是否 SAFER_RESOLVER?
   │      ── 是 → 低风险,记录即可
   │
   ├─ ③ 是否有输入过滤?白名单映射 → 合格;黑名单/正则 → 判不合格(可绕过)
   │
   └─ ④ 上下文未受限 + 输入可控 → 高危表达式注入

1.6 Fastjson autotype 机制(点到为止)

本节点到为止:讲清机制与防护演进,不构造具体 payload。autotype 虽属反序列化范畴,但其”类型可控即实例化任意类”的机制与表达式注入的”上下文未受限”高度同源,面试常与表达式注入同问。

为什么需要 autotype(从设计动机讲起)

Fastjson 序列化对象时默认不写入类型信息:一个 Dog 对象序列化出来就是 {"name":"旺财"},没有地方写”这是 Dog”。

问题来了:反序列化时,如果目标字段是接口/抽象类/Object 类型(比如 Animal pet;),fastjson 拿着 {"name":"旺财"} 根本不知道该还原成 Dog 还是 Cat。@type 字段(autotype 机制)就是为此设计的:序列化时写入类全名 {"@type":"com.x.Dog","name":"旺财"},反序列化时按 @type 指定的类实例化。

攻击面由此产生,而且非常直白——@type 可控,就意味着攻击者可以指定任意类让 fastjson 实例化,并自动调用其 setter/getter。只要classpath里存在一个”setter/getter 里有危险逻辑”的类(史称”利用类”/gadget,最经典的是 JNDI 数据源类,setter 会发起远程类加载),配合它即可 RCE。

生活类比:autotype 像快递柜的”凭取件码取货”。本来取件码对应你自己的包裹;但如果取件码是你自己填的,而快递柜对”取件码”不做任何校验,你填隔壁柜的码就能开隔壁柜的门——再碰巧那个柜子里放着整栋楼的总钥匙(危险类),游戏结束。

checkAutoType 黑白名单

  • 1.2.25 引入 checkAutoType:默认 autoTypeSupport=false,维护内置黑名单(已知危险类/包)+ 用户白名单(ParserConfig.getGlobalInstance().addAccept(...))。
  • 黑名单校验逻辑早期存在缺陷:比对前会做 L...; 形式处理等字符串规整,攻击者通过字符串变形/进制混淆/缓存投毒反复绕过,于是出现 1.2.25 → 1.2.41 / 42 / 43 / 45 / 47 等一连串”补黑名单—绕过—再补”的版本演进。
    • 审计记忆点:版本区间 = 风险区间。看到 fastjson 1.2.x,先对版本号,落在某个绕过的影响区间里就是”按未修复处理”。
  • safeMode(1.2.68+):ParserConfig.getGlobalInstance().setSafeMode(true) 完全关闭 autotype,与 @type 白名单无关,是最硬的开关。快递柜的故事里,这相当于”取件码功能直接下线”。

autotype=false 时的绕过思路(机制层面)

即使 autoTypeSupport=false,仍存在两条研究过的攻击面,了解思路即可:

  1. 期望类(expectClass):反序列化时如果调用方显式指定了目标类型(JSON.parseObject(json, SomeClass.class)),fastjson 会检查 @type 是否为期望类的子类——攻击者寻找期望类体系中合法但危险的子类(例如实现了 AutoCloseable/Throwable 等宽泛接口的危险实现类),使其通过类型检查。
  2. 反序列化器(deserializer)链:某些类在 fastjson 注册表中有自定义 deserializer,其反序列化过程本身会触发危险行为(实例化、文件、网络);配合上面的期望类检查逻辑,把危险 deserializer 类塞进合法子类型位置。

审计结论写法(可直接抄进报告)

fastjson 1.2.x 项目,默认配置 + parseObject(String) 处理外部 JSON = 按未修复处理;唯一可信的缓解是升级 + safeMode,或迁移 Jackson(不开 default typing)。

1.7 案例

案例一:SpEL 注入(显式求值点)

漏洞代码(逐行注释)

// SearchController.java —— 用户输入的搜索条件拼进 SpEL
@RestController
public class SearchController {
 
    private final ExpressionParser parser = new SpelExpressionParser();  // SpEL 解析器,全局一个
 
    @GetMapping("/search")
    public String search(@RequestParam String condition) {   // ① condition 完全由用户控制
        User user = new User("alice", 30);                   // 模拟一个待匹配的用户对象
        // ❌ 危险:condition 直接作为 SpEL 表达式求值,且不传上下文 → 默认 StandardEvaluationContext
        Object result = parser.parseExpression(condition).getValue(user);
        return "matched: " + result;                         // 求值结果回显给攻击者
    }
}

数据流四步:

  1. 输入从哪进来:@RequestParam String condition,URL 里的 ?condition=xxx,无任何过滤。
  2. 经过什么处理:parseExpression(condition) 把输入编译成表达式对象——注意,这里没有任何校验,用户写什么就编译什么。
  3. 到达哪个危险函数:getValue(user)。user 是求值的根对象(表达式里的裸属性名如 name 会到它身上找),但因为没传上下文参数,Spring 内部创建默认的 StandardEvaluationContext。
  4. 为什么防线失效:唯一的潜在防线是”上下文”,而默认上下文不限制任何东西——T() 类型引用、构造器调用全部放行。公式三项全中。

正常用法 vs 恶意用法对比:

# 正常:匹配名字叫 alice 的用户
GET /search?condition=name == 'alice'        → 返回 matched: true

# 恶意:表达式里不再是"查数据",而是"调系统命令"
GET /search?condition=T(java.lang.Runtime).getRuntime().exec("calc")

payload 逐段拆解:

  • T(java.lang.Runtime) —— T(...) 是 SpEL 的类型引用语法,拿到 Runtime 这个类的句柄;
  • .getRuntime() —— 调它的静态方法,拿到运行时实例;
  • .exec("calc") —— 弹计算器(Windows 下证明 RCE 的经典动作;真实攻击换成任意命令)。

审计结论与修复

结论:表达式可控 + 上下文未受限 + 无任何过滤 → 高危 RCE。

// ✅ 修复一:受限上下文(只允许读数据绑定,T()/new 直接抛异常)
@GetMapping("/search")
public String search(@RequestParam String condition) {
    SimpleEvaluationContext ctx =
            SimpleEvaluationContext.forReadOnlyDataBinding().build();
    // 注意:根对象通过 getValue 的第二个参数传入,SimpleEvaluationContext 的 Builder
    // 并没有 withRootObject 方法,老笔记里那种写法是错的。
    Object result = parser.parseExpression(condition).getValue(ctx, user);
    return "matched: " + result;
}
 
// ✅ 修复二(更推荐):白名单映射,用户输入永远不进表达式字符串
private static final Map<String, String> CONDITIONS = Map.of(
        "byName", "name == #q",      // 表达式是服务端写死的常量
        "adult",  "age >= 18");
 
@GetMapping("/search")
public String search(@RequestParam String type, @RequestParam String q) {
    String expr = CONDITIONS.get(type);          // 用户只能选 key,选不到就拒绝
    if (expr == null) return "unknown condition";
    SimpleEvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    ctx.setVariable("q", q);                     // 用户的值以变量身份进上下文,是数据不是代码
    return "matched: " + parser.parseExpression(expr).getValue(ctx, user);
}

修复二为什么更推荐:表达式字符串(name == #q)是服务端常量,攻击者连”图纸”的边都摸不到;他能控制的只有 #q 这个变量值,而变量在求值时被当数据处理。这就是 0.3 节口诀”输入只作渲染变量”在 SpEL 场景下的落地。

案例二:Thymeleaf 预处理注入

漏洞代码(逐行注释)

// LangController.java —— 视图名拼接可控参数
@Controller
public class LangController {
 
    @GetMapping("/doc")
    public String doc(@RequestParam String lang) {   // ① lang 完全可控
        // ❌ 危险:lang 进入视图名,而视图名会被 Thymeleaf 当表达式解析
        return "doc/" + lang;                        // ② 拼接发生在这里
    }
}

数据流四步:

  1. 输入从哪进来:@RequestParam String lang。
  2. 经过什么处理:return "doc/" + lang——Controller 返回的字符串不是直接发给浏览器的,Spring 视图解析器会把它交给 Thymeleaf 当”视图名/片段表达式”解析。
  3. 到达哪个危险函数:Thymeleaf 的片段表达式解析器。这就是为什么说 Sink 在 Controller 的 return 语句,不在模板文件里。
  4. 为什么防线失效:视图名解析时支持 __...__ 预处理,这是一个”先算内层、结果再参与外层解析”的二次求值机制;没有任何配置告诉 Thymeleaf”这个参数不可信”,它也不知道——它只知道自己在解析一个视图名。

利用

GET /doc?lang=__${T(java.lang.Runtime).getRuntime().exec("calc")}__::.x

payload 逐段拆解:

  • __ …… __ —— 预处理边界,Thymeleaf 见到后先求值中间的部分;
  • ${T(java.lang.Runtime).getRuntime().exec("calc")} —— 标准 SpEL 命令执行(Thymeleaf 在 Spring 环境下表达式底层就是 SpEL);
  • ::.x —— 片段选择器语法尾巴,保证整个视图名在语法上合法,解析不中断(没有它,模板解析可能先抛异常,payload 活不到执行那刻)。

审计结论与修复

结论:视图名可控 + 无白名单 → 高危 SpEL RCE(经 Thymeleaf 预处理触发)。

// ✅ 修复:视图名白名单映射,参数只用于选 key
private static final Set<String> ALLOWED = Set.of("zh", "en");
 
@GetMapping("/doc")
public String doc(@RequestParam String lang) {
    if (!ALLOWED.contains(lang)) {   // 不在白名单里的值,根本到不了 return 那行
        return "doc/default";
    }
    return "doc/" + lang;            // 此时 lang 只可能是 zh/en,不构成注入
}

前后对比的核心变化:修复前 lang 是任意字符串,修复后 lang 是枚举值——攻击面从”无限”收窄到”两个合法选项”。

1.8 Java 防御与修复汇总

技术修复手段
SpELSimpleEvaluationContext;白名单映射表达式;杜绝拼接
OGNL / Struts2升级 struts2-core 到无已知 OGNL RCE 的版本;不在业务代码中用 Ognl.getValue 处理用户输入
JSP EL用户输入只做”值”,不二次渲染为 EL;JSP 输出统一 c:out
Thymeleaf视图名/片段白名单;参数放 Model 渲染而非拼接视图名
Velocity升级 2.x + SecureUberspector;模板内容不可控
FreemarkerSAFER_RESOLVER;模板内容不可控
Fastjson升级到 1.2.68+ 并 setSafeMode(true);或迁移 Jackson 不开 default typing

2. Python

Python 侧的表达式/模板注入主战场是 Jinja2 SSTI(Flask 生态),Mako、Tornado 模板同理。与 Java 的关键区别:Python 没有”受限求值上下文”的官方概念——Jinja2 没有一个 SimpleEvaluationContext 给你换。所以防御完全押注在一件事上:“模板本体不可控”。而沙箱逃逸,是 SSTI 之后面试官的必然追问。

2.1 成因与 Sink

为什么会出现

Flask 官方文档里 render_template_string 的示例是渲染常量模板。但新手要做”个性化欢迎页”时,第一反应往往是把名字拼进 HTML 字符串再渲染——毕竟字符串拼接是学 Python 第一周就会的东西,而”渲染变量”这个概念还没建立。这就是动机:图省事 + 不知道危险,和第 0 章说的一模一样。

一段代码看懂对错只在一线之间

from flask import Flask, request, render_template_string
app = Flask(__name__)
 
@app.route('/hello')
def hello():
    name = request.args.get('name', '')                       # ① Source:用户输入
    # ❌ 危险:name 拼进了模板本体。模板里出现 {{7*7}} 会被当表达式求值为 49
    return render_template_string('<h1>Hello ' + name + '</h1>')
 
    # ✅ 安全:模板本体是常量,name 是渲染变量。引擎只转义输出,不解析其中的 {{ }}
    # return render_template_string('<h1>Hello {{ name }}</h1>', name=name)

两种写法的本质区别:

  • ❌ 写法里,name 的内容会经过 Jinja2 的词法分析——用户输入 {{7*7}},引擎看到 {{ }} 就当作”这里有个表达式”,算出 49 填进去。输入改了图纸。
  • ✅ 写法里,模板里只有一个占位符 {{ name }},引擎求值时发现变量值是字符串 {{7*7}},它只会把这个字符串当普通文本输出(还会做 HTML 转义),绝不会再解析一遍。输入只是填进姓名栏的字。

审计关键字(拿到 Flask 项目先搜这些):

  • Sink:render_template_string(、Template((Jinja2 直接实例化)、Mako Template(,以及任何 f-string / + 拼接进模板第一参数的写法。
  • Source:request.args / request.form / request.values / request.cookies / request.headers。

2.2 利用链:Python 对象图漫游

原理:从”一个空字符串”爬到”整个操作系统接口”

SSTI 让攻击者能在 {{ }} 里写 Python 表达式。但模板上下文里没有 os、没有 open,怎么办?答案是:Python 里万物皆对象,每个对象都自带通往全局的”任意门”(魔术属性)。

任意对象 ──__class__──▶ 它所属的类 ──__mro__──▶ 继承链(顶端是万物之父 object)
        ──__subclasses__()──▶ object 的所有子类(程序里加载过的类全在这,含"带路党")
        ──__init__.__globals__──▶ 该类方法所在模块的全局命名空间(里面可能就有 os、builtins)

生活类比:你被关在一个空房间里(模板上下文),什么都没有。但你脚下踩的地板(任意对象)有出厂标签(__class__),标签上有厂家族谱(__mro__),族谱顶端是总厂(object),总厂有全系列产品目录(__subclasses__()),目录里某款产品的说明书(__globals__)里写着仓库钥匙在哪(os 模块)。整个过程你不需要任何进口(import),全靠摸对象身上的隐藏属性——这就是为什么”禁 import”挡不住 SSTI。

经典 payload(Flask/Jinja2)

# 第一步:探测。回显 49 即确认 SSTI 存在
{{7*7}}
 
# 第二步:列出所有可用子类,人肉/fuzz 找"带路党"(索引号随环境变化,需现场试)
{{''.__class__.__mro__[1].__subclasses__()}}
 
# 第三步:读文件。从某个子类摸到 __builtins__ 里的 open
{{''.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
 
# 第四步:命令执行。某个子类的 globals 里 import 过 os 时
{{''.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__['os'].popen('id').read()}}
 
# Flask 便捷路径一:config 对象直接在模板上下文里,一条泄露全部配置(含 SECRET_KEY)
{{config}}
 
# Flask 便捷路径二:lipsum/url_for/get_flashed_messages 这些 Flask 注入的模板全局函数,
# 函数的 __globals__ 里常有 os(注意:具体可用性随 Flask/Jinja2 版本变化,不在就 fuzz 别的)
{{lipsum.__globals__['os'].popen('id').read()}}

payload 主链逐段拆解(以命令执行那条为例):

  • '' —— 一个空字符串,房间里随手可得的”地板”;
  • .__class__ —— 它的类,即 <class 'str'>;
  • .__mro__ —— str 的方法解析顺序(继承链),形如 (str, object);
  • [1] —— 取继承链第 2 个,即万物之父 object;
  • .__subclasses__() —— object 的全部子类列表,程序加载过的类基本都在;
  • [INDEX] —— 挑一个”带路党”(比如 os._wrap_close,它的模块里 import 了 os);
  • .__init__.__globals__ —— 该类的初始化方法所属模块的全局命名空间字典;
  • ['os'].popen('id').read() —— 拿到 os 模块,开管道执行 id 并读回结果。

INDEX 为什么不确定:子类列表的内容取决于目标程序加载了哪些库,顺序无规律,所以实战中是先回显整个列表再数下标,或者写脚本批量 fuzz。

2.3 过滤绕过

目标站点加了关键字过滤怎么办?这张表是”见招拆招”清单。核心思想一句话:Python 里同一个属性有无数种写法,黑名单只能堵死其中几种。

被过滤内容绕过手法原理一句话
下划线 _`”attr(request.args.a)(a=class,属性名从请求参数带进来);或十六进制 ’\x5f\x5fclass\x5f\x5f’`
点号 .`”attr(‘class’) 用过滤器代替属性访问;”[‘class’]` 下标访问
引号用 request.args.x 取值当字符串;字符串拼接 ''['__cl'+'ass__']黑名单扫不到拼出来的结果
关键字 class/mro拼接 '__cla'+'ss__'、反转 'ssalc__'[::-1]、hex/base64 编码后 decode同上,绕过的是”文本匹配”这个弱鸡机制
{{ }} 被拦{% if %}...{% endif %}、{%print(...)%} 语句块Jinja2 执行表达式不止 {{ }} 一种入口
黑名单 import/os走 __builtins__['eval']、subprocess.Popen、__loader__.load_module 等替代路径达到 RCE 的路从来不止 os 一条

2.4 沙箱逃逸专题(给思路,不堆 payload)

SSTI 遇上沙箱(禁 import、裁剪 builtins、关键字黑名单)时的逃逸方法论。先把沙箱分分类,再讲通用破解法。

常见沙箱形式

沙箱形式典型实现固有弱点
禁 importAST 扫描拒绝 Import 节点、接管 __import__对象图里仍藏着早就 import 过的模块(os 几乎必然在)
禁内建函数裁剪 __builtins__(删 open/eval/exec/import)漫游找回:别的模块的 __globals__ 里 __builtins__ 没被裁
关键字黑名单正则过滤 os/system/eval 等字符串拼接、编码、getattr 动态取属性,见 2.3
audit hook(Py3.8+ sys.addaudithook)运行时审计事件拦截历史上多次被绕过(对象漫游触发不审计的 C 层操作);只能作纵深防御
seccomp / 容器系统调用层限制与代码审计无关,属部署层,不讨论

逃逸思路(按优先级,面试说出前 3 条即可)

  1. 对象图漫游(主力):从可达对象(''、()、函数、异常)出发,__class__ → __mro__ → __subclasses__(),在全量子类中找”带路党”:
    • os._wrap_close:__init__.__globals__ 里有 os、system;
    • warnings.catch_warnings:__init__.__globals__['__builtins__'] 是全量内建;
    • 任何 import 过 os/sys/subprocess 的第三方类。
  2. import 机制本体:<class '_frozen_importlib.BuiltinImporter'>(沿 sys.modules 或子类链可达)提供 load_module,等于自带 import;__loader__、importlib 模块本身常被某个子类的 globals 持有。
  3. 恢复 builtins:拿到任一函数对象的 __globals__,若其中 __builtins__ 未被裁剪,直接取 eval/exec/__import__/open——沙箱裁的是自己那份,裁不掉所有模块手里的副本。
  4. Format string / f-string 注入:'{0.__class__.__init__.__globals__}'.format(obj)——很多沙箱只防 eval,不防 format。
  5. 异常与代码对象:func.__code__、types.CodeType 手工构造代码对象等深度路线(CTF 高阶内容)。

Django 模板说明(常见误区)

Django 自带模板能力很弱:不支持任意表达式求值,{{ }} 里只能做属性访问和过滤器,一般不构成 SSTI RCE,和 Jinja2 不是一个量级。Django 模板侧的审计点是 XSS:{% autoescape off %}、mark_safe()、|safe 这三处。

2.5 案例:Flask SSTI → RCE

动手实验(30 秒建立直觉)

在学案例之前,先在自己电脑上跑一遍最小复现,亲眼看看”图纸被改”长什么样:

python3 -c "
from jinja2 import Template
# 模拟 ❌ 写法:用户输入拼进模板本体
user_input = '{{7*7}}'
print(Template('result: ' + user_input).render())   # 输出 result: 49 ← 输入被执行了
 
# 模拟 ✅ 写法:用户输入只作渲染变量
print(Template('result: {{ v }}').render(v=user_input))  # 输出 result: {{7*7}} ← 输入只是文本
"

看到第一行输出 49、第二行原样输出 {{7*7}},你就真正理解了整个 Python 章节的全部攻防。

漏洞代码(逐行注释)

from flask import Flask, request, render_template_string
app = Flask(__name__)
 
@app.route('/calc')
def calc():
    expr = request.args.get('expr', '')          # ① Source:用户输入,无过滤
    tpl = f"<p>result: {expr}</p>"               # ② f-string 把输入拼进模板本体
    return render_template_string(tpl)           # ③ Sink:整体交给 Jinja2 编译执行

数据流分析

  1. Source:request.args.get('expr'),用户完全可控,无任何过滤。
  2. 传播:expr 经 f-string 拼入 tpl。注意此时 expr 里的 {{ }} 还只是普通字符,危险发生在下一步。
  3. Sink:render_template_string(tpl) 把整个 tpl 当模板源码编译。Jinja2 词法分析时发现 {{ }},把里面当 Python 表达式求值。攻击者请求 /calc?expr={{7*7}},回显 result: 49,注入确认。
  4. 升级 RCE:expr={{ ''.__class__.__mro__[1].__subclasses__()[N].__init__.__globals__['os'].popen('cat /flag').read() }}(N 为”带路党”子类在列表中的索引,现场 fuzz 得到)→ 命令结果回显,RCE 成立。

审计结论:render_template_string 的第一个参数中出现任何用户输入拼接即 SSTI 高危,与框架版本无关——这不是 bug,是误用。

修复

@app.route('/calc')
def calc():
    expr = request.args.get('expr', '')
    # ✅ 输入只作为渲染变量传入:模板本体是常量,Jinja2 对变量值自动转义
    return render_template_string("<p>result: {{ expr }}</p>", expr=expr)

对比修复前后:用户再提交 {{7*7}},页面会原样显示 result: {{7*7}} 而不是 49——因为模板里唯一的 {{ expr }} 占位符是把变量值当文本填进去的,不会二次解析。这个”再发一遍旧 payload 看是否失效”的动作,也是你验证修复是否到位的标准手法。

2.6 Python 防御与修复汇总

  1. 模板本体永远不可控:用户输入只作渲染变量传入(render_template_string(tpl, var=user_input) / render_template + 模板文件),禁止任何形式的字符串拼接进模板第一参数。
  2. 必须动态模板时走白名单映射(key → 预定义模板),不做黑名单过滤。
  3. 真有表达式求值需求,用 ast.literal_eval(只允许字面量,安全),杜绝 eval/exec 承接用户输入。
  4. 不迷信沙箱:禁 import / 裁 builtins / 关键字黑名单均可被对象图漫游绕过,沙箱只能作为纵深防御的一层,不能当主防线。

3. PHP

PHP 的表达式/模板注入形态主要落在 Twig 与 Smarty 两大模板引擎(Blade 属 Laravel 编译型模板,用户输入进模板本体的场景少见)。原理与 Jinja2 完全一致:用户输入被拼进模板字符串/模板名,引擎把输入当模板语法编译为 PHP 执行。所以这一节你可以读得很快——换汤不换药。

  • Twig:
    • 注入面:Twig\Environment::createTemplate($userInput)->render()(模板本体可控)、render($name) 中模板名可控。
    • 探测:{{7*7}}(Twig 表达式语法和 Jinja2 长得很像)。
    • 升级路径:_self 对象;过滤器回调 {{['id']|filter('system')}}——filter/map/sort 这类过滤器会把字符串参数当 PHP 函数名回调,'system' 一传就是命令执行;{{app}} 上下文泄露(Symfony 框架注入的对象,含内核、环境变量)。
    • 审计关键字:createTemplate、render( 拼接、|filter(、registerUndefinedFilterCallback。
  • Smarty:
    • 注入面:display('string:'.$input) / fetch('string:'.$input)——string: 资源前缀表示”后面这串字符就是模板源码”,拼上用户输入即 SSTI。
    • 危险语法:旧版本 {php} 标签(Smarty 3.1 起默认移除,但 SmartyBC 兼容层会把它复活——审计时看到项目引入 SmartyBC 要提高警惕)、{eval}、{include} 可控文件。
    • 审计关键字:display('string:、fetch('string:、{eval}、SmartyBC、trusted_dir 配置。

小案例:Twig 的 createTemplate 误用

<?php
// ❌ 危险写法:用户昵称直接当模板源码编译
$twig = new \Twig\Environment($loader);
$name = $_GET['name'];                          // ① Source:用户输入
$template = $twig->createTemplate("Hi " . $name); // ② 输入拼进模板本体
echo $template->render();                        // ③ Sink:渲染 = 编译执行

数据流四步:① $_GET['name'] 完全可控 → ② createTemplate("Hi " . $name) 把它变成模板源码的一部分 → ③ render() 编译执行 → ④ 全程无过滤,Twig 见到输入里的 {{ }} 就求值。

payload 拆解:?name={{['id']|filter('system')}}

  • ['id'] —— 一个只含字符串 id 的数组,数组元素就是要执行的命令;
  • |filter('system') —— Twig 的 filter 过滤器,会把 'system' 当 PHP 函数名,对数组每个元素回调一次;
  • 合起来等价于 PHP 的 system('id')——命令执行,结果回显。

审计判定与修复,一句话:模板内容/模板名不可控,输入只作 assign 变量。Twig 用 createTemplate 处理任何外部输入即高危;Smarty 见到 string: 资源拼用户输入即高危;保持引擎版本最新(历史 SSTI CVE 多为版本问题),不使用 SmartyBC 兼容层。更细的审计手法见 PHP 模板注入专项(php-tpl-audit)。


4. 面试题眼速答

题目一句话答案展开两三句
四.36 表达式注入如何审计?(SpEL/OGNL/Thymeleaf)找求值 Sink → 查输入拼接 → 验上下文限制,三步走。Sink 清单:parseExpression().getValue()(SpEL)、Struts2 框架二次解析(OGNL,版本审计)、Controller 返回语句拼视图名命中 __${...}__ 预处理(Thymeleaf)。SpEL 利用核心是 T(java.lang.Runtime).getRuntime().exec(),防御看是否换用 SimpleEvaluationContext;OGNL 核心是操纵 _memberAccess 恢复静态方法调用,防御=升级 struts2-core;Thymeleaf 核心是视图名拼接二次求值,防御=视图名白名单。
四.37 Python SSTI 怎么审/怎么利用Sink 是 render_template_string/模板第一参数拼接用户输入;利用靠对象图漫游找 os。审计搜 render_template_string(、Template(,看第一参数是否拼 Source(request.args 等)。利用链 __class__→__mro__→__subclasses__()→__init__.__globals__→os,在子类列表里找 os._wrap_close 等”带路党”拿 os.popen 或 __builtins__。过滤绕过:attr() 过滤器、下标访问、字符串拼接/编码、{% %} 语句块。
Python 沙箱怎么逃逸核心思路是对象图漫游:禁 import、删 builtins、关键字黑名单都挡不住。从任意对象经 __class__.__mro__[1].__subclasses__() 枚举全量子类,找持有 os/__builtins__/import 能力的(os._wrap_close、warnings.catch_warnings、_frozen_importlib.BuiltinImporter),恢复内建函数后执行任意代码。原理:沙箱裁的是”自己那份”命名空间,裁不掉程序里其他模块早就 import 好的副本。
四.38 Fastjson autotype 原理与防护演进?@type 可控即可实例化任意类并触发危险 getter/setter → RCE;正解是升级 + safeMode。autotype 为解决”反序列化时目标类型未知”而设计:序列化写入类全名,反序列化按 @type 实例化。1.2.25 引入 checkAutoType(默认关 + 黑白名单),之后 1.2.41~47 是”补黑名单—字符串变形/缓存投毒绕过”的拉锯;1.2.68 的 safeMode 彻底关闭 autotype 才是终点。autotype=false 时仍有 expectClass 合法子类、危险 deserializer 两条研究过的绕过面。审计结论:1.2.x 默认配置处理外部 JSON 按未修复处理,或迁移 Jackson(不开 default typing)。
PHP 模板注入怎么审Twig 搜 createTemplate,Smarty 搜 string: 资源,判定核心都是”模板本体或模板名可控”。Twig:createTemplate($userInput) / render($name) 拼接即高危,利用走 `

背诵锚点(考前扫一眼)

  • 通用公式:Sink + 输入拼接进表达式/模板本体 + 上下文未受限 = 高危。
  • 判别口诀:输入进图纸 → 注入;输入只填姓名栏 → 安全。
  • SpEL 三要素:parseExpression + getValue + StandardEvaluationContext;修复关键词 SimpleEvaluationContext。
  • OGNL 三字母:_memberAccess(操纵它 = 恢复静态方法调用)。
  • Thymeleaf 双下划线:__${...}__ 预处理 = 二次求值;Sink 在 Controller 的 return,不在模板文件。
  • Jinja2 对象链:__class__ → __mro__ → __subclasses__() → __init__.__globals__。
  • Fastjson 三版本线:1.2.25(checkAutoType)→ 黑名单拉锯(1.2.41~47)→ 1.2.68(safeMode)。