Orbot CLI
5 minute read
During my first weeks at LTH, I encountered Orbi - an app used for organizing student events. Some events would sell out incredibly fast (<30s), and keeping track of different release times quickly became problematic. After sleeping through one too many ticket releases, I decided to create a CLI tool to automate the process.
Tools Used
- Python: The primary programming language
- Fiddler: For intercepting and analyzing network traffic
- SSL Kill Switch 2: For bypassing SSL pinning on iOS
- Jailbroken iPhone: For installing SSL Kill Switch 2
Method
Mapping the API
Unlike the Trafikverket project, Orbi came with some new challenges. Being mobile-only (with no web version) meant that inspecting network traffic would require a different approach, as mobile operating systems make debugging more challenging. You also need to pay upfront as tickets sell out so quickly - I wouldn't have time to react if I just got a notification to my phone.
The first problem was SSL pinning. Even after installing Fiddler's certificate, the app would block network requests through client-side certificate validation. This resulted in only some requests getting through, and the display becoming completely black and unresponsive after launch. To bypass SSL-pinning we generally need to hook into the source code of the app itself, and bypass the checks by changing the return value of the certificate validation function(s).
There is a great tool for this called Frida which I usually use when reversing Android applications, but as I had recently jailbroken my iPhone I wanted to try something else for once. After looking around I found a jailbreak tweak made specifically for this called SSL Kill Switch 2, which I took the opportunity to try instead. This solved the issue on the first attempt (rarely the case with frida), and I could start mapping the API.
The mapping process itself is quite straight-forward:
- Start a recording in Fiddler
- Point the iPhone proxy to Fiddler under
Settings > WiFi > Configure Proxy
(in iOS) - Close Orbi completely to avoid cached requests
- Open Orbi again and attempt to buy a ticket
- Filter out and mark the relevant requests in Fiddler
- Re-implement the API in your language of choice as a reusable library
The relevant endpoints I found are listed below:
Authentication (googleapis.com/identitytoolkit/v3):
/relyingparty/verifyPassword - User login
/relyingparty/getAccountInfo - Get account information
Main API (data.orbiapp.io):
/student-app/v3/activities/by-department - List activities
/student-app/v4/tickets/by-activity - Get tickets
/student-app/v3/activities/activity/{key}/save - Save activity
/student-app/v3/calendar-feed - Calendar feed
/student-app/v4/membership-types/owned - User memberships
Purchase API (hugin.orbiapp.io):
/v3/purchase/init - Initialize purchase
/v3/purchase/status - Check status
This gave me enough information to list activities and check ticket availability. I also found some data returned from the API that was not shown in the app, like exact number of tickets remaining and event coordinates. I guess it was excluded for saving space on a mobile UI, but I found it useful for context when deciding on which ticket to buy and included some of this "hidden" data in the CLI app.
Payment Integration
The last step was handling payments. I had always made all purchases in orbi through Apple Pay, which likely wouldn't play well with automation. After looking around, I found that you had the option to pay with a credit card instead, which also seemed to be stored for future use after filling it in once. After buying a test ticket, I saw connections to a Stripe API that worked through standard bank verification. This looked like the most promising option.
Here is the re-implementation of the Stripe API in Python:
def handle_payment(orbi_client: OrbiAppClient, activity: Activity):
# Initialize purchase with Orbi
cart_identifier = orbi_client.init_activity_purchase(activity.key)
purchase_status = orbi_client.get_purchase_status(cart_identifier)
# Handle Stripe payment
stripe_client = StripeClient()
stripe_params = purchase_status["paymentIntentParams"]
stripe_secret_key = stripe_params["stripeCustomerEphemeralKey"]
stripe_client.retrieve_elements_sessions(
client_secret=stripe_params["paymentIntentClientSecret"],
type_="payment_intent"
)
# Secret key needed after this point
payment_methods = stripe_client.retrieve_payment_methods(
type_="card",
customer=stripe_params["stripeCustomerId"],
secret_key=stripe_secret_key,
)
# Confirm payment
stripe_client.confirm_payment_intent(
payment_intent_id=stripe_params["paymentIntentId"],
client_secret=stripe_params["paymentIntentClientSecret"],
payment_method=payment_methods[0]["id"],
)
# Verify purchase completion
if orbi_client.get_purchase_status(cart_identifier) == "completed":
return True
return False
It took some time to reverse engineer the payment handling, but luckily no other security measures existed to prevent automation. The entire flow could now be automated, from selecting an event to completing the purchase.
Result
The tool works exactly as intended. It can monitor upcoming events, await specific release times, and complete purchases faster than any human using the app. Instead of having to refresh and click through multiple screens, everything can now be handled automatically by the script.
I won't share the source code publicly since giving everyone access to such a tool wouldn't be ethical. Still, building this taught me a lot about iOS app reverse engineering and payment system integration.
One thing that stood out was how the app handled authentication and payments. While Apple Pay was front and center in the UI, it was Stripe that made the automation possible. It's a reminder that systems sometimes have multiple paths to the same goal, and that the less obvious path might be the one that's useful for you.