Dutch
English
Vraag adviesgesprek aan
android
api-security
idor
rabbitmq
responsible-disclosure

Roulette Royale Onderzoek, deel 2: de RabbitMQ-broker en de account-IDORs

Joel Aviad Ossi
20 September, 2026

Roulette Royale gekraakt, deel 2: de RabbitMQ-broker en de account-IDORs

Inleiding

In deel 1 haalden we de economie aan de clientkant onderuit. We patchten de getters in de app om onszelf gratis VIP en premium unlocks te geven, lieten zien dat de server de betaalmuur meestuurt die hij daarna door de client laat handhaven, en pikten onderweg twee bouwstenen op: de offline MWHDR-ondertekenaar (com.mw.secure.S) en de gedeelde srscore-sleutel. In dit deel houden we die bouwstenen en veranderen we één veld, het account-ID in het verzoek. De server controleert nooit of dat account van ons is. Die ontbrekende controle, samen met een realtime broker die iedereen als dezelfde identiteit inlogt, vormt het volledige aanvalsoppervlak aan de serverkant.

Roulette Royale heeft meer dan 10 miljoen downloads in Google Play. Er wordt geen echt geld uitgekeerd, de chips en gems zijn virtueel en er is geen uitbetaling, dus dit is een spel en geen echt casino. De schaal doet er hier alsnog toe, want de persoonsgegevens die hieronder naar buiten komen zijn van echte spelers.

Alles hieronder loopt tegen de productie-endpoints die de normale app gebruikt, zonder aangepaste client. We hebben dit op 28 augustus 2026 bij de leverancier gemeld en twee keer herinnerd zonder antwoord, dus dit wordt ongepatcht gepubliceerd; de tijdlijn en de onderbouwing staan onderaan. Het gedeelde wachtwoord, de srscore-sleutel, echte account-identificatienummers en de proof of concept-scripts blijven achter. De live lobby toont andere spelers, dus op lobby-schermafdrukken zijn echte profielfoto's zwart gemaakt; op de opname hieronder staan toevallig alleen standaardsilhouetten.

Het realtime- en accountaanvalsoppervlak

De realtime laag, en waarom Burp er niets van ziet

Chat, aanwezigheid, inzetten en spins gaan niet over HTTP. De app opent een WebSocket naar wss://roulmulti.mywavia.com:443/roul-ws (Sec-WebSocket-Protocol: v12.stomp, v11.stomp, v10.stomp, User-Agent: okhttp/4.9.1) en spreekt STOMP, het tekstprotocol dat RabbitMQ aanbiedt. Burp ziet de upgrade en daarna alleen ondoorzichtige frames, dus er valt niets te replayen. We hebben een WebSocket en een STOMP-client nagebouwd met alleen de Python-standaardbibliotheek en zijn direct met de broker gaan praten.

Bevinding 1: de broker logt elke speler in als dezelfde gedeelde identiteit

De STOMP-CONNECT gebruikt één login en wachtwoord, hardcoded in classes2.dex (in deel 1 teruggehaald) en in elke installatie identiek:

CONNECT
accept-version:1.1,1.2
heart-beat:5000,5000
login:roulette_stomp_mywavia
passcode:kiG9w0............OC6s          (weggelaten, hardcoded, gelijk voor alle clients)

^@

Op de realtime laag zit geen authenticatie per gebruiker. Zodra de verbinding staat kan de broker de ene speler niet van de andere onderscheiden.

Impact. Wie het wachtwoord één keer uit de APK haalt (het is een statische string) heeft een permanente, niet aan een persoon toe te schrijven verbinding met de realtime laag van alle gebruikers. Dit is de oorzaak onder bevinding 2 en 3.

Bevinding 2: één wildcard-abonnement streamt de hele broker

RabbitMQ topic exchanges accepteren # als wildcard. De client hoort zich op zijn eigen kamer te abonneren. Wij abonneerden ons op alles:

SUBSCRIBE
id:sub-1
destination:/exchange/upstream/#         (client->server verkeer van elke kamer)
ack:auto

^@
SUBSCRIBE
id:sub-2
destination:/exchange/downstream/#       (server->client verkeer van elke kamer)
ack:auto

^@

Daarna levert de broker de frames van elke kamer af. Aanwezigheid en chat dragen het account-ID en het profiel gewoon mee:

{"TYPE":"ADD","RID":"sankara_1787900000000","URID":"L10V5QE",
 "p":{"rdsid":"mw_<speler>","n":"<weergavenaam>","c":987340000,"lvl":42}}
{"TYPE":"CHAT","RID":"sankara_...","rdsid":"mw_<speler>","m":"hi all"}
{"TYPE":"BETS","rdsid":"mw_<speler>","p":{"c":..., "a":[...]}}

