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

Post module for gather Docker credentials #8774

Merged
merged 4 commits into from Sep 14, 2017

Conversation

flibustier
Copy link
Contributor

@flibustier flibustier commented Jul 26, 2017

This module will collect the contents of all users' .docker directories on the targeted machine. If the user has already push to docker hub, chances are that the password was saved in base64 (default behavior).

For testing, I have the following config.json file on my ~/.docker

{
	"auths": {
		"https://index.docker.io/v1/": {
			"auth": "ZmxpYnVzdGllcjpiYWd1ZXR0ZQ=="
		}
	}
}

The above example is flibustier:baguette, corresponding to username:password

Verification

List the steps needed to make sure this thing works

  • Start msfconsole
  • Use any exploit to create a session
  • use post/multi/gather/docker_creds
  • set session SESSION
  • run
  • Verify that you get the content of your ~/.docker/config.json

# Array#select! is only in 1.9
paths = paths.select { |d| directory?(d) }

if paths.nil? or paths.empty?
Copy link
Contributor

@cbrnrd cbrnrd Jul 27, 2017

Choose a reason for hiding this comment

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

stick to && and || instead of and and or as per the ruby style guide

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, It comes from my inspiration file (gpg_creds), I will fix it too

file = "config.json"
target = "#{path}/#{file}"

if file?(target)
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be File.file? target

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks!


def download_loot(paths)
print_status("Looting #{paths.count} directories")
paths.each do |path|
Copy link
Contributor

Choose a reason for hiding this comment

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

you should check if paths is nil here to prevent future bugs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But it's check before the call to the function

if paths.nil? || paths.empty?
      print_error("No users found with a .docker directory")
      return
    end

    download_loot(paths)

What is better?

@cbrnrd
Copy link
Contributor

cbrnrd commented Jul 28, 2017 via email

def extract(target)
file = read_file(target)
parsed = JSON.parse(file)
creds = parsed["auths"]["https://index.docker.io/v1/"]["auth"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't this result in exceptions when the parsed JSON doesn't have the nested auths -> https://index.docker.io/v1/ -> auth? For example, mine currently looks like:

{
	"auths": {
		"12345.dkr.ecr.us-east-1.amazonaws.com": {}
	},
	"credsStore": "osxkeychain"
}

In this case it is because I've authenticated to AWS ECR, not docker.io. I suspect the same issue exists for other container repositories.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really good point, I was thinking that if creds.length > 0 would prevent this, but it wasn't the case, so I added a verification condition!
Thanks!

@jmartin-tech jmartin-tech self-assigned this Aug 18, 2017
Copy link
Contributor

@jmartin-tech jmartin-tech left a comment

Choose a reason for hiding this comment

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

Looks good, verification testing in progress. A couple possible improvements are noted here.

if creds.length > 0
plain = Rex::Text.decode_base64(creds)
print_good("Found #{plain}")
loot_path = store_loot("docker.credentials", "text/plain", session, plain,
Copy link
Contributor

Choose a reason for hiding this comment

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

It would nice to see this stored as a credential vs loot. We all know about password reuse. Having the credential object can be very helpful.

Consider adjustment to:

username, password = plain.split(':')
credential_data = {
  origin_type: :import,
  module_fullname: self.fullname,
  filename: target,
  workspace_id: myworkspace_id,
  service_name: 'docker',
  realm_value: 'https://index.docker.io/v1/',
  realm_key: Metasploit::Model::Realm::Key::WILDCARD,
  private_type: :password,
  private_data: password,
  username: username
}
create_credential(credential_data)

While this is not the 100% expected usage for realm it will produce a nice usable shared credential with output as follows:

msf> creds
Credentials
===========

host  origin  service  public  private  realm                        private_type
----  ------  -------  ------  -------  -----                        ------------
                       test    test     https://index.docker.io/v1/  Password

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very nice, I had integrated the suggestions! Let me know what you think

Thanks

file = read_file(target)
parsed = JSON.parse(file)
if parsed["auths"] && parsed["auths"]["https://index.docker.io/v1/"]
creds = parsed["auths"]["https://index.docker.io/v1/"]["auth"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Given @jhart-r7's feedback may be worth converting to loop thru all keys in parsed["auths"] and store any nested ['auth'] details that decode.

Keep in mind the impacts looping would have on the realm_value on the request below for conversion to store the detailed credential.

@jmartin-tech jmartin-tech merged commit abaf80f into rapid7:master Sep 14, 2017
@jmartin-tech
Copy link
Contributor

jmartin-tech commented Sep 14, 2017

Release Notes

The Multi Gather Docker Credentials Collection post-exploitation module has been added to the framework. It extracts Docker credentials that are often stored in the Docker config.json configuration file.

@tdoan-r7 tdoan-r7 added the rn-enhancement release notes enhancement label Sep 26, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants