sqlmapproject / sqlmap

Automatic SQL injection and database takeover tool
http://sqlmap.org
Other
32.19k stars 5.68k forks source link

Dynamically refreshing a session before each injection attempt to avoid client-side lockout controls #4437

Closed mzpqnxow closed 3 years ago

mzpqnxow commented 3 years ago

Descrption

What I'm looking for is a way to retrieve a fresh cookie between each injection attempt, similar to the --csrf-url feature. I have a feeling this functionality exists and I just couldn't find it. Normally this isn't required, you can leave the cookie unset initially, the the page will issue you a set-cookie. In this case, the cookie was issued by a different URL path.

This is for when you must have a valid cookie, but you can't use the same cookie for more than 3 requests. Additonally, the cookie is set from a different URL path than the POST target (for injection) is, so you can't just marge it in automatically using the magic functionality already in sqlmap that is detected and handled very nicely in most cases. This is the first time I've actually had this issue

More Context

I don't deal much in crummy applications, so it's pretty rare that I encounter an application with injection in the username or password field. But recently, I did run into this issue, in a legacy application, that probably went undetected by others who didn't pay much attention to the fact that they were encountering a silly cookie-based lockout.

In this vulnerable application, three successive POST attempts to the affected endpoint locked out that session, based on the session cookie, which was a standard PHPSESSID. If the session cookie could be refreshed between between each injection attempt, there wouldn't be an issue with lockouts; there was no IP checking or anything else on the server side.

What I needed was functionality similar to the --csrf-url feature, where you can dynamically grab a cookie or form value before making an injection attempt. This feature is flexible and powerful in the sense that it lets you specify exactly how to get the CSRF value via regex, but not flexible enough to allow you to get multiple variables/cookies with separate values in separate fields (at least as far as I know)

I ended up working around this limitation with a horribly hacky tamper script- now how tamper scripts are intended to be used, I know. I manually did a GET request to get a new session ID within the tamper script, at the URL that assigned the session, inserted it into the headers via the kwargs accesible from tamper(), and then basically reconstructed the entire POST body dynamically to the baseline one (hardcoded into the tamper script, instead of in the request file) inside tamper()- then in the request file used with -r, I ended up replacing the body with *. I then had to use --skip-urlencode and do the urlencode() myself inside tamper(), mindful of what was a variable, a separator, and a value- since sqlmap wouldn't know which parts of the tampered payload were the baseline body variable names, separators (& and =) and values and which was the original payload blob being tampered. At the very least, the payload returned by a tamper script gets urlencoded, unless explicitly told not to- in which case you have to do it yourself, except in cases where the HTTP server is incredibly broken/forgiving with malformed requests

I'm far from an expert with sqlmap and only rarely perform appsec assessments anymore, but I've used it at least 100-200 times over the past 4-5 years during penetration tests, so I'm reasonably competent. I've written custom tamper scripts before, though tamper scripts in general are very simple and limited and have a very specific objective- just modifying the payload of the injectable variable. I had to go far outside that for this case, hence the uglyness

I did a significant amount of searching before resorting to my workaround- maybe I just wasn't looking for the right terms, or there are just too many simple tutorials out there flooding the more advanced or uncommon topics. I'm wondering, did I completely miss the simple, correct, supported way to do this? I noticed --second-url but this isn't quite what I wanted. I've used --csrf-token (and originally had to use it in this case- though it turned out easier just merging that into the tamper script with the rest of the hacky stuff) but in this case, unlike what I've encountered in the past, the session was issued by a completely different page than the CSRF token. Is there some other simple way to do this without all of the ugly hacks I did with a tamper script?

The flow needs to be:

  1. GET /session, grab the set-cookie PHPSESSID
  2. POST /auth/login with injection value in _username, with that fresh PHPSESSID cookie set, as well as _password, _csrf and _token, the latter two must be retrieved dynamically as is common with CSRF tokens. To be clear, the CSRF token was not the issue here
  3. Drop the PHPSESSID from the sqlmap context, repeat from step 1, never using the same PHPSESSID more than once (more than twice, really, but easier to just refresh every time rather than every other time)

Failure to refresh the cookie locks that session out with this app

Desired Solution

Assuming it doesn't already exist (it must, right?) the desired solution would be something like --pre-url, a URL that can be visited before each injection attempt is made, with the main goal of accepting a set-cookie directive and merging that into the next request that's made to the injection target. This would allow each injection request to be associated with a fresh cookie, avoiding lockouts that are (essentially) client-side

