Sitemap

Business Logic Flaw: Chaining two endpoints to illegitimately claim purchasable rewards

7 min readJun 30, 2025

Hello everyone! After nearly 3 years away from bug bounty due to military service and pursuing a master’s in cybersecurity, I’m finally back in the game. This time, I decided to focus strictly on logic-based vulnerabilities. For this write-up, let’s refer to the targets asredacted.com, api.redacted.com , a single-app platform built around a business model heavily reliant on purchasing rewards. These rewards are used for actions like completing tasks, sharing content, rating, ranking up, and more. The app also provides time-based and purchase-based rewards. Our mission is to analyze and break the logic behind the reward system to illegitimately gain rewards without any payout.

Phase 1 → Understanding app logic:

As I focused on business logic vulnerabilities, my first step was identifying the core functionality of the app , in this case, purchasing and claiming rewards. Just like unauthorized access to private content would break a photo-sharing app’s core value, bypassing the coin/reward purchasing mechanism here would directly undermine the app’s main functionality.

Firstly, after signing up for the app, and tried every single function related to bonuses, rewards, and checking related fields that can give me legitimate rewards. After finishing this process, I went back into burp history to search for any requests that I could. manipulate.

Then, I found a possible vulnerable endpoint /bonus/v3/bonus/collect?bonusId=<ID>

Press enter or click to view image in full size

The first thing that came to my mind was to fuzz this id value , only a few digits, so sending the request to the intruder to see if it’s an easy win?

Press enter or click to view image in full size

and only got 400 Bad request , and this bonusId is not valid for this userId

Press enter or click to view image in full size

So, our next step is to inform the server that we got a reward.

Phase 2 → Tricking server to reward us

Now the next mission is to search for a way that can induce the server to give us illegitimate rewards. Moreover, to do so, I searched in the HTTP history again for bounsId , looking for JS file or endpoint. And I found that nothing can be beneficial.

So, I decided to be trickier, using burp search functionality with regex, I searched for any response that may have the bouns keyword in the response. I used this regex to search for any JSON response contains that keyword:

(?i)”\w*bonus\w*”\s*

that searched for any before/after thebouns keyword. And Surprisingly, I found a request has been sent to the server, and in its response

Press enter or click to view image in full size

and this was a response for a POST request to /redacted/redacted/v3/<ID>/action endpoint with body:{“action”:”ClaimRewards”}

At that point, the logic clicked: when a user completes a mission, the frontend notifies the server, which in turn generates a reward bonusId. From an attacker’s perspective, I realized I could brute-force these IDs (just 2-digit fuzzing) and receive reward IDs for staff I never completed.

I moved to Burp Repeater, grabbed the bonusId from the first response, and chained it into a call to: /bonus/v3/bonus/collect?bonusId=<ID>

But… it failed with a 401 Unauthorized. That was unexpected.

Phase 3 → Combining small pieces

So now, let’s combine the small pieces together for better clarification. At this point, I realized the exploitation isn’t about a single vulnerable endpoint, but rather a logical sequence of actions that needed to be replicated to trick the server. From my perspective, this was clearly a chained logic flaw, not a simple IDOR or broken auth.

Specifically, making a direct request to /bonus/v3/bonus/collect?bonusId=<ID> , even with a valid bonusId doesn’t grant the rewards unless it is executed in the context of the correct workflow. This meant the reward claim process was tied to some internal application logic: possibly a prerequisite signal to the server that the user is eligible for the reward.

So, to successfully exploit this, I needed to mimic the full logic path the application follows when a user legitimately earns and claims a reward. That meant hitting the initial endpoint (/redacted/redacted/v3/<ID>/action) first , which seemed to signal or register the intention to claim a specific bonus , then immediately chaining it with the reward collection request.

At this point, I suspected there must be a token or value passed implicitly between the two requests that links them together. That’s when I started analyzing headers and responses more deeply , leading to the next key discovery.

Phase 4 → SessionToken little story

At this point, I felt stuck. I revisited the repeater to study how the second endpoint (/bonus/v3/bonus/collect) behaves under different conditions. Here's what I observed:

  • 200 OK → normal legitimate behavior
  • 400 OK → attempting to claim an unearned reward.
  • 410 Unauthorized → Is that for admin ? Or did I become unauthorized?

So, I analyzed all the requests that have been issued to this endpoint, and I figured out the missing piece. I found out that all endpoints have the normal Authorization: Bearer header, but this endpoint doesn’t use it; instead, it uses a different header/token. The server creates a temporary sessiontokenspecific for that request and passes it as an additional header, and sends the second request to the second endpoint with the same value of sessiontoken .

