Skip to main content

From low-privileged user to Remote Code Execution: step-by-step pentest journey

Illustration of From low-privileged user to Remote Code Execution: step-by-step pentest journey
Adam Borczyk

In the world of web application security, some vulnerabilities are naturally less impactful than others. We often hear about direct, short, and simple attacks that can compromise an entire server or application. Sometimes, however, it is chaining multiple, less dangerous vulnerabilities that leads to serious consequences. Here we will go through a case from one of the pentests from a couple of weeks ago, where having a low-privileged user account allowed us first to read the application source code, then to escalate to admin, and finally to obtain remote code execution.

The tested application was a CMS app written in PHP. The initial “redactor” role allowed us to publish and edit pages in this CMS using HTML/JS/CSS templates plus upload custom images and documents, like logos or privacy policies. Any changes would later be shown in the frontend web application.

Initial recon

Let’s start with the file upload. This is a PHP app, so what about uploading the simplest webshell? It turns out we can upload pretty much any file content, but the app properly handles file extensions – we cannot pass .php (nor variations like .php5 and similar). This upload request:

File upload request

ends up having its extension switched to “txt”:

File extension changed

We can access the file by simply querying:

File access

but the server won’t parse it as PHP – it will only return the text content. There are several ways we could try to bypass the sanitizer, for example:

  • trying a null byte in the filename,
  • duplicate file extensions,
  • possible discrepancies in handling “file” and “filename” parameters from the above request,
  • race condition (maybe the file is stored somewhere first, then the name is changed).

None of these, however, work – the application just works well and doesn’t let us pass a PHP file. Since it doesn’t look like we can get anywhere with this for now, let’s focus on another feature – the web content editor.

Turning it into a white box pentest

It was quickly discovered that the underlying engine is Smarty, simply by typing Smarty’s global variable into example page’s HTML and rendering it:

Smarty detection

Above discloses the version:

Smarty version

One can also list environmental variables this way. This code:

Environment variables

Returns the following output when Smarty renders it on the page:

Environment variables output

Environment variables didn’t, however, give us any valuable data, except for the current absolute application path. Fortunately, Smarty supports the “include” keyword:

Smarty include

Using this, we can read files on disk knowing their path. The rendered page shows the content of the “/etc/hostname” file, suggesting that it is probably a Docker container:

Hostname file

So, what’s next? We already know that the root path of the application is in “/var/www/html.” We also know that it’s a PHP app. Therefore, the next step is to simply render the “/var/www/html/index.php” file. Once again, note that it appears in textual form, not executed by the PHP server, so this way we are now able to read the application source code. The returned code had a few imports from subsequent modules, and this way, one after another, we collected most of the codebase.

One of the modules contained the following PHP line:

SQL injection vulnerability

We can immediately see that this is a primary example of SQL injection, described in PHP documentation (https://www.php.net/manual/en/security.database.sql-injection.php). Following the code flow from bottom to top, we found that “options” is a parameter passed in the URL of one of the modules, waiting for our exploitation.

It was enough to spin up sqlmap and let it figure out that we need a UNION-based technique here. The original, expected “options” value that the web app was setting was:

Original options value

Sqlmap, in order to execute “SELECT USER();”, suggested:

Sqlmap suggestion

An HTTP request with this value gave us in the HTML response what we needed – MySQL username and host:

MySQL user info

But we don’t stop here – the database hides more valuable information.

Privilege escalation

Further enumeration of DB tables shows that there is a custom table “admins” that stores information about web app users, permissions to modules, and an admin flag. The simplified table structure looks like this:

Admin table structure

Analysis of existing records tells us that we have to set “admin” to TRUE and “status” to “2” for our web app user in order to become administrators. Sqlmap has this handy –sql-query flag for executing custom queries:

Admin privilege escalation

The query worked indeed – after signing out and back in, we have access to administrative modules:

Admin access

Since we already have full access to the app’s database, you may ask – why do we even need administrative privileges in the web app if we can control the database? We have all the crucial data, can change user properties or passwords, and inject all kinds of stuff into the web content – we are admins already. The answer is:

Remote Code Execution

Now that we are admins of the web app, we have access to more modules. More modules mean more bugs. More modules also mean more source code that is relevant to us. After a couple of hours of subsequent source code analysis, we find a helper module “lib/import.inc.php” and read the content of it using our loyal Smarty. The shortened code goes like this (red color are our comments):

Import module code

Wait, what’s that? This module just copies a file from one path to another. That’s going to help us bypass the file upload sanitizer that we played with in the beginning. We also see that we need admin privileges for it, but fortunately, we can use our brand new escalated cookies. So let’s try this:

File copy attempt

We receive a success message:

Success message

And this way we have it – a PHP file in the application directory:

PHP file created

Execution shows that the webshell works. This simple request to the webshell:

Webshell execution

Gets us the result of our final target – the ability to issue commands on the application server:

Command execution result

That’s it, the infamous remote command execution! Thanks for joining me in this throwback pentest.

We could possibly try to escalate higher, but the application was running in an Azure-managed Docker container through Azure App Service, so this was out of scope for this assessment.

What’s next

The whole penetration testing business we do is not (only) about having fun; first and foremost, we help our customers secure their environments. What is the conclusion here and what can we recommend to the client? Let’s break it down briefly:

  • Harden the configuration of web content editors: They often allow restricting access to dangerous functions, such as reading files from the server. Default settings may not be enough.

  • Use battle-tested frameworks to prevent common injection attacks like SQL injection: Using direct SQL queries is not wrong compared to ORM’s, but always use parameterized queries, no matter if you expect the data to come from your users or not. Never concatenate SQL strings or interpolate variables in them.

  • Validate file uploads in the app: Validate not only the extension but also the content. Files that contain webshells, malware, or other suspicious content should be discarded. Store uploaded files outside of the web root, preferably on another domain, to prevent server-side (like RCE) or client-side (like XSS) attacks.

  • Get rid of unused functionalities: Forgotten modules or outdated running services may be a convenient entry point for the attacker.

  • Finally, subject your apps to regular security audits!

Other Insights

A professional cybersecurity consultant ready to assist with your inquiry.

Any questions?

Happy to get a call or email
and help!