-
Notifications
You must be signed in to change notification settings - Fork 14k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add solarwinds_orion_dump post module #17278
Add solarwinds_orion_dump post module #17278
Conversation
Post module for extracting encrypted credentials from SolarWinds Orion NPM. Tested on the 2020 version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @npm-cesium137-io for this great module! I left a few comments and suggestions for you to review when you get a chance.
Particularly, I noticed a common pattern in the module methods: it returns a boolean false
on error whereas the method is expected to return a string or something else. It is recommended to use custom exceptions (generic or specific exceptions defined in the module itself) and properly handle them in the callers. This way, you don't need to print_error
the cause of the error and return false
, but just raise an exception with the appropriate message. The caller just need to handle the exception and call fail_with
with the exception error message.
@@ -0,0 +1,263 @@ | |||
This module exports and decrypts credentials from SolarWindows Orion Network Performance Monitor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, would you mind addressing the issue reported by msftidy_docs.rb
?
ruby tools/dev/msftidy_docs.rb documentation/modules/post/windows/gather/credentials/solarwinds_orion_dump.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not even know this was a thing, will do. This will be cleaned up in the next commit, and I will be sure to invoke the same before a PR in the future. I get [INFO] Missing Section: ## Options
as the only finding now, but assume that is permissible.
documentation/modules/post/windows/gather/credentials/solarwinds_orion_dump.md
Show resolved
Hide resolved
of the source code and technical information published by Djordje Atlialp and | ||
Atredis Partners. | ||
}, | ||
'Author' => 'npm[at]cesium137.io', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good idea to also credit the authors of the original research and tools.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, will update!
end | ||
|
||
def init_module | ||
@orion_hostname = get_env('COMPUTERNAME') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently, @orion_hostname
is only used locally in this method. Would you mind make this variable local instead of an instance variable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you're correct. That's a holdover from the module I copied from. I will correct this.
if require_sql | ||
# TODO: Orion does not install SSMS / sqlcmd by default if it is using an external SQL server. | ||
# Even when sqlcmd is available we have to do hideous things; MSSQL client functionality built | ||
# into Exploit does not extend to Post, and trying to mix it in makes weird errors. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, can you submit an issue with all the details for this? This looks like something that should be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will do that, certainly. Just for the record here, add include Msf::Exploit::Remote::MSSQL
to the module, and you will get undefined method 'register_autofilter_ports'
. I got past that by doing
def register_autofilter_ports(arg = {})
end
Trying to load that will get you undefined method 'register_autofilter_services'
, which can be corrected the same way. This at least lets the module load, though I didn't take it further than that since I knew I'd get a bloody lip trying to pass that off in a PR. This approach also has significant problems, namely that the source address becomes the MSF console itself, and the local Meterpreter session. That's great if sqlcmd doesn't exist on the local system, not so good if the local system is using Express and/or has TCP/IP access explicitly turned off or blocked.
print_error('Provided AES key is not valid 256-bit / 64-byte hexidecimal data') | ||
return false | ||
end | ||
@orion_aes_key_hex = datastore['AES_KEY'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@orion_aes_key_hex = datastore['AES_KEY'] | |
orion_aes_key_hex = datastore['AES_KEY'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will update. Also occurs to me I should probably record the AES key in creds.
if datastore['AES_KEY'] | ||
unless datastore['AES_KEY'].match?(/^[0-9a-f]+$/i) && datastore['AES_KEY'].length == 64 | ||
print_error('Provided AES key is not valid 256-bit / 64-byte hexidecimal data') | ||
return false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Option validations should be done early, ideally, the first thing to do in the exploit
method. This would avoid executing unnecessary commands on the target and failing afterwards because an option is not valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I usually do a whole validate_options
method, even, just neglected to here. Easy to implement, will do this.
key_hex_encrypted = orion_enc_conf_bytes[8..key_len + 8].unpack('H*').first.to_s.upcase | ||
orion_enc_conf_b64 = ::Base64.strict_encode64([key_hex_encrypted].pack('H*')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless I'm missing something, it looks like calling unpack
and then pack
is not necessary here. Is there any reason for this?
key_hex_encrypted = orion_enc_conf_bytes[8..key_len + 8].unpack('H*').first.to_s.upcase | |
orion_enc_conf_b64 = ::Base64.strict_encode64([key_hex_encrypted].pack('H*')) | |
key_hex_encrypted = orion_enc_conf_bytes[8..key_len + 8] | |
orion_enc_conf_b64 = ::Base64.strict_encode64(key_hex_encrypted) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact you are not. Artifacts from the early prototypes I failed to strip out. I will fix this
loop do | ||
next_char = plaintext_conf[working_offset, 1] | ||
break if next_char == "\n" | ||
|
||
working_bytes << next_char | ||
working_offset += 1 | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think index
can be used instead of a loop, passing an offset as argument:
plaintext_conf.index("\n", working_offset)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works great and looks nicer - thanks
# Because the db_conf hash was created by split('='), Base64 values will lose their padding if they had any. | ||
# .NET [System.Convert] requires Base64 input to be strictly RFC 4648 compliant, so we must manually pad |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this can be fixed by using split
with the limit
parameter to keep any ending =
in the base64 string:
[19] pry(main)> "param=AgABAgADAAk=".split('=')
=> ["param", "AgABAgADAAk"]
[20] pry(main)> "param=AgABAgADAAk=".split('=', 2)
=> ["param", "AgABAgADAAk="]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome to know! It kills me how much brilliant stuff is out there hidden.
Made modifications to documentation to add further detail for each action. Significant refactor of error handling, now with (hopefully) proper use of exceptions. Various suggested code improvements and optimization. Fixed some redundant and buggy code.
@cdelafuente-r7 good morning! I just pushed a commit with some of these requested changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @npm-cesium137-io for updating this. I just left a few minor comments for you to review when you get a chance.
Also, during my testing, I had issue to get the expected CSV format to feed the CSV_FILE
option. I believe this should be documented somewhere. I also had issue with some fields that had extra spaces characters. For example:
CredentialID ,Name , Description. , etc.
The code in the module parsing the CVS breaks with this kind of CVS. I think some sanitization or better error handling would be helpful:
[*] Process SolarWinds Orion DB ...
[-] Post failed: NoMethodError undefined method `[]' for nil:NilClass
[-] Call stack:
[-] /home/msfuser/dev/src/metasploit-framework/modules/post/windows/gather/credentials/solarwinds_orion_dump.rb:190:in `block in decrypt_orion_db'
[-] /home/msfuser/.rvm/rubies/ruby-3.0.2/lib/ruby/3.0.0/csv/table.rb:539:in `each'
[-] /home/msfuser/.rvm/rubies/ruby-3.0.2/lib/ruby/3.0.0/csv/table.rb:539:in `each'
[-] /home/msfuser/dev/src/metasploit-framework/modules/post/windows/gather/credentials/solarwinds_orion_dump.rb:181:in `decrypt_orion_db'
[-] /home/msfuser/dev/src/metasploit-framework/modules/post/windows/gather/credentials/solarwinds_orion_dump.rb:121:in `decrypt'
[-] /home/msfuser/dev/src/metasploit-framework/modules/post/windows/gather/credentials/solarwinds_orion_dump.rb:103:in `run'
[*] Post module execution completed
## Vulnerable Application | ||
|
||
This module exports and decrypts credentials from SolarWindows Orion Network Performance Monitor | ||
to a CSV file; it is intended as a post-exploitation module for Windows hosts with SolarWindows |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for updating the documentation. I noticed SolarWindows
is used in many places instead of SolarWinds
here and also in the module's Description
. Please, would you mine to also update this?
to a CSV file; it is intended as a post-exploitation module for Windows hosts with SolarWindows | |
to a CSV file; it is intended as a post-exploitation module for Windows hosts with SolarWinds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How embarrassing. Should be fixed now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem! I think the module Description
section should be updated too:
'Description' => %q{
This module exports and decrypts credentials from SolarWindows Orion Network
Performance Monitor (NPM) to a CSV file; it is intended as a post-exploitation
module for Windows hosts with SolarWindows Orion NPM installed. The module
supports decryption of AES-256, RSA, and XMLSEC secrets. Separate actions for
extraction and decryption of the data are provided to allow session migration
during execution in order to log in to the SQL database using SSPI. Tested on
the 2020 version of SolarWinds Orion NPM. This module is possible only because
of the source code and technical information published by Djordje Atlialp and
Atredis Partners.
},
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That damn phrase somehow made it into my custom dictionary, I finally nuked it and this IS the last one. Sorry about that!
of the source code and technical information published by Djordje Atlialp and | ||
Atredis Partners. | ||
}, | ||
'Author' => [ 'npm[at]cesium137.io', 'djordje.atlialp@gmail.com', 'https://atredis.com' ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Author
section should not contain URL's. We usually use names or handles (Github, Twitter, etc.), you can also add more information in an inline comment:
'Author' => [ 'npm[at]cesium137.io', 'djordje.atlialp@gmail.com', 'https://atredis.com' ], | |
'Author' => [ | |
'npm[at]cesium137.io', # Metasploit Module | |
'Djordje Atlialp' # @rhazdon - Original research | |
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made the update, I will remember this guideline!
extra_service_data = { | ||
address: ::Rex::Socket.getaddress(rhost), | ||
port: 17777, | ||
service_name: 'sis', | ||
protocol: 'tcp', | ||
workspace_id: myworkspace_id, | ||
module_fullname: fullname, | ||
origin_type: :service, | ||
realm_key: Metasploit::Model::Realm::Key::WILDCARD, | ||
realm_value: ::Rex::Socket.getaddress(rhost) | ||
} | ||
store_valid_credential(user: 'Orion NPM AES Key', private: orion_aes_key_hex, service_data: extra_service_data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it is a post module, I don't think it makes sense to set the remote service options when storing the encryption key. store_valid_credential
takes care of setting all the needed attributes. Also, this helper sets the private_type
to :password
by default, which is wrong for an encryption key. I suggest to set it to :nonreplayable_hash
instead:
extra_service_data = { | |
address: ::Rex::Socket.getaddress(rhost), | |
port: 17777, | |
service_name: 'sis', | |
protocol: 'tcp', | |
workspace_id: myworkspace_id, | |
module_fullname: fullname, | |
origin_type: :service, | |
realm_key: Metasploit::Model::Realm::Key::WILDCARD, | |
realm_value: ::Rex::Socket.getaddress(rhost) | |
} | |
store_valid_credential(user: 'Orion NPM AES Key', private: orion_aes_key_hex, service_data: extra_service_data) | |
store_valid_credential(user: 'Orion NPM AES Key', private: orion_aes_key_hex, private_type: :nonreplayable_hash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am floored about how much I continue to learn from these reviews, thank you. Did not know this was possible, incorporated this change.
db_instance_path = db_conf['DATA SOURCE'] | ||
db_name = db_conf['INITIAL CATALOG'] | ||
db_user = db_conf['USER ID'] | ||
db_pass_enc = db_conf['ENCRYPTED.PASSWORD'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On my test environment, I don't have the ENCRYPTED.PASSWORD
configuration setting, but just PASSWORD
:
[2] pry(#<Msf::Modules::Post__Windows__Gather__Credentials__Solarwinds_orion_dump::MetasploitModule>):1> db_conf
=> {"PROVIDER"=>"SQLNCLI11",
"PERSIST SECURITY INFO"=>"False",
"INITIAL CATALOG"=>"SolarWindsOrion",
"DATA SOURCE"=>"(local)\\SOLARWINDS_ORION",
"USE PROCEDURE FOR PREPARE"=>"1",
"AUTO TRANSLATE"=>"True",
"PACKET SIZE"=>"4096",
"WORKSTATION ID"=>"MYWS1",
"TAG WITH COLUMN COLLATION WHEN POSSIBLE"=>"False",
"USER ID"=>"SolarWindsOrionDatabaseUser",
"PASSWORD"=>"<redacted>",
"MAX POOL SIZE"=>"1000\r"}
The module then fails to extract SQL login information without this information. If I understand this correctly, there is no encrypted password but only the cleartext password, which I believe can be used directly here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, I never considered or encountered that config. I made a single line change that I think should handle this case, if you want to test again?
Fixed humiliating typos in the markdown doc. Updated the Author section of the module per guidelines. Changed credential type for AES key loot storage. Updated database config code to include the case where the SQL password is not encrypted (needs testing). Additional tweaks and fixes.
@cdelafuente-r7 impeccable timing as I had some time this afternoon to work on personal projects. I've pushed another commit that incorporates your feedback. This PR has been awesomely valuable in terms of picking up neat new tricks and features I didn't know existed, so thank you for spending all the time on this. I am already incorporating these improvements into the other module I'm working on for decrypting Veeam creds! Would you be willing to share your "bad" CSV file? I cannot reproduce this when I manually add spaces, but whatever is happening in your case is probably not the same deformity.
|
Thanks for the updates @npm-cesium137-io! I'm happy to hear the suggestions and comments are helpful. So, I generated the CVS using
The This generates this kind of file, which breaks the module:
It also breaks without the header dashed line separator:
I finally found the right switches to use:
It works after removing the dashed line and the extra
I believe that would be a good idea to handle errors when parsing the CVS file to avoid breaking the application with a |
Nuked the last embarrassing typo in the module description. Updated the documentation to include detail on sqlcmd / CSV export process when manually exporting the data.
@cdelafuente-r7 thanks for this info, it all makes sense. You are absolutely correct, I presumed folks would be using SSMS and it didn't occur to me to include the needed info in the docs when invoking sqlcmd, the presumption being if sqlcmd were available, the module would just use it. I have since updated the docs with the suggested command line switches (the -W is crucial because it strips the trailing whitespace) as well as affirming that the CSV must be well-formed and not have trailing whitespace - hopefully, that should be enough to point others in the right direction if they are in the same situation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @npm-cesium137-io! I just added a last suggestion to make sure extra space characters are removed when parsing the CSV. Other than that, it all looks good to me and it is good to land. I tested against SolarWinds Orion Build 2022.4.0.1216 and verified the credentials were correctly extracted.
modules/post/windows/gather/credentials/solarwinds_orion_dump.rb
Outdated
Show resolved
Hide resolved
Improved CSV input error handling and various minor bug fixes.
@cdelafuente-r7 hi there, sorry this is late - I've committed your suggestion, as well as a couple other minor fixes. The converters you introduced resolved a problem I had using osql vs. sqlcmd (namely the -W param does not exist in the former) so this now opens up the potential to use osql as well - which I don't expect to see a lot of but I was nevertheless very happy to learn this. I will def. be incorporating these optimizations in future modules, thanks! |
actions for extraction and decryption of the data are provided to allow session migration during | ||
execution in order to log in to the SQL database using SSPI. Tested on the 2020 version of | ||
SolarWinds Orion NPM. This module is possible only because of the source code and technical | ||
information published by Djordje Atlialp: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small comment. That's my blog. Djordje Atlialp is the person who made the template I use for my website. I see where the confusion came into play since the footer has his name. I'll get that fixed. Please fix the attribution here though :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed if you need proof :)
Updated original research attribution to align with reality.
@mubix Wow I must've been on autopilot, sorry about that! I updated the module description and the markdown doc. Nice to meet you by the way, I had the opportunity to use SolarFlare during an engagement over the fall - very powerful, and it inspired me to author this module. Credential decryption is the most civilized form of priv esc, don't you think?! |
Thanks @npm-cesium137-io! Everything looks good now. I tested against SolarWinds Orion Build 2022.4.0.1216 and verified the secrets were correctly decrypted. I'll go ahead and land it. Thanks again for your contribution. Example outputDump
Export
Decrypt
|
Release NotesThis adds a post module for extracting encrypted credentials from SolarWinds Orion NPM. |
Post module for extracting encrypted credentials from SolarWinds Orion NPM, tested on the 2020-2022 versions of the product in various configurations. This is basically an MSF/Ruby port of the C# utility SolarFlare:
This should work on an out-of-box install of SolarWinds Orion with embedded SQL, but YMMV on instances where Orion is configured to use an external SQL server. The module requires sqlcmd to be available on the system to do data extraction, which may not be installed depending on the Orion deployment type - in those cases the module can take properly formatted CSV output acquired out-of-band and perform decryption that way. I tried to stick with the conventions picked up so far through various PRs to (hopefully) minimize everybody's review effort.