https://images.pexels.com/photos/5483149/pexels-photo-5483149.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1

Introduction

Discovering a CVE was always an idea that enticed me, but I had no idea how to achieve it. Encountering the authentication bypass that I will explain in this article was both unexpected and deeply inspiring. In my preparation for the OSWE, I decided to practice identifying the exam vulnerabilities in real, open-source applications, which would both give me practice for the exam and enable me to contribute to the security community. The first of these applications is the subject of this article.

Prerequisite Knowledge

  • A basic understanding of scripting languages and web applications

Setup

I decided to audit Macs Framework v1.14f. The reasoning behind this target choice is that I wanted to gain familiarity with analyzing large code bases before challenging myself with a more modern target in order to establish a robust methodology. Additionally, Macs CMS seemed like a target that was compatible with the machines used in the OSWE certification exams and the additional preparation would increase my chance of passing on the first attempt.

My first attempt to launch the web application locally was unsuccessful. I was unable to install the deprecated PHP5 version on my host OS. As a solution, I decided to run the application in a Docker container. I made a Dockerfile in the Application/ folder of the source code. I was unable to find documentation or online resources as to the process of configuring this CMS, so after some tweaking, I composed the following Dockerfile:

FROM php:5.6-apache  
COPY . /var/www/html/  
WORKDIR /var/www/html  
  
# add working apt sources  
RUN echo deb http://archive.debian.org/debian/ stretch main > /etc/apt/sources.list  
RUN echo deb http://archive.debian.org/debian-security/ stretch/updates main >> /etc/apt/sources.list  
  
# install mysql  
RUN apt-get update && \  
 apt-get install -y mysql-server && \  
 apt-get clean  
  
# remove symlinks on error logs  
RUN unlink /var/log/apache2/error.log && \  
 unlink /var/log/apache2/access.log  
  
# remove .htaccess file because it isn't needed and it caused issues  
RUN rm /var/www/html/.htaccess  
  
# change ownership of web files to www-data  
RUN chown www-data /var/www/html -R && \  
 chmod 755 /var/www/html -R  
  
# start mysql and apache2  
RUN service mysql restart  
RUN service apache2 restart

I then built the new image and ran it as a container named macs-cms.

cd Macs-CMS/Application  
sudo docker build -t macs-cms .  
sudo docker run -it -exec --name macs-cms macs-cms /bin/bash

After these commands were complete, I was able to visit the live CMS at <http://172.17.0.2>.

Finding CVE-2023–43154

My first encounter with the source code aimed to identify key metadata that would provide context as to which vulnerabilities to look for. By briefly browsing the project in VSCodium’s project explorer and running enumeration commands on the Docker container, I was able to deduce the following information:

  • Programming language: PHP 5.6
  • Architecture: Model-View-Controller (MVC)
  • Routes: Organized with the following pattern: /<controller_name>/<function>
  • Database: MariaDB 10.1.48
  • Operating System: Debian GNU/Linux 9

Informed on the technology stack, I was able to focus my research on vulnerabilities that are common in PHP and MariaDB, one of which being PHP type confusion.

I proceeded with navigating the web application and monitoring my HTTP traffic through my BurpSuite web proxy while taking notes on details that I deemed interesting for future vulnerability analysis. In particular, I honed in on the authentication functionality because of its potential to be high severity.

Upon my login attempt as admin, I noticed the following traffic:

Intercepted login request

The URI, excluding the prefix index.php, was /main/cms/login. My interpretation of this route is that the mainController is initially called, which loads to the CMS plugin with $this->cms = $this->loadPlugin('CMS');. Then, the login() function is invoked within the CMS plugin with access to the HTTP POST data, which is globally accessible.

I visit the login() function within the CMS plugin located at plugins/CMS/controllers/CMS.php to discover that in order for the user to log in successfully, they must already be logged in as admin or the method isValidLogin() must return true.

Since the return value of the function shown above must the true and the value returned is stored in the variable $loggedIn, I annotated the occurrences of $loggedIn to highlight the execution flow that must be triggered in order to achieve the intended return value. From here, I began to work backwards.

For $loggedIn to be set to true, a conditional statement must be met. Luckily, I noticed that a loose comparison was being made for both of the comparisons that can lead to the value being set to true. This article will not be covering PHP loose comparison vulnerabilities in-depth, however, I will give a high-level overview of the relevant details of loose comparison and more detail can be found at this resource.

