DEFCON is the website controlling the security level of the room. The 5 different levels allows to warn the population of the risk of a potential nuclear war. Levels ranges from 5 (peacetime) to 1 (maximum alert). Try level 2.
DEFCON website is pretty graphic website with lots of data, bet relevant part is "DEFCON LEVEL CONSOLE", where clicking on each level shows a popup with title "UNAUTHORIZED ACTION", text "The server requires a password for this action." and a single input field (password).
Using Chrome Developer Tools, inspecting button,
following "Event Listeners" and searching defcon-2
in JavaScript files, shows how password is validated.
<div id="console">
<button id="defcon-1">1</button>
<button id="defcon-2">2</button>
<button id="defcon-3">3</button>
<button id="defcon-4">4</button>
<button id="defcon-5">5</button>
</div>
async function validate() {
if (modalLevel === "defcon-1") {
return check1($("#modal-password").val());
} else if (modalLevel === "defcon-2") {
return await check2($("#modal-password").val());
} else if (modalLevel === "defcon-3") {
return check3($("#modal-password").val());
} else if (modalLevel === "defcon-4") {
return check4($("#modal-password").val());
} else if (modalLevel === "defcon-5") {
return check5($("#modal-password").val());
} else {
return false;
}
}
Looking further at the level 2 validation in http://defcon.challs.malice.fr/static/js/scripts.js,
reveals that it is using Doppio JVM (A Java Virtual Machine written in JavaScript) to run App
class,
providing password as argument.
function check2(pwd) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout while executing JVM')), 200000);
new Doppio.VM.JVM({
doppioHomePath: '/sys',
classpath: ['/sys/classes'],
}, function (err, jvm) {
if (err) {
return reject(err);
}
const process = BrowserFS.BFSRequire('process');
process.initializeTTYs();
process.stdout.on('data', function (data) {
const messageStr = data.toString();
if (messageStr.startsWith('result=')) {
process.removeAllListeners();
process.stdout.removeAllListeners();
resolve(messageStr.slice('result='.length) === 'true');
clearTimeout(timeout);
}
});
jvm.runClass('App', [pwd], function (exitCode) {
console.log('JVM started successfully');
if (exitCode !== 0) {
reject(new Error('Script execution failed'));
}
});
});
});
}
To understand what App
is doing, it must be decompiled. Using Command line Java Decompiler that is easy enough.
$ wget http://defcon.challs.malice.fr/static/java/classes/App.class
$ wget https://github.com/kwart/jd-cmd/releases/download/jd-cmd-0.9.2.Final/jd-cli-0.9.2-dist.tar.gz
$ tar -xvzf jd-cli-0.9.2-dist.tar.gz jd-cli.jar
x jd-cli.jar
$ java -jar jd-cli.jar App.class -od .
22:58:50.579 INFO jd.cli.Main - Decompiling App.class
22:58:50.598 INFO jd.core.output.DirOutput - Directory output will be initialized for path .
22:58:51.555 INFO jd.core.output.DirOutput - Finished with 1 class file(s) and 0 resource file(s) written.
Looking at main
function of decompiled App.java, reveals how password is validated.
It is being run through 10 different functions and then compared to static base64-encoded string.
public static void main(String[] paramArrayOfString)
{
byte[] arrayOfByte = paramArrayOfString[0].getBytes();
arrayOfByte = base64_0(arrayOfByte);
arrayOfByte = password_enchiffragement_1(arrayOfByte);
arrayOfByte = base64_2(arrayOfByte);
arrayOfByte = password_enchiffragement_3(arrayOfByte);
arrayOfByte = base64_4(arrayOfByte);
arrayOfByte = password_enchiffragement_5(arrayOfByte);
arrayOfByte = base64_6(arrayOfByte);
arrayOfByte = password_enchiffragement_7(arrayOfByte);
arrayOfByte = password_enchiffragement_8(arrayOfByte);
arrayOfByte = password_enchiffragement_9(arrayOfByte);
boolean bool = Base64.getEncoder().encodeToString(arrayOfByte).equals("ZvhdxFZGZaN/M1IvAD8qaJI9UQqPluT45250BfOZPygRl68GGZqZ7QS+GTFUGcONT8r7cEkTh8gSgELMXdcURGPb25wfCIrVG6ptrLr6GJ9IpBeLP40Gu1VKwZJtdw75ud+LNgXop0KE4CGm8cCx6eqwnAFioKvBkJQEjQ==");
System.out.println("result=" + bool);
}
Looking at all of those functions, turns out that there are only 2 kind of functions, - password_enchiffragement_*
, which simply xor
's the argument
and base64_*
, which does similar things to base64 encoding, but with a twist.
public static byte[] password_enchiffragement_9(byte[] paramArrayOfByte)
{
(..)
byte[] arrayOfByte = new byte[124];
arrayOfByte[0] = ((byte)(paramArrayOfByte[5] ^ 0x8D));
arrayOfByte[1] = ((byte)(paramArrayOfByte[98] ^ 0x4B));
arrayOfByte[2] = ((byte)(paramArrayOfByte[123] ^ 0x36));
(..)
arrayOfByte[123] = ((byte)(paramArrayOfByte[120] ^ 0x0));
return arrayOfByte;
}
public static byte[] base64_6(byte[] paramArrayOfByte)
{
String str = "V-GCxZTakQFBs1oRUEtySWOiYJ0rwghfmpqub7394dejc58_AzIlDH6MNvLX2PnK";
byte[] arrayOfByte1 = new byte[paramArrayOfByte.length + 3 - paramArrayOfByte.length % 3];
for (int i = 0; i < paramArrayOfByte.length; i++) {
arrayOfByte1[i] = paramArrayOfByte[i];
}
byte[] arrayOfByte2 = new byte[arrayOfByte1.length * 4 / 3];
for (int j = 0; j < arrayOfByte1.length; j += 3)
{
arrayOfByte2[(j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[j] & 0xFF) >> 2));
arrayOfByte2[(1 + j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[j] & 0xFF & 0x3) << 4 | (arrayOfByte1[(j + 1)] & 0xFF) >> 4));
arrayOfByte2[(2 + j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[(j + 1)] & 0xFF & 0xF) << 2 | (arrayOfByte1[(j + 2)] & 0xFF) >> 6));
arrayOfByte2[(3 + j * 4 / 3)] = ((byte)str.charAt(arrayOfByte1[(j + 2)] & 0xFF & 0x3F));
}
return arrayOfByte2;
}
To reverse the the password_enchiffragement_*
functions, the same xor function is applied, as reverse of xor is xor.
To reverse base64_*
functions, a brute force was chosen. It has a static str
with uniquely ordered characters,
which are re-arranged based on input. Each 3 input characters, after mathematical calculations, select 4 characters from str
.
So, to reverse it, take each 4 characters and find such 3 character combination, which fullfill mathematical conditions.
To automate this, as there are 10 functions, a Python script was written, which uses App.java
to read conditions (xor bytes and magic str
).
#!/usr/bin/env python
import io, re, base64
def solve_xor(c, text, level):
text = base64.b64decode(text)
r = [0] * len(text)
start, srch = False, 'password_enchiffragement_{0}'.format(level)
for line in c.split('\n'):
if not start and srch in line:
start = True
if not start:
continue
if 'arrayOfByte' not in line:
continue
if 'return' in line:
break
mx = re.match('^.*?\[(\d+)\].*?\[(\d+)\].*?\s+\^\s+0x([0-9A-F]+).*$', line)
if not mx:
continue
o1, o2, x = mx.groups()
r[int(o2)] = chr(ord(text[int(o1)]) ^ int(x, 16))
r = base64.b64encode(''.join(r))
print(r)
return r
def solve_b64(c, text, level):
text, x = base64.b64decode(text), None
start, srch = False, 'base64_{0}'.format(level)
for line in c.split('\n'):
if not start and srch in line:
start = True
if not start:
continue
if 'return' in line:
break
if 'str = ' in line:
x = line.split('"')[1]
break
if x is None:
return None
r = ''
for j in range(0, len(text), 4):
p = text[j:j+4]
c1, c2, c3, c4 = list(p)
p1, p2, p3, p4 = x.index(c1), x.index(c2), x.index(c3), x.index(c4)
f = None
for t1 in range(256):
if f:
break
if (t1 & 0xff) >> 2 != p1:
continue
for t2 in range(256):
if f:
break
if (t1 & 0xff & 0x3) << 4 | (t2 & 0xff) >> 4 != p2:
continue
for t3 in range(256):
if (t2 & 0xff & 0xf) << 2 | (t3 & 0xff) >> 6 != p3:
continue
if (t3 & 0xff & 0x3f) != p4:
continue
f = chr(t1) + chr(t2) + chr(t3)
break
r += f
r = r.replace('\x00', '')
r = base64.b64encode(r)
print(r)
return r
with io.open('App.java') as fh:
c = fh.read()
text = 'ZvhdxFZGZaN/M1IvAD8qaJI9UQqPluT45250BfOZPygRl68GGZqZ7QS+GTFUGcONT8r7cEkTh8gSgELMXdcURGPb25wfCIrVG6ptrLr6GJ9IpBeLP40Gu1VKwZJtdw75ud+LNgXop0KE4CGm8cCx6eqwnAFioKvBkJQEjQ=='
text = solve_xor(c, text, 9)
text = solve_xor(c, text, 8)
text = solve_xor(c, text, 7)
text = solve_b64(c, text, 6)
text = solve_xor(c, text, 5)
text = solve_b64(c, text, 4)
text = solve_xor(c, text, 3)
text = solve_b64(c, text, 2)
text = solve_xor(c, text, 1)
text = solve_b64(c, text, 0)
print(base64.b64decode(text))
Running this script, returns the flag.
$ ./solve.py
e4G8ZAvrRVdOf1eeg0NWIImWl8rF2jxX9Vr0IWtR+2hOAwQQnWElHUH6a5gLAmiluiQzRIsOY/4ytOMEM0DS6/W2zWHm5a1NDs162qmZTB6+caI7bNKfSkt6NcvZmBHBruiz7FuANgM4IVWchWjze0SV1oTckjcBjSRLaw==
hoh+op9JI0w5jAQzXHTdvQ3ClkIYoZJ8oOg8urc7DVJSzW2h+5fUSJql2caQxVmXVKXgeqMhtygXXEYYM4wPi4Hi0bdRI2yckyCkIUnkqd/Sv+x3LPjbsY/xZYFeseD1zvre3KUIZFmCINC9y1UYSvXUDYm90+x+u8aGjg==
WUV4clJ5c0VNRmFCSzZ6UHRYUjhucnJwemZNZXFyaERrWG1BcWd1YWVleEhRbk01dE9HZlVoQVNLdVpqVlkyamc1amZkOEdMMHhwNzAtZUtvNlduM0YtWUNrNDBqNmVYaERGMG5rdngtdmhUTTJsSjZSZVQtS0lYVDROVg==
YREbPTMR3KHL/2x9S7Pu+bbhxf3qibe0I7gwidjHqqE1J+3tSWCfQewU/jFrAY8rdtrfpuC6aEhlaBq/O2V+mKBYDIoar2q7e0Ka+I5EB5eG38zZ2PqGB/y7Go4=
TmxUT25DLXhnZ2FRZFdQU2J5SnNTdXdtMVRQUkw1bTQ5a0VFVUkyX3IyanliZXBtU3Jfa0NpVHFVUURiTkt2NmdtTWNQbWhuQmJTbkFyWUsyVFdDWTRid3RPZEI=
IfEYsqvQNNH8Ulgc+SRxc3LmkEgd0om5/DnnjPhCuhzS+KwmcuCDqbEBj87+Ire1NmpTgmGsA+csMuZrhElqZ5+LiYU=
dnFwdmpXcm1hUkphSmU2Y1RFMkRhekItYXhVZ1VOcUpSeDNpTE52aDdLUGlQREhCQjBmVnprcjkwZ3lPbnB4c1FRUVE=
gPJgZ6yv9WF9FkUn1EL+9w0f9VNrNsPFWVu3Csgl/Tz3z+j00xohwtyDxrHKSJVa
YTNoV1Z1MkNXYWxiUHU0S0gxbGJQdTQ4NHFZS2FlNEtNc3hsLU5PaFAyaFotbUJC
UnVObjFuOUo0djQxTko0djQ1Q3IxUHQxNXRoM2ZVdFVSMw==
RuNn1n9J4v41NJ4v45Cr1Pt15th3fUtUR3
Flag is RuNn1n9J4v41NJ4v45Cr1Pt15th3fUtUR3
.