代码审计 | 通达OA 任意用户登录漏洞(匿名RCE)分析

官网更新了v11.5版本后,漏洞分析和PoC逐渐浮出了水面,其实这漏洞结合后台的一些功能是可以进一步实现匿名RCE的。

漏洞说明

伪造任意用户(含管理员)登录漏洞的触发点在扫码登录功能,服务端只取了UID来做用户身份鉴别,由于UID是整型递增ID,从而导致可以登录指定UID用户(admin的缺省UID为1)。

影响版本

通达OA v2017、v11.x < v11.5 支持扫码登录版本。

漏洞分析

v11.5更新修复了2个地方:

  • 客户端扫码登录接口;
  • Web端扫码登录接口。

Web端扫码登录过程

Web端扫码登录流程大致是这样:

  • 第一步

Web端访问 /general/login_code.php?codeuid=随机字符串 生成一个二维码,codeuid作为这个二维码凭证。

  • 第二步

Web端通过循环请求/general/login_code_check.php,将codeuid发送到服务端,判断是否有人扫了这个二维码。

  • 第三步

移动端扫码这个二维码,然后将codeuid等数据发送到/general/login_code_scan.php服务端进行保存。

  • 第四步

Web端通过login_code_check.php取得codeuid等扫码数据后(其实取数据这一步已经产生$_SESSION["LOGIN_UID"]登录了),再通过Web端发送到logincheck_code.php进行登录。

Web端登录请求脚本如下:

可以看到最终的登录数据只发送了UID到服务端,从而导致了任意用户登录。

Web端补丁分析

这里对比一下v11.4v11.5修复前和修复后的源代码:

源文件: /logincheck_code.php

修复后的版本对UID进行了初始化,并增加token的校验。

从redis中取得token并进行td_authcode()解密,然后从中取出UID,避免了前段直接控制UID

再看下token的生成方式,源文件: /general/login_code_scan.php

通过PHPSESSID从在线用户表user_online中取UID,然后封装数据MD5哈希后存入redis中。

客户端补丁分析

客户端和Web端大同小异,登录也加了token校验:

1
/ispirit/interface/login.php

同样是增加了token校验。

漏洞利用

手动漏洞复现可以在浏览器直接打断点修改数据或者BurpSuite修改。

脚本构造数据包的时候只需两步即可:

  • 将数据写入缓存: /general/login_code.php
  • 从缓存读取登录: /logincheck_code.php

任意用户登录 PoC

知道了漏洞过程,那么实现PoC就很简单了,以获取Web目录绝对路径为例:

  • tongda_v11.4_get_webroot_poc.py

修改脚本里的oa_addr为目标OA地址,成功后会输出当前OA的安装绝对路径。

1
2
3
4
$ python3 tongda_v11.4_get_webroot_poc.py

webroot: C:\\MYOA\\webroot
cookies: PHPSESSID=xxxxx

匿名 RCE ExP

有了后台管理权限和Web目录绝对路径,可以利用MySQL日志进一步写Shell,实现匿名RCE:

  • tongda_v11.4_rce_exp.py

修改脚本里的oa_addr为目标OA地址,成功后会在/api/目录下生成一个test.php,密码为:cmdGET请求方式。

1
2
3
4
5
$ python3 tongda_v11.4_rce_exp.py

webroot: C:\\MYOA\\webroot
cookies: PHPSESSID=xxxxx
webshell: (GET) http://192.168.0.3:8080/api/test.php?cmd=ipconfig

扩展思考

我们脑洞一下这个补丁是否可以绕过:

认证地址一:

源文件: /logincheck_code.php

1
2
3
$authInfo = $redis->get("OA:authcode:token:" . $token);

$Info = td_authcode($authInfo, "DECODE");

如果我们能找到一个可控的:

1
TRedis::redis()->setex($xxx, $xxx);

td_authcode()的key是固定的,从而就可以伪造token进行任意用户登录。

认证地址二:

源文件: /ispirit/login_code_check.php

1
2
3
4
5
6
7
8
9
$login_codeuid = TD::get_cache("CODE_LOGIN_PC" . $codeuid);

...

$code_info = TD::get_cache("CODE_INFO_PC" . $login_codeuid);

...

$UID = intval($code_info["uid"]);

如果我们能找到一个可控的:

1
TD::set_cache($xxx, $xxx);

也是可以伪造任意用户登录的。


PoC & ExP 传送门: https://github.com/zrools/tools/tree/master/python