Cash Show: A Trivia App Leaking PII and Money

Copy Paster
6 min readSep 20, 2018

--

With the recent craze of trivia “game show” apps that pay money to users for answering trivia questions correctly, such as HQ Trivia, it is no surprise that others are popping up to take advantage. One of those apps, Cash Show, provides a similar idea: answer trivia questions correctly during one of the game shows and get paid.

As of now, Cash Show has over 100k reviews across the iOS and Android app stores with separate US, FR, AU, UK, CN, and GER apps; there are an estimated 5 million installs. In the US, it boasts payouts of nearly $6k per day across two game shows.

Unfortunately, Cash Show has a couple of vulnerabilities that allows anyone to view payout information of users (including PayPal email addresses and amounts), deny/approve payouts, and the ability to cash out the same money to PayPal multiple times. Cash Show has previously been notified of these vulnerabilities with no resolution.

Process of Discovery

Typically one of the fastest ways to understand the structure of an app is through proxying it’s network traffic data.

I patched out system calls for certificate pinning in the app and setup a reverse proxy through Burp Suite. After setting up the Wifi settings on my Android phone to go through it… there was no data.

The app itself was loading perfectly fine, but none of the traffic was getting redirected through the proxy. But how?

This led me to believe that it was communicating over Firebase Messaging’s long-lived TCP connection, but that wasn’t the case. After debugging, it turns out that it is possible to use non-standard networking libraries on Android and iOS that completely ignore the default system proxy settings.

As a result, I thought that an effective way to bypass this would be to setup my own DNS server that resolves any domain to my local computer IP and point my phone’s DNS to that. Therefore it wouldn’t matter what networking library they were using since DNS settings are seemingly global. Burp Suite actually has a setting for this in order to proxy reverse DNS traffic since the format is slightly different than typical proxy traffic.

Cash Show Network Traffic in Burp Suite

Looking through it briefly, it seems like most data is going through a WebSocket, but it doesn’t look like the messages are plain ASCII text…

Raw Hex of a Cash Show Message

Sifting through the binary blob with a hex editor, it doesn’t seem to be encrypted, but rather some binary serialization protocol, hmm… that looks a lot like Protocol Buffers (Protobufs).

Now there are two ways to proceed: figure out the types in the protos from binary samples or dump the protos from the app and know every message.

I chose the latter.

Statically Reverse Engineering Weird Apps

Since Android apps are typically easier to decompile, I went with that route. Using typical Android decompilers, I wasn’t able to find the main logic of the application. Maybe this is using some sort of cross-platform framework?

After looking through the res folder, I found an interesting and large binary blob with the jsc extension. Researching online, this seems to be part of a Cocos-2D application and allows for all the fancy animations in the app; it turns out the entire app is written in this.

jsc is what stores compiled JavaScript bytecode in the Cocos-2D framework which is compiled with Firefox’s Spider-Monkey 34.

So how can we get source out of this?

After searching around on the internet, @irelance on GitHub recently started work on a decompiler written in PHP that can seemingly decompile this, but without fully runnable code.

After looking at the issues on GitHub it seems like others are also trying to decompile Cash Show, but are running into issues with unimplemented instructions in this prototype decompiler. Looking through the source briefly and just patching it to ignore some instructions that aren’t implemented, it now successfully produces a decompiled version of Cash Show in readable JavaScript.

After briefly going through the source, my suspicion was correct, they’re using protos generated by Google’s protobufjs library. Since all of these protos are already compiled into JavaScript (and are also fully named!), we want to find a way to extract them back into a .proto definition file.

Excerpt of decoded deserialization of the GameProgressResp proto:

