Use pycryptodome and default key derivation hashing algorithm.
Ce challenge tourne sur un docker et n'est pas disponible
Solution
La première chose à faire est de décompiler l'apk. Dans sources/com/example/flagen on trouve les fichiers Java important.
Reverse de l'application
Dans NotesActivity.java on trouve la fonction qui sert à faire les requêtes HTTP, on voit qu'elle ajoute l'entête X-MOBISEC=ef75826d9de13292593aa57f82a7763d et un identifiant unique de type UUID à la fin de l'url
public final HttpURLConnection n(String method, String path) {
StringBuilder url = new StringBuilder("http://");
url.append(this.host);
url.append(":");
url.append(this.port);
url.append(path);
SharedPreferences sharedPreferences = getSharedPreferences("unique_id_prefs", 0);
String uniqueId = sharedPreferences.getString("unique_id", (String) null);
// Création du UUID si aucun n'est définit dans les préférences
if (uniqueId == null) {
uniqueId = UUID.randomUUID().toString();
SharedPreferences.Editor prefEdit = sharedPreferences.edit();
prefEdit.putString("unique_id", uniqueId);
prefEdit.apply();
}
// Ajout du UUID
url.append(uniqueId);
HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(url.toString()).openConnection();
httpURLConnection.setConnectTimeout(20000);
httpURLConnection.setReadTimeout(20000);
httpURLConnection.setRequestMethod(method);
// Ajout de l'entête
httpURLConnection.setRequestProperty("X-MOBISEC", "ef75826d9de13292593aa57f82a7763d");
httpURLConnection.setRequestProperty("Accept", "application/json");
return httpURLConnection;
}
Une autre fonction intéressante est celle qui sert de hashage :
public static String o(String str) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(("LbhXabjVaCenpgvprFnygfNerHavdhrylTrarengrqSbeRirelCnffOhgVzGbbYnmlGbPbqrGung:)" + str).getBytes());
StringBuilder hexFormat = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(b & 255);
if (hex.length() == 1) hexFormat.append("0");
hexFormat.append(hex);
}
return hexFormat.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
En cherchant les fichiers utilisant NotesActivity, on trouve sources\z0\a.java qui utilise la fonction de requêtes HTTP pour faire un GET sur l'endpoint /api/v1/acc/pass/.
String hash = new JSONObject(sb.toString()).getJSONObject("secret").getString("hash");
String password = ((TextView) notesActivity.findViewById(R.id.password)).getText().toString();
if (!hash.isEmpty()) {
if (!NotesActivity.o(password).equals(hash)) {
new Handler(Looper.getMainLooper()).post(new e(8, notesActivity));
}
}
notesActivity.password = password;
Ce passage vérifie que le hash du password entré est le même que celui renvoyé par le serveur.
On a également une fonction intérressante dans le même fichier :
Ici on récupère un secret sur le serveur, on le décode en base64 puis on le passe dans la fonction z1.a.J() avec le mot de passe. Pour déchiffrer un secret on a donc visiblement besoin des mots de passe.
On utilise PBKDF2 (une méthode pour dériver un secret) avec la fonction de hashage SHA256, le sel 0123456789abcdef et 100000 itérations. On génère un secret dérivé de 256 bits.
Ce secret dérivé est utilisé pour déchiffrer avec de l'AES en mode GCM. Le nonce du GCM est situé sur les 12 premiers octets du ciphertext, on a également le tag (qui sert de vérification) situé sur les 16 derniers octets. Le véritable ciphertext est situé entre les deux. En gros :
nonce, ciphertext, tag = ciphertext[:12], ciphertext[12:-16], ciphertext[-16:]
Récupération des comptes existants
On va tester les UUID présents dans wordlist.txt pour récupérer ceux qui possède un hash et un secret :
import json
import requests
HOST = '35.242.231.98'
PORT = 30512
def request(method: str, path: str, uuid: str):
headers = {
'X-MOBISEC': 'ef75826d9de13292593aa57f82a7763d',
'Accept': 'application/json'
}
url = f'http://{HOST}:{PORT}{path}{uuid}'
return requests.request(method=method, url=url, headers=headers).json()
def main():
with open('wordlist.txt', 'r') as f:
uuids = f.read().splitlines()
accounts = []
for uuid in uuids:
_hash = request('GET', '/api/v1/acc/pass/', uuid)['secret']['hash']
if _hash:
account = {'uuid': uuid, 'hash': _hash}
_secret = request('GET', '/api/v1/sec/', uuid)['secret']['text']
if _secret:
account['secret'] = _secret
accounts.append(account)
print(json.dumps(accounts, indent=2))
if __name__ == '__main__':
main()
3 comptes parmis les uuid dans wordlist.txt existes, il faut maintenant casser leur hash avec un bruteforce. Comme indiqué dans la description, on part sur rockyou.txt :
from hashlib import sha256
import json
import requests
HOST = '35.242.231.98'
PORT = 30512
ACCOUNTS = [
{
"uuid": "c8d8a726-a7c2-4b13-98a4-15f9c3831ef4",
"hash": "77518b39e620ac271bfc58639796160cb3984af0a3e5f4367230ad768855e8e7"
},
{
"uuid": "f79dd76f-2ce4-420f-bf46-f0ba82af04fb",
"hash": "87bcb0554d72bd277ae6c2795b8e09e03c56ed4314352449c3d371b70cdc1ea8"
},
{
"uuid": "4d1713c1-ef9e-46b1-9fee-9ac57d4180b8",
"hash": "e045171f3d3d93ee538b4673f7b5184bfd7d9eaa200f29f81ae1b7123a32ebca"
}
]
def request(method: str, path: str, uuid: str):
headers = {
'X-MOBISEC': 'ef75826d9de13292593aa57f82a7763d',
'Accept': 'application/json'
}
url = f'http://{HOST}:{PORT}{path}{uuid}'
return requests.request(method=method, url=url, headers=headers).json()
def hashing(s: bytes) -> str:
return sha256(b'LbhXabjVaCenpgvprFnygfNerHavdhrylTrarengrqSbeRirelCnffOhgVzGbbYnmlGbPbqrGung:)' + s).hexdigest()
def main():
to_find = len(ACCOUNTS)
found = 0
with open('E:/Downloads/rockyou.txt', 'rb') as f:
for password in f:
if password == b'':
break
password = password.strip(b'\n')
_hash = hashing(password)
for account in ACCOUNTS:
if account['hash'] == _hash:
account['password'] = password.decode()
found += 1
if found == to_find:
break
print(json.dumps(ACCOUNTS, indent=2))
if __name__ == '__main__':
main()
Maintenant que l'on connaît les mots de passe, on peut déchiffrer les secrets ?
Eh bien non, il y a eu un GROS foirage dans le CTF et la façon dont sont stockés les secrets (et donc le flag) sur le serveur n'est pas la même que celle sur l'application.
Il faut utiliser PBKDF2 avec SHA1 (et pas SHA256) et ensuite, comme indiqué dans la description (changée 2 fois en 2j de CTF), le nonce, le tag et le ciphertext sont placé différemment.