Creating a Request Bucket with Express and Socket.io
What is a request bucket/bin?
A request bucket/bin is just a tool for catching and displaying incoming http requests.
Why is this useful?
A lot of CTF-style challenges have you using XSS or SSRF attacks to exploit vulnerable web apps. These exploits often involve using http requests to exfil data. However, the problem is that most people dont have an exposed http service on their machines that they can use to catch these requests. A request bucket provides that public endpoint for you to catch and view these requests. Apart from that, the request bucket is useful for sanity checking any service that makes outbound http get/post requests.
Implementation
The implementation starts with a basic but already functioning express server. If needed, express offers some really good guides/examples for initial setup on their site here.
The final overall file structure (only relevant files).
/routes
|-- bucket.js
|-- tools.js
/views
|-- tools.ejs
/tools
|-- requestbucket.ejs
activebucketstore.js
index.js
socket.js
.ejs files are EJS templates. Basically just pages that are rendered to html before being served to the user. The rest is pure nodejs.
Started by adding a route to serve the page when the user requests tools/requestbucket.
//tools.js
const express = require('express');
const router = express.Router();
// /tools root
router.get('/', (req, res) =>{
res.render('tools')
})
router.get('/requestbucket', (req, res) =>{
res.render('tools/requestbucket')
})
module.exports = router;
Next, installed Socket.io by running npm install socket.io
in the project directory.
Socket.io is a library that allows for real-time, bidirectional communication between client and server. I will be using this to send the received requests in real time to the user and also to inform the user of their current bucket url.
The Socket.io instance attaches to the existing express http server like this.
//index.js
function startSocket(server) {
//attach socketio and pass io to handler
var io = require('socket.io')(server);
require('./socket.js')(io);
}
var http_server = http.createServer(app);
//start server
http_server.listen(4000, () => {
console.log('App listening on port 4000')
});
startSocket(http_server);
The socket.js file exports a function to initialize the socket.io listener and lets us keep index.js relatively clean. At this stage, it looks like this.
//socket.js
module.exports = function(io) {
io.on('connection', (socket) => {
console.log('a user connected');
socket.on('disconnect', () => {
console.log('user disconnected');
});
});
};
Next, we need to set up the client side socket.io. Conveniently, The server side socket.io exposes an endpoint where we can easily fetch the client side js. All it takes to set up the initial connection is:
<!---requestbucket.ejs--->
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
Now, when navigating to the /tools/requestbucket endpoint we can see a successful socket connection. Closing the page also shows that disconnects are working.
The next step is to assign a bucket to each new session. This bucket is ultimately just an endpoint but it needs to be unique for each user.
//activebucketstore.js
const bucketStore = []
module.exports = function(){
return bucketStore;
}
First, I needed a way to store active buckets in a place that I could access them from other modules. I'm not currently using a database in my application and it seemed a bit overkill to spin one up for a simple list so I found this little trick. Exports in nodejs work by value not by reference so I can't export the bucket store directly BUT if I export a function that returns the store I can get the reference instead. This gives me an easily accessible in-memory store that I can access from any module that needs it. This also lets me avoid circular dependency nightmare.
Next, implement an assignClientBucket function that generates a unique bucket for each new connection. I decided on using a 6 character base58 string to identify each room. After adding, socket.js looks like this:
//socket.js
const getBucketStore = require('./activebucketstore.js')
function generateBucket(length) {
var result = '';
var characters = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
var charactersLength = 58;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function assignClientBucket(socket){
//generate 6 character room ID (base58)
bucketID = generateBucket(6);
//check if bucket already exists
while(getBucketStore().some(e => e.bucketID === bucketID)){
bucketID = generateBucket(6);
}
getBucketStore().push({socketID: socket.id, bucketID: bucketID})
return bucketID;
}
module.exports = function(io) {
io.on('connection', (socket) => {
console.log('a user connected');
io.to(socket.id).emit("send bucket", assignClientBucket(socket));
console.log(getBucketStore())
socket.on('disconnect', () => {
console.log('a user disconnected');
//clear bucket from bucket store
console.log('Removed ' + getBucketStore().splice(getBucketStore().findIndex(e => e.socketID === socket.id), 1)[0].bucketID);
});
});
};
The generateBucket function isn't true random but it shouldn't matter for this purpose. I am also storing the client socket ID in an object with each bucket ID so that I have an address to send incoming requests to later. On disconnect I also make sure to free the bucket ID so it can be reused later.
Finally, added a function to render the passed bucket id client side.
<!---requestbucket.ejs--->
<div class="content">
request bucket
<p id="bucketID"></p>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
socket.on("send bucket", function(bucketID) {
var urlField = document.getElementById("bucketID");
urlField.innerHTML = "https://undercover.sh/bucket/" + bucketID;
console.log(bucketID);
});
</script>
Notice how simple socketio makes the communication. On server side just emit an event with a plaintext identifier and then on client side on receiving an event with the same identifier do something. Socketio handles all the nitty gritty of managing protocols and connections.
Now, back in index.js update the startSocket function with app.set('io', io)
. This lets as access the socket.io instance from other modules such as our bucket router.
//index.js
function startSocket(server) {
//attach socketio and pass io to handler
var io = require('socket.io')(server);
require('./socket.js')(io);
app.set('io', io)
}
Also, include the bucket route.
//index.js
const bucketRouter = require('./routes/bucket');
app.use('/bucket', bucketRouter);
Then, set up route to recieve on bucket/[bucketID] endpoint.
//bucket.js
const express = require('express');
const router = express.Router();
const getBucketStore = require('../activebucketstore.js')
// /bucket root
router.get('/:id', (req, res) =>{
res.send('success')
})
module.exports = router;
Quick sanity check
Now set up the logic to check for active bucket and emit the request data to the matching client socket.
const express = require('express');
const router = express.Router();
const getBucketStore = require('../activebucketstore.js')
// /bucket root
router.get('/:id', (req, res) =>{
//check if given id is an active bucket
bucket = getBucketStore().find(e => e.bucketID === req.params.id)
//if active bucket then send request details to client
if(bucket){
req.app.get('io').to(bucket.socketID).emit("send request",
{url: req.originalUrl, headers: req.headers, ip: req.ip});
}
res.send('success')
})
module.exports = router;
Add handling on client side. Just populates a paragraph field with the received data.
<!---requestbucket.ejs--->
<p id="requests"></p>
<script>
var socket = io();
socket.on("send bucket", function(bucketID) {
var urlField = document.getElementById("bucketID");
urlField.innerHTML = "https://undercover.sh/bucket/" + bucketID;
console.log(bucketID);
});
socket.on("send request", function(request) {
var urlField = document.getElementById("requests");
urlField.innerHTML = JSON.stringify(request);
console.log(request);
});
</script>
What it looks like so far. Looking good.
At this point I noticed I wasn't handling post requests and thought that would be useful to add. This wasn't as easy to add as the get request handler. This is because Express doesn't natively parse request bodies. Also, the normal use case for body parsers involves only adding the particular body parsers your application needs. In this use case, I want to convert every body type to plain text so it can be shown in the browser. After doing a bit of research a found a neat solution.
The express app.use function which you use to add middleware, such as routes, accepts series of middleware. Using this I can include this particular body parser only on the bucket route. I can also pass the type: '/'
option to the text body parser so that it parses any body type into text. Without this it would only parse text/plain
and other types such as application/x-www-form-urlencoded
would be ignored.
//Index.js
//use text body parser for bucket post requests
app.use('/bucket', bodyParser.text({type: '*/*'}), bucketRouter)
Updated bucket route file to also handle post requests. Also added more data to pass to client.
//bucket.js
const express = require('express');
const router = express.Router();
const getBucketStore = require('../activebucketstore.js')
// /bucket root
router.get('/:id', (req, res) =>{
sendRequest(req);
res.send('success')
})
router.post('/:id', (req, res) =>{
sendRequest(req);
res.send('success')
})
function sendRequest(req){
//check if given id is an active bucket
bucket = getBucketStore().find(e => e.bucketID === req.params.id)
//if active bucket then send request details to client
if(bucket){
req.app.get('io').to(bucket.socketID).emit("send request",
{method: req.method, url: req.originalUrl, http: req.httpVersion, headers: req.headers, body: req.body, ip: req.ip});
}
}
module.exports = router;
Now post requests work and body is displayed.
With that done, the underlying functionality is all complete. The last step is to make it pretty (or at least try to...)
Final result doesn't look toooooo bad but definitely works well. Check out the live version if you have time. Would greatly appreciate it.
Final Code
I may just make my site repo public at some point but heres the final code for reference.
/routes
|-- bucket.js
|-- tools.js
/views
|-- tools.ejs
/tools
|-- requestbucket.ejs
activebucketstore.js
index.js
socket.js
//index.js
const express = require('express');
const blogRouter = require('./routes/blog');
const linksRouter = require('./routes/links');
const toolsRouter = require('./routes/tools');
const aboutRouter = require('./routes/about');
const bucketRouter = require('./routes/bucket');
const bodyParser = require('body-parser');
var http = require('http');
var https = require('https');
//use routers
app.use('/links', linksRouter);
app.use('/tools', toolsRouter);
app.use('/blog', blogRouter);
app.use('/about', aboutRouter);
//use text body parser for bucket post requests
app.use('/bucket', bodyParser.text({type: '*/*', limit: '20kb'}), bucketRouter)
var http_server = http.createServer(app);
//start server
http_server.listen(4000, () => {
console.log('App listening on port 4000')
});
startSocket(http_server);
function startSocket(server) {
//attach socketio and pass io to handler
var io = require('socket.io')(server);
require('./socket.js')(io);
app.set('io', io)
}
//socket.js
const getBucketStore = require('./activebucketstore.js')
function generateBucket(length) {
var result = '';
var characters = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
var charactersLength = 58;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function assignClientBucket(socket){
//generate 6 character room ID (base58)
bucketID = generateBucket(6);
//check if bucket already exists
while(getBucketStore().some(e => e.bucketID === bucketID)){
bucketID = generateBucket(6);
}
getBucketStore().push({socketID: socket.id, bucketID: bucketID})
return bucketID;
}
module.exports = function(io) {
io.on('connection', (socket) => {
io.to(socket.id).emit("send bucket", assignClientBucket(socket));
socket.on('disconnect', () => {
//clear bucket from bucket store
getBucketStore().splice(getBucketStore().findIndex(e => e.socketID === socket.id), 1);
});
});
};
//activebucketstore.js
const bucketStore = []
module.exports = function(){
return bucketStore;
}
//requestbucket.ejs (only body)
<body>
<div class="header">
<b class="back"><a href="/">/home</a></b><b class="back"><a href="/tools">/tools</a></b><b class="back">/~</b>
<p class="home"><a href="/">undercover.sh</a></p>
</div>
<div class="content">
<div class="intro-container">
<h1>request bucket</h1>
<h2 class="description">This is a tool for catching http get/post requests. Useful for receiving XSS or SSRF payloads. Requests will be shown in real time. If you want a new bucket URL simply refresh the page.</h2>
<hr style="border-color: rgb(109, 109, 109);">
</div>
<div class="url-container">
<p>Your bucket --> </p>
<p id="bucketID"></p>
<div class="tooltip">
<button onclick="copyURL()" onmouseout="outFunc()" class="copyButton"><span class="tooltiptext" id="copyTooltip">Copy to clipboard</span>
<svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 24 24" width="16"><path d="M0 0h24v24H0z" fill="none"/><path fill="#0f0" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>
</div>
<hr style="border-color: rgb(109, 109, 109);">
<div id="requests"></div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
var requests = document.getElementById("requests");
socket.on("send bucket", function(bucketID) {
var urlField = document.getElementById("bucketID");
urlField.innerHTML = "https://undercover.sh/bucket/" + bucketID;
console.log(bucketID);
});
socket.on("send request", function(request) {
var requestElement = document.createElement("div")
var requestDiv = document.createElement("div")
requestDiv.className = "request-div";
var requestField = document.createElement("p");
requestField.textContent = request.method + " " + request.url + " HTTP/" + request.http;
requestField.className = "request-field";
requestDiv.appendChild(requestField);
for (const key of Object.keys(request.headers)) {
var header = document.createElement('li');
header.textContent = key + ": " + request.headers[key];
header.className = "request-header";
requestDiv.appendChild(header);
}
if(request.body && Object.keys(request.body).length !== 0){
var body = document.createElement('p');
body.textContent = request.body;
body.className = "request-body";
requestDiv.appendChild(body);
}
requestElement.appendChild(requestDiv);
var ip = document.createElement('p');
ip.textContent = "Request from " + request.ip;
ip.className = "request-ip";
requestElement.appendChild(ip);
requestElement.className = "request-container";
requests.prepend(requestElement);
console.log(request);
});
function copyURL() {
var aux = document.createElement("input");
aux.setAttribute("value", document.getElementById("bucketID").innerHTML);
document.body.appendChild(aux);
aux.select();
document.execCommand("copy");
document.body.removeChild(aux);
var tooltip = document.getElementById("copyTooltip");
tooltip.innerHTML = "Copied!";
}
function outFunc() {
var tooltip = document.getElementById("copyTooltip");
tooltip.innerHTML = "Copy to clipboard";
}
</script>
</body>
//bucket.js
const express = require('express');
const router = express.Router();
const getBucketStore = require('../activebucketstore.js')
// /bucket root
router.get('/:id', (req, res) =>{
sendRequest(req);
res.send('success')
})
router.post('/:id', (req, res) =>{
sendRequest(req);
res.send('success')
})
function sendRequest(req){
//check if given id is an active bucket
bucket = getBucketStore().find(e => e.bucketID === req.params.id)
//if active bucket then send request details to client
if(bucket){
req.app.get('io').to(bucket.socketID).emit("send request",
{method: req.method, url: req.originalUrl, http: req.httpVersion, headers: req.headers, body: req.body, ip: req.ip});
}
}
module.exports = router;
//tools.js
const express = require('express');
const router = express.Router();
// /tools root
router.get('/', (req, res) =>{
res.render('tools')
})
router.get('/requestbucket', (req, res) =>{
res.render('tools/requestbucket')
})
module.exports = router;