Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Herramientas/GitHubGitHub/hpt-intern-task-submission/cve-2023-39361
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebAprendizaje y EducaciónLabs y Práctica
GitHubhpt-intern-task-submission/cve-2023-39361

CVE-2023-39361

Análisis paso a paso y configuración de laboratorio para CVE-2023-39361, una inyección SQL no autenticada en Cacti v1.2.24, con un recorrido de explotación y mitigación.

Ver Repositorio
3hace 2 añosAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

[CVE-2023-39361] Inyección SQL no autenticada en Cacti v1.2.24

Resumen

Cacti es una herramienta de monitoreo operativo de código abierto escrita en PHP, MySQL/MariaDB, que proporciona una interfaz amigable. La vulnerabilidad fue descubierta en 2023 y afecta a todas las versiones anteriores a la 1.2.24. Este fallo de seguridad radica en la implementación incorrecta al insertar valores en la consulta SQL. Se trata de una vulnerabilidad crítica de inyección SQL que permite a los atacantes modificar la base de datos y también ejecutar código de forma remota.

Configuración del laboratorio:

En este análisis, ejecutaré Cacti en Docker por simplicidad. Primero, crearemos el archivo docker-compose.yml como se muestra a continuación y ejecutaremos el comando docker-compose up -d.

root@kitploit:~
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:

Después de construir el contenedor, podemos acceder a Cacti navegando a http://localhost:80. El siguiente paso es actualizar Cacti a la versión 1.2.24. Puedes descargar el script de actualización aquí: https://pastebin.com/NfRiHLjR. Guarda el archivo en el mismo directorio que el archivo docker-compose.yml y renómbralo a upgrade_cacti.sh. Luego ejecuta estos 2 comandos:

root@kitploit:~
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"

Ahora ya estamos listos; vamos a sumergirnos en el código para ver qué es lo que permite que ocurra un ataque de inyección SQL. El archivo vulnerable es graph_view.php y solo necesitamos el usuario guest para acceder a este archivo, lo que permite que cualquiera pueda explotar esta vulnerabilidad. En este archivo, la función insegura es grow_right_pane_tree().

grow_right_pane_tree

Antes de profundizar en esta función, necesitamos rastrear hacia atrás para saber cómo se ejecuta la función.

root@kitploit:~
<?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);
	}

Del fragmento de código anterior, podemos ver que la función se ejecutará si $tree_id > 0. Todos los pasos se pueden explicar a continuación:

  • Primero, el programa tomará el valor del parámetro action de la entrada del usuario y entrará en una declaración switch/case.

  • En el caso tree_content, el código tomará el parámetro de solicitud node del usuario. A continuación, dividirá la entrada por el carácter -, la guardará en $part y, finalmente, comprobará si tree_anchor aparece en node. Si todas las condiciones coinciden, $tree_id se asignará como el segundo elemento de $part. Por ejemplo, tree_content-1 o 1-2-tree_content serán válidos y el valor de $tree_id se asignará en consecuencia.

Desde este punto, nuestra URL se verá así: http://localhost:80?action=tree_content&node=tree_anchor-1. Ahora es el momento de analizar la función grow_right_pane_tree() para explotar el fallo.

grow_right_pane_tree_analyze

El parámetro vulnerable es rfilter, que se pasa directamente al operando RLIKE dentro de la cláusula WHERE. Lo que lo hace vulnerable es que rfilter está envuelto entre comillas dobles ", aunque el parámetro es verificado por la función html_validate_tree_vars(), que se ve así:

root@kitploit:~
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 ================= */
	// ........
}

El tipo de filtro de rfilter se establece en FILTER_VALIDATE_IS_REGEX y se almacena dentro de $filters; luego, $filter se pasa a la función validate_store_request_vars(). Profundicemos en esta función; esto revelará el resto de nuestro análisis:

root@kitploit:~
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;
	}

La función validate_store_request_vars() es responsable de validar la entrada del usuario. Cuando el tipo de filter es FILTER_VALIDATE_IS_REGEX, la función llama a la función validate_is_regex() con la función preg_match para verificar si nuestra entrada es válida. Según la documentación de PHP:

preg_match() devuelve 1 si el pattern coincide con el subject proporcionado, 0 si no coincide, o false en caso de error.

La declaración if utiliza comparación estricta, lo que significa que solo cuando se produce un error no podemos entrar en la declaración if. El $regex, que es nuestra entrada, está envuelto entre dos comillas simples '. La intención de los desarrolladores es evitar que el usuario inyecte la comilla simple ', que a menudo se utiliza para iniciar un ataque de inyección SQL. Esto parece una implementación de seguridad robusta, pero echemos un vistazo una vez más a cómo se pasa rfilter a la cláusula WHERE:

root@kitploit:~
$sql_where  .=  ' (gtg.title_cache RLIKE "'  .  get_request_var('rfilter') .  '" OR gtg.title RLIKE "'  .  get_request_var('rfilter') .  '")';

Oh-oh, el esfuerzo de validar la entrada del usuario se vuelve inútil, ya que rfilter está envuelto entre comillas dobles ", lo que permite a los atacantes inyectar fácilmente " para escapar de la declaración.

Habiendo entendido por qué es vulnerable, ahora profundizaremos en la consulta en la que se pasa $sql_where.

grow_right_pane_tree_analyze

Una vez que se asigna $sql_where, el programa llamará a la función get_allowed_tree_header_graphs(). La función se verá así:

root@kitploit:~
function  get_allowed_tree_header_graphs($tree_id, $leaf_id = 0, $sql_where = '', $sql_order = 'gti.position', $sql_limit = '', &$total_rows = 0, $user_id = 0) {
	// ......
	if ($sql_where != '') {
		$sql_where = " AND ($sql_where)";
}
	$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
	
	$graphs = db_fetch_assoc("SELECT gti.id, gti.title, gtg.local_graph_id, h.description, gt.name AS template_name, gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id
	FROM graph_templates_graph AS gtg
	INNER JOIN graph_local AS gl
	ON gl.id = gtg.local_graph_id
	INNER JOIN graph_tree_items AS gti
	ON gti.local_graph_id = gl.id
	LEFT JOIN graph_templates AS gt
	ON gt.id = gl.graph_template_id
	LEFT JOIN host AS h
	ON h.id = gl.host_id
	$sql_where
	$sql_order
	$sql_limit");

	$sql = "SELECT  COUNT(*)
	FROM graph_templates_graph AS gtg
	INNER JOIN graph_local AS gl
	ON gl.id=gtg.local_graph_id
	INNER JOIN graph_tree_items AS gti
	ON gti.local_graph_id=gl.id
	LEFT JOIN graph_templates AS gt
	ON gt.id=gl.graph_template_id
	LEFT JOIN host AS h
	ON h.id=gl.host_id
	$sql_where";
	$total_rows = get_total_row_data($user_id, $sql, array(), 'graph');
	return  $graphs;
}

Aquí es donde $sql_where se pasa a una consulta. Ten en cuenta que a la variable se le asignan 2 valores más antes de pasarse: $sql_where = " AND ($sql_where)"; y $sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;. Combinado con la primera asignación, así es como se crea finalmente $sql_where:

root@kitploit:~
$sql_where  .=  ' (gtg.title_cache RLIKE "'  .  get_request_var('rfilter') .  '" OR gtg.title RLIKE "'  .  get_request_var('rfilter') .  '")';
$sql_where = " AND ($sql_where)";
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;

Después de estas 3 asignaciones, $sql_where se verá así:

root@kitploit:~
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id) AND ((gtg.title_cache RLIKE "' . get_request_var('rfilter') . '" OR gtg.title RLIKE "' . get_request_var('rfilter') . '"))";

La consulta en la que se introduce $sql_where parece abrumadora, pero podemos crear otra consulta simple con una estructura similar:

select * from users WHERE (TRUE) AND ((username RLIKE "' get_request_var('rfilter') '" -- something behind that isn't neccessary))

Para inyectar nuestro payload con éxito, necesitamos escapar de " y )). Por lo tanto, este payload podría funcionar: "));SELECT SLEEP(5) --. Nuestro payload se verá así: select * from users WHERE (TRUE) AND **((username RLIKE "'"));SELECT SLEEP(5) --. ¡Se ve bien! Probemos en Cacti

failed_injection_1

La razón por la que el servidor no duerme durante 2 segundos es porque provocamos un error en la función preg_match(). Veamos la función de nuevo

root@kitploit:~
if (@preg_match("'"  .  $regex  .  "'", NULL) !== false) {
ini_set('track_errors', $track_errors);
return  true;
}

Esta función tomará nuestra entrada como regular expression. Y el ) se utiliza para agrupar un conjunto de caracteres. Como no está cerrado, la función devuelve un error. Por otro lado, si probamos "(());SELECT SLEEP(5) --, no podemos escapar de (( en la consulta SQL. Sin embargo, aquí hay un truco, y es "OR"(("));SELECT SLEEP(5) --. Déjame explicarlo: envuelvo (( entre comillas dobles para convertirlo en una cadena en la consulta SQL y uso OR para que coincida con el username anterior. La comilla doble " en la expresión regular, sin embargo, se considera solo un carácter normal, por eso podemos aprovechar esta característica para escapar de la consulta SQL.

successful_injection

Según el tiempo de respuesta, podemos asegurar que inyectamos SQL con éxito en la base de datos. Esta es una vulnerabilidad crítica de inyección SQL. El atacante puede aprovechar este fallo para tomar el control de la cuenta de administrador o incluso ejecutar comandos de forma remota.

Mitigación

Como es habitual, las vulnerabilidades de inyección en general se producen por la ausencia de validación de entrada. Sin embargo, en este caso, los desarrolladores aplicaron validaciones, pero de forma incorrecta. Para solucionarlo, simplemente envolvemos el parámetro rfilter entre comillas simples ' en lugar de comillas dobles ".

fix

Con este sencillo arreglo, el esfuerzo de validar la entrada del usuario ahora funcionará correctamente. Probemos el payload de nuevo para ver si sigue siendo vulnerable.

fixed_request

Con el mismo payload, pero ahora la respuesta es muy rápida, lo que significa que ya no podemos inyectar comandos SQL.


De este análisis del CVE podemos aprender que incluso un pequeño error puede tener un resultado catastrófico. Debemos revisar cuidadosamente el código y probar nuestro sitio web con frecuencia para hacerlo cada vez más seguro. Este es el final del análisis, espero que hayas aprendido algo útil hoy. ¡Feliz hacking!

Descargar herramienta