====================================================================
==================================591===============================
------------------Argv------------------
msg,reader,field,value
---------------------------------------
----------------Content----------------
while(reader.nextField()){
if(reader.isEndGroup()){
break;
}
_local0=reader.getFieldNumber();
switch(_local0){
case 1:
_local1=reader.readEnum();
msg.setResult(_local1);
break;
case 2:
_local1=reader.readEnum();
msg.setPhase(_local1);
break;
case 3:
_local1=reader.readInt64();
msg.setGameStartTime(_local1);
break;
case 4:
_local1=reader.readInt32();
msg.setTotalRound(_local1);
break;
case 5:
_local1=reader.readInt32();
msg.setRound(_local1);
break;
case 6:
_local1=reader.readInt64();
msg.setTs(_local1);
break;
case 7:
_local1=reader.readEnum();
msg.setProgress(_local1);
break;
....

I built a custom decoder that could, using the output from the decoded JavaScript, reconstruct the original proto messages back. This was mostly based upon the embedded JS source for the deserialization/serialization routines from a JavaScript object to serialized binary. It would parse the JS routine to figure out the types, indices, and names for fields. This is not exactly trivial due to enums, repeated fields, and some newer features in proto3, but it got the job done after some small human tweaking.

Now I know every proto message in the application along with enums.

Excerpt of a reconstructed message:

message GameProgressResp {
ErrorCode result = 1;
enum GamePhase {
PHASE_IDLE = 0;
PHASE_GAME = 1;
PHASE_GAME_QUIZ = 2;
PHASE_GAME_ENDING = 3;
}
GamePhase phase = 2;
int64 game_start_time = 3;
int32 total_round = 4;
int32 round = 5;
int64 ts = 6;
enum GameProgress {
PROGRESS_WATCH = 0;
PROGRESS_QUIZ = 1;
PROGRESS_ANSWER = 2;
}
GameProgress progress = 7;
bool is_live = 8;
string live_url = 9;
Quiz quiz = 10;
enum UserRole {
ROLE_WATCHER = 0;
ROLE_PLAYER = 1;
ROLE_LOSER = 2;
ROLE_GUEST = 3;
}
UserRole role = 11;
...
}

Creating a Custom Client

Looking at the decompiled source, not all of the protobuf messages are meant to be sent over the wire; only ones included in a globally defined dictionary with static Proto -> ID mappings. The network message consists of 2 bytes containing the Proto ID and the rest is the serialized proto.

Now I can decode the WebSocket messages easily along with field names, so the next step is making a custom client.

The protobuf messages had what you’d expect: account info, question info, authentication, facebook linking, etc…

It also had protos in relation to admin activities such as banning users, approving/denying payouts, adding new questions to the show, starting a show, etc… I would have never found these unless I decompiled the app.

Side Note: It turns out that the client receives the next trivia question a couple of seconds before it is displayed in the app most likely to account for propagation delay.

Most of the admin protos simply won’t cause a response on the WebSocket when sent from my lowly user account. But… it seems like the protos in relation to payouts are not so lucky.

Lack of Payout Checks

Cash Show has a system where they have to “review” your payout before they send it, which typically takes 2 weeks. If I send the proto for fetching pending payout requests, it returns a list of PayPal emails and payout amounts of users on the platform! I even have the power to approve/deny any payout.

You’d expect an endpoint in relation to money and PII to be more protected, but unfortunately it was the complete opposite.

This then leads to another vulnerability in the app, payouts have the following stages according to the protos:

Requested -> In Review -> Approve/Deny

After they pay your PayPal however, your payout isn’t automatically in the “Approved” status. It seems like this is done in order to prevent users from repeatedly cashing out and having to wait an additional 2 weeks before the system times it out and makes it approved. It seems like the PayPal payouts themselves are done manually since approving a payout over the protocol doesn’t send money on PayPal.

Since we have the power to deny payouts though, you could do the following:

Requested -> In Review -> Paid via PayPal -> Deny the Payout Ourselves and Request it Again

This would effectively let us cash out the same amount of money repeatedly.

Disclosure Timeline

  • August 8: Sent emails to Cash Show and Zenjoy (Zentertain) contact email addresses
  • September 6: Due to not being able to find any contact, resorted to tweeting to the Cash Show handle. Responded asking me to send an email to privacy@cashshow.com.
  • September 7: Sent email to privacy@cashshow.com
  • September 20: No response from any emails, evidence of possible public exploitation, public disclosure

--

--