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 adding/deleting computers via MS-SAMR #16677

Merged
merged 8 commits into from Jun 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Gemfile.lock
Expand Up @@ -445,7 +445,7 @@ GEM
ruby-progressbar (1.11.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
ruby_smb (3.1.3)
ruby_smb (3.1.5)
bindata
openssl-ccm
openssl-cmac
Expand Down
100 changes: 100 additions & 0 deletions documentation/modules/auxiliary/admin/dcerpc/samr_computer.md
@@ -0,0 +1,100 @@
## Vulnerable Application
Add, lookup and delete computer accounts via MS-SAMR. By default standard active directory users can add up to 10 new
computers to the domain. Administrative privileges however are required to delete the created accounts.

## Verification Steps

1. From msfconsole
2. Do: `use auxiliary/admin/dcerpc/samr_computer`
3. Set the `RHOSTS`, `SMBUser` and `SMBPass` options
1. Set the `COMPUTER_NAME` option for `DELETE_COMPUTER` and `LOOKUP_COMPUTER` actions
4. Run the module and see that a new machine account was added
zeroSteiner marked this conversation as resolved.
Show resolved Hide resolved

## Options

### SMBDomain

The Windows domain to use for authentication. The domain will automatically be identified if this option is left in its
default value.

### COMPUTER_NAME

The computer name to add, lookup or delete. This option is optional for the `ADD_COMPUTER` action, and required for the
`LOOKUP_COMPUTER` and `DELETE_COMPUTER` actions.

### COMPUTER_PASSWORD

The password for the new computer. This option is only used for the `ADD_COMPUTER` action. If left blank, a random value
will be generated.

## Actions

### ADD_COMPUTER

Add a new computer to the domain. This action will fail with status `STATUS_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED` if the
user has exceeded the maximum number of computer accounts that they are allowed to create.

After the computer account is created, the password will be set for it. If `COMPUTER_NAME` is set, that value will be
used and the module will fail if the selected name is already in use. If `COMPUTER_NAME` is *not* set, a random value
will be used.

### DELETE_COMPUTER

Delete a computer from the domain. This action requires that the `COMPUTER_NAME` option be set.

### LOOKUP_COMPUTER

Lookup a computer in the domain. This action verifies that the specified computer exists, and looks up its security ID
(SID), which includes the relative ID (RID) as the last component.

## Scenarios

### Windows Server 2019

First, a new computer account is created and its details are logged to the database.

```
msf6 auxiliary(admin/dcerpc/samr_computer) > set RHOSTS 192.168.159.96
RHOSTS => 192.168.159.96
msf6 auxiliary(admin/dcerpc/samr_computer) > set SMBUser aliddle
SMBUser => aliddle
msf6 auxiliary(admin/dcerpc/samr_computer) > set SMBPass Password1
SMBPass => Password1
msf6 auxiliary(admin/dcerpc/samr_computer) > show options

Module options (auxiliary/admin/dcerpc/samr_computer):

Name Current Setting Required Description
---- --------------- -------- -----------
COMPUTER_NAME no The computer name
COMPUTER_PASSWORD no The password for the new computer
RHOSTS 192.168.159.96 yes The target host(s), see https://github.com/rapid7/metasploit-framework/wiki/Using-Metasploit
RPORT 445 yes The target port (TCP)
SMBDomain . no The Windows domain to use for authentication
SMBPass Password1 no The password for the specified username
SMBUser aliddle no The username to authenticate as


Auxiliary action:

Name Description
---- -----------
ADD_COMPUTER Add a computer account


msf6 auxiliary(admin/dcerpc/samr_computer) > run
[*] Running module against 192.168.159.96

[*] 192.168.159.96:445 - Using automatically identified domain: MSFLAB
[+] 192.168.159.96:445 - Successfully created MSFLAB\DESKTOP-2X8F54QG$ with password MCoDkNALd3SdGR1GoLhqniEkWa8Me9FY
[*] Auxiliary module execution completed
msf6 auxiliary(admin/dcerpc/samr_computer) > creds
Credentials
===========

host origin service public private realm private_type JtR Format
---- ------ ------- ------ ------- ----- ------------ ----------
192.168.159.96 192.168.159.96 445/tcp (smb) DESKTOP-2X8F54QG$ MCoDkNALd3SdGR1GoLhqniEkWa8Me9FY MSFLAB Password

msf6 auxiliary(admin/dcerpc/samr_computer) >
```
249 changes: 249 additions & 0 deletions modules/auxiliary/admin/dcerpc/samr_computer.rb
@@ -0,0 +1,249 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'ruby_smb/dcerpc/client'

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::SMB::Client::Authenticated
include Msf::Exploit::Remote::DCERPC
include Msf::Auxiliary::Report

def initialize(info = {})
super(
update_info(
info,
'Name' => 'SAMR Computer Management',
'Description' => %q{
Add, lookup and delete computer accounts via MS-SAMR. By default
standard active directory users can add up to 10 new computers to the
domain. Administrative privileges however are required to delete the
created accounts.
},
'License' => MSF_LICENSE,
'Author' => [
'JaGoTu', # @jagotu Original Impacket code
'Spencer McIntyre',
],
'References' => [
['URL', 'https://github.com/SecureAuthCorp/impacket/blob/master/examples/addcomputer.py'],
],
'Notes' => {
'Reliability' => [],
'Stability' => [],
'SideEffects' => [ IOC_IN_LOGS ]
},
'Actions' => [
[ 'ADD_COMPUTER', { 'Description' => 'Add a computer account' } ],
[ 'DELETE_COMPUTER', { 'Description' => 'Delete a computer account' } ],
[ 'LOOKUP_COMPUTER', { 'Description' => 'Lookup a computer account' } ]
],
'DefaultAction' => 'ADD_COMPUTER'
)
)

register_options([
OptString.new('COMPUTER_NAME', [ false, 'The computer name' ]),
OptString.new('COMPUTER_PASSWORD', [ false, 'The password for the new computer' ], conditions: %w[ACTION == ADD_COMPUTER]),
Opt::RPORT(445)
])
end

def connect_samr
vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol')
samr = @tree.open_file(filename: 'samr', write: true, read: true)

vprint_status('Binding to \\samr...')
samr.bind(endpoint: RubySMB::Dcerpc::Samr)
vprint_good('Bound to \\samr')

samr
end

def run
begin
connect
rescue Rex::ConnectionError => e
fail_with(Failure::Unreachable, e.message)
end

begin
smb_login
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e
fail_with(Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e}).")
end
report_service(
host: rhost,
port: rport,
host_name: simple.client.default_name,
proto: 'tcp',
name: 'smb',
info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"
)

begin
@tree = simple.client.tree_connect("\\\\#{sock.peerhost}\\IPC$")
rescue RubySMB::Error::RubySMBError => e
fail_with(Failure::Unreachable, "Unable to connect to the remote IPC$ share ([#{e.class}] #{e}).")
end

begin
@samr = connect_samr
@server_handle = @samr.samr_connect
rescue RubySMB::Dcerpc::Error::FaultError => e
elog(e.message, error: e)
fail_with(Failure::UnexpectedReply, "Connection failed (DCERPC fault: #{e.status_name})")
end

if datastore['SMBDomain'].blank? || datastore['SMBDomain'] == '.'
all_domains = @samr.samr_enumerate_domains_in_sam_server(server_handle: @server_handle).map(&:to_s).map(&:encode)
all_domains.delete('Builtin')
if all_domains.empty?
fail_with(Failure::NotFound, 'No domains were found on the SAM server.')
elsif all_domains.length > 1
print_status("Enumerated domains: #{all_domains.join(', ')}")
fail_with(Failure::BadConfig, 'The SAM server has more than one domain, the target must be specified.')
end

@domain_name = all_domains.first
print_status("Using automatically identified domain: #{@domain_name}")
else
@domain_name = datastore['SMBDomain']
end

@domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: @domain_name)
@domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: @domain_sid)
send("action_#{action.name.downcase}")
rescue RubySMB::Dcerpc::Error::DcerpcError => e
elog(e.message, error: e)
fail_with(Failure::UnexpectedReply, e.message)
rescue RubySMB::Error::RubySMBError
elog(e.message, error: e)
fail_with(Failure::Unknown, e.message)
end