Uit die stroom bouwen we een live spelerslijst op rdsid, met weergavenaam en chipsaldo van iedereen die online is, in elke kamer.

Een live tafel. De uitnodigingscode van de kamer en de chat lopen allebei over deze broker. De avatars hier zijn standaardsilhouetten

De lobby loopt over dezelfde broker: wereldtafel, tafels van vrienden en de uitnodigingscodes per kamer (URID)

Impact. Eén verbinding leest in realtime de chat, inzetten, spins en het in- en uitstappen van elke tafel op de dienst mee, en harvest rdsid, weergavenaam en saldo van elke online speler. Diezelfde stroom levert ook de ID's die bevinding 4, 5 en 6 nodig hebben om een specifiek account te raken.

Bevinding 3: de server vertrouwt de afzender-ID in een chatframe

Een chatbericht wordt op de upstream exchange van de kamer gepubliceerd, en de afzender is een veld in de JSON-body. De server neemt dat voor waar aan en controleert nooit of de publicerende verbinding die rdsid bezit:

SEND
destination:/exchange/upstream/sankara.logger
content-type:application/json
content-length:74

{"TYPE":"CHAT","RID":"sankara_1787900000000","rdsid":"mw_<slachtoffer>","m":"hi"}^@

Chat-impersonatie: de afzender-ID is gewoon een veld in het frame

Tijdens het testen publiceerde de tool twee regels als mw_guest_129, een ID van een andere speler, en accepteerde de broker beide terwijl de live tafel gewoon doorliep.

Impact. Elk bericht kan onder elke speler in elke kamer worden geplaatst. Samen met de live spelerslijst uit bevinding 2 kan een aanvaller een specifiek slachtoffer aan een specifieke tafel nadoen: social engineering, valse "de dealer zegt stort hier"-berichten, of een echte gebruiker laten bannen voor woorden die hij nooit getypt heeft.

Bevinding 4: get_user_details en sync_account lezen elk account op ID

Aan de HTTP-kant neemt /gameserver_roul?action=get_user_details&rdsids=[...] een lijst ID's en geeft volledige profielen terug. De MWHDR-header uit deel 1 zorgt dat het verzoek geaccepteerd wordt, en de server doet geen eigendomscontrole op de ID's, dus je geeft gewoon een willekeurig ID uit de spelerslijst mee:

GET /gameserver_roul?action=get_user_details&rdsids=["mw_<slachtoffer>"]
--> {"ActResp":[{
      "rds_id":"mw_<slachtoffer>", "id":"mw_<shortid>", "udid":"<toestel-ID>",
      "fbid":"<facebook-ID>", "displayName":"<naam>", "emailid":"<e-mail>",
      "imgurl":"https://classspace.in/files/<hash>", "user_room_id":"<privécode>",
      "score":{"myscore":{"CurrentWorth":41000,"gems":0,"diamonds":0}} }]}

action=sync_account&udid=<udid slachtoffer>&rdsid=<rdsid slachtoffer> geeft hetzelfde terug voor één account. De udid is het derde segment van achteren in de rdsid, dus dat is geen apart geheim.

Impact. Volledige blootlegging van persoonsgegevens van elk account waarvan je het ID kunt zien: e-mailadres, een vaste toestel-identificatie (udid), het gekoppelde Facebook-ID, de privécode van de tafel en het volledige saldo. Bevinding 2 levert een ID voor elke speler die online is, dus het bereik is de actieve spelerspopulatie en niet één account.

Bevinding 5: update_un hernoemt elk account

Dezelfde ontbrekende controle geldt voor schrijfacties. update_un zet de weergavenaam van het ID dat je meegeeft:

GET /gameserver_roul?action=update_un&rdsid=mw_<slachtoffer>&displayname=owned_by_websec

Impact. De weergavenaam van elk account kan door een ongeauthenticeerde aanvaller worden gewijzigd. Op zichzelf beperkt, maar het bewijst dat ook het schrijfpad geen eigendomscontrole heeft, en dat is van belang voor bevinding 6.

Bevinding 6: de economie-schrijfactie uit deel 1, nu tegen elk account

In deel 1 zetten we ons eigen vermogen door een srscore-inzending te ondertekenen. De server koppelt de schrijfactie aan de uuid in de querystring en controleert geen eigendom, dus hetzelfde verzoek werkt met de rdsid van een slachtoffer:

