It looks like our target’s developers got wind of our previous exploits and fixed them, as stated in their changelog.
The app looks the same but we are required to log in as an admin to use the Edit and Create features. One caveat : there is no way for us to create an account. I guess we’ll have to borrow someone else’s then.
The Log In page looks simple enough. A basic form with a username
and password
fields, no CSRF token.
Of course, I’m going to try admin:admin
first!
Well, at least we tried. But the error message is quite interesting. It doesn’t say “Bad login attempt” or “Sorry try again”, it says “Unknown user”. And for a hacker, this is a nice piece of information to have.
The first thing I want to try on a Log In page (after the obvious credentials) is an SQL injection on the username
field.
Now, on my first attempt I messed up. I wrote ' or 1=1
instead of ' or 1='1
(I was missing a '
before the final 1
).
But it turns out that it was a good thing!
You see, the application did not handle my syntax error properly and returned an error containing a bunch of useful information.
We now know :
- that the database is MariaDB (this will impact our syntax);
- that our input is directly appended to the query instead of using parameters making it vulnerable to SQLi (but we already knew that);
- the query being ran (table and fields names).
All that free information will facilitate the crafting of the payloads.
Going back to the Log In page, trying the proper always true payload (' or 1='1
) without the error returned a new and interesting message.
We previously had “Unknown user” and now it’s “Invalid password”. Sending an always false payload (' and 1='1
) returns “Unkown user”. We are able to manipulate this message based on the boolean value of our request. This screams Blind SQLi.
So I turned on the Interceptor in Burp, captured the POST
request to the login
endpoint and sent it to the Intruder.
I crafted a Sniper attack with the following body based on the information gathered from the server error. I used a simple alphanumerical list as the payload and I grepped on “Invalid password” since this means true.
POST /login HTTP/2
[...]
username=' OR (SELECT SUBSTRING(username, 1, 1) FROM admins LIMIT 1) = '§§&password=
I want to grab the username of the first entry in the admins table. Being a simple example in a CTF, I did not bother with writing a script and simply launched the attack manually, incrementing the SUBSTRING
’s starting index and relaunching the attack until no request returned “Invalid password”.
Then I did the same for the password field.
POST /login HTTP/2
[...]
username=' OR (SELECT SUBSTRING(password, 1, 1) FROM admins LIMIT 1) = '§§&password=
Slowly but surely I exfiltrated the username and password of the first admin in the table, one character at the time.
Using these credentials to log in returned the first flag.
So now I should be logged in as an admin… But strangely, I only got a flag. No session ID, no cookie, nothing. I still don’t have access to the Create and Edit features.
I thought I could use the username and password somehow in the requests. I added the header Authorization: Basic YW[REDACTED]=
but it did not work. I tried using the credentials as parameters in the requests, but still nothing.
Let’s find another way to access the features then! The GET
requests are obviously protected, but what about the POST
requests? Knowing the endpoints from the previous assessment I went back in Burp and crafted my requests.
I started with the create
endpoint but it did not work. I went on to try with the edit
endpoint.
POST /page/edit/2 HTTP/2
[...]
title=MY+SUPER+TITLE&body=My+super+body%0D%0AWith+some+edit
And the server kindly replied the second flag :
HTTP/2 200 OK
[...]
^FLAG^[REDACTED]$FLAG$
Only one flag missing.
I started enumerating pages since it was still an autoincremented ID. I got a Forbidden
on /page/3
. I kept going a bit for good measure, everything else seemed to return 404
. Looks like we’ve got something! Now, how can I see this? As previously stated, I don’t have a session even after using the credentials. I kept messing around in the app, maybe I had missed something.
I finally gave in and checked the hint on the CTF platform : “Regular users can only see public pages”. I already knew that, thank you very much. I kept reading the hint. Then the words “regular users” started to resonate. Maybe there is something with roles!
I went down the rabbit hole of adding parameters and fuzzing the requests with admin=true
, isadmin=true
, role=1
, regular=false
and so on. All that led to a whole lot of nothing.
I took a break. Poured myself another coffee, ate some toasts and got back in.
Okay, so I need to be authenticated as an admin to see this page, but using the credentials I know to be accurate doesn’t actually authenticate me (it only gives me a flag). In a real app, I’d be authenticated by now. But this is a CTF. The goal is to find flags, learn and demonstrate various skills and vulnerabilities. What if I have to log in with a different approach? One that would demonstrate other skills…
We already know that the Log In page is vulnerable to SQLi. Instead of exfiltrating the credentials, let’s just bypass them altogether!
POST /login HTTP/2
[...]
username=' UNION SELECT 'letmein' AS password FROM admins WHERE '1' = '1&password=letmein
And done. Logged in and kinda pissed that I should have been for a while now (I had the actual credentials, c’mon!). On the Home page, we can finally see the 3rd and previously forbidden page, which contains the last flag.
Lessons learned
- An error message speaks volume
- Take a break and come back with fresh eyes
- Take a step back and try another way
- Always note everything down, your findings in a previous version might still be relevant after an update
- CTFs are not real app