Loose Comparison Logic

In PHP, loose comparisons refer to the use of two equal signs (==). This differs from a strict comparison (===) in the way that two operands are compared. A loose comparison will attempt to interpret the operands and, if deemed applicable, convert them to a data type that allows for improved compatibility between them.

The following example illustrates the difference between a strict and loose comparison in PHP. I make two comparisons between the string “0e3264578” and the integer 0. The first is a loose comparison and the second is strict. The result of the comparison between the string and integer then outputs whether the comparison returned true or false.

<?php  
if ("0e3264578" == 0) {  
 echo "Loose comparison returns True\n";  
} else {  
 echo "Loose comparison returns False\n";  
};  
  
if ("0e3264578" === 0) {  
 echo "Strict comparison returns True";  
} else {  
 echo "Strict comparison returns False";  
};  
?>

I save the above program as test-comparison.php and execute the code to discover that the first, loose comparison returned true and the second, strict comparison returned false.

To understand why this is the case, it is important to understand what PHP is doing during a loose comparison like the one in this example. An integer followed by the letter e and additional digits is interpreted by PHP as an integer raised to an exponential power. In this case, “0e3264578” was interpreted as 0 raised to the 3264578 power. Of course, 0³²⁶⁴⁵⁷⁸ is equal to 0, hence the return value of true when comparing “0e3264578” to 0.

This did not return true in the strict comparison because PHP was not interpreting the string as an integer like it was in the loose comparison. It took the operands for their literal value, which was a string and an integer of different values, which are not the same.

This can be abused in the context of a login form where two password hashes that are both 0-like such as “0e123” and “0e345” can be loosely compared and result in a true outcome since 0¹²³ and 0³⁴⁵ both equal the same thing: 0. As a result, PHP would signal that the hashes are equivalent, allowing for a successful login, when they are not.

Formulating the Exploit

To exploit this vulnerability, I needed to fully understand what the input in isValidLogin() consists of. It is evident in the login() function that there is manipulation done on the received password before it is passed to the isValidLogin() function for comparison with previously stored credentials.

Revisiting the initial function call, it appears that the user-controlled parameter password is being passed through an additional method called encrypt() before being passed to isValidLogin().

$this->isValidLogin(Post::getByKey('username'), $this->encrypt(Post::getByKey('password')) )

Visiting this function reveals that it simply returns an unsalted MD5 hash of the parameter passed into it, in this case, password.

I did more digging to uncover that the password saved in the database was also hashed. This means that in the isValidLogin() function, the password comparison is made between two MD5 hashes like the following:

$account['password'] == $password // -> md5(user\_inputted\_password) == stored\_md5\_password\_hash

As explained before, in PHP, “0e123..” will be interpreted as 0 in a loose comparison. Hashes that follow this format (0e followed by digits) are known as magic hashes. Theoretically, if we were to compare two magic hashes together, the result would be true and the $loggedIn variable would subsequently be set to true and returned to the login() function resulting in a success authentication.

I will first verify this with a sample PHP script that compares two different zero-like strings.

<?php  
if ("0e123123123" == "0e456456456") {  
 echo "Comparison returns True\n";  
} else {  
 echo "Comparison returns False\n";  
};  
?>

As expected, executing the script results in a true comparison.

Now, all that is left to do is reproduce this in the CMS application. Using my cheat sheet of magic hashes, I identify two magic hashes to use:

fh70QgaGIfYM:0e564472166873750526572156675923  
hello10672785079:0e859173238273273455651853557908

I create an test user called test-admin with the first password fh70QgaGIfYM. The intercepted request can be seen in BurpSuite on the right.

Finally, for the moment of truth, I log out of my admin account and try to log in with the second password hello10672785079. The resulting request is shown below.

The intercepted response of my login attempt returns a status code of 200 and redirects me to the home page, indicating a successful login.

With my new administrator privileges, I can change configuration information about the site including the password of the other administrator.

Conclusion

I was able to log in to an administrator account using one of many incorrect passwords that work to bypass authentication. This opened the door to potential breaches in confidentiality, integrity, and availability.

As stated in my original PoC, there are limitations to this since the admin password hash must already be a 0-like string in PHP and the username must be previously known, 0-like, or easy to guess. The username can be enumerated on the platform through other means, so this is not as big of an issue.

Thank you to everyone who read this far and I hope that you found value in this article!