
Apache Spark 4.2.0 - Alta - Remoto - Privilégio remanescente - mapa global de processo do Python Data Source
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · spark-python-ds-static
Write-up: https://abraxaslabs.tech/research/spark-python-ds-static
Classe: Privilégio residual Alcance: Remoto
Apache Spark 4.2.0 (32f72996) - Apache Software Foundation
DataSourceManager.dataSourceBuilders é um mapa JVM-global em que o primeiro vence. O SPARK-45917 registra automaticamente Data Sources Python pyspark_* a partir do sys.path do worker de lookup. O isolamento de sessão do Connect mantém os artefatos em SparkFiles/<sessionUUID>/, e o registerPython por sessão permanece em runtimeDataSourceBuilders. O primeiro lookup ainda coloca o diretório SparkFiles da sessão que dispara no sys.path, serializa (pickle) cada DefaultSource e armazena essas classes em cache no objeto companheiro. Depois, um SQL do Connect (ou SQL do Hive Thrift) read.format(name) / USING name executa essa classe como o UID do Spark sem enviar proto Python.
O plantador é um tenant Python do Connect que já tem permissão de Python. A vítima é um tenant somente-SQL no mesmo driver do Connect. Os aliases Java (parquet, csv, json, orc, jdbc) permanecem no caminho do ServiceLoader Java. Plantios posteriores, depois que o mapa está preenchido, não são atualizados. AC:H é o primeiro lookup mais um nome que não seja um alias Java.
| ID | ainda sem CVE |
| Classe | Privilégio residual (Data Source Python entre sessões como UID do Spark; não é envio de job sem autenticação) |
| Alcance | Remoto (driver Spark Connect compartilhado) |
| CWE | CWE-269, CWE-502 |
| CVSS | Alto: 8.8 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Produto | Apache Spark |
| Afetado | 4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8) registro estático de Data Source Python |
| Autenticação | Tenant Python do Connect (plantador) mais um usuário posterior somente-SQL do Connect / SQL do Hive Thrift (vítima) |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1:18970. Arquivo testemunha. Gadget do mapa companheiro. |
Manter uma sessão Python do Spark Connect em um driver compartilhado. Plantar pyfiles/pyspark_<name>.py cujo DefaultSource.name() seja um nome curto de conector ausente (delta, iceberg, hudi, ...). Disparar o primeiro lookup de Data Source Python nessa sessão. Toda sessão posterior no mesmo JVM que fizer read.format(name) ou SQL USING name instancia a classe serializada como o UID do Spark no worker do planejador do driver e nos executores.
Esse é o conjunto completo de privilégios. O plantador já executa Python. O residual é que um tenant somente-SQL, que nunca enviou proto de REGISTER_DATA_SOURCE / UDF Python, ainda executa essa classe.
| Método | Destino | Privilégio |
|---|---|---|
Plantador: AddArtifacts com nome pyfiles/pyspark_<name>.py e então primeiro format(name) / lookup | DataSourceManager.dataSourceBuilders companheiro (objeto DataSourceManager.scala) mais iter_modules de pyspark_* em lookup_data_sources.py | Armazenar em cache o DefaultSource serializado da sessão que dispara, em todo o JVM. Lab: layer='static' trigger='attacker' first-wins=yes. |
Vítima: SQL USING <name> / spark.read.format(name) em uma sessão que nunca plantou | PythonDataSourceV2 createDataSourceInPython | Executar essa classe como UID do Spark. Lab: planted_by='attacker' witness_written=True static-cross-session=yes. O arquivo oráculo contém SPARK-PYTHON-DS-STATIC-WITNESS. |
| Método | Sink | Resultado |
|---|---|---|
| Plantio posterior de um segundo nome após o mapa preenchido | mesmo mapa estático | Fechado. Primeiro vence. Lab: later-lookup found=False, companion ainda apenas delta. |
registerPython("alpha") por sessão | runtimeDataSourceBuilders | Isolado. Clone copia apenas runtime. Lab: runtime-isolated=yes. Leitura da vítima de alpha é found=False. |
Alias Java parquet | Java DataSourceRegister ServiceLoader | Fechado. Lab: java-alias-closed=yes layer='java'. O mesmo para csv/json/orc/jdbc. |
| Método | Sink | Privilégio |
|---|---|---|
USING da vítima após o plantio | workers Python do driver + executor | Código em todo o cluster como Spark UID. O lab escreve um arquivo testemunha em vez de um payload. |
| Mesmo caminho | crash / spin do worker | Disponibilidade como Spark UID. Não é o High. O High é a execução de código. |
Hive Thrift SQL USING em um SparkSession compartilhado | mesmo mapa estático | Mesmo resíduo se Thrift e Connect compartilharem a JVM do driver. Não exercitado. |
parquet / csv / json / orc / jdbc. ServiceLoader vence.concatenatePaths rejeita .. e nomes absolutos).pythonExec fornecido pelo cliente. O planner do Connect usa PYSPARK_PYTHON / python3 do servidor.registerPython. O mapa de runtime é por instância de DataSourceManager.pyspark local de usuário único com uma sessão. Não há outro tenant.Fixe em apache/spark v4.2.0 (32f7299601108917fb01920a54e084595b7b3bf8). Vinte e oito buscas no código-fonte. A submissão de job executando código do usuário é por design. History Jackson, shell da UI, impersonação local do PySpark e caminho de recebimento RPC standalone estão fechados nesta análise. Artefatos do Connect isolam pyfiles sob um UUID de sessão. registerPython em runtime é por sessão. O objeto companion não é.
SPARK-45917 auto-registra pacotes pyspark_* a partir do PYTHONPATH. Isso é intencional para conectores instalados pelo operador. O resíduo é que o worker de busca também faz sys.path.insert do diretório SparkFiles da sessão que disparou (worker_util.setup_spark_files), e então serializa com pickle qualquer DefaultSource pyspark_* que encontrar em um mapa global de processo que nunca atualiza.
Caminho errado: tratar o isolamento de registerPython como toda a barreira de tenant. O runtime é isolado. O estático não é.
cd lab
./run.sh
Loopback 127.0.0.1:18970. Projeto Compose spark-python-ds-static. Witness SPARK-PYTHON-DS-STATIC-WITNESS. Gadget Python de object DataSourceManager mais mapas de runtime por sessão. Planta pyspark_delta.py apenas no SparkFiles do atacante. A consulta do atacante preenche o companion. A planta posterior de iceberg não atualiza. A read da vítima de delta grava o oráculo. registerPython("alpha") não vaza. O parquet Java permanece Java.
Última linha: