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 a command to customise DNS resolution (including through sessions) #18526

Merged
merged 14 commits into from
Nov 23, 2023

Conversation

smashery
Copy link
Contributor

This work implements the dns command in Metasploit, to allow the user to customise the behaviour of DNS resolution in the framework.

The functionality, for now, is behind a feature flag. To use it, the feature needs to be enabled (feature set dns_feature true).

I use the existing CachedResolver implementation, registering it with Rex::Socket as the DNS resolver. Any existing code that resolves addresses through Rex will start using this functionality automatically.

This work allows users to resolve domains through communication channels (i.e. sessions). There are two ways this occurs:

  • The new dns command supports setting a session value (-s or --session) to set the comm channel through which the communication will be sent.
  • In the absence of a comm channel, the packet will be routed as per Metasploit's standard routing rules i.e. if a route has been set up for a particular CIDR, through a particular session, if the provided nameserver matches that CIDR, the DNS packet will be sent through it.
  • Otherwise, it will be sent directly from the MSF host.

The save command will save any DNS rules that have been configured by the user, which will be loaded upon application restart.

An important aspect of this work is to ensure that DNS leaks don't happen: if someone sets a particular nameserver for a particular domain, DNS queries within that DNS zone should not go to the default nameservers. Thus, the resolution behaviour is as follows:

  • If one or more rules match the domain being resolved, use the nameservers associated with those rules.
  • If none match, use the custom nameservers that have been manually specified without any rule
  • If no custom nameservers have been specified, use the existing behaviour (resolv.conf settings)

If a specific session was specified for a particular DNS rule, and then that session dies, DNS resolution will not fall back to custom or default nameservers, since we interpret the user's intentional use of a particular session to mean "I don't want DNS to fallback to other nameservers and potentially leak". Falling back upon a session closure would violate this.

This is also applicable when the application closes and re-opens. If the user's DNS configuration was saved, and that configuration had included a specific session to route certain DNS requests through, MSF will remember that there was a session associated with the rule, and that rule will be treated as a black hole: these DNS requests will deliberately fail, and not fall back.

If sending through a separate session was configured only using the route command and an application restarts,

Verification

To test DNS resolution, a separate module could be created just to do that, or an existing basic module such as auxiliary/scanner/http/http_header could be used.

  • Start msfconsole.
  • feature set dns_feature true (enable the feature).
  • Verify each of the dns commands (add, remove/del, flush, print, help) behave as expected.
  • Verify that when a rule is configured with a specific session, the DNS request goes out via that session (check Wireshark).
  • Verify that DNS pivoting works on various types of sessions (meterpreter and SSH) - check Wireshark.
  • Verify that a closed session causes DNS resolution to fail for that particular rule.
  • Verify that the save command saves the DNS configuration (reloaded upon application restart)
  • Verify that, when the save command is used to save an entry with a session associated with it, upon application restart (when the session will no longer exist), DNS does not fall back to nameservers that would not have been applicable prior to the application restart (DNS resolution should fail, unless other rules apply).
  • Verify that setting a specific DNS entry gives a warning that "this won't match subdomains"
  • Verify tab completion works.

@cdelafuente-r7 cdelafuente-r7 self-assigned this Nov 14, 2023
end

def sid
'previous MSF session'
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious - why was this needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having the sink-holed DNS class was to satisfy the condition described above:

If a specific session was specified for a particular DNS rule, and then that session dies, DNS resolution will not fall back to custom or default nameservers, since we interpret the user's intentional use of a particular session to mean "I don't want DNS to fallback to other nameservers and potentially leak". Falling back upon a session closure would violate this.

This is also applicable when the application closes and re-opens. If the user's DNS configuration was saved, and that configuration had included a specific session to route certain DNS requests through, MSF will remember that there was a session associated with the rule, and that rule will be treated as a black hole: these DNS requests will deliberately fail, and not fall back.

Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 left a comment

Choose a reason for hiding this comment

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

