Client Hell

Cyber night 3 Client Hell challenge

Application Resources

Application source code

const express = require('express');
const cookieParser = require("cookie-parser");
const path = require('path')
const sessions = require('express-session');
const nunjucks = require('nunjucks');
const parser = require('url');
const { userTable, notesTable } = require('./database');
const { visit } = require('./bot');

const app = express();

app.use(express.urlencoded({ extended: true }));

const oneDay = 1000 * 60 * 60 * 24;
app.use(sessions({
    secret: process.env.SECRET,
    saveUninitialized: true,
    sameSite: 'none',
    cookie: { maxAge: oneDay },
    resave: false
}));

app.use(cookieParser());

nunjucks.configure('views', {
	autoescape: true,
	express: app
});

app.set('views', './views');
app.use('/static', express.static(path.resolve('static')));

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader(
      'Access-Control-Allow-Methods',
      'OPTIONS, GET, POST, PUT, PATCH, DELETE'
    );
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    next();
});

app.get('/', (req, res) => {
    if(req.session.loggedIn){
        const query = "SELECT notes from notes where username = ?";
        const param = [req.session.username];
        let result = [];
        notesTable.all(query, param, (err, rows) => {
            if(err) console.log(err);
            for(let i = 0; i < rows.length; i++){
                result.push(rows[i].notes);
            }
            return res.render('home.html', { username: req.session.username, notes: result });
        });
    }else{
        return res.redirect('/login')
    }
});

app.get('/login', (req, res) => {
    return res.render('login.html');
});

app.get('/register', (req, res) => {
    return res.render('register.html')
});

app.post('/register', (req, res) => {
    const { username } = req.body;
    const { password } = req.body;
    let msg;
    const query = "SELECT username from user where username = ?";
    const param = [username];

    userTable.all(query, param, (err, rows) => {
        if(err) console.log(err);
        if(rows.length != 0){
            msg = "username already exists";
            return res.render('register.html', { msg: msg });
        }else{
            msg = "User have been created";
            const query2 = "INSERT INTO user(username, password) VALUES (?,?)";
            const param = [username, password]
            userTable.run(query2, param);
            return res.render('register.html', { msg: msg });
        }
    });
});

app.post('/login', (req, res) => {
    const { username } = req.body;
    const { password } = req.body;
    let msg = ""
    const query = "SELECT username, password from user where username = ? and password = ?";
    const param = [username, password];
    if(username && password){
        userTable.all(query, param, (err, rows) => {
            if(err) return;
            if(rows.length != 0){
                req.session.loggedIn = true;
                req.session.username = username;
                return res.redirect('/');
            }else{
                msg = "username or password is incorrect";
                return res.render('login.html', { msg: msg });
            }
        })
    }
});

app.post("/note", (req, res) => {
    if(req.session.loggedIn){
        const { note } = req.body;
        const query = "INSERT INTO notes(username, notes) VALUES (?,?)";
        const param = [req.session.username, note];
        notesTable.run(query, param, () => {
            return res.redirect('/');
        });
    }else{
        return res.redirect('/login');
    }
})

app.get('/report', (req, res) => {
    if(req.session.loggedIn){
        return res.render('report.html');
    }else{
        if(req.ip.includes('127.0.0.1')){
            return res.render('report.html');
        }else{
            return res.redirect('/login');
        }
    }
});

app.post('/admin/review', async (req, res) => {
    const { url } = req.body;
    regex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/
    if(decodeURIComponent(url).match(regex)){
        const parse_url = parser.parse(url);
        if(parse_url.host.split(':')[0] == "127.0.0.1"){
            await visit(url).then(res => {
                console.log(url);
            }).catch(e => {
                console.log(e);
            });
            return res.json({msg: "We sent your report to the admin"});
        }else{
            return res.json({msg: "Invalid url"});
        }
    }else{
        res.json({msg: "please submit a url"});
    }
});

app.get('/admin/note', (req, res) => {
    if(!req.ip.includes('127.0.0.1')) return res.redirect('/');
    return res.json({flag: process.env.FLAG});
});

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect('/');
});

app.listen(1337, () => {
    console.log("Listening on port 1337")
});

report.html content

Exploitation

Quick analysis

  • Route /admin/review requires a url in the POST request body. Line 2

  • Regex could be bypassed by encoding the special characters with URLEncode. Line 3 to Line 4

  • The provided URL host must be equal to 127.0.0.1 Line 6

  • The bot (chromium) accesses the URL. Line 7

  • The flag can be obtained via /admin/note route only if the client ipAddress is 127.0.0.1. Line 22 to 23

the developer used a vulnerable Adobe Dynamic Tag Management that is included in report.html (Client Side Prototype Pollution). [1]

Exploit

I developed a custom payload for the client-side prototype pollution that bypasses most regex special characters without encoding.

Note you must change VPS.IPAddress to your VPS IPAddress.

Payload

POST Request Payload

  • encodes []only.

file.js Content

server.py Content

Result

References

Last updated

Was this helpful?