def random_hostname(prefix: 'DESKTOP')
"#{prefix}-#{Rex::Text.rand_base(8, '', ('A'..'Z').to_a + ('0'..'9').to_a)}$"
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
"#{prefix}-#{Rex::Text.rand_base(8, '', ('A'..'Z').to_a + ('0'..'9').to_a)}$"
"#{prefix}-#{Rex::Text.rand_text_alphanumeric(8).upcase}$"

end

def action_add_computer
if datastore['COMPUTER_NAME'].blank?
computer_name = random_hostname
4.downto(0) do |attempt|
break if @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [ computer_name ]).nil?

computer_name = random_hostname
fail_with(Failure::BadConfig, 'Could not find an unused computer name.') if attempt == 0
end
else
computer_name = datastore['COMPUTER_NAME']
if @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [ computer_name ])
fail_with(Failure::BadConfig, 'The specified computer name already exists.')
end
end

result = @samr.samr_create_user2_in_domain(
domain_handle: @domain_handle,
name: computer_name,
account_type: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT,
desired_access: RubySMB::Dcerpc::Samr::USER_FORCE_PASSWORD_CHANGE | RubySMB::Dcerpc::Samr::MAXIMUM_ALLOWED
)

user_handle = result[:user_handle]
if datastore['COMPUTER_PASSWORD'].blank?
password = Rex::Text.rand_text_alphanumeric(32)
else
password = datastore['COMPUTER_PASSWORD']
end

user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW,
member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new(
i1: {
password_expired: 1,
which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED
Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand it correctly, the i1's NtPasswordPresent field will be processed by the server, and since it is set to zero by default, the NtOwfPassword field will be ignored (here). Any specific reason for this? Not a blocker, but I'm just trying to understand the process.

},
user_password: {
buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password(
password,
@simple.client.application_key
)
}
)
)
@samr.samr_set_information_user2(
user_handle: user_handle,
user_info: user_info
)

user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
tag: RubySMB::Dcerpc::Samr::USER_CONTROL_INFORMATION,
member: RubySMB::Dcerpc::Samr::UserControlInformation.new(
user_account_control: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT
)
)
@samr.samr_set_information_user2(
user_handle: user_handle,
user_info: user_info
)
print_good("Successfully created #{@domain_name}\\#{computer_name} with password #{password}")
report_creds(@domain_name, computer_name, password)
end

def action_delete_computer
fail_with(Failure::BadConfig, 'This action requires COMPUTER_NAME to be specified.') if datastore['COMPUTER_NAME'].blank?
computer_name = datastore['COMPUTER_NAME']

details = @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [ computer_name ])
fail_with(Failure::BadConfig, 'The specified computer was not found.') if details.nil?
details = details[computer_name]

handle = @samr.samr_open_user(domain_handle: @domain_handle, user_id: details[:rid])
@samr.samr_delete_user(user_handle: handle)
print_good('The specified computer has been deleted.')
end

def action_lookup_computer
fail_with(Failure::BadConfig, 'This action requires COMPUTER_NAME to be specified.') if datastore['COMPUTER_NAME'].blank?
computer_name = datastore['COMPUTER_NAME']

details = @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [ computer_name ])
if details.nil?
print_error('The specified computer was not found.')
return
end
details = details[computer_name]
sid = @samr.samr_rid_to_sid(object_handle: @domain_handle, rid: details[:rid]).to_s
print_good("Found #{@domain_name}\\#{computer_name} (SID: #{sid})")
end

def report_creds(domain, username, password)
service_data = {
address: datastore['RHOST'],
port: datastore['RPORT'],
service_name: 'smb',
protocol: 'tcp',
workspace_id: myworkspace_id
}

credential_data = {
module_fullname: fullname,
origin_type: :service,
private_data: password,
private_type: :password,
username: username,
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: domain
}.merge(service_data)

credential_core = create_credential(credential_data)

login_data = {
core: credential_core,
status: Metasploit::Model::Login::Status::UNTRIED
}.merge(service_data)

create_credential_login(login_data)
end
end