Thanks @smashery for these great additions! I just left a few minor comments. I also tested on different platforms (OSX, Windows, Linux) and it works great.

I noticed a few points that might be improved:

  1. Adding the same rule after loading it from the config file

When some saved rules with sessions are loaded while msfconsole starts, these will deliberately fail, which is expected. However, when creating the same rule again with an up-to-date session ID, the previous one still applies and the module fails to resolve names matching this rule. For example:

msf6 auxiliary(scanner/http/http_header) > dns

Custom nameserver rules
=======================

   ID  Rule(s)     DNS Server  Comm channel
   --  -------     ----------  ------------
   0   *.myawesomesite.ch   9.9.9.9     Closed session (previous MSF session)
   1   *.myawesomesite.fr   1.1.1.1
   2   *.myawesomesite.com  9.9.9.9     Closed session (previous MSF session)
   3   *.myawesomesite.com  9.9.9.9     Session 1

Here, rule #2 has been loaded from the config file and rule #3 has been created afterwards. Name resolution fails:

msf6 auxiliary(scanner/http/http_header) > run verbose=true rhosts=www.myawesomesite.com

[-] Msf::OptionValidateError The following options failed to validate: RHOSTS

Rule #2 needs to be specifically removed to make it work.

I'm wondering if there is a way to have some logic that detects the user is trying to add a rule that is the same as an existing rule with a "dead" session and simply update it instead of adding it? Would it make sense? At least, this could be optional, asking him if he wants to replace the existing rule.

  1. More descriptive error message

When a name resolution fails, whether it is because it doesn't exist or the dns route is deliberately blocked to avoid leak, the message is very generic:

[-] Msf::OptionValidateError The following options failed to validate: RHOSTS

I'm wondering if there is a way to display something more descriptive to help the user understand it is a DNS issue and why?

  1. Setting DNS feature off

I'm not sure if it is by design, but when you disable the DNS feature with features set dns_feature false, the rules still apply. This means DNS requests are routed according to the rules even if the feature is off.

  1. DNS feature not loaded correctly at launch

When the configuration file contains the DNS feature enabled, Framework doesn't seem to enable it when it launches.
Config file:

…
[framework/features]
dns_feature=true
…
msf6 auxiliary(scanner/http/http_header) > features

Features table
==============

   #  Name                         Enabled  Description
   -  ----                         -------  -----------
   …
   6  dns_feature                  false    When enabled, allows configuration of DNS resolution behaviour in Metasploit


msf6 auxiliary(scanner/http/http_header) > dns
[-] Unknown command: dns

That being said, rules still applies, as I noted in #3.

lib/msf/core/framework.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/rex/proto/dns/resolver.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/msf/ui/console/command_dispatcher/dns.rb Outdated Show resolved Hide resolved
lib/rex/proto/dns/resolver.rb Outdated Show resolved Hide resolved
lib/rex/proto/dns/resolver.rb Outdated Show resolved Hide resolved
@smashery
Copy link
Contributor Author

Thanks for the review @cdelafuente-r7. I believe I've addressed the issues. To the points you raised up top:

  1. This was an error in the send_udp method - that should be fixed up now.
  2. Agreed - the error messages are fairly useless. I've got some work in a separate branch to address this, which I can PR once this one is landed.
  3. Hadn't considered that - I've changed it to look at the feature flag before any DNS resolution, and just fall back to the normal implementation if it's turned off.
  4. I can't replicate this; are you able to provide more details?

Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 left a comment

Choose a reason for hiding this comment

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

Thanks for updating this @smashery. These changes looks good to me and the issues I found are now fixed. I just left one minor comment for a typo.

I was not able to reproduce the issue #⁠4 neither. Probably the last changes also fixed this one. But, I found another one: disabling the DNS feature makes DNS requests fail:

msf6 auxiliary(scanner/http/http_header) > sessions

Active sessions
===============

  Id  Name  Type                     Information                                Connection
  --  ----  ----                     -----------                                ----------
  1         meterpreter x64/windows  DESKTOP-26CQRHP\n00tmeg @ DESKTOP-26CQRHP  192.168.100.77:4444 -> 192.168.1.50:64316 (192.168.100.50)

