This short, sometimes funny, sometimes not so funny report will tell you how I managed to find three bugs in the IPScore Check service in one evening and replenish my balance for 5,000,000$.
It all starts with a bug Link to heading
I often use the IPScore Check bot when there is a question of checking IP addresses. It aggregates most of the publicly available information about addresses, and I know the developer well.
I clicked on the “Check my IP” button (I’ve clicked on it before, but it was fine, read on) and found a strange thing:
For some reason the bot was getting the CloudFlare IP address instead of my address from WebView. It would seem that the backend takes the real address of the connection instead of the client’s IP address in the headers - but everything was working before, and the developer assures me that everything is fine on his machine…. A few seconds of googling and I remember that I have recently configured IPv6 on my router:
If Pseudo IPv4 is set to Overwrite Headers - Cloudflare overwrites the existing Cf-Connecting-IP and X-Forwarded-For headers with a pseudo IPv4 address while preserving the real IPv6 address in CF-Connecting-IPv6 header.1
However, Pseudo IPv4 was disabled, and I saw not a pseudo address, but the IP of the CloudFlare server, which changed between requests.
IP address manipulation Link to heading
And then my buddy drops a piece of code with the phrase “Maybe there’s a bug somewhere in the code? Can you see it?”
async def get_ip(self, request: Request):
if not request:
raise HTTPException(status_code=400, detail="Request object is required")
headers = request.headers
ipv4 = None
ipv6 = None
cf_ip = headers.get("CF-Connecting-IP")
if cf_ip:
if ':' in cf_ip:
ipv6 = cf_ip
else:
ipv4 = cf_ip
if not ipv4 or not ipv6:
x_forwarded_for = headers.get("X-Forwarded-For")
if x_forwarded_for:
ip_list = x_forwarded_for.split(',')
for ip in ip_list:
ip = ip.strip()
if ':' in ip:
ipv6 = ipv6 or ip
else:
ipv4 = ipv4 or ip
if not ipv4:
x_real_ip = headers.get("X-Real-IP")
if x_real_ip and ':' not in x_real_ip:
ipv4 = x_real_ip
client_ip = request.client.host
if not ipv4 and not ipv6:
if ':' in client_ip:
ipv6 = client_ip
else:
ipv4 = client_ip
ip = ipv4 if ipv4 else ipv6
return {
"status": "success",
"data": {
"ip": ip,
"ipv4": ipv4,
"ipv6": ipv6
}
}
Yeah, I can see that. And not even just one…
- First - and most importantly - if the request came from CloudFlare servers, the whole function should be reduced to one single action -
cf_ip = headers.get("CF-Connecting-IP")
, other headers should not be checked; - Second - in case the request came from third-party addresses - use the
X-Real-IP
header; - Third - forget about
request.client.host
andX-Forwarded-For
forever (almost).
request.client.host
will always contain a NATed IP address from the private subnet 172.16.0.0/12.
The reverse-proxy sets an X-Real-IP
header containing the real IP address from which the client is connecting.Actually, the reason I was seeing CloudFlare’s IPv4 address instead of my IPv6 address was the buggy (and insecure) logic for handling headers containing IP addresses, in particular the backend wanted very badly to have the client’s IPv4 address even if it didn’t have one.
I also noticed right away that there is absolutely no checks to see if the real IP address of the connection belongs to CloudFlare network ranges, and the API microservice listens to the port on the external address, allowing it to be accessed directly (but CloudFlare filters it). And then I was flooded with an idea:
- If you access the API via reverse-proxy, bypassing CloudFlare, you can manipulate the IP address using the
CF-Connecting-IP
header; - If accessing the API via reverse-proxy bypassing CloudFlare, the
X-Real-IP
header can also be used; - There is no IP address validation in the code - you can specify arbitrary text.
PoC: Link to heading
Impact: Link to heading
- Address hiding in logs and bypassing blocking. The situation is aggravated by the fact that the API service port is available from outside, and the service itself is behind NAT, i.e. it is impossible to catch the real IP address of the client inside the service;
- The developer hinted that it was possible to trigger SSRF in some places;
- Under certain conditions, it is possible to send arbitrary messages to some Telegram users via the official bot.
Weak encryption and IDOR Link to heading
And then I get into the taste, my hands are itching to poke something else, and the developer himself said that there are probably more holes here - part of the API is based on the old code on the puff, and he hasn’t gotten around to rewriting and getting rid of legacy yet.
I go to https://ipscore.me/, from there to https://docs.ipscore.me/ and find the client documentation for the service’s REST API. Logging in to the main domain via Telegram and checking the API (https://api.ipscore.me), I find that I’m under exactly the same user as the TG-bot on my account. On the one hand, this is very convenient, but on the other hand, it’s a real turnoff:
- When working with the REST API, I am free from restrictions and protection mechanisms implemented in Telegram itself and in the bot code;
- I significantly expand the number of automated and manual tools and utilities to explore the application;
- It is much easier and more convenient to interact with the API than with the bot via Telegram;
- I get an opportunity to interact with methods and parameters that are difficult or limited to interact with through a bot.
It’s so wonderful, it makes me want to cry. I’m running through different methods, listening to Never gonna give you up (gotcha?). I was mostly interested in the methods that accept data from the user - and there were very few of them, but create_invoice
seemed very interesting to me:
GET /create_invoice/Bitcoin/BTC/5000000/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D45"
}
}
GET /create_invoice/Bitcoin/BTC/1/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D44"
}
}
Did you notice that too? The invoice_id
sequence is very easy to calculate. No, it’s not linear, but it’s very simple.
The first thing I thought of was IDOR, and I was not mistaken. As it turned out, the API did not check user authorization in invoice management methods, but required it only to provide access to the methods. As a result, it was possible to view invoices of all users of the service, including archived records:
{
"status": "success",
"data": {
"invoice_id": ***,
"merchant_name": "Ip Score Checker",
"merch_id": 20,
"logo_img": "https://ipscore.me/favicon.ico",
"transaction_id": ***,
"currency": 840,
"currency_code": "USD",
"coin": "USDT",
"network": "Tron",
"wallet_address": "T*********************************",
"wallet_id": ***,
"exchange_rate": 0.999845,
"amount": 10.0,
"client_amount": 9.99845,
"order_id": "****",
"description": "********* Top-up 10.0 USD",
"lang": "en",
"status": "success",
"confirmation_status": null,
"created_at": "2024-**-**T00:07:35",
"payment_due_date": "2024-**-**T02:07:39"
}
}
It also turned out that the update_invoice
method could update both foreign and archived invoices. This method could change the network and coin for payment and, as it turned out, update the invoice payment_due_date
. By the way, the api_key
in the query parameters does absolutely nothing, as I understand it. Perhaps it’s some crutch pulled from old legacy, when authorization was not Bearer by token, but by API keys (I assume).
Impact: Link to heading
- You can collect information about user invoices, calculate the developer’s income. I calculated it, honestly - I don’t know what he uses to maintain servers :(
- Invoices do not contain information about customer wallet addresses, but it can be easily pulled from the blockchain;
- it is possible to revive all archived invoices of users;
- comes from 3: probability of DoS if there are a lot of archived invoices. At least transaction verification time will increase (the gateway has its own nodes, by the way), response delay will increase, because the gateway will start generating addresses in large quantities - and this is not the fastest task;
- You can change the address and/or network/monet in the current user invoice. If the invoice update disables the old address for payment, and the user manages to transfer coins to it - the payment will fail. The payment verification corridor at the gateway is about 1 minute.
$5,000,000 worth a few cents Link to heading
While investigating the behavior of API methods for working with invoices, I discovered a rather interesting thing:
POST /update_invoice/ HTTP/1.1
{
"invoice_id": "403F386D45",
"network": "Tronium",
"coin": "TRahiX",
"api_key": "string"
}
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": "403F386D45"
}
}
It turns out that the API does not check the validity of the network and coin values received from the client. When creating an invoice, it’s the same story. Let’s see what happened to the invoice:
GET /get_invoice/403F386D45/ HTTP/1.1
HTTP/1.1 200 OK
{
"status": "success",
"data": {
"invoice_id": ***,
"merchant_name": "Ip Score Checker",
"merch_id": 20,
"logo_img": "https://ipscore.me/favicon.ico",
"transaction_id": null,
"currency": 840,
"currency_code": "USD",
"coin": null,
"network": null,
"wallet_address": "bc1q**************************************",
"wallet_id": ***,
"exchange_rate": 0.0,
"amount": 5000000.0,
"client_amount": 0.0,
"order_id": "****",
"description": "********* Top-up 5000000.0 USD",
"lang": "en",
"status": "success",
"confirmation_status": null,
"created_at": "2024-08-29T01:08:12",
"payment_due_date": "2024-08-29T03:08:15"
}
}
The fields network, coin, rate and number of coins to send turned into null and zeros, but the address for sending funds and the number of c.u. for crediting remained the same. And then it clicked for me - since client_ammount
became null - the invoice can be paid by literally any transaction with any number of coins. And then I transfer a few cents to the address for payment and catch a message in the bot about successful account replenishment:
Your balance has been successfully replenished by 5000000 $ through Ugate!
PoC: Link to heading
Impact: Link to heading
- I’m a multimillionaire now :)
In conclusion Link to heading
And there will be no conclusion, there will be some memes instead. If you really want water in the style of “always test your services and APIs” - ask ChatGPT to write it. Yes, by the way, I didn’t use it when writing this article, and the developer has patched all the holes at the time of this article.
List of used tools aka Masochist’s Kit Link to heading
- Android phone
- Kiwi Browser (apk)
- Restler (apk)
- Termux (apk)
- cURL (termux)
BugBounty is 100 fucking rubles, I’d rather become a black hat Link to heading
In fact, the screenshots above - just a funny situation, no one is offended by anyone, money for this Bounty: Paradise Delight I did not ask, did not wait and did not take, and the research itself was organized on enthusiasm in a free evening…. Wednesday?
In general, IPScore is a really useful service, and the developer is a really cool guy. In the near future (which should have been yesterday, but instead I made the developer patch the holes) a major update is planned, aimed at improving performance and optimizing the infrastructure, and a little later the functionality of the IP Tracker tool will be expanded and other useful tools for working with networks will be introduced. So go ahead, click this link, use it, pay out the taxes.
P.S. actually don’t pay it out - this mirror will soon have a discount instead of my referral percentage. With hate love, FazaN ❤️
-
Restoring original visitor IPs, CloudFlare. ↩︎