Apache Spark 4.2.0 - Alta - Remota - Escritura de archivos - History appStorePath App ID no saneado
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-history-store-path
Clase: Escritura de archivos (condicionada por configuración) Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
HistoryServerDiskManager.appStorePath hace new File(appStoreDir, appId + extension) sin sanitizar. Utils.sanitizeDirName es solo para nombres de archivo del registro de eventos. El App ID del JSON es asText(). El listado almacena event.appId.orNull. Lease.commit usa esa cadena. El App ID ../ escribe un .rdb fuera de spark.history.store.path. El valor predeterminado no establecido de store.path es en memoria (sin escritura). El contenido es un archivo de almacén, no RCE.
| ID | sin CVE todavía |
| Clase | Escritura de archivos (almacén en disco del History condicionado por configuración) |
| Alcance | Remoto (escritor del registro de eventos más History con spark.history.store.path establecido; ACLs de la UI desactivadas por defecto) |
| CWE | CWE-22 |
| CVSS | Alto: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N cuando spark.history.store.path está establecido |
| Producto | Apache Spark History Server |
| Afectado | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Autenticación | escritor de spark.history.fs.logDirectory (por defecto file:/tmp/spark-events); History debe tener spark.history.store.path establecido |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | 127.0.0.1:18990 únicamente. Gadget del join citado. Testigo .rdb fuera de store.path. |
Mismo modelo de víctima que CVE-2025-54920: History Server apuntando a un directorio de registros en el que el atacante puede escribir. Jackson ya no construye gadgets de la clase HiveConnection. La ruta del almacén en disco todavía confía en el App ID del JSON de eventos.
| Método | Sink | Privilegio | Laboratorio vs inferido |
|---|---|---|---|
Plantar un registro de eventos cuyo App ID en JSON sea ../<name> (o ../../oracle/...). Esperar el escaneo. GET que carga la UI de la app / getAppUI / loadDiskStore / createDiskStore | HistoryServerDiskManager.appStorePath luego Lease.commit renameTo | Integridad: crear una ruta .rdb (RocksDB por defecto) o .ldb (LevelDB) fuera de {store.path}/apps como el UID de History. Probado en laboratorio sobre el gadget: escaped=yes, el archivo /work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb contiene el testigo. El contenido es un archivo de almacén, no bytecode del host elegido por el atacante, no un shell. | Probado en laboratorio (escritura del gadget de bytes testigo en la ruta escapada). El renameTo completo de History de un directorio RocksDB es el mismo join. |
| Mismo App ID cuando el destino escapado ya es un directorio | Lease.commit if (dst.isDirectory()) deleteStore(dst) luego Utils.deleteRecursively | Disponibilidad: eliminar un directorio existente cuyo nombre ya termina en el sufijo del backend, luego reemplazarlo con el almacén arrendado. | Inferido (fijar HistoryServerDiskManager.scala commit). El gadget escribió un archivo, no llamó a deleteRecursively. |
spark.history.store.path predeterminado no establecido | LOCAL_STORE_DIR createOptional; diskManager = storePath.map | Cerrado. Listado en memoria. Sin HistoryServerDiskManager. Probado en laboratorio gated=yes written=no. | Probado en laboratorio |
Aplicar Utils.sanitizeDirName al App ID primero | helper del nombre de archivo del registro de eventos | Permanece dentro de apps/. / y : se convierten en -. Probado en laboratorio negativo. | Probado en laboratorio |
Controlar el App ID local-control con store.path establecido | mismo join | Escribe {store.path}/apps/local-control.rdb. Permanece dentro de apps/. | Probado en laboratorio |
Adyacente cerrado: almacén predeterminado no establecido (en memoria). Jackson sparkEventFromJson todavía requiere SparkListenerEvent (CVE-2025-54920). El fallback de URL getAppUI eventlog_v2_ + nameForAppAndAttempt sí está sanitizado. La clave KV del listado no es una ruta del sistema de archivos. El mapper de HybridStore / kvstore no tiene Id.CLASS. History HTTP no acepta una ruta de carga. Compactación desactivada por defecto. Cleaner desactivado por defecto. Envío de trabajos en un clúster abierto (SECURITY.md FAQ). Carga de modelos ML (docs/ml-security.md). Este remanente no es RCE del host.
Fijé Spark 4.2.0, no master, no un RC de 4.3. Veintiocho búsquedas. El RCE público de Jackson en History está cerrado en este peel. Los escritores del registro de eventos ya llaman a Utils.sanitizeDirName por lo que el archivo de registro no puede contener /. El nombre del archivo de caché en disco usa el App ID sin procesar.
private[history] def appStorePath(appId: String, attemptId: Option[String]): File = {
val fileName = appId + attemptId.map("_" + _).getOrElse("") + extension
new File(appStoreDir, fileName)
}
appStoreDir es {spark.history.store.path}/apps. En Unix new File(parent, "../../oracle/name.rdb") resuelve fuera del padre. JsonProtocol.applicationStartFromJson toma json.get("App ID").asText(). AppListingListener.onApplicationStart almacena event.appId.orNull. lease.commit(appId, attempt.info.attemptId) alimenta esa cadena a appStorePath.
Caminos equivocados ya registrados: tratar el store.path predeterminado no establecido como una escritura (está en memoria); esperar que new File(dir, "/abs") de Unix ignore el padre (no lo hace; el remanente es ..); afirmar RCE porque el sufijo es .rdb; arrancar un History Server completo cuando el join es el oráculo; tratar sanitizeDirName como si cubriera esta ruta.
python3 spark-history-store-path-Abraxas-Labs.py
o
cd lab
./run.sh
Gadget de HistoryServerDiskManager.appStorePath en 127.0.0.1:18990. Extractos fijados bajo lab/pin/. No se arranca el History Server completo. Enlázalo a loopback.
SUCCESS SPARK-HISTORY-STORE-PATH how=gadget escaped=yes gated=yes outside=/work/oracle/SPARK-HISTORY-STORE-PATH-WITNESS.rdb SPARK-HISTORY-STORE-PATH-WITNESS
Ejecutar Utils.sanitizeDirName (o rechazar .. / / / NUL) sobre appId y attemptId dentro de appStorePath antes de new File. Tras el join, exigir que dst.getCanonicalFile() permanezca bajo appStoreDir.getCanonicalFile(). No establecer spark.history.store.path sigue siendo el valor predeterminado seguro.