
Part 1 broke the economy on the client. It patched the in app getters to hand itself free VIP and premium unlocks, showed the server ships the paywall it then trusts the client to enforce, and recovered two request primitives along the way: the offline MWHDR signer (com.mw.secure.S) and the shared srscore key. This part keeps those primitives and changes one field, the account id in the request, and finds the server never checks we own it. That missing check, plus a realtime broker that authenticates everyone as the same identity, is the whole server side attack surface.
Roulette Royale has over 10 million downloads on Google Play. No real money is paid out, the chips and gems are virtual and there is no cash out, so this is a game rather than a real casino. The scale still matters for what follows, because the personal data exposed below belongs to real players.
Everything here runs against the production endpoints the normal app uses, with no modified client. We reported this to the vendor on 28 August 2026 and followed up twice without a reply, so this is being published unfixed; the timeline and the reasoning are at the end. The shared passcode, the srscore key, real account identifiers, and the proof of concept scripts are all withheld. The live lobby shows other players, so lobby screenshots have any real avatar photos blacked out; the capture below happens to show only default silhouettes.

Chat, presence, bets, and spins do not go over HTTP. The app opens a WebSocket to wss://roulmulti.mywavia.com:443/roul-ws (Sec-WebSocket-Protocol: v12.stomp, v11.stomp, v10.stomp, User-Agent: okhttp/4.9.1) and speaks STOMP, the text protocol RabbitMQ exposes. Burp sees the upgrade and then opaque frames, so there is nothing to replay. We reimplemented a WebSocket and a STOMP client in the Python standard library and talked to the broker directly.
The STOMP CONNECT uses a single login and passcode, hardcoded in classes2.dex (recovered in Part 1), identical for every install:
CONNECT
accept-version:1.1,1.2
heart-beat:5000,5000
login:roulette_stomp_mywavia
passcode:kiG9w0............OC6s (redacted, hardcoded, same for all clients)
^@
There is no per user authentication on the realtime layer. Once connected, the broker cannot tell one player from another.
Impact. Anyone who extracts the passcode once (it is a static string in the APK) has a permanent, unattributable connection to the realtime layer for every user. This is the root cause of findings 2 and 3.
RabbitMQ topic exchanges accept # as a wildcard. The client is meant to subscribe to its own room. We subscribed to everything:
SUBSCRIBE
id:sub-1
destination:/exchange/upstream/# (every room's client->server traffic)
ack:auto
^@
SUBSCRIBE
id:sub-2
destination:/exchange/downstream/# (every room's server->client traffic)
ack:auto
^@
The broker then delivers every room's frames. Presence and chat carry the account id and profile inline:
{"TYPE":"ADD","RID":"sankara_1787900000000","URID":"L10V5QE",
"p":{"rdsid":"mw_<player>","n":"<display name>","c":987340000,"lvl":42}}
{"TYPE":"CHAT","RID":"sankara_...","rdsid":"mw_<player>","m":"hi all"}
{"TYPE":"BETS","rdsid":"mw_<player>","p":{"c":..., "a":[...]}}
From that stream we build a live roster keyed by rdsid, with display name and chip count for everyone online, in every room.


Impact. A single connection reads the chat, bets, spins, joins, and leaves of every table on the service in real time, and harvests the
rdsid, display name, and balance of every online player. It also supplies the ids that findings 4, 5, and 6 need to target a specific account.
A chat message is published to the room's upstream exchange, and the sender is a field in the JSON body. The server takes it at face value and never checks the publishing connection owns that rdsid:
SEND
destination:/exchange/upstream/sankara.logger
content-type:application/json
content-length:74
{"TYPE":"CHAT","RID":"sankara_1787900000000","rdsid":"mw_<victim>","m":"hi"}^@

