
Análise passo a passo e configuração de laboratório para CVE-2023-39361, uma injeção SQL não autenticada no Cacti v1.2.24, com exploração e guia de mitigação.
Cacti é uma ferramenta de monitoramento operacional de código aberto escrita em PHP, MySQL/MariaDB, que fornece uma interface amigável. A vulnerabilidade foi encontrada em 2023 e afetou todas as versões anteriores à 1.2.24. A falha de segurança reside na implementação inadequada ao inserir valores em consultas SQL. Esta é uma vulnerabilidade crítica de injeção SQL que permite que atacantes modifiquem o banco de dados e também executem código remotamente.
Nesta análise, executarei o Cacti no Docker por simplicidade. Primeiro, criaremos o arquivo docker-compose.yml como abaixo e executaremos o comando docker-compose up -d.
version: '3.5'
services:
cacti:
image: "smcline06/cacti"
container_name: CVE-2023-39361
domainname: example.com
hostname: cacti
ports:
- "80:80"
- "443:443"
environment:
- DB_NAME=cacti_master
- DB_USER=cactiuser
- DB_PASS=cactipassword
- DB_HOST=db
- DB_PORT=3306
- DB_ROOT_PASS=rootpassword
- INITIALIZE_DB=1
- TZ=America/Los_Angeles
volumes:
- cacti-data:/cacti
- cacti-spine:/spine
- cacti-backups:/backups
links:
- db
db:
image: "mariadb:10.3"
container_name: CVE-2023-39361_db
domainname: example.com
hostname: db
ports:
- "3306:3306"
command:
- mysqld
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --max_connections=200
- --max_heap_table_size=128M
- --max_allowed_packet=32M
- --tmp_table_size=128M
- --join_buffer_size=128M
- --innodb_buffer_pool_size=1G
- --innodb_doublewrite=ON
- --innodb_flush_log_at_timeout=3
- --innodb_read_io_threads=32
- --innodb_write_io_threads=16
- --innodb_buffer_pool_instances=9
- --innodb_file_format=Barracuda
- --innodb_large_prefix=1
- --innodb_io_capacity=5000
- --innodb_io_capacity_max=10000
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- TZ=America/Los_Angeles
volumes:
- cacti-db:/var/lib/mysql
volumes:
cacti-db:
cacti-data:
cacti-spine:
cacti-backups:
Após construir o container, podemos acessar o Cacti navegando para http://localhost:80. O próximo passo é atualizar o Cacti para a versão 1.2.24. Você pode baixar o script de atualização aqui: https://pastebin.com/NfRiHLjR. Salve o arquivo no mesmo diretório do arquivo docker-compose.yml e renomeie-o para upgrade_cacti.sh. Em seguida, execute estes 2 comandos:
docker cp upgrade_cacti.sh CVE-2023-39361:/tmp/upgrade_cacti.sh && docker exec -it CVE-2023-39361 bash -c "bash /tmp/upgrade_cacti.sh"
Agora estamos prontos, vamos mergulhar no código para ver o que aconteceu que nos permite ter um ataque de injeção SQL. O arquivo vulnerável é graph_view.php e precisamos apenas do usuário convidado para acessar este arquivo, permitindo que qualquer pessoa explore esta vulnerabilidade. Neste arquivo, a função insegura é grow_right_pane_tree().

Antes de mergulharmos profundamente nesta função, precisamos rastrear para trás para saber como as funções são executadas.
<?php
switch (get_nfilter_request_var('action')) {
//....
// Many other cases
case 'tree_content':
//..... Some code here
// ---------
if (isset_request_var('node')) {
$parts = explode('-', sanitize_search_string(get_request_var('node')));
// Check for tree anchor
if (strpos(get_nfilter_request_var('node'), 'tree_anchor') !== false) {
$tree_id = $parts[1];
$node_id = 0;
}
//..... Some code here
if ($tree_id > 0) {
if (!is_tree_allowed($tree_id)) {
header('Location: permission_denied.php');
exit;
}
// Vulnerable function is called here
grow_right_pane_tree($tree_id, $node_id, $hgdata);
}
A partir do trecho de código acima, podemos ver que a função será executada se $tree_id > 0. Todos os passos podem ser explicados abaixo:
Primeiro, o programa pegará o valor do parâmetro action da entrada do usuário e entrará em uma estrutura switch/case.
No caso tree_content, o código pegará o parâmetro de requisição node do usuário. Em seguida, dividirá a entrada pelo caractere -, salvará em $part e finalmente verificará se tree_anchor aparece em node. Se todas as condições corresponderem, $tree_id será atribuído como o segundo elemento em $part. Por exemplo, tree_content-1 ou 1-2-tree_content serão válidos e o valor de $tree_id será respectivamente.
A partir deste ponto, nossa URL será assim: http://localhost:80?action=tree_content&node=tree_anchor-1. Agora é hora de analisar a função grow_right_pane_tree() para explorar o bug.

O parâmetro vulnerável é rfilter que é passado diretamente para o operando RLIKE dentro da cláusula WHERE. O que o torna vulnerável é que rfilter é envolvido entre aspas duplas ", porém o parâmetro é verificado pela função html_validate_tree_vars(), que se parece com isso:
function html_validate_tree_vars() {
/* ================= input validation and session storage ================= */
$filters = array(
// .......
'rfilter' => array(
'filter' => FILTER_VALIDATE_IS_REGEX,
'pageset' => true,
'default' => '',
),
// ..........
);
validate_store_request_vars($filters, 'sess_grt');
/* ================= input validation ================= */
// ........
}
O tipo de filtro de rfilter está definido como FILTER_VALIDATE_IS_REGEX e armazenado dentro de $filters, então $filter é passado para a função validate_store_request_vars(). Vamos mergulhar nesta função, isso revelará o restante da nossa análise:
function validate_store_request_vars($filters, $sess_prefix = '') {
// .......
if (cacti_sizeof($filters)) {
foreach($filters as $variable => $options) {
// ....
elseif ($options['filter'] == FILTER_VALIDATE_IS_REGEX) {
if (is_base64_encoded($_REQUEST[$variable])) {
$_REQUEST[$variable] = base64_decode($_REQUEST[$variable]);
}
$valid = validate_is_regex($_REQUEST[$variable]);
if ($valid === true) {
$value = $_REQUEST[$variable];
} else {
$value = false;
$custom_error = $valid;
}
}
// ........
function validate_is_regex($regex) {
// ........
if (@preg_match("'" . $regex . "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return true;
}
A função validate_store_request_vars() é responsável por validar a entrada do usuário. Quando o tipo de filter é FILTER_VALIDATE_IS_REGEX, a função chama a função validate_is_regex() com a função preg_match para verificar se nossa entrada é válida. De acordo com a documentação do PHP:
preg_match() retorna 1 se o
patterncorresponder aosubjectfornecido, 0 se não corresponder, ou false em caso de falha.
A instrução if usa comparação estrita, o que significa que apenas quando ocorre um erro não podemos entrar na instrução if. O $regex, que é nossa entrada, é envolvido entre duas aspas simples '. A intenção dos desenvolvedores é evitar que o usuário injete aspas simples ', que são frequentemente usadas para iniciar um ataque de injeção SQL. Isso parece ser uma implementação de segurança robusta, mas vamos dar uma olhada novamente em como o rfilter é passado para a cláusula WHERE:
$sql_where .= ' (gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '")';
Ah-oh, o esforço de validar a entrada do usuário se torna inútil, pois o rfilter é envolvido entre aspas duplas ", o que permite que atacantes injetem facilmente " para escapar da declaração.