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
Análisis: https://abraxaslabs.tech/research/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 ejecuta new File(appStoreDir, appId + extension) sin sanitizar. Utils.sanitizeDirName solo se usa para los nombres de archivo del registro de eventos. El App ID del JSON se obtiene con asText(). El listado almacena event.appId.orNull. Lease.commit usa esa cadena. Un App ID ../ escribe un .rdb fuera de spark.history.store.path. El valor predeterminado sin definir de store.path es en memoria (sin escritura). El contenido es un archivo de almacén, no RCE.
| ID | aún sin CVE |
| 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 definido; las ACL 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á definido |
| Producto | Apache Spark History Server |
| Afectado | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) |
| Autenticación | escritor de spark.history.fs.logDirectory (predeterminado file:/tmp/spark-events); History debe tener spark.history.store.path definido |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1:18990. 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 sigue confiando 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 History UID. 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 de host elegido por el atacante, no una shell. | Probado en laboratorio (escritura del gadget de los bytes testigo en la ruta escapada). El renameTo completo del History de un directorio RocksDB es la misma unión. |
| 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 sin establecer por defecto | 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 -. Negativo probado en laboratorio. | Probado en laboratorio |
Controlar el App ID local-control con store.path establecido | misma unión | Escribe {store.path}/apps/local-control.rdb. Permanece dentro de apps/. | Probado en laboratorio |
Adyacente cerrado: almacén sin establecer por defecto (en memoria). Jackson sparkEventFromJson aún requiere SparkListenerEvent (CVE-2025-54920). El fallback de URL getAppUI eventlog_v2_ + nameForAppAndAttempt sí está saneado. La clave KV del listado no es una ruta del sistema de archivos. El mapper de HybridStore / kvstore no tiene Id.CLASS. El HTTP del History no acepta una ruta de carga. La compactación está desactivada por defecto. El limpiador está 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 el History está cerrado en esta capa. 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") se resuelve fuera del padre. JsonProtocol.applicationStartFromJson toma json.get("App ID").asText(). AppListingListener.onApplicationStart almacena event.appId.orNull. lease.commit(appId, attempt.info.attemptId) pasa esa cadena a appStorePath.
Errores ya registrados: tratar el store.path predeterminado no establecido como una escritura (está en memoria); esperar que en Unix new File(dir, "/abs") ignore el padre (no lo hace; el residuo es ..); afirmar RCE porque el sufijo es .rdb; arrancar un History Server completo cuando la unión 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. Fije los extractos en lab/pin/. El History Server completo no está iniciado. Vincúlelo 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 la unión, exigir que dst.getCanonicalFile() permanezca bajo appStoreDir.getCanonicalFile(). Dejar spark.history.store.path sin establecer sigue siendo el valor predeterminado seguro.