In testing the tool published two lines as mw_guest_129, an id belonging to another player, and the broker accepted both while the live table carried on.
Impact. Any message can be posted as any player in any room. Combined with finding 2's live roster, an attacker can impersonate a named victim at a specific table: social engineering, fake "the dealer says deposit here" messages, or getting a real user banned for words they never typed.
On the HTTP side, /gameserver_roul?action=get_user_details&rdsids=[...] takes a list of ids and returns full profiles. The MWHDR header from Part 1 gets the request accepted, and the server does no ownership check on the ids, so pass any id from the roster:
GET /gameserver_roul?action=get_user_details&rdsids=["mw_<victim>"]
--> {"ActResp":[{
"rds_id":"mw_<victim>", "id":"mw_<shortid>", "udid":"<device id>",
"fbid":"<facebook id>", "displayName":"<name>", "emailid":"<email>",
"imgurl":"https://classspace.in/files/<hash>", "user_room_id":"<private code>",
"score":{"myscore":{"CurrentWorth":41000,"gems":0,"diamonds":0}} }]}
action=sync_account&udid=<victim udid>&rdsid=<victim rdsid> returns the same for one account. The udid is the third from last segment of the rdsid, so it is not a separate secret.
Impact. Full disclosure of personal data for any account whose id you can see: email address, a stable device identifier (
udid), the linked Facebook id, the private table code, and the full balance. Finding 2 supplies an id for every player online, so the reachable set is the active player base rather than a single account.
The same missing check applies to writes. update_un sets the display name of whatever id you pass:
GET /gameserver_roul?action=update_un&rdsid=mw_<victim>&displayname=owned_by_websec
Impact. Any account's display name can be changed by an unauthenticated attacker. Low on its own, though it proves the write path has no ownership check either, which matters for finding 6.
Part 1 set our own worth by signing an srscore submission. The server keys the write off the uuid in the query string and does not check ownership, so the identical request works with a victim's rdsid:
GET /srscore?type=put&un=<anything>&all=Y&appid=MW_RR_ANDROID&cv=17.2
&uuid=mw_<victim>&o_uuid=<victim udid>&isocc=
&score=...***CurrentWorth<<>>0***...&hash=<md5(payload + shared key)>
&gems=0&diamonds=0&unlock=0 (tail still unsigned)
Impact. An attacker can set any account's worth, leaderboard stats, gems, diamonds, and unlocks: wiping a top player's worth to zero, planting an impossible score to dominate the leaderboard, or inflating an account before selling it. Part 1's client side mod reached one account. This reaches every account, and the server treats the result as authoritative.
Avatars chain an unauthenticated upload to an unsigned bind. The image goes to a tusd resumable endpoint that accepts uploads with no auth:
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>
Then a gameserver_roul action points a victim's avatar at that URL, and this action is not signature enforced:
GET /gameserver_roul?action=update_im_url&rdsid=mw_<victim>&...=https://classspace.in/files/<hash>
Impact. Any image can be set on any account's profile with no authentication. During testing a live table showed a player whose avatar was explicit adult content, which is what an open "any image on any profile" primitive produces in a game with a public lobby used by minors and adults. That is why every live lobby screenshot in this series has real avatars blacked out.
RouletteRoyale_Chat.py (standard library only, run from PyCharm) wires the chain together: subscribe to the broker, build the roster, pick a name off it, resolve it to a full profile through the IDOR, then rename, re-worth, re-avatar, or speak as that player. Economy writes sit behind an --arm flag and default to a dry run that prints the exact request without sending. Every frame in and out is logged to JSONL and text for evidence.
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
The design maps to the findings: a StompWS for findings 1 to 3, a MywaviaClient that does the MWHDR and srscore signing for findings 4 to 7, and an ImpersonationPoC that joins a roster entry to a full account takeover.
The tool went to the vendor with the original report, along with a second script that reads, writes, and intercepts upstream and downstream broker traffic. Neither is published here, and neither are the broker passcode, the srscore key, or any real account identifier.
Nothing described in this series is fixed. The broker still accepts the shared passcode, the account endpoints still skip the ownership check, and the avatar bind still takes any URL for any account. A runnable copy of either script would let anyone pull the email address and device id of every player currently online, rewrite their balances, and post messages under their names, against a live service with real users on it. This write up gives the vendor what it needs to reproduce and fix each finding, which is the reason to publish at all, and stops at the point where it would also hand a working tool to everyone else.
The vendor can have the complete package, both scripts, the secrets, and the evidence logs, at any time on request.
| # | Finding | Root cause | Attacker gain |
|---|---|---|---|
| 1 | Shared STOMP credentials | Static passcode in APK, no per user auth | Unattributable realtime connection for all users |
| 2 | Wildcard subscribe | Topic # allowed to any client |
Read every table's chat/bets/spins, harvest every online rdsid + name + balance |
| 3 | Chat impersonation | Server trusts rdsid in the frame body |
Post as any player in any room |
| 4 | Account read IDOR | No ownership check on rdsids |
Email, udid, fbid, worth, private code of any account |
| 5 | Account write IDOR | No ownership check on update_un |
Rename any account |
| 6 | Cross account economy | No ownership check on srscore uuid + shared key |
Set worth / stats / gems / diamonds on any account |
| 7 | Avatar takeover | Unauthenticated tusd upload + unsigned update_im_url |
Any image on any profile |
One root cause runs under all seven: identity is asserted by the client and never verified by the server, on the broker and the account API alike. Fixing one finding does not help. The server has to establish who the caller is and check what they are allowed to touch.
- Bind each STOMP connection to an authenticated per user session and reject any published frame whose
rdsiddoes not match that session. Retire the single shared passcode. Deny#subscriptions. - Enforce ownership on
/gameserver_roulfor every read and write: the authenticated caller must own therdsidoruuidin the request. This closes findings 4, 5, and 6 at once. - Sign scores with a server held key and include every economy field (
gems,diamonds,unlock, coupons,defer) in the signed data, so there is no unsigned tail. - Authenticate the avatar store and validate uploaded content; require a signed, ownership checked
update_im_url. - Treat
MWHDRas non security and do not rely on it for anything.
- 28 August 2026. Reported to the vendor. We used their own vulnerability reporting page at
http://profile.mywavia.com/im/report_vulnerability.htmland followed the instructions it gives, and we sent the same report by email. That page states that their personnel will make contact. Nobody did. The report named the WebSocket exposure of the RabbitMQ credentials and the 129 character key hardcoded in the APK, described the resulting avatar, currency, username, and chat impersonation abuse, and attached two proof of concept scripts: one showing administrative control over every lobby and user, one showing read, write, and intercept access to upstream and downstream broker traffic. - 6 September 2026. First follow-up asking whether the report had been read. No reply.
- 15 September 2026. Second follow-up. No reply.
- 20 September 2026. Published. Twenty three days on from first contact, after a submission through the vendor's own reporting form and three emails, there has been no acknowledgement of any kind. The findings are published in full. The proof of concept scripts, the working credentials, and the signing key are not.
Part 1 showed the client is trusted to sign its own requests, so a player can forge their own economy. Part 2 showed the server extends that trust to everyone: one static passcode opens the entire realtime layer, and one missing ownership check exposes every account's data, name, economy, and avatar. Both halves come from the same decision to let the client assert who it is, so the fix is server side authorization on every request, on the broker and the account API alike.
