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:
ends up having its extension switched to “txt”:
We can access the file by simply querying:
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:
Above discloses the version:
One can also list environmental variables this way. This code:
Returns the following output when Smarty renders it on the page:
Environment variables didn’t, however, give us any valuable data, except for the current absolute application path. Fortunately, Smarty supports the “include” keyword:
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:
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:
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:
Sqlmap, in order to execute “SELECT USER();”, suggested:
An HTTP request with this value gave us in the HTML response what we needed – MySQL username and host:
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:
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:
The query worked indeed – after signing out and back in, we have access to administrative modules:
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):
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:
We receive a success message:
And this way we have it – a PHP file in the application directory:
Execution shows that the webshell works. This simple request to the webshell:
Gets us the result of our final target – the ability to issue commands on the application server:
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!
#CyberSecurity #PentestChronicles #PenetrationTesting #NetworkSecurity #Infosec #RCE #PrivilegeEscalation #TechInsights #RealWorldPentest