HOME/Articles/

Zoh-no Zogo: Manufacturing pineapples (and cash) with Burp Suite

Article Outline

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.

{% include image.html file="zogo-map" alt="Some of the modules available on Zogo" %}

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.

{% include image.html file="zogo-redeem" alt="5,000 pineapples can be redeemed for a $5 Amazon 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.

{% include image.html file ="zogo-question" alt="Answer a question about HMOs" %}

As soon as I answered that question, this network request fired off:

{% include image.html file="zogo-question-req" alt="Burp suite showing the Zogo API request" %}

And the response was even better:

{% include image.html file="zogo-question-resp" alt="Burp suite showing the Zogo API response, including the valid answer" %}

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.

{% include image.html file="zogo-question-resp" alt="Burp suite showing the Zogo API response, including the valid answer" %}

Burp suite has a great extension called "Copy as requests". It will allow you to copy any network request as a python snippet.

{% include image.html file="zogo-burp" %}

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.

{% include image.html file="zogo-notebook" alt="Jupyter Notebook to play around with the request" %}

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.

{% include image.html file="zogo-force-running" alt="Answering every question ID correctly" %}

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

{% include image.html file="zogo-email" %}

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.