GET /srscore?type=put&un=<willekeurig>&all=Y&appid=MW_RR_ANDROID&cv=17.2
  &uuid=mw_<slachtoffer>&o_uuid=<udid slachtoffer>&isocc=
  &score=...***CurrentWorth<<>>0***...&hash=<md5(payload + gedeelde sleutel)>
  &gems=0&diamonds=0&unlock=0                         (staart nog altijd niet ondertekend)

Impact. Een aanvaller kan het vermogen, de ranglijststatistieken, gems, diamonds en unlocks van elk account zetten: het vermogen van een topspeler op nul zetten, een onmogelijke score planten om de ranglijst te domineren, of een account opblazen voordat het verkocht wordt. De clientmod uit deel 1 kwam bij één account. Dit komt bij elk account, en de server behandelt de uitkomst als de waarheid.

Bevinding 7: elke afbeelding op elk profiel

Avatars ketenen een ongeauthenticeerde upload aan een niet-ondertekende koppeling. De afbeelding gaat naar een tusd-endpoint voor hervatbare uploads dat zonder enige authenticatie uploads aanneemt:

POST https://classspace.in/files/
Tus-Resumable: 1.0.0
Upload-Length: <bytes>
Upload-Metadata: filename <base64>
Content-Type: application/offset+octet-stream
--> 201 Created,  Location: https://classspace.in/files/<hash>

Daarna wijst een gameserver_roul-actie de avatar van een slachtoffer naar die URL, en op deze actie wordt geen handtekening afgedwongen:

GET /gameserver_roul?action=update_im_url&rdsid=mw_<slachtoffer>&...=https://classspace.in/files/<hash>

Impact. Elke afbeelding kan zonder authenticatie op het profiel van elk account worden gezet. Tijdens het testen zat aan een live tafel een speler met expliciet pornografisch materiaal als avatar, en dat is precies wat een open "elke afbeelding op elk profiel"-primitief oplevert in een spel met een publieke lobby waar zowel minderjarigen als volwassenen komen. Daarom zijn op elke live lobby-schermafdruk in deze serie de echte avatars zwart gemaakt.

Eén tool die alles aan elkaar knoopt

RouletteRoyale_Chat.py (alleen standaardbibliotheek, gedraaid vanuit PyCharm) rijgt de keten aan elkaar: abonneren op de broker, spelerslijst opbouwen, een naam eruit kiezen, die via de IDOR naar een volledig profiel oplossen, en dan hernoemen, het vermogen zetten, de avatar vervangen of praten als die speler. Economie-schrijfacties zitten achter een --arm-vlag en doen standaard een dry run die het exacte verzoek print zonder te verzenden. Elk frame in en uit wordt als JSONL en tekst gelogd voor de bewijslast.

MAIN MENU
  1) Lobbies        browse rooms + invite codes (URID)
  2) Players        browse / search every user (from the live roster)
  3) Watch chat     stream chat messages
  4) Resolve names  get_user_details IDOR  -> email / udid / fbid / worth
  5) Settings       arm writes, raw bets, focus a lobby
  6) Lobby by code  resolve a lobby by invite code
  7) Live monitor   auto-update as players join / leave

Het ontwerp volgt de bevindingen: een StompWS voor bevinding 1 tot 3, een MywaviaClient die de MWHDR- en srscore-ondertekening doet voor bevinding 4 tot 7, en een ImpersonationPoC die een regel uit de spelerslijst doortrekt naar een volledige accountovername.

Waarom de proof of concept niet wordt vrijgegeven

De tool is samen met het oorspronkelijke rapport naar de leverancier gegaan, plus een tweede script dat upstream- en downstreamverkeer van de broker leest, schrijft en onderschept. Geen van beide wordt hier gepubliceerd, en het brokerwachtwoord, de srscore-sleutel en echte account-identificatienummers ook niet.

Niets uit deze serie is opgelost. De broker accepteert nog steeds het gedeelde wachtwoord, de account-endpoints slaan de eigendomscontrole nog steeds over, en de avatarkoppeling neemt nog steeds elke URL voor elk account aan. Met een werkende kopie van een van beide scripts kan iedereen het e-mailadres en het toestel-ID van elke speler die op dat moment online is ophalen, hun saldo's herschrijven en berichten onder hun naam plaatsen, tegen een live dienst met echte gebruikers erop. Deze publicatie geeft de leverancier wat nodig is om elke bevinding te reproduceren en te repareren, en dat is de reden om überhaupt te publiceren, maar stopt op het punt waar het ook iedereen anders een werkende tool in handen zou geven.

De leverancier kan het volledige pakket krijgen, beide scripts, de geheimen en de bewijslogs, op elk moment op verzoek.

Impactoverzicht

