利用 CodeQL 自动化寻找 Apache MyFaces 反序列化利用链教学文档
本文档旨在系统性地教授如何利用 CodeQL 自动化挖掘 Apache MyFaces 框架中的反序列化漏洞利用链,并深入剖析其反序列化机制的细节,为安全研究与漏洞挖掘提供详尽的指引。
第一部分:CodeQL 自动化挖掘链
1.1 环境准备与数据库构建
目标项目是 Apache MyFaces,一个中等规模的 Java 项目。为了便于分析,推荐采用一种“简单粗暴”的方式下载其所有源码(包括依赖项的源码),并使用 CodeQL 的无构建模式创建数据库。
步骤 1:下载项目及依赖源码
mvn dependency:unpack-dependencies -Dclassifier=sources -DoutputDirectory=./deps-src
此命令会将项目所有依赖库的源代码解压到当前目录的 deps-src 文件夹中。为了丰富后续分析的 Source 和 Sink 路径,可以在此步骤选择性加入其他相关源码,例如 Tomcat 的源码。
步骤 2:创建 CodeQL 数据库
codeql database create myfacesDB --language=java --source-root=. --build-mode=none
通过 --build-mode=none 参数,无需编译即可为项目及其所有下载的依赖源码创建数据库,这对于没有完整构建脚本或构建复杂的大型项目非常便捷。
1.2 自动化挖掘思路与核心挑战
利用 CodeQL 自动化寻找反序列化利用链的通用思路是:预先定义一组 Sink 方法和 Source 方法,然后通过查询寻找从 Source 到 Sink 的连通路径。
-
核心组件:需要维护两个核心查询库文件:
sink.qll: 定义反序列化链的终点(Sink),如readObject()、EL表达式执行等方法。source.qll: 定义反序列化链的起点(Source),通常是那些在反序列化过程中会被自动调用的方法,如readObject、hashCode、equals等。
-
查询实现:在查询语句中,利用
polyCalls谓词配合edges来探索方法间的调用关系。建议在查询语句前加上注释/*@ kind path-problem */,这样 CodeQL 可以直观地展示出完整的调用路径。
面临的细节挑战与解决方案:
-
JDK 内部类方法缺失:在不编译 JDK 源码的情况下,CodeQL 可能会忽略 JDK 内部类的
readObject等方法,导致链的源头缺失。- 解决方案:将常见的、能够“衔接”到
readObject的方法也纳入 Source 点。例如,将HashMap#hashCode、HashTable#equals等方法作为备选的 Source,如果查询到从这些方法到 Sink 的链条,可以手动或通过规则将其与readObject连接。
- 解决方案:将常见的、能够“衔接”到
-
Sink 点条件过松导致误报:需要对不同的 Sink 类型制定精确的判定逻辑以减少误报。
- 以 EL 注入 Sink 为例的判定逻辑:
- 目标方法需要调用包名含有
el或expression特征的类(如javax.el.xxx)的getValue()或findValue()方法。 - 调用上述方法的对象(
Expression实例)必须是该方法所属类的成员变量,以确保其可以通过反射被修改(此逻辑为简化版,暂未考虑通过复杂参数传递的可控对象情况)。
- 目标方法需要调用包名含有
- 示例代码:
public class Test{ // 类成员变量,可被反射修改 javax.el.%.%Expression% expression; public void imSink(){ // 调用 Expression 的 getValue 方法 expression.getValue(); } }
- 以 EL 注入 Sink 为例的判定逻辑:
1.3 实践工具与结果
作者提供了一个初始版本的 CodeQL 查询脚本,包含了较为通用的 Sink 和 Source 点定义,可用于快速开始。该脚本托管于 GitHub:
https://github.com/byname66/GadgetWalker
在针对 MyFaces 框架运行此查询后,能够自动化地发现多条潜在的原生反序列化利用链。虽然存在一定的误报率,但许多结果经测试是真实可用的,后续可通过完善 Sink/Source 定义来优化准确率。
第二部分:MyFaces 反序列化漏洞点剖析
MyFaces 处理请求的架构与 Mojarra(另一个 JSF 实现)类似。请求经由 javax.faces.webapp.FacesServlet#service 处理后,进入 MyFaces 自身实现的 org.apache.myfaces.lifecycle 生命周期,循环执行六个阶段(Phase)。
核心漏洞点位于 恢复视图(Restore View) 阶段。在此阶段,MyFaces 会尝试恢复客户端或服务器端保存的视图状态(viewState),此过程涉及反序列化操作。
MyFaces 支持两种 viewState 存储方式,由配置 javax.faces.STATE_SAVING_METHOD 决定:
client: 存储在客户端(如 Cookie 或隐藏表单域)。server: 存储在服务器端 Session 中。
恢复视图的核心逻辑涉及两个关键接口方法:
org.apache.myfaces.application.viewstate.token.StateTokenProcessor#decode(): 负责从请求参数中提取并初步处理viewState,结果存入savedStateObject。org.apache.myfaces.application.StateCache#restoreSerializedView(): 对savedStateObject进行“后处理”,最终触发反序列化。
根据存储位置的不同,上述接口有不同实现:
- 客户端模式:
ClientSideStateTokenProcessor和ClientSideStateCacheImpl。 - 服务端模式:
ServerSideStateTokenProcessor和ServerSideStateCacheImpl。
2.1 客户端模式 (in client) 反序列化流程
在 ClientSideStateTokenProcessor#decode() 方法中,完成了主要的反序列化逻辑。核心处理位于 StateUtils#reconstruct() 方法,其步骤可简化为:
public static final Object reconstruct(String string, ExternalContext ctx){
bytes = string.getBytes(ZIP_CHARSET); // 字符串转字节
bytes = decode(bytes); // Base64 解码
bytes = decrypt(bytes, ctx); // 密码学解密
return getAsObject(bytes, ctx); // 等价于 readObject,触发反序列化
}
关键安全限制:MyFaces 默认对客户端 viewState 进行强加密和签名(AES/CBC/PKCS5Padding + HMAC),遵循先验签后解密的流程。这导致:
- Padding Oracle 等密码学攻击失效。
- 攻击者必须知晓加密密钥才能构造可被成功解密的恶意序列化数据。
利用前提:需要通过其他漏洞(如任意文件读取)获取到 web.xml 等配置文件中的加密密钥。如果应用未显式配置密钥,MyFaces 会随机生成,则无法利用。
2.2 服务端模式 (in server) 反序列化流程
在服务端模式下,ServiceSideStateTokenProcessor#decode 仅进行 Base64 解码,得到的是一串十六进制字符串。随后在 ServerSideStateCacheImpl#restoreSerializedView() 中,该字符串被转换为字节数组,并作为参数传递给 ServerSideStateCacheImpl#getSerializedViewFromServletSession()。
该方法的核心反序列化逻辑如下:
SerializedViewCollection viewCollection = (SerializedViewCollection) externalContext
.getSessionMap().get(SERIALIZED_VIEW_SESSION_ATTR); // 从 Session 获取视图集合
if (viewCollection != null) {
// ... 构造 SerializedViewKey ...
Object state = viewCollection.get(key); // 从集合中根据 Key 获取状态对象
if (state != null) {
serializedView = deserializeView(state); // 反序列化
}
}
关键发现与限制:
deserializeView方法仅对byte[]类型的入参进行反序列化。- 在 MyFaces 的默认配置 (
org.apache.myfaces.SERIALIZE_STATE_IN_SESSION=false) 下,viewCollection中存储的state对象是对象本身而非序列化字节数组。这意味着即使能控制viewCollection中的值,默认情况下也无法触发deserializeView。 - 通过深入分析,
SerializedViewCollection在 Tomcat 的getSession()方法中创建,且在整个请求处理链中,外部不可控地调用其put方法。因此,无法在默认配置下通过控制_sessionMap来注入恶意的SerializedViewCollection。
结论:在默认配置下,服务端存储模式 (server) 的 viewState 无法被直接利用进行反序列化攻击。有效的攻击面被限制在客户端模式 (client)。
第三部分:利用链构造与利用
3.1 利用场景
综合以上分析,针对 MyFaces 框架的反序列化漏洞利用,需满足以下苛刻条件:
- 必须为客户端存储模式 (
javax.faces.STATE_SAVING_METHOD=client)。 - 必须获取到加密密钥。通常需要结合另一个漏洞(如任意文件读取)来读取 web.xml 等配置文件中的硬编码密钥。如果应用使用随机密钥,则攻击无法进行。
3.2 利用链构造
利用第一部分提到的 CodeQL 自动化挖掘工具,可以在 MyFaces 及其依赖中寻找到多条可用的原生利用链(即不依赖第三方库的链)。文档中给出了一个具体的示例链,其核心是构造一个特殊的 HashMap,其 key 设置为一个精心构造的 ContextAwareTagValueExpression 对象。
利用链核心逻辑简述:
- 构造一个包含恶意 EL 表达式(如
${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(...)})的ValueExpression。 - 将此
ValueExpression包装进ContextAwareTagValueExpression对象中。 - 将该对象作为
key放入一个HashMap。 - 当此
HashMap被反序列化时,在readObject过程中会计算其键的哈希码,从而触发ContextAwareTagValueExpression的一系列方法调用,最终执行恶意的 EL 表达式,达到命令执行的目的。
3.3 生成合法攻击载荷
由于客户端 viewState 被加密,攻击者需要模拟 MyFaces 的加密流程,将恶意序列化数据封装成合法的、经过加密和签名的字符串。文档中提供了 GenerateLegitString 类的示例代码框架,展示了如何:
- 序列化恶意对象。
- 使用与目标服务器相同的算法(默认 AES/CBC/PKCS5Padding)和密钥进行加密。
- 计算 HMAC 签名。
- 将加密数据和签名拼接后,进行 Base64 编码,生成最终可放置在
viewState参数中发送的字符串。
攻击者需要将代码中的 SECRET_KEY_BYTES 和 MAC_KEY_BYTES 替换为目标应用的实际密钥,才能生成有效的攻击载荷。
总结
本教学文档详细阐述了利用 CodeQL 自动化挖掘 Apache MyFaces 反序列化链的方法论与实践步骤,并深入分析了 MyFaces 框架中两处反序列化点的具体实现、安全限制和利用条件。总体而言,该漏洞的利用门槛较高,严重依赖于客户端存储模式及加密密钥的获取。自动化工具能有效辅助安全研究人员发现潜在的利用链,但成功的攻击需要结合具体的应用配置和其他辅助漏洞。