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
AWS SSM Sessions #17430
Conversation
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.
This is cool. I'm also a fan of SSM's port forwarding. |
Why do you hate me so? 😛 |
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
@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). |
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
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.
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
d546935
to
7666b30
Compare
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
Thanks for the neat new toy. Please add a Gemfile.lock change required by the
Further review is still pending. |
@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) |
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.
Possible retry_until_truthy
call from retry mixin?
def retry_until_truthy(timeout:) |
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 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.
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.
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.
- Tested enum_ssm without a region specified 🔴
- Tested enum_ssm with a region specified to get a shell, also 🔴
- 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.
Explicitly `require 'aws-sdk-ec2'` in the aux module Fix the hard-coded region to use datastore option
@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? |
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.
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
- Sessions don't exit on the
exit
command enum_ssm
crashes whenREGION
is not specifiedenum_ssm
crashes when::IO.console
is nil (as it is when run from a debugger)- 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.
@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? |
* 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.
Looks like the SetupCreate a file and the download command should work:
Existing powershell_reverse_tcp working 🟢Expected behavior from a powershell session:
Local disk as expected:
New SSM Windows session failing 🔴In comparison to the ssm powershell shell, which echos out the input command first - breaking the parsing:
|
lib/msf/core/handler/bind_aws_ssm.rb
Outdated
end | ||
# | ||
# Returns the handler specific string representation, in this case | ||
# 'bind_tcp'. |
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.
# 'bind_tcp'. | |
# 'bind_aws_ssm'. |
lib/rex/proto/http/web_socket.rb
Outdated
@@ -37,6 +37,7 @@ def type? | |||
# @!attribute [r] params | |||
# @return [Rex::Socket::Parameters] | |||
attr_reader :params | |||
# Boolean flag to control frame masking on output |
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.
Can this line be removed now? Looks like it used to be attached to a :mask_write
attribute that's no longer present
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'] |
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 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) |
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.
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) |
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.
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? 👀
metasploit-framework/lib/msf/core/exploit/remote/http_client.rb
Lines 236 to 265 in 459cf87
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') |
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 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$
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 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 differentsh
processes attempting to cat from/tmp/tmp.etc.etc
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:
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 |
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? |
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 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 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. |
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. Could we "cheat" and instantiate |
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. |
If this is what they're using, then it appears to not have been updated for 5y. Also looks like the thing has a debug mode similar to how meterp is wired:
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
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.
All my previous issues have been fixed - i.e.
-
ctrl+z/ctrl+c support works as expected now
-
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
- 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
- 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 👍
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). |
Release NotesThis 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. |
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.
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.
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:
use auxiliary/cloud/aws/enum_ssm
set SECRET_ACCESS_KEY
to your secret keyset ACCESS_KEY_ID
to your access key IDset CreateSession false
exploit
set CreateSession true
if there are valid targets and they are safe to shell (over TLS, but still worth considering)exploit
Advanced users can test the payloads (unix^windows) directly via the handler and setting the provided datastore options.
Notes:
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).cloud-init
after boostrapping as well, while at it)