m0leCon 2026 — In (Secure) Vault 2 Çözümü
Bir .NET 8 WPF kasa uygulaması kapısını istemci tarafında CheckPassword ile koruyor: ptm bayrağı MD5, SHA-256, base64 ve base32 ile doğrulanan leet parçalara bölünmüş, ikili dosya decompile edilerek birleştiriliyor.
m0leCon 2026 — In (Secure) Vault 2
"In (Secure) Vault 2" (Beginner, rev) tek bir arşiv gönderiyor: InsecureVault.tar. Açıldığında karşımıza kendi kendine yeten bir .NET 8 WPF masaüstü uygulaması çıkıyor — InsecureVault.exe + InsecureVault.dll ve birkaç runtime assembly'si. Uygulama, parolası bayrağın kendisi olan bir giriş penceresi gösteriyor: CheckPassword true döndürürse uygulama "Database Manager" ekranını açıyor.
Yazarın açıklamada adını koyduğu nokta ("şirketlerim için süper güvenli bir kasa") kontrolün tamamen istemcide yaşaması ve hatta iyi saklanmamış olması — sadece küçük hash bulmacaları zinciri. Yönetilen ikili dosyayı decompile etmek tüm kısıtları önünüze seriyor.
Kapsam: gönderilen ikili dosyaların statik reverse-engineering'i. Uygulama Windows WPF hedefliyor, dolayısıyla çalıştırmadım; bayrak yalnızca IL'den geri kazanılabiliyor.
- Etkinlik: m0leCon CTF 2026
- Kategori: rev (Beginner)
- Bayrak biçimi:
ptm{...}
1. Keşif
Arşiv üzerinde tar/inceleme bir apphost ve bir yönetilen assembly gösteriyor. Hızlı bir strings taraması, decompiler olmadan parçaları adlandırıyor:
strings -n 5 InsecureVault.dll | grep -iE "sha|md5|password|flag|Login"
# SHA256
# HashAlgorithm
# CheckPassword
# loginPasswordTBox
# LoginButton_Click
# " Inserisci la flag per continuareİki şey öne çıkıyor. Birincisi, LoginButton_Click içinden çağrılan bir CheckPassword metodu — kapının kendisi bu. İkincisi, strings içinde hiçbir düz metin bayrak yok; karşılaştırma bir digeste karşı olmalı, bir literal'e karşı değil. Arayüz metni İtalyanca ("devam etmek için bayrağı gir"), İtalyan konferansıyla tutarlı.
ILSpy tarzı bir sınıf listesi hedefi küçük tutuyor:
Class InsecureVault.App
Class InsecureVault.DbManager
Class InsecureVault.Company / Employee / Project / Document
Class InsecureVault.MainWindowMainWindow girişi tutuyor; DbManager giriş sonrası ekran.
2. Decompile etmek
İkili dosyalar yönetilen (.NET) olduğu için ilspycmd neredeyse özgün C# geri kazanıyor. Bunu bir .NET SDK konteynerinde çalıştırdım (makinede dotnet araç zinciri yoktu). Kayda değer iki ortam pürüzü:
ilspycmd8.2 birnet6.0aracı;net8.0SDK runtime'ı altında roll-forward gerektiriyor (DOTNET_ROLL_FORWARD=Major).- Bağlanan dizin salt-okunurdu, dolayısıyla
-o <dir>Read-only file systemile başarısız oldu; tek bir tipi stdout'a decompile etmek bunu aşıyor.
dotnet tool install -g ilspycmd --version 8.2.0.7535
DLL=$(find /root/.dotnet/tools/.store -name ilspycmd.dll | head -1)
DOTNET_ROLL_FORWARD=Major dotnet "$DLL" -t InsecureVault.MainWindow InsecureVault/InsecureVault.dll3. CheckPassword
Tüm görev bu metot. Bayrağı tam olarak 45 karaktere ve ptm{ önekine sabitliyor, ardından 4 ve 5 karakterlik dilimleri hash, kodlama ve tamsayı hileleri karışımıyla doğruluyor:
private static bool CheckPassword(string s)
{
if (s.Length != 45) return false;
if (s.Substring(0, 4) != "ptm{") return false;
using (MD5 md = MD5.Create())
if (BitConverter.ToString(md.ComputeHash(Encoding.UTF8.GetBytes(s.Substring(4, 4))))
.Replace("-", "").ToLower() != "33635414851f62863a5cb7825481433d") return false;
if (BitConverter.ToString(Encoding.UTF8.GetBytes(s.Substring(8, 4)))
.Replace("-", "").ToLower() != "5f345f46") return false;
if (Convert.ToBase64String(Encoding.UTF8.GetBytes(s.Substring(12, 4))) != "dW5ueQ==") return false;
string text = "0x" + ((int)s[19]).ToString("x");
if (s[16] != '_' || (s[17] ^ 5) != 114 || s[18] != '4' || text != "0x79") return false;
using (SHA256 sha = SHA256.Create())
if (BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(s.Substring(20, 4))))
.Replace("-", "").ToLower() != "c1cd3414aea97cfb005cf3bf3f39f3c1b5412f5dd8a2602c74a38181d94f5a2d") return false;
if (ToBase32String(Encoding.UTF8.GetBytes(s.Substring(24, 5))) != "MNUDGY3L") return false;
string text2 = "0b" + Convert.ToString(s[30], 2);
if (s[29] != '_' || text2 != "0b1100110" || s[31] != '0' || s[32] != 'R') return false;
if (s.Substring(33, 3) != "_4_" || (int)s[36] >> 4 != 7) return false;
int num = int.Parse(s[39].ToString());
if ((s[37] ^ s[38]) != 1 || s[37] != '4' || num + 10 != 15 || s[40] != 'w') return false;
if (BitConverter.ToString(Encoding.UTF8.GetBytes(s.Substring(41)))
.Replace("-", "").ToLower() != "3072647d") return false;
return true;
}Her dilim bağımsız olarak tersinir. İlginç çağrı ToBase32String: A–Z2–7 alfabesi üzerinde elle yazılmış bir RFC 4648 kodlayıcısı (= ile 8'in katına dolgu yapıyor), dolayısıyla MNUDGY3L sıradan base32 gibi çözülüyor.
4. Yeniden birleştirme
Dilimler indeks sırasıyla işlendiğinde:
| Dilim | Kontrol | Değer |
|---|---|---|
[0:4] | literal | ptm{ |
[4:8] | MD5 = 33635414851f... | Wh4t |
[8:12] | baytlar = 5f 34 5f 46 | _4_F |
[12:16] | base64 = dW5ueQ== | unny |
[16:20] | _, s[17]^5==114, 4, hex(s[19])=="0x79" | _w4y |
[20:24] | SHA-256 = c1cd3414... | _t0_ |
[24:29] | base32 = MNUDGY3L | ch3ck |
[29:33] | _, bin(s[30])=="1100110", 0, R | _f0R |
[33:37] | _4_, s[36]>>4==7 | _4_p |
[37:41] | s[37]^s[38]==1, 4, int(s[39])+10==15, w | 455w |
[41:45] | baytlar = 30 72 64 7d | 0rd} |
İki dilim 4 karakterlik hash preimage'idir, dolayısıyla brute-force edilir. Bariz tahminler (such, just, r34l, n0t_) tutmuyor; MD5 ve SHA-256 hedefleri üzerinde tam yazdırılabilir-ASCII araması onları hemen buluyor:
def brute(target, hashfn, charset):
for combo in itertools.product(charset, repeat=4):
s = "".join(combo)
if hashfn(s.encode()).hexdigest() == target:
return s
charset = [chr(i) for i in range(32, 127)] # yazdırılabilir ASCII
# MD5 33635414851f62863a5cb7825481433d -> 'Wh4t'
# SHA256 c1cd3414aea97cfb005cf3bf3f39f3c1b5412f5dd8a2602c74a38181d94f5a2d -> '_t0_'MNUDGY3L ise ch3ck olarak çözülüyor. Geri kalan her şey düz aritmetik ve kodlama tersleri.
5. Doğrulama
CheckPassword'ı Python'da yeniden uyguladım (elle yazılmış base32 dahil) ve adayı her dala soktum:
PASS length==45
PASS prefix
PASS md5 s[4:8]
PASS hex s[8:12]
PASS b64 s[12:16]
PASS s[16:20]
PASS sha256 s[20:24]
PASS base32 s[24:29]
PASS s[29:33]
PASS s[33:37]
PASS s[37:41]
PASS s[41:]
FLAG: ptm{Wh4t_4_Funny_w4y_t0_ch3ck_f0R_4_p455w0rd} len 45
ALL PASSLeet'i sesli okumak şakayı veriyor: "What a funny way to check for a password" (parolayı kontrol etmenin ne komik bir yolu). Parola bayrağın kendisi ve "kasa" sunucuda hiçbir şeyi korumuyor — tüm kontroller istemcide çalışıyor.
Bütünlük için, giriş sonrası DbManager, aynı assembly'de sabit kodlanmış kimlik bilgileriyle (giovanni / MD5'e benzeyen bir literal) insecurevault.challs.m0lecon.it:5432'ye bağlanıp Companies/Employees/Projects/Documents yüklüyor. Bu kimlik bilgileri de istemci ikilisinin içinde — uygulamanın hiçbir güven sınırı yok.
6. Çıkarımlar
- İstemci taraflı bir parola kontrolü dekoratiftir. İstemcinin bir parolayı kabul etmesi için ihtiyaç duyduğu her şey — digestler, kodlamalar, sıralama — saldırgana gider. Yönetilen bir .NET assembly'si okunabilir C#'a decompile olur.
- Tek bir sırrı MD5/SHA-256/base64/base32/bit hilelerine bölmek güvenlik katmaz; her parça bağımsız olarak tersinirdir ve kısa preimage'ler saniyeler içinde brute-force'a düşer.
- Aynı assembly'de sabit kodlanmış veritabanı kimlik bilgileri, görevin adının arkasındaki asıl hata sınıfıdır: kasa, ikili dosyaya sahip olan herkese güveniyor.
7. Bayrak
ptm{Wh4t_4_Funny_w4y_t0_ch3ck_f0R_4_p455w0rd}Ne düşünüyorsun?
Tepki bırakarak geri bildirim ver