From SQLi to PHP deserialize to RCE on Pandora FMS 742
Welcome to my blog, dedicated to unraveling the intricated world of PHP vulnerabilities! Today, I’m thrilled to share my latest research findings on Pandora FMS 742, a prominent network monitoring tool. In my pursuit of enhancing the security landscape, I’ve meticulously analyzed the critical code vulnerabilities inherent in this widely-used software.
I Introducing Pandora FMS 742
- Pandora FMS is a monitoring software that collects data from any system, generates alerts based on that data and shows graphs, reports and maps of our environment. There are two versions of Pandora FMS: a free or Open Source version and a paid or Enterprise version, available starting from 100 devices.
II Building the environment
To Install Pandora FMS, you can refer directly to Pandora FMS Documentation Installing [Pandora FMS Documentation] or follow the installation guide for Ubuntu from the following links:
- How To Install Pandora FMS Monitoring Tool in Ubuntu 18.04 (tecmint.com)
- source: Pandora FMS: Flexible Monitoring System - Browse /Pandora FMS 7.0NG/742/Debian_Ubuntu at SourceForge.net
III Exploitation
1. SQL Injection (pre authentication) (CVE-2021-32099)
- The vulnerability originates from the application’s failure to validate the input values from users in the
include/chart_generator.php:
|
|
- At line 2, the
session_id
value is controlled by the attacker. Let’s delve deeper into the constructor function of thePandoraFMS\User
class:
|
|
-
At lines 12-15, the
phpsessionid
value will be used as an argument for thedb_get_row_filter()
function. The output of thedb_get_row_filter()
function is saved in theinfo
variable and$info['data']
will be deserialized using thesession_decode()
fuction. Let’s delve deeper into thedb_get_row_filter
function:1 2 3 4 5 6 7 8
function db_get_row_filter($table, $filter, $fields=false, $where_join='AND', $historydb=false) { global $config; switch ($config['dbtype']) { case 'mysql': return mysql_db_get_row_filter($table, $filter, $fields, $where_join, $historydb); ...
-
Because the database being used is MariaDB-mysql, let’s delve deeper into the
mysql_db_get_row_filter
function at line 7:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
function mysql_db_get_row_filter($table, $filter, $fields=false, $where_join='AND', $historydb=false) { if (empty($fields)) { $fields = '*'; } else { if (is_array($fields)) { $fields = implode(',', $fields); } else if (! is_string($fields)) { return false; } } if (is_array($filter)) { $filter = db_format_array_where_clause_sql($filter, $where_join, ' WHERE '); } else if (is_string($filter)) { $filter = 'WHERE '.$filter; } else { $filter = ''; } $sql = sprintf('SELECT %s FROM %s %s', $fields, $table, $filter); return db_get_row_sql($sql, $historydb); }
-
Yeah, it indicates that the program will construct a SQL query. It is easy to observe the value of SQL query by using a sample payload and debugging program.
- The used payload is
- The value of SQL query by debugging:
- Đây là dấu hiệu của SQLi, bây giờ chúng ta thử truyền vào những kí tự break xem sao
- This is a signature of a SQL Injection attack. Now, we will use a payload with some ‘break characters’ and observe the results:
- It doesn’t have any function to filter my payload! Nice!
- This following payload will make the WHERE clause always true and add a
#
to comment out any characters following it, if the program allows it.
1
xxxx' or 1=1 #
- Going deeper into the
db_get_row_sql
, we can see:
1 2 3 4 5 6 7 8 9 10 11
function db_get_row_sql($sql, $search_history_db=false) { global $config; switch ($config['dbtype']) { case 'mysql': return mysql_db_get_row_sql($sql, $search_history_db); break; case 'postgresql': return postgresql_db_get_row_sql($sql, $search_history_db);
- This program calls the
mysql_db_get_row_sql($sql, $search_history_db);
function since it utilizes the MYSQL database.
-
The following code represents the definition of
mysql_db_get_row_sql
at line 7:
|
|
- This function adds the string
' LIMIT 1'
to retrieve the first value. It executes thedb_get_all_rows_sql($sql, $search_history_db)
function, saves the output to the$result
array and returns theresult[0]
. - I tried sending my payload and retrieving data directly from MySQL to observe the results:
- It is clear that sessions that did not successfully log in will a have null value in the ‘data’ field, while sessions with successful logins will have data stored in this field. So we can use this payload to exploit
|
|
-
POC
2. FUNNY: PHP deserialization attack via session_decode()
function
a. Session_decode()
- In the blog post, there is a description as follows:
Note that the function
session_decode()
is capable of deserializing arbitrary objects similar to the functionunserialize()
. This means that an attacker could deserialize arbitrary objects via the SQL Injection and this can be another attack vector.
- Let’s examine the code in
__construct()
function ofinclude/lib/User.php
|
|
- At line 19, we can see:
|
|
-
The value of
$info['data']
is passed tosession_decode()
function. Below is an example of a valid value stored in the database:1 2
id_usuario|s:5:"admin";alert_msg|a:0:{}new_chat|b:0 id_usuario|s:5:"admin";alert_msg|a:0:{}new_chat|b:0;csrf_code|s:32:"e7c2afc1ece9d83b21ca017a98586068"
-
These values are in the form of serialized strings. So, the attack vector here would be as follows:
- The attacker utilizes SQL injection to insert exploit code into
$info['data']
. - The program will load the content provided by the attacker and trigger the payload in the
session_decode($info['data'])
function.
- The attacker utilizes SQL injection to insert exploit code into
-
One advantage of setting up a debug environment is that we can modify the values of variables as desired before proceeding with the next command. Here, I will make direct modifications during the debug process to verify.
-
In order for the attacker to insert data into the SQL, we have two approaches:
- How to customize the payload into an insert data statement, as our query statement has the following format:
1
"SELECT * FROM tsessions_php WHERE `id_session` = '<payload_injected>' LIMIT 1;"
- Locate the location where the SQL saves the session data into the database and modify it.
-
After about an hour of searching and thinking, I realized that I had been going in the wrong direction 😟. Actually, we don’t need to insert data directly into the database, but rather manipulate the query in a way that it returns the value we control. If it seems a bit confusing, take a look at the following query:
|
|
-
The above query uses the
UNION
operator to combine two sets of data. In this case, the first part of theWHERE
clause is always false, resulting in a null value. However, the second part of the query returns the desired values:'1', 0, 'xxx'
.By ultilzing the
UNION
opertor, we can merge our own data with the original query’s result set. This allows to control the returned values and manipulate the query’s outcome to our advantage. -
By doing this, I have successfully gained control over the value passed to the
session_decode()
function. Now we can move on the next step, which is building the exploit.
b. Building a POP chain.
a. Methology:
- Identify magic methods within classes that can be called by
session_decode
. - Search for existing chains related to Pandora FMS on the internet and customize them.
- Utilize chains from other frameworks in PHP-GCC as inspiration.
- New idea:
- Invoke functions, use SQL to bypass authentication, and use
session_decode()
to modify properties within classes.
- Invoke functions, use SQL to bypass authentication, and use
Building a pop chain
One challenge encountered when building a pop chain is not knowing which classes have been instantiated and can be invoked at runtime.
⇒ Solution: Use console debug or print statements to identify the instantiated classes
|
|
- By using the above method, we have obtained a list of classes that can be invoked
|
|
-
I have found an unauthenticated endpoint, but I haven’t been able to control the path:
1 2 3 4 5 6 7 8 9
GET /pandora_console/include/Image/image_functions.php?getFile=/tmp/test.png&file=tumb_600x400_test.png&thumb=edisc&thumb_size=600x400 HTTP/1.1 Host: 192.168.159.135 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/112.0 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 DNT: 1 Connection: close Upgrade-Insecure-Requests: 1
-
When searching on the internet, I came across a paper discussing it : https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
If we have complete control of a string within the $_SESSION array which occurs within the first 100
bytes we can use a similar technique to cause the session file to be a valid Phar/Tar archive -
To gain a better understanding, I would recommend asking ChatGPT for further clarification 😉
The session data in PHP is typically stored on the server-side as files, which are created and managed by PHP’s session handler. By default, these files are stored in a directory specified by the
session.save_path
directive in thephp.ini
configuration file.In some cases, an attacker may be able to manipulate the session data to execute arbitrary code on the server. One such attack involves abusing the fact that PHP’s
session_start()
function automatically unserializes the session data stored in the session file.If an attacker has complete control over a string within the first 100 bytes of the session data, they can use a technique called “phar deserialization” to create a valid Phar/Tar archive within the session file. This can be accomplished by crafting a string that contains a serialized Phar object, followed by arbitrary data that will be interpreted as the contents of the Phar archive.
When the session data is unserialized by PHP, the Phar object will be deserialized and the contents of the archive will be written to disk. This can allow an attacker to write arbitrary files to the server, which could be used to execute arbitrary code or perform other malicious actions.
To protect against this attack, it is important to properly sanitize and validate all user input, and to avoid using user-controlled data within the session data. Additionally, it is recommended to set the
session.serialize_handler
directive to a value other thanphp
(such asigbinary
), which can provide additional protection against deserialization attacks. -
The phpgcc tool provides a chain to exploit the vendor
Swiftmailer
, which Pandora is using. We can build an exploit chain using it. However, one issue we encountered is that thesession_decode()
function does not loadSwiftmailer
, causing the classes and objects of that class to be unreadable, resulting in the payload being unable to execute.
- After a period of research, I discovered that the payload passed into the session is stored in the database. Each time the website is accessed, the session is loaded and decoded. At that point, our payload will be deserialized. With this mindset, after injecting the session into the database, we only need to access a location that loads the vendor to exploit it. In this case, it is the path
ws.php
.
For more details about the deserialization vulnerability in the Swiftmailer vendor, you can refer to the following link: https://github.com/CFandR-github/advisory/blob/main/phpmailer_rce_poi/phpmailer_unserialize_rce_0day.md
Sure, let’s proceed with building the payload and exploit. One thing to note here is that there is a difference between the exploitation payload for this context and the payload used in PHPGCC.
Additionally, there are differences in the construction of the serialized string with various properties.
For the serialization of objects, the data serialized by member variables under different permission modifiers is also different:
1 2 3 4 5 6 7 8 9
<?php class TestClass { PermissionDecorator $testMember; public function __construct($t) { $this->testMember = $t; } } $data = new TestClass("edisc12");
public:
1
"O:9:"TestClass":1:{s:10:"testMember";s:7:"edisc12";}"
protected:
1
"O:9:"TestClass":1:{s:13:"\x00*\x00testMember";s:7:"edisc12";}"
private:
1
"O:9:"TestClass":1:{s:21:"\x00TestClass\x00testMember";s:7:"edisc12";}"
- Below is my exploit code:
exploit
|
|