Skip to content

Add SOCKS5H Support#76

Merged
dledda-r7 merged 8 commits intorapid7:masterfrom
zeroSteiner:feat/proxy/socks5h
May 21, 2025
Merged

Add SOCKS5H Support#76
dledda-r7 merged 8 commits intorapid7:masterfrom
zeroSteiner:feat/proxy/socks5h

Conversation

@zeroSteiner
Copy link
Contributor

This adds SOCKS5H support to Rex::Socket, allowing TCP connections established with the local comm to be made through SOCKS5 proxies. The difference between SOCKS5 and SOCKS5H is where hostnames are resolved to DNS. There is a common convention where SOCKS5 proxy clients (including Metasploit) resolve hostnames themselves and issue the IP address to the SOCKS server when connecting. SOCKS5H on the other hand is an unofficial standard whereby the hostname is passed to the SOCKS server, allowing it to be resolved by the proxy instead of Metasploit.

These changes lay the ground work for Metasploit to support both conventions. There will be a PR to rapid7/metasploit-framework shortly that will take advantage of these changes.

This PR also adds a number of unit tests to show how proxies are handled and ensure resolution does and does not happen when appropriate.

Proxy Strings

Proxies are now parsed into URI instances instead of arrays. This means Ruby's builtin URI class and it's parsing capabilities can be leveraged instead of using a custom solution. In the future this should also allow additional options to be specified in the URI, so if an HTTP proxy requires authentication, it can be specified in the URI. The // part of the URi is optional to ensure backwards compatibility, e.g. http:localhost:8080 will still work but it's normalized to http://localhost:8080 under the hood so it can be processed as a URI.

Test Script

This test script will target an SSH server on TCP port 22 which works well as a test case because SSH will send it's banner, showing that the connection was established. This script allows a proxy to be set so a user can interactively observe how their options affect the socket creation. Use wireshark to check the connect request made to the SOCKS server to see if a hostname or IP address was sent.

$LOAD_PATH.unshift 'lib'
require 'optparse'
require 'rex/socket'

options = {}
parser = OptionParser.new do |opts|
  opts.banner = "Usage: script.rb TARGET [--proxy PROXY]"

  opts.on("--proxy PROXY", String, "Proxy (optional)") do |proxy|
    options[:proxy] = proxy
  end
end

begin
  parser.parse!
  raise OptionParser::MissingArgument, "TARGET is required" if ARGV.empty?
  options[:target] = ARGV.shift
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
  puts e.message
  puts parser
  exit 1
end

puts "Target: #{options[:target]}"
puts "Proxy:  #{options[:proxy]}" if options[:proxy]

client = Rex::Socket::Tcp.create({
  'PeerHost' => options[:target],
  'PeerPort' => 22,
  'SSL'      => false,
  'Proxies'  => options[:proxy]
})
data = client.get_once

$stderr.puts data
client.close

Comment on lines +358 to +360
if proxies?
best_comm = Rex::Socket::Comm::Local
elsif server && localhost
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 now, only the local comm supports proxied connections. If a comm is specified, the proxy options are silently ignored. This seems like a problem. Solutions are probably to:

  1. Warn that proxies will be ignored.
  2. Raise an exception that the selected comm doesn't support proxies.
  3. Refactor proxy connection login into a mixin that can be included into any comm. The comm would be used to establish the connection to the first proxy in the chain, then it'd continue on from there. This would be the most difficult option and is thus left out of scope for this change set.

Kind of related is the fact that proxies are ignored for non-TCP socket types. Also seems like a problem, but supporting proxies in comms won't change that. SOCKS4 and HTTP simply don't support UDP and they never will.

Comment on lines -443 to +429
resp = Rex::Proto::Http::Response.new
resp.update_cmd_parts(ret.split(/\r?\n/)[0])
if (match = ret.match(/HTTP\/.+?\s+(\d+)\s?.+?\r?\n?$/))
status_code = match[1].to_i
end
Copy link
Contributor Author

@zeroSteiner zeroSteiner May 7, 2025

Choose a reason for hiding this comment

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

This fixed a bug where an exception would be raised if Rex::Socket was used out side the context of Metasploit because Rex::Proto::Http::Response is unavailable. The other proxies just do the bare minimum parsing necessary to ensure the connection was established, so that pattern was copied here as well. Removing this dependency also means the HTTP proxy can be tested in the specs.

@zeroSteiner zeroSteiner force-pushed the feat/proxy/socks5h branch 2 times, most recently from f372719 to c5281fb Compare May 7, 2025 23:14
Only the local comm supports proxies. This makes the priority:
1. The explicit comm that was set
2. Local if any proxies where set
3. The comm selected from the switchboard
4. The local comm
It doesn't exist here, so just do some simple parsing like the other
proxy protocol handlers do.
@zeroSteiner zeroSteiner force-pushed the feat/proxy/socks5h branch from c5281fb to a19f90f Compare May 7, 2025 23:26
@dledda-r7 dledda-r7 self-assigned this May 21, 2025
@dledda-r7 dledda-r7 merged commit b220b3c into rapid7:master May 21, 2025
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants