Resumen
OpenAlias permite pagar a un nombre legible (por ejemplo donate.getmonero.org) en lugar de a una dirección Monero larga: la cartera consulta un registro TXT en DNS y obtiene de ahí la dirección real. La única garantía de que esa respuesta no ha sido manipulada es DNSSEC.
En Monero GUI, el resultado de la resolución llega a la capa QML como una cadena true|dirección o false|dirección, según si DNSSEC validó o no. El problema estaba en cómo se trataba el caso false: si la dirección devuelta era sintácticamente válida, TxUtils.handleOpenAliasResolution() devolvía a la vez un mensaje de advertencia y la dirección. Las páginas de Transferencia y de Libreta de direcciones mostraban el aviso, pero a continuación copiaban response.address en el campo de destinatario sin pedir ninguna confirmación.
La cadena de código implicada, según el investigador (monero-gui en el commit 003e667576f38812e27cbb79125a0ba96036225d de master y monero en el commit 2c48374ecd2449c02bb400e5bcf20b7c6f11649b):
monero/src/common/dns_utils.cpp:418-442:addresses_from_url()marcadnssec_valid = falsesi DNSSEC no está disponible o no valida, pero aun así devuelve las direcciones encontradas en los TXT.monero/src/wallet/api/wallet_manager.cpp:338-344:WalletManagerImpl::resolveOpenAlias()devuelve la primera dirección aunquednssec_validsea falso.monero-gui/src/libwalletqt/WalletManager.cpp:391-396:WalletManager::resolveOpenAlias()lo serializa como"true"/"false"más la dirección.monero-gui/js/TxUtils.js:82-105: conisDnssecValid === "false"eisAddressValid, devuelve{ address: resolvedAddress, message: "..." }.monero-gui/pages/Transfer.qml:430-438ymonero-gui/pages/AddressBook.qml:389-397: muestran el aviso y asignanresponse.addressal destinatario de forma incondicional. En la libreta, además, la dirección queda guardada si el usuario salva el contacto.
Como contraste, la cartera de línea de comandos (monero/src/simplewallet/simplewallet.cpp:501-534) sí muestra el estado de DNSSEC y exige una confirmación explícita antes de usar la dirección.
Clasificación propuesta: CWE-345 (verificación insuficiente de la autenticidad de los datos) y CWE-20 (validación de entrada incorrecta), ya que solo se comprobaba que la cadena tuviese formato de dirección Monero, no que estuviese autenticada.
Pasos de reproducción
La prueba de concepto no hace consultas DNS reales, no abre ninguna cartera ni toca la red Monero. Ejecuta el fichero real js/TxUtils.js de monero-gui dentro de un contexto vm de Node con los objetos globales de QML simulados, de modo que el resolvedor devuelve siempre false|<dirección> (DNSSEC fallido con una dirección válida). Después aplica la misma asignación que hace Transfer.qml. El investigador la incluyó como script independiente y como versión en línea equivalente; aquí se reproduce la versión en línea. En ambas, la dirección del "atacante" es una dirección Monero válida cualquiera, que aquí se ha sustituido por [REDACTADO].
- Situarse en el árbol de código de monero-gui y ejecutar el arnés:
cd /home/peko/monero/monero-gui
node - <<'JS'
const fs = require("fs");
const vm = require("vm");
const source = fs.readFileSync("js/TxUtils.js", "utf8");
const attackerAddress = "[REDACTADO]";
const context = {
console,
qsTr: (text) => text,
appWindow: { persistentSettings: { nettype: "mainnet" } },
walletManager: {
resolveOpenAlias: () => `false|${attackerAddress}`,
addressValid: (address) => address === attackerAddress,
},
};
vm.createContext(context);
vm.runInContext(source, context, { filename: "js/TxUtils.js" });
let recipientField = "donate.getmonero.org";
const response = context.handleOpenAliasResolution(recipientField, "");
console.log("resolver_response=", response);
if (response.message) console.log("warning_shown=", response.message);
if (response.address) recipientField = response.address;
console.log("recipient_after_resolve=", recipientField);
if (recipientField !== attackerAddress || !response.message || !response.address) process.exit(1);
console.log("RESULT: vulnerable autofill reproduced with actual TxUtils.js");
JS
- Comprobar la salida: la función devuelve tanto el aviso como la dirección, y el campo de destinatario, que contenía el alias, acaba con la dirección no autenticada:
resolver_response= {
address: '[REDACTADO]',
message: 'Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed'
}
warning_shown= Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed
recipient_after_resolve= [REDACTADO]
RESULT: vulnerable autofill reproduced with actual TxUtils.js
- Confirmar el flujo en el código de la interfaz:
nl -ba js/TxUtils.js | sed -n '82,105p'
nl -ba pages/Transfer.qml | sed -n '430,438p'
nl -ba pages/AddressBook.qml | sed -n '389,397p'
En un ataque real el escenario sería este:
- La víctima quiere pagar a
merchant.exampleomerchant@examplemediante OpenAlias. - El atacante controla su resolvedor DNS, envenena el DNS de la red local o hace de algún otro modo que la consulta TXT devuelva
oa1:xmr recipient_address=<attacker address>;sin DNSSEC válido. WalletManager::resolveOpenAlias()entregafalse|<attacker address>.handleOpenAliasResolution()devuelve el aviso y la dirección.Transfer.qmlenseña el aviso pero sobrescribe el destinatario con la dirección del atacante.- Si la víctima sigue con la confirmación de envío habitual, el pago va al atacante.
Impacto
Quien pudiera influir en la resolución DNS de la víctima (resolvedor malicioso o comprometido, atacante en la red local, o un dominio sin DNSSEC bajo su control) conseguía que la GUI colocase su propia dirección en el campo de destinatario. Si la víctima no comparaba a mano la dirección larga final y confirmaba el envío, los XMR acababan en manos del atacante, y las transferencias de XMR son irreversibles. Además, si la resolución se hacía desde la Libreta de direcciones y se guardaba el contacto, la dirección falsa quedaba almacenada y ponía en riesgo todos los pagos futuros a ese contacto.
Remediación
El reporte figura como resuelto, pero no detalla el cambio que se aplicó finalmente. La corrección propuesta por el investigador es tratar un DNSSEC inválido o no disponible como un bloqueo del autorrelleno: en ese caso, handleOpenAliasResolution() debe devolver solo el mensaje, sin la dirección:
} else if (isDnssecValid === "false") {
if (isAddressValid) {
return {
message: qsTr("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed"),
};
} else {
return { message: qsTr("No valid address found at this OpenAlias address, but the DNSSEC signatures could not be verified, so this may be spoofed") };
}
}
Como medidas adicionales sugería:
- En
Transfer.qmlyAddressBook.qml, asignarresponse.addresssolo cuando DNSSEC haya validado, sin fiarse de que la dirección tenga un formato correcto. - Si hubiera que admitir OpenAlias sin firmar, mostrar un diálogo de confirmación bloqueante con el alias, la dirección completa y el fallo de DNSSEC, con "cancelar" como opción por defecto.
- Hacer que
WalletManager::resolveOpenAlias()devuelva campos estructurados ({dnssec_valid, address, error}) en lugar de una cadena separada por|. - Añadir pruebas de regresión:
true|dirección válidadevuelve la dirección;false|dirección válidadevuelve solo el aviso y no toca el destinatario;false|dirección inválidadevuelve solo el aviso o error.