You feel lost? don’t mind, I was too, so let’s clarify it.

For a legitimate claim of rewards, either purchasing or completing tasks, that’s what typically happens:

  1. The server generates a specific token for this process
  2. Then, informs the server that the user is legitimate for the reward associated with a specific ID /redacted/redacted/v3/<ID>/action
  3. After that, it retrieves the ID value of the reward and sends it to the second endpoint bonus/v3/bonus/collect?bonusId=<ID> with the same pre-generated sessiontoken , after that, the user can claim the amount associated with the <ID> from the first request.

So why the sessiontoken became invalid at the first try?

That's because it is generated per-claim or refreshed after a short period of time.

So if you FUZZED and successfully got the <ID> , then sent the reward ID in the response to the second endpoint, you will get 401 unauthorized , WHY then ?

The sessiontoken became invalid because the server generated a new one

so, getting a new, freshly generated sessiontoken will success ?

  • NO, because the server checks maybe I think the timestamp and latency between the two requests.

So now, what’s the next step to defeat this quick refreshing?

Phase 5 → Beating quickly to WIN

So, I decided to create a Python script that fuzzes for the <ID> in second request, then fetches the valid ID from a valid response, then sends the value with the same sessiontoken to get rewards.

I wrote some of the code, but AI refined it more, made it more efficient. The following code typically do what I've explain above.

Initially, I hardcoded a freshly generated Sessiontoken, and thanks to the script’s speed, I managed to hit both endpoints before the token expired. Although the token eventually rotated (as expected), I was able to successfully chain enough requests to accumulate illegitimate rewards. That alone was sufficient for a powerful PoC.

For more advanced exploitation (if speed weren’t enough), the next step would be to fully automate the login process and refresh the Sessiontoken for every claim attempt. Fortunately, that wasn’t needed.

#!/usr/bin/env python3
import requests, json, time

BASE = "https://api.redacted.com"
SESSIONTOKEN = (
"eyJraWQiOiJjQjlRQ0FWQ1Q1M1lKRWNQS2FrOFpIbGVIa3BYSG5uTzFmUThBYVdlVjBI"
)

CLIENT_IDS = range(ID1, ID10) # i made 50 range only

ACTION_BODY = {"action": "ClaimRewards"}

HEADERS = {
"Content-Type": "application/json; charset=utf-8",
"Sessiontoken": SESSIONTOKEN,
"Origin": "https://redacted.com",
"User-Agent": "PoC-script",
}

session = requests.Session()
session.headers.update(HEADERS)

def try_id(cid: int):

action_url = f"{BASE}/redacted/redacted/v3/<ID>/{cid}/action"
r = session.post(action_url, json=ACTION_BODY, timeout=6)

if r.status_code != 200:
return False # silent on non‑200

try:
data = r.json()
except Exception:
return False

user_bonus = data.get("userBonus", {})
bonus_id = user_bonus.get("id")
if not bonus_id:
return False

# Sum 'amount' from all skuUnits
amount = sum(
unit.get("additionalData", {}).get("amount", 0)
for unit in user_bonus.get("details", [])
)

collect_url = f"{BASE}/bonus/v3/bonus/collect?bonusId={bonus_id}"
r2 = session.post(collect_url, timeout=6)

if r2.status_code == 200:
print(f"[+] client_id={cid} bonusId={bonus_id} amount={amount}")
return True
return False

if __name__ == "__main__":
for cid in CLIENT_IDS:
try:
try_id(cid)
time.sleep(0.2)
except KeyboardInterrupt:
print("\nInterrupted by user.")
break
except Exception as exc:
print(f"[!] Error on {cid}: {exc}")

And with only a 50 range of fuzzing, I made it XD, and after refreshing my page, I found that I claimed more rewards/coins without purchasing ;)

Press enter or click to view image in full size

In the end, this vulnerability boils down to a simple but impactful business logic flaw, where chaining two predictable endpoints, one to fetch a reward ID and another to redeem it, allowed unauthorized collection of paid rewards. By replicating the app’s internal logic and automating the flow with the required Sessiontoken, I was able to bypass intended restrictions like purchase requirements or task completion.I finalized the PoC and promptly reported it to the bug bounty program, where it was accepted as a P1 due to its financial impact, allowing users to bypass purchases and claim paid rewards illegitimately.

Finally, thanks for reading. For any clarification, ping me on X .

--

--

Mahmoud Youssef
Mahmoud Youssef

Written by Mahmoud Youssef

Cyber Security Researcher | Bug Hunter