利用codeQL自动化寻找反序列化链—myfaces反序列化
字数 4820
更新时间 2026-03-07 10:04:57

利用 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),通常是那些在反序列化过程中会被自动调用的方法,如 readObjecthashCodeequals 等。
  • 查询实现:在查询语句中,利用 polyCalls 谓词配合 edges 来探索方法间的调用关系。建议在查询语句前加上注释 /*@ kind path-problem */,这样 CodeQL 可以直观地展示出完整的调用路径。

面临的细节挑战与解决方案:

  1. JDK 内部类方法缺失:在不编译 JDK 源码的情况下,CodeQL 可能会忽略 JDK 内部类的 readObject 等方法,导致链的源头缺失。

    • 解决方案:将常见的、能够“衔接”到 readObject 的方法也纳入 Source 点。例如,将 HashMap#hashCodeHashTable#equals 等方法作为备选的 Source,如果查询到从这些方法到 Sink 的链条,可以手动或通过规则将其与 readObject 连接。
  2. Sink 点条件过松导致误报:需要对不同的 Sink 类型制定精确的判定逻辑以减少误报。

    • 以 EL 注入 Sink 为例的判定逻辑
      1. 目标方法需要调用包名含有 elexpression 特征的类(如 javax.el.xxx)的 getValue()findValue() 方法。
      2. 调用上述方法的对象(Expression 实例)必须是该方法所属类的成员变量,以确保其可以通过反射被修改(此逻辑为简化版,暂未考虑通过复杂参数传递的可控对象情况)。
    • 示例代码
      public class Test{
          // 类成员变量,可被反射修改
          javax.el.%.%Expression% expression;
      
          public void imSink(){
              // 调用 Expression 的 getValue 方法
              expression.getValue();
          }
      }
      

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 中。

恢复视图的核心逻辑涉及两个关键接口方法:

  1. org.apache.myfaces.application.viewstate.token.StateTokenProcessor#decode(): 负责从请求参数中提取并初步处理 viewState,结果存入 savedStateObject
  2. org.apache.myfaces.application.StateCache#restoreSerializedView(): 对 savedStateObject 进行“后处理”,最终触发反序列化。

根据存储位置的不同,上述接口有不同实现:

  • 客户端模式:ClientSideStateTokenProcessorClientSideStateCacheImpl
  • 服务端模式:ServerSideStateTokenProcessorServerSideStateCacheImpl

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),遵循先验签后解密的流程。这导致:

  1. Padding Oracle 等密码学攻击失效。
  2. 攻击者必须知晓加密密钥才能构造可被成功解密的恶意序列化数据

利用前提:需要通过其他漏洞(如任意文件读取)获取到 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); // 反序列化
    }
}

关键发现与限制

  1. deserializeView 方法仅对 byte[] 类型的入参进行反序列化
  2. 在 MyFaces 的默认配置 (org.apache.myfaces.SERIALIZE_STATE_IN_SESSION=false) 下,viewCollection 中存储的 state 对象是对象本身而非序列化字节数组。这意味着即使能控制 viewCollection 中的值,默认情况下也无法触发 deserializeView
  3. 通过深入分析,SerializedViewCollection 在 Tomcat 的 getSession() 方法中创建,且在整个请求处理链中,外部不可控地调用其 put 方法。因此,无法在默认配置下通过控制 _sessionMap 来注入恶意的 SerializedViewCollection

结论:在默认配置下,服务端存储模式 (server) 的 viewState 无法被直接利用进行反序列化攻击。有效的攻击面被限制在客户端模式 (client)。

第三部分:利用链构造与利用

3.1 利用场景

综合以上分析,针对 MyFaces 框架的反序列化漏洞利用,需满足以下苛刻条件:

  1. 必须为客户端存储模式 (javax.faces.STATE_SAVING_METHOD=client)。
  2. 必须获取到加密密钥。通常需要结合另一个漏洞(如任意文件读取)来读取 web.xml 等配置文件中的硬编码密钥。如果应用使用随机密钥,则攻击无法进行。

3.2 利用链构造

利用第一部分提到的 CodeQL 自动化挖掘工具,可以在 MyFaces 及其依赖中寻找到多条可用的原生利用链(即不依赖第三方库的链)。文档中给出了一个具体的示例链,其核心是构造一个特殊的 HashMap,其 key 设置为一个精心构造的 ContextAwareTagValueExpression 对象。

利用链核心逻辑简述

  1. 构造一个包含恶意 EL 表达式(如 ${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(...)})的 ValueExpression
  2. 将此 ValueExpression 包装进 ContextAwareTagValueExpression 对象中。
  3. 将该对象作为 key 放入一个 HashMap
  4. 当此 HashMap 被反序列化时,在 readObject 过程中会计算其键的哈希码,从而触发 ContextAwareTagValueExpression 的一系列方法调用,最终执行恶意的 EL 表达式,达到命令执行的目的。

3.3 生成合法攻击载荷

由于客户端 viewState 被加密,攻击者需要模拟 MyFaces 的加密流程,将恶意序列化数据封装成合法的、经过加密和签名的字符串。文档中提供了 GenerateLegitString 类的示例代码框架,展示了如何:

  1. 序列化恶意对象。
  2. 使用与目标服务器相同的算法(默认 AES/CBC/PKCS5Padding)和密钥进行加密。
  3. 计算 HMAC 签名。
  4. 将加密数据和签名拼接后,进行 Base64 编码,生成最终可放置在 viewState 参数中发送的字符串。

攻击者需要将代码中的 SECRET_KEY_BYTESMAC_KEY_BYTES 替换为目标应用的实际密钥,才能生成有效的攻击载荷。

总结

本教学文档详细阐述了利用 CodeQL 自动化挖掘 Apache MyFaces 反序列化链的方法论与实践步骤,并深入分析了 MyFaces 框架中两处反序列化点的具体实现、安全限制和利用条件。总体而言,该漏洞的利用门槛较高,严重依赖于客户端存储模式及加密密钥的获取。自动化工具能有效辅助安全研究人员发现潜在的利用链,但成功的攻击需要结合具体的应用配置和其他辅助漏洞。

相似文章
相似文章
 全屏