JS Safe 2.0 - Google CTF

1
You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner..

Ek

Sayfayı incelemeye başlayalım

1
<input id="keyhole" autofocus onchange="open_safe()" placeholder="🔑">

id’i keyhole olan inputun onchange eventinde opensafe() fonksiyonunu çağırdığını görüyoruz. O zaman opensafe() fonksiyonuna bakacağız

1
2
3
4
5
6
7
8
9
function open_safe() {
keyhole.disabled = true;
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
if (!password || !x(password[1])) return document.body.className = 'denied';
document.body.className = 'granted';
password = Array.from(password[1]).map(c => c.charCodeAt());
encrypted = JSON.parse(localStorage.content || '');
content.value = encrypted.map((c, i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}

password değişkenine keyhole içeriğini regex’e göre ayırdığını görüyoruz.

1
2
3
eğer `CTF{ABC}` girersek
`password[0]` => `CTF{ABC}`
`password[1]` => `ABC`
1
if (!password || !x(password[1])) return document.body.className = 'denied';

yani şuanlık opensafe() fonksiyonunu bırakıp x() fonksiyonuna bakacağız

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function x(х) {
ord = Function.prototype.call.bind(''.charCodeAt);
chr = String.fromCharCode;
str = String;

function h(s) {
for (i = 0; i != s.length; i++) {
a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
}
return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)
}

function c(a, b, c) {
for (i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
return c
}
for (a = 0; a != 1000; a++) debugger;
x = h(str(x));
source = /Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;
source.toString = function () {
return c(source, x)
};
try {
console.log('debug', source);
with(source) return eval('eval(c(source,x))')
} catch (e) {}
}

kodu incelemeye başladık ve burada bir anti-debugger konulmuş

1
for (a = 0; a != 1000; a++) debugger;

a=1000 diyip yolumuza devam ediyoruz

1
x = h(str(x));

Diye hileli bir satır var burada. function x(х) diye belirtilmiş. h(str(x)) ve buradaki x değişken x değil, fonksiyon olan x yani x = h(str(x)); degeri sabit. x fonksiyonunu değiştirmeden çalıştırıp x değerini çekiyoruz.

Yani x fonksiyonuna paslanan değerin hiçbir önemi yok

x = h(str(x));‘den sonra x‘in son hali [130,30,10,154] (unicode olduğu için charcode halini yazdık) a = 2714 ve b = 33310 olduğunu gözlemledik.

c fonksiyonuna source değişkenini x‘in son halini yolladığımızda

1
2
3
c(source,x);

'х==c(\'¢×&Ê´cʯ¬$¶³´}ÍÈ´T—©Ð8ͳÍ|Ԝ÷aÈÐÝ&›¨þJ\',h(х))//᧢'

şifreli textimizi öğrendik

1
c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])))

şifreli texti decrypt etmek için key‘i bilmemiz gerekiyor. ([0-9a-zA-Z_@!?-]+) regexine uygun bir şekilde çıktı veren şekilde xor‘lamamız gerek.

bildiklerimiz

  • key uzunluğunun 4 hane olması
  • 0-255 arası range olduğu
  • her 4 cycleda keyin tekrar edeceği
  • çıktının ([0-9a-zA-Z_@!?-]+)‘e uygun olacağı

yapılacaklar

  • regex’e uygun char listesi oluşturmak
  • 4 defa dönecek bir for
  • 255 defa dönecek diğer bir for

eğer key 4 defada bir tekrar ediyorsa her defasında aynı keyi denemeye her 4. karakterde aynı keyi deniyerek false positive oranını düşürürüz

1
sifreliMetin = str(chr(162)  + chr(215)  + chr(38)  + chr(129)  + chr(202)  + chr(180)  + chr(99)  + chr(202)  + chr(175)  + chr(172)  + chr(36)  + chr(182)  + chr(179)  + chr(180)  + chr(125)  + chr(205)  + chr(200)  + chr(180)  + chr(84)  + chr(151)  + chr(169)  + chr(208)  + chr(56)  + chr(205)  + chr(179)  + chr(205)  + chr(124)  + chr(212)  + chr(156)  + chr(247)  + chr(97)  + chr(200)  + chr(208)  + chr(221)  + chr(38)  + chr(155)  + chr(168)  + chr(254)  + chr(74))
1
regexeUygun = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', '@', '!', '?', '-']
1
2
3
4
5
6
7
8
9
10
for i in range(4):
secenekler = set(range(256))

for sifreliChar in sifreliMetin[i::4]:
for secenek in list(secenekler):
metinHal = chr(secenek ^ ord(sifreliChar))
if metinHal not in regexeUygun:
secenekler.remove(secenek)

print("Olabilir key {}: {}".format(i+1, secenekler))

çıktı ise

1
2
3
4
Olabilir key 1: {253}
Olabilir key 2: {149, 153}
Olabilir key 3: {21}
Olabilir key 4: {249}

c fonksiyonun sifreli texti ve keyi yolladigimizda

1
2
3
> c(str(chr(162)  + chr(215)  + chr(38)  + chr(129)  + chr(202)  + chr(180)  + chr(99)  + chr(202)  + chr(175)  + chr(172)  + chr(36)  + chr(182)  + chr(179)  + chr(180)  + chr(125)  + chr(205)  + chr(200)  + chr(180)  + chr(84)  + chr(151)  + chr(169)  + chr(208)  + chr(56)  + chr(205)  + chr(179)  + chr(205)  + chr(124)  + chr(212)  + chr(156)  + chr(247)  + chr(97)  + chr(200)  + chr(208)  + chr(221)  + chr(38)  + chr(155)  + chr(168)  + chr(254)  + chr(74)),str(chr(253) + chr(153) + chr(21) + chr(249)))

'_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_'

Ve flag

CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}