Workaround

My workaround in this case was described above, but detailed at the end of this issue. I used a tamper script, but in a way that was clearly not intended- as I had to disable the native URL encoding in sqlmap, to avoid it from mangling the variable names and separators in the baseline request. I also had to then hack around some undesired behavior- for example, the Host header got dropped when using this approach, as did the Content-Type (though I'm not sure why either happened- the other header values remained intact from the request file)

More Context

I'll be a bit embarrassed if there's a simple, reliable, already implemented (and documented, in -hh or in docs elsewhere) way to do this without resorting to the workaround I went with, but still very happy to use that rather than this horrid tamper script- which amazingly, did actually get the injection working. Maybe this requires writing code, but there's a more suitable interface, a pointer to the docs for that would be appreciated too

The Workaround (It's horrible, I know)

Two parts, the tamper script and the payload for context

The Tamper Script

from requests import get
import string
import random
from urllib import urlencode
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOWEST

from re import compile, MULTILINE, DOTALL

HOST = 'www.redacted.net'

def dependencies():
    pass

def refresh():
    response = get('https://www.redacted.net/newsession')
    phpsessid = response.cookies.get('PHPSESSID', path='/auth')
    cookie_list = []
    cset = set()
    for k, v in response.cookies.items():
        if k in cset:
            continue  # Skip duplicate cookies, broke-ass server ...
        cookie_list.append('{}={}'.format(k, v))
        cset.add(k)
    cookie_string = '; '.join(cookie_list)
    csrf_pattern = compile(r'.*"_csrf" value="(?P<csrf>.*?)".*_token" value="(?P<token>.*?)"', MULTILINE | DOTALL)
    html = response.content
    m = csrf_pattern.match(html)
    if not m:
        raise RuntimeError('Incompetence has occurred, try again later')
    d = m.groupdict()
    csrf = d['csrf']
    token = d['token']
    return csrf, token, cookie_string

def tamper(payload, **kwargs):
    """Grab a fresh cookie before each request, and reconstruct the entire HTTP body as well
    Must be used with --skip-urlencode since your're basically hacking variables into the payload
    value here- not what tamper was intended for
    """
    if payload is None:
        payload = ''
    headers = kwargs.get("headers", {})
    csrf, token, cookie_string = refresh()
    headers["Cookie"] = cookie_string
    headers['Host'] = HOST
    headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
    tmp = '_csrf={}&_token={}&_password=password'.format(csrf, token)
    out = tmp +  '&' + urlencode({'_username': '{}'.format(payload)})
    return out

The "baseline" body, used with -r

POST /auth/login HTTP/1.1
Host: www.redacted.net
Connection: close
Content-Length: 239
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: https://www.redacted.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://www.redacted.net/auth/login
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

*

Originally, the payload with the _username and _password values was in the request file like normal, but I had to move it into the code- in retrospect, maybe I could have accessed the POST body from the kwargs in tamper, but if I remember correctly, that was not accessible in kwargs- though I was able to access the headers

Thanks, appreciate all the work put into this project, it's been amazing to watch it progress over the past 5+ years and makes the occasional web security assessment much more bearable!

mzpqnxow commented 3 years ago

Note, I may have screwed up the description and the code a bit while trying to redact things (the path of the cookie seems wrong- sorry about that, hopefully you still get the gist of the situation- the cookie was not getting set when making the injecting POST, and setting it in the request file wasn't allowing it to ever be updated)

mzpqnxow commented 3 years ago

Actually.. I'm looking more closely at the -vvvvvvvvv output logs, and I think this may actually be a bug as a result of the web app setting the PHPSESSID twice.. I'm going to close this for the moment until I can look more closely.. I think possibly once the injection triggered once (or maybe due to some other not immediately reproducible situation) the the extra cookie value was set (so there were two PHPSESSID values set) and then sqlmap was only "unsetting" and dynamically re-setting the other. The app was seeing the duplicate cookie and on the third instance of that the lock out was occurring...

stamparm commented 3 years ago

A) What I'm looking for is a way to retrieve a fresh cookie between each injection attempt, similar to the --csrf-url feature. I have a feeling this functionality exists and I just couldn't find it. <- try --preprocess or --live-cookies B) Maybe you could also try to --drop-set-cookie, because in such case no session could be pinned by the remote as "problematic"

mzpqnxow commented 3 years ago

I wasn't familiar with either --preprocess or --live-cookies- thanks! I'll take a look at both