IP Spoofing to Account Takeover: You Patched It? Really?

Abstract
In my previous article, I described how I found a security flaw in a popular desktop app's OAuth flow that allowed me to steal any user's account with just one click. I reported it, saw it patched, and then bypassed the patch again. Since the process of bypassing and exploiting the flaw is interesting to me, I decided to write a second article about it. I also tried to get permission to disclose the report, but they didn't allow me to do so. Therefore, throughout this post, I'll refer to the domain as redacted.com.
Setup
Since the setup environment hasn't changed, I refer to the setup section of this write-up.
Reconnaissance
After the vulnerability was patched, I tried to do the same thing I did before to see what would prevent me from taking over a victim's account.
Analyzing The Patch
I followed the previous attack scenario explained here, and you can see its diagram in the image below.
As an attacker, I initiated the OAuth login process in my desktop app. I copied the generated OAuth URL (https://oauth2.redacted.com/auth/google?remote_key=[ATTACKER_REMOTE_KEY]&jwt=1&client_id=[CLIENT_ID]&lang=en&return_skip=1) from the browser it opened and then opened it on the victim’s device with a different IP. However, after completing the OAuth process on the victim’s device, I wasn't able to log in to the attacker’s desktop app as the victim, as I expected.
The patched version showed me the following page:
I tried again using a different device but the same IP through a VPN. I discovered that as an attacker, I could take over the victim's account only if our IPs matched. This showed that the prevention relied only on IP, which is usually not enough.
IP Spoofing via HTTP Headers
By checking all the requests initiated by the desktop application, I noticed that all domains are behind the CDN, and when a developer puts a domain behind a CDN, finding the real client IP can be challenging. This is because there is no universal, standardized way for CDNs to convey the original visitor's IP to the backend, and this process can vary based on the CDN provider, programming language, server configuration, and other factors.
To obtain the real client IP behind a proxy (such as a CDN) and pass it to the server, several HTTP headers are commonly used:
X-Forwarded-Foris a list of comma-separated IPs that gets appended to by each traversed proxy. The idea is that the first IP (added by the first proxy) is the true client IP. Each subsequent IP is another proxy along the path. The last proxy’s IP is not present (because proxies don’t add their own IPs, and because it connects directly to the server so its IP will be directly available anyway).Forwardedis the most official but seemingly least-used header.There are also special single-IP headers like
X-Real-IP(Nginx),CF-Connecting-IP(Cloudflare), orTrue-Client-IP(Cloudflare and Akamai).
I prepared a victim's device, saved its IP, and initiated the OAuth flow with the attacker's device. This time, I used the X-Forwarded-For header in every request made by the Desktop application. Then I opened the generated OAuth URL using the victim’s device, and I successfully logged in as the victim on the attacker's Desktop application.
As we saw, the patch is vulnerable, but one piece of the puzzle is missing for the exploit: How does the attacker find out the victim's IP to initiate the OAuth flow with it?
Finding Victim’s IP
I previously identified a stored XSS vulnerability in the redirect_uri parameter of a specific endpoint, which has since been patched. In brief, when users accessed the Manage Account section within the desktop application, the backend generated a token and then triggered a browser window opening with a URL structured as https://redacted.com/token?t=Code. This URL facilitated authentication transfer from the application to the browser by leveraging data linked to the Code passed in the t parameter. The Code contained both account information and a redirect_uri value, which dictated the destination URL after the authentication process completed. The vulnerability happened because there were no checks on the redirect_uri, allowing harmful scripts to be added. This issue was fixed after my report.
The following request to the backend API shows the request body for generating the Code passed in the t parameter in the URL:
And the Code that you'll receive in response:
I attempted to exploit this feature to achieve an open redirect by altering the redirect_uri parameter to something like mirzadzare.net. I obtained the generated code from the response and opened the URL (https://redacted.com/token?t=code) in a browser. As a result, it redirected me to mirzadzare.net after the authentication transfer. With this low-impact open redirect, I could redirect the victim to my website and log their IP! The puzzle is finally complete.
Attack Scenario
Based on the analyzed flow, the process of logging the victim's IP and initiating the OAuth flow needed to be automated. I decided to write a PHP exploit to mimic the desktop application, capture the victim's access token, and send it to the attacker via a Telegram bot.
The attack scenario is illustrated in the diagram below:
The attacker alters the
redirect_uriand sends it to the API endpoint, receiving an authorization code in response.The attacker crafts a malicious link containing this code and sends it to the victim.
The victim opens the malicious link.
The exploit sends a request to the desktop application's API to initiate the OAuth flow and simultaneously establishes a WebSocket connection with the backend server.
The exploit opens a login window in the victim's browser (I could do this without popping up a window, but for simplicity and clarity, I did it for the proof of concept.).
The victim enters their credentials in the OAuth provider login window.
The exploit continuously monitors the WebSocket connection and detects when the victim successfully logs in and the access token is received.
Finally, the exploit sends the stolen access token to the attacker’s Telegram bot, enabling the attacker to take over the victim's account.
In the POC above, the current browser tab displays the exploit page, while the other shows the company's OAuth login result page. Once completed, the access token is sent from the victim's browser to the attacker via a Telegram bot.
Conclusion
This writeup shows how partial fixes can create a false sense of security instead of removing the risk completely. Even though the original issue was fixed, the patch used IP-based validation as its main trust method, which is weak in modern web setups with proxies, CDNs, and client-controlled headers. By combining small issues like IP spoofing and an open redirect, a full account takeover was possible despite the initial fix.
This case teaches important lessons for developers and security teams. First, don't rely only on changeable client properties like IP addresses for authentication and authorization. Second, test fixes against real attacker models, not just the original example. Third, small issues like open redirects or header misconfigurations can become serious when combined.
Security is about how small weaknesses interact, not just one flaw.