msf6 auxiliary(scanner/http/http_header) > dns

Custom nameserver rules
=======================

   ID  Rules(s)      DNS Server  Commm channel
   --  --------      ----------  -------------
   0   *.rapid7.com  9.9.9.9
   2   *.github.com  1.1.1.1     Session 1


Default nameservers
===================

   ID  DNS Server  Commm channel
   --  ----------  -------------
   1   8.8.4.4

msf6 auxiliary(scanner/http/http_header) > run verbose=true rhosts=www.github.com

[*] 140.82.113.4:80      : requesting / via HEAD
[*] 140.82.113.4:80      : deleted header Content-Length
[+] 140.82.113.4:80      : LOCATION: https://www.github.com/
[+] 140.82.113.4:80      : detected 1 headers
[*] Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed
msf6 auxiliary(scanner/http/http_header) > features set dns_feature false
dns_feature => false
[*] Reloading module...
msf6 auxiliary(scanner/http/http_header) > dns
[-] Unknown command: dns
msf6 auxiliary(scanner/http/http_header) > run verbose=true rhosts=www.github.com

[-] Msf::OptionValidateError The following options failed to validate: RHOSTS

Comment on lines 301 to 303
columns = ['ID', 'Rules(s)', 'DNS Server', 'Commm channel']
else
columns = ['ID', 'DNS Server', 'Commm channel']
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
columns = ['ID', 'Rules(s)', 'DNS Server', 'Commm channel']
else
columns = ['ID', 'DNS Server', 'Commm channel']
columns = ['ID', 'Rules(s)', 'DNS Server', 'Comm channel']
else
columns = ['ID', 'DNS Server', 'Comm channel']

@smashery
Copy link
Contributor Author

Ah, the fallback failed because it was passing an IpAddress, but expecting a string. That should be fixed now. I also realised that the zone transfer (AXFR) code was broken as a result of the changes, so I've fixed that up too.

@cdelafuente-r7
Copy link
Contributor

Thanks @smashery ! Everything looks good to me now. I tested on Mac OSX, Linux and Windows host, using both Meterpreter and SSH-based sessions. Routing of the DNS works as expected and no leaks were observed using Wireshark. I'll go ahead and land it.

  • Example output
msf6 auxiliary(scanner/http/http_header) > sessions

Active sessions
===============

  Id  Name  Type                     Information                                Connection
  --  ----  ----                     -----------                                ----------
  1         shell linux              SSH n00tmeg @                              192.168.9.3:61361 -> 192.168.9.2:22 (192.168.9.2)
  2         meterpreter x64/windows  DESKTOP-26CQRHP\n00tmeg @ DESKTOP-26CQRHP  192.168.9.3:4444 -> 192.168.9.98:50242 (192.168.9.98)

msf6 auxiliary(scanner/http/http_header) > dns

Custom nameserver rules
=======================

   ID  Rules(s)      DNS Server   Comm channel
   --  --------      ----------   ------------
   0   *.myhst1.com  9.9.9.9
   2   *.myhst2.com  1.1.1.1      Session 1
   3   *.myhst3.com  76.76.19.19  Session 2


Default nameservers
===================

   ID  DNS Server  Commm channel
   --  ----------  -------------
   1   8.8.4.4

@cdelafuente-r7 cdelafuente-r7 merged commit 0d591a3 into rapid7:master Nov 23, 2023
60 checks passed
@cdelafuente-r7
Copy link
Contributor

cdelafuente-r7 commented Nov 23, 2023

Release notes

This adds a new dns command in Metasploit, to allow the user to customise the behaviour of DNS resolution in the framework. DNS resolution can be set to be routed through a session via a specific Comm channel or to request a specific DNS server. Routing rules ensure DNS queries are not sent to unwanted DNS servers and avoid the leak of information.

@smashery smashery mentioned this pull request Nov 24, 2023
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

3 participants