This post spoils a CTF challenge … Don’t read if you want to try it !

ECW (European Cyber Week) is a Jeopardy student CTF challenge. It is organized by Thales, Airbus and the Bretagne region. I had a great time solving these challenges, despite a little abusive guessing in stegano challenges. Hurry to hand over this !

AdmYSsion is a Web challenge with an LDAP Injection. I will explain here how I solved it without guessing anything by using a simple trick.

[+] Recon

We are facing a basic login form :

Login form

After a little fuzzing, it appears that this login form is vulnerable to LDAP Injection with (very) verbose error messages.

For example : if you input a simple * in login field, you will receive the following error message, which indicate that your request matched with all accounts in the LDAP database directory.

Login form error

login=*&password=try

We can also obtain others error messages :
+ Server error: Account query failed => when our injection makes syntax instable.
+ Server error: Account not found, please check your username => when the login isn’t associated with an account.
+ Server error: Wrong password => when the username is found but the associated password is wrong.

[+] Exploitation

I did all the exploitation in Burp Intruder because I was too lazy to write a script.

First, we need to enumerate accounts attributes. I used this list which I parsed to have a formatted list. Let’s create a boolean condition in our request to exploit this blind LDAP Injection.
We can use this payload login=*)(cn=*&password=pass.
If “cn” attribute exists, this request will result in the ‘too many account message”. If not, we won’t see it.

After several tries, we have the attribute list :
+ userPassword
+ surname
+ name
+ cn
+ sn
+ objectClass
+ mail
+ givenName
+ commonName

Most of these are default attributes, then we will only focus on mail, name and userPassword attributes, which can hold interesting informations.

Now, it’s time to identify which of these 179 accounts is admin. We will use a simple payload for that : login=*)(name=*admin*&password=pass.
No luck, there is no result. We will have more chance with this payload : login=*)(mail=*admin*&password=pass which tell us that there is 1 result.
Let’s try to obtain the full mail, by finding the remaining characters with a boolean condition like this : login=*)(name=*<character>admin<character>*&password=pass.
The admin email is sarah.connor.admin@yoloswag.com and associated login is s.connor. Fine, we now need the password…

Here comes the trick, we have the attribute list, then we can suppose that the password is in the ‘userPassword’ field. But for some reasons, we can’t extract this password using the same payload we used for the email because login=*)(userPassword=*&password=pass is always true and whatever character we input before or after the wildcard makes the payload false (login=*)(userPassword=a*&password=pass for example).
After reading some documentation, I learned that the ‘userPassword’ attribute in not a string like the ‘cn’ attribute for example but it’s an OCTET STRING, which is a kind of byte array. That’s the reason why the classic comparison using a ‘*’ wildcard doesn’t work : a bytearray has different properties and comparison operators than a string.
But then, how can we retrieve this attribute … ?
After more documentation, I finally learned something I absolutely didn’t know. In LDAP, every object, type, operator etc. is referenced by an OID.
Since, some comparison operators can be directly used (for example the ‘equal’ operation for strings can be used with ‘=’ symbol, the ‘greater than’ operation for integers can be used with ‘>’ symbol etc.), but there are other comparison operators which can only be used by indicating their OID identifier in front of an ‘=’ symbol.
On this page are listed all the available operators. We will use the ‘octetStringOrderingMatch’ which has the OID 2.5.13.18.
From ldap.com,

octetStringOrderingMatch (OID 2.5.13.18): An ordering matching rule that will perform a bit-by-bit comparison (in big endian ordering) of two octet string values until a difference is found. The first case in which a zero bit is found in one value but a one bit is found in another will cause the value with the zero bit to be considered less than the value with the one bit.


We can extract the password with the following boolean payload :
login=s.connor)(userPassword:2.5.13.18:=\8f&password=try (\xx represent a byte in LDAP request, so we can send 8 bits blocs in our request but not less, we can’t perform the comparison bit after bit).

If we obtain the ‘wrong password’ error message, then our first 4 bits (here 0x7) are greater than or equal to the password’s first four bits. We just have to decrease this value, and when we receive the ‘no account’ error message we will know that the first four bits we sent are lower than the password’s first four bits. The right first four bits are the lowest which trigger the ‘wrong password’ error. The comparison then operates on the second four bits bloc of our input (here 0xF).
login=s.connor)(userPassword:2.5.13.18:=\7f&password=try
NB : It’s important to set the second part of our byte input to 0xF when we want to test the first four bits to avoid false positive comparisons in case of equality on first four bits.
After some bruteforce we obtain the following value for userPassword attribute :

\7b\4d\44\35\7d\36\61\69\54\31\39\61\52\66\32\67\69\71\70\2b\58\59\4f\6a\46\34\77\3d\3d

which is equal to {MD5}6aiT19aRf2giqp+XYOjF4w== after ASCII encoding.
After base64 decoding, we obtain the following MD5 hash e9a893d7d6917f6822aa9f9760e8c5e3. Google quickly tells us that it’s the ‘yoloswag’ hash.
Great, we have the admin username and password, let’s login !
login=s.connor&password=yoloswag
woot


Flag is : KLN7Dq8i7UNwvhQbPLjM6iDgI3fmsfLP.

[+] Bye

Thanks to maki for the screenshots ! I totally forgot to take some during the CTF :(
Feel free to tell me what you think about this post :)