# Bevinding Oorzaak Wat de aanvaller wint
1 Gedeelde STOMP-credentials Statisch wachtwoord in de APK, geen auth per gebruiker Niet toe te schrijven realtime verbinding voor alle gebruikers
2 Wildcard-abonnement Topic # toegestaan voor elke client Chat/inzetten/spins van elke tafel meelezen, elke online rdsid + naam + saldo harvesten
3 Chat-impersonatie Server vertrouwt rdsid in de framebody Plaatsen als elke speler in elke kamer
4 Account-IDOR lezen Geen eigendomscontrole op rdsids E-mail, udid, fbid, vermogen en privécode van elk account
5 Account-IDOR schrijven Geen eigendomscontrole op update_un Elk account hernoemen
6 Economie over accounts heen Geen eigendomscontrole op srscore-uuid plus gedeelde sleutel Vermogen / statistieken / gems / diamonds op elk account zetten
7 Avatarovername Ongeauthenticeerde tusd-upload plus niet-ondertekende update_im_url Elke afbeelding op elk profiel

Onder alle zeven ligt één oorzaak: identiteit wordt door de client beweerd en door de server nooit gecontroleerd, op de broker en op de account-API. Één bevinding repareren helpt niet. De server moet vaststellen wie de aanroeper is en nagaan wat die mag aanraken.

Aanbevelingen

  • Koppel elke STOMP-verbinding aan een geauthenticeerde sessie per gebruiker en weiger elk gepubliceerd frame waarvan de rdsid niet bij die sessie hoort. Schaf het enkele gedeelde wachtwoord af. Weiger #-abonnementen.
  • Dwing eigendom af op /gameserver_roul voor elke lees- en schrijfactie: de geauthenticeerde aanroeper moet de rdsid of uuid in het verzoek bezitten. Dat sluit bevinding 4, 5 en 6 in één keer.
  • Onderteken scores met een sleutel die op de server blijft en neem elk economieveld (gems, diamonds, unlock, coupons, defer) op in de ondertekende data, zodat er geen niet-ondertekende staart is.
  • Authenticeer de avataropslag en valideer de geüploade inhoud; eis een ondertekende update_im_url waarop eigendom wordt gecontroleerd.
  • Behandel MWHDR als niet-beveiligend en vertrouw er nergens op.

Disclosure-tijdlijn

  • 28 augustus 2026. Gemeld bij de leverancier. We gebruikten hun eigen meldpagina voor kwetsbaarheden op http://profile.mywavia.com/im/report_vulnerability.html en volgden de instructies die daar staan, en stuurden hetzelfde rapport per e-mail. Op die pagina staat dat hun personeel contact zal opnemen. Dat is niet gebeurd. Het rapport noemde de blootstelling van de RabbitMQ-credentials via de WebSocket en de 129 tekens lange sleutel die in de APK hardcoded staat, beschreef het misbruik van avatars, valuta, gebruikersnamen en chat-impersonatie dat daaruit volgt, en had twee proof of concept-scripts als bijlage: één die administratieve controle over elke lobby en elke gebruiker laat zien, en één die lees-, schrijf- en onderscheptoegang tot het upstream- en downstreamverkeer van de broker laat zien.
  • 6 september 2026. Eerste herinnering met de vraag of het rapport gelezen was. Geen antwoord.
  • 15 september 2026. Tweede herinnering. Geen antwoord.
  • 20 september 2026. Gepubliceerd. Drieëntwintig dagen na het eerste contact, na een melding via het eigen formulier van de leverancier en drie e-mails, is er geen enkele vorm van bevestiging gekomen. De bevindingen worden volledig gepubliceerd. De proof of concept-scripts, de werkende credentials en de ondertekeningssleutel niet.

Tot slot

Deel 1 liet zien dat de client zijn eigen verzoeken mag ondertekenen, waardoor een speler zijn eigen economie kan verzinnen. Deel 2 liet zien dat de server dat vertrouwen naar iedereen uitbreidt: één statisch wachtwoord opent de hele realtime laag, en één ontbrekende eigendomscontrole legt de gegevens, de naam, de economie en de avatar van elk account bloot. Beide helften komen uit dezelfde beslissing om de client te laten bepalen wie hij is, dus de oplossing is autorisatie aan de serverkant op elk verzoek, op de broker en op de account-API.

Authored By
Joel Aviad Ossi

Managing Director

Deel met de wereld!

Beveiligingsbehoeften?

Bent u er echt zeker van dat uw organisatie veilig is?

Bij WebSec helpen we u deze vraag te beantwoorden door geavanceerde beveiligingsbeoordelingen uit te voeren.

Wil je meer weten? Plan een gesprek in met een van onze experts.

Afspraak Inplannen
Authored By
Joel Aviad Ossi

Managing Director