MMD-0062-2017 - Credential harvesting by SSH Direct TCP Forward attack via IoT botnet

原文: 2017-02-27-MMD-0062-2017 Credential harvesting by SSH Direct TCP Forward attack via IoT botnetd

我们把这种威胁叫做“馅饼攻击”

1. 背景

这份报告里没有分析恶意软件或者病毒,但是这是坏人为了更大的犯罪过程,利用弱证书管理来恶意感染物联网设备的影响之一。这里唯一提到的恶意方面是牵涉到并且参与这些攻击的个人,并且,好吧,我个人不认为使用的工具或者方法(SSH TCP转发)是“恶意的”,因为,某种程度上说,它是UNIX网络和开发的非常有用的概念。

请忍耐这份又长又像论文的技术文章,这确实是必要的,因为这篇文章是犯罪新方向(通过SSH级联代理缓冲区的TCP转发来自动收集大量凭证)概念的第一证据,并且这也是一个正在持续的威胁,所以这在未来研究的学术上是有用的(希望)。但是,如果你需要更简易短小的技术解释,你可以先阅读Infosec Insitute网站上的我关于这件事的问答形式的采访(感谢Mr. Paganini和 Infosec Institute的工作人员),另外,如果你只需要一个极其简短的摘要,你可以在reddit网站这里找到它。

下面,让我们开始吧: SSH端口转发的概念:

“(1)SSH端口转发说一个仅仅作用于TCP协议的一个通用代理机制。转发不能在非TCP类的协议上使用哦,例如基于UDP的DNS、DHCP、NFS和NetBIOS,[121],也不能再非IP类的协议上使用,例如AppleTalk或者Novell的SPX/IPX。(2)端口转发只有在你创建SSH链接的时候才能指定。虽然SSH协议里并没有针对这个的约定,但是在我们所知道的任何SSH接口中,你都不能在一个已经存在的SSH链接上添加一个转发,这一点有时候非常有用。”

以上引用部分来自 “O’Reilly’s SSH: The Secure Shell - The Definitive Guide, 9.2. Port Forwarding”

2016年1月24日起我们就监控到了这个威胁。我首先和我们在日本东京的团队成员在2016年的AVTokyo会议讨论了这种攻击。之后我们跟进了它的进展并在2016年12月首先向当局报告(对PlayStation商店里的账户认证攻击),同时我们尝试邮件Pokemon Go,因为我认识到在2016年12月他们也遭到了这个攻击,但是邮件的接收/发送似乎没有很好的开展。

从那个时候起,我们继续我们的分析,进入2017年之后这些类型的攻击变得“更好”了(自动化&利用了更多漏洞),所以我们试图通过我们社区公开告警,这也是PlayStation Network(PSN)账户认证被攻击者列为目标的时候。接下来,在2017年2月份,我们的报告被送往了我们的法律部门,包括请求为了公众意识而披露这种持续的威胁的允许,同时我们也通过我们的CERT频道报告了另一起对一个未公开的加拿大在线批发商店的攻击,以上升到加拿大执法和相关部门。

上面提到的公告和交流都得到了终止滥用的满意结果。而在我写这封报告的时候这种滥用还在进行中。为了支持以上描述的事实,我们有一些截图,细节如下:

2017-03-20-1.png 2017-03-20-2.png

此外:在这篇报道公布之后我接到了很多关于SSH的TCP转发滥用方面的其他好的分析的电话(其中有一个在本篇报告的序言部分提到了)。我,代表我的团队,想要确保,我们,在MalwareMustDie,有严格的政策,通过尽可能多的避免外部影响开展分析,以保持我们的分析的原始性,因此,这份分析是不一样的,虽然它说描述TCP端口转发,但它不是关于黑到HTTP服务或者无认证的SSH转发的情况。这份报告是通过被黑掉的SSH级联基础设备使用几种TCP黑客模式(HTTP/HTTPS/POP3/SMTP/IMAP)来有组织对大量证书的自动窃取过程的披露。

给法律部门的消息: 给法律部门的消息: video-2017-03-20-1.PNG

2. 威胁定义

正如上面引用的O’Reilly的SSH指导手册中所说,在SSH连接上有认证权限的合法用户可以通过代理机制转发TCP协议。简言之这是当前的通用做法,特别是用做当从本地网络区域来查看的那些服务。

这个威胁的定义是指对SSH上TCP转发合法利用的滥用,利用暴力强制账户的认证或者密码来执行对远程设备(服务器或IoT)弱SSH帐户的自动或手动攻击,通过TCP直接转发技术在SSH转发功能里利用这个“强制接入SSH连接来攻击远程服务。

根据这一定义,这种威胁正在创造一个双判决,一个用于SSH服务的黑客攻击;第二,试图攻击目标服务(S)。因此,如果这可以被视为第三判决,坏人使得被攻击SSH服务看起来是“恶意的”,就好像是它们在执行对目标的攻击。

3. 威胁细节

4. 攻击细节

5. 支持分析

6. 统计

7. 如何发现、阻止和缓解

to be continued…

一种将eclipse的pydev环境配置成BLACK风格的方式

分两步:

1.从eclipsemarket下载moonlight风格主题,安装并配置到eclipse中

+首先安装插件,可以直接将页面插件拖到eclipse中,会自动安装 +重启eclipse,菜单Window > Preferences > General > Appearance,选择MoonRise(standalone) 参考

2.导入pydev要使用的epf文件

文件可以本地创建,内容如下,假定命名为dark_theme.dpf: 打开菜单 File > Import > Preferences,导入dark_theme.dpf文件。 参考

file_export_version=3.0

/instance/org.python.pydev/CODE_COLOR=180,180,180
/instance/org.python.pydev/DECORATOR_COLOR=127,159,191
/instance/org.python.pydev/NUMBER_COLOR=63,127,95
/instance/org.python.pydev/EDITOR_MATCHING_BRACKETS_COLOR=165,42,42
/instance/org.python.pydev/KEYWORD_COLOR=136,3,91
/instance/org.python.pydev/SELF_COLOR=10,10,10
/instance/org.python.pydev/STRING_COLOR=63,95,191
/instance/org.python.pydev/COMMENT_COLOR=154,154,184
/instance/org.python.pydev/BACKQUOTES_COLOR=165,42,42
/instance/org.python.pydev/PARENS_COLOR=127,0,85
/instance/org.python.pydev/OPERATORS_COLOR=47,47,47
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.SelectionForeground.SystemDefault=false
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.Background.SystemDefault=false
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.Background=0,0,0
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.Foreground.SystemDefault=false
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.Foreground=255,255,255
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.SelectionBackground.SystemDefault=false
/instance/org.eclipse.ui.editors/AbstractTextEditor.Color.SelectionBackground=0,0,136
/instance/org.eclipse.ui.editors/pydevOccurrenceIndicationColor=128,64,0
/instance/org.eclipse.ui.editors/lineNumberColor=255,255,255
/instance/org.eclipse.ui.editors/printMargin=true
/instance/org.eclipse.ui.editors/printMarginColor=255,0,0
/instance/org.eclipse.ui.editors/currentLineColor=70,70,70
/instance/org.eclipse.ui.editors/currentIPTextStyle=BOX
/instance/org.eclipse.ui.editors/currentIPIndication=true
/instance/org.eclipse.ui.editors/currentIPHighlight=false
/instance/org.eclipse.ui.editors/secondaryIPTextStyle=DASHED_BOX
/instance/org.eclipse.ui.editors/secondaryIPHighlight=false
/instance/org.eclipse.ui.editors/secondaryIPIndication=true

以上COLOR数据可以按你自己的要求修改,重启eclipse,打开py文件,可以看到生效的black风格的UI了。

一种特殊的科学上网方案

一套科学上网解决方案,原文

py代码:

#!/usr/bin/python
#
# This is the relay script mentioned in http://blog.zorinaq.com/?e=81
#
# Listens on the address and port specified by --local-ip and --local-port, and
# relay all connections to the endpoint specified by --remote-hosts and
# --remote-port. Multiple remote hosts can be specified: one will be selected
# randomly for each connection.
#
# Optionally, if --mode 1:<secret> is specified, insert the secret key as the
# first bytes of data transmitted through each relayed connection, and if
# --mode 2:<secret> is specified, verify and remove the secret key (ignore
# the connection by discarding all data if the key does not match).
#
# I recommend a long hex string for the secret, for example:
# $ secret=`ps aux | md5sum | cut -c 1-32`
# $ ./tcprelay-secret-exp.py [...] -m 1:"$secret"

#import asyncake
import asyncore
import socket, random, struct
import re

class forwarder(asyncore.dispatcher):
    def __init__(self, ip, port, remoteip, remoteport, mode, secret, backlog=600):
        asyncore.dispatcher.__init__(self)
        self.remoteip=remoteip
        self.remoteport=remoteport
        self.mode=mode
        self.secret=secret
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((ip,port))
        self.listen(backlog)

    def handle_accept(self):
        conn, addr = self.accept()
        # print '--- Connect --- '
        sender(receiver(conn, addr[0], self.mode, self.secret),self.remoteip,self.remoteport, addr[0])

class receiver(asyncore.dispatcher):
    def __init__(self, conn, client_ip, mode, secret):
        asyncore.dispatcher.__init__(self,conn)
        self.mode=mode
        self.secret=secret
        self.from_remote_buffer=''
        self.to_remote_buffer=''
        self.sender=None
        self.client_ip = client_ip
        self.zero_bytes_forwarded = True
        # for framing
        self.look_for_type = True
        self.field_type = None
        self.look_for_len_byte_nr = None
        self.field_len = None
        self.bytes_left_to_extract = None

    def handle_connect(self):
        pass

    def detect_and_remove_framing(self, read):
        processed_read = ''
        for b in read:
            if self.look_for_type:
                self.field_type = b
                self.look_for_type = False
                self.look_for_len_byte_nr = 0
                self.field_len = 0
            elif self.look_for_len_byte_nr != None:
                self.field_len <<= 8
                self.field_len += ord(b)
                self.look_for_len_byte_nr += 1
                if self.look_for_len_byte_nr >= 2:
                    self.look_for_len_byte_nr = None
                    self.bytes_left_to_extract = self.field_len
            elif self.bytes_left_to_extract != None:
                if self.field_type == 'd':
                    processed_read += b
                else:
                    pass
                self.bytes_left_to_extract -= 1
                if self.bytes_left_to_extract == 0:
                    self.bytes_left_to_extract = None
                    self.look_for_type = True
        return processed_read

    def handle_read(self):
        """Read from TCP client."""
        read = self.recv(4096)
        if self.mode == '1': # insert the secret key
            # Implement simple framing ('d' for data packets, 'p' for padding packets)
            read = 'd' + struct.pack('>h', len(read)) + read
            rlen = 0
            padding = 1
            if padding == 0: # no padding
                rlen = 0
            elif padding == 1 and len(read) < 1500: # padding
                if len(read) < 1000:
                    rlen = random.randint(1000 - len(read), 1500 - len(read))
                else:
                    rlen = random.randint(0, 1500 - len(read))
            if rlen:
                read += 'p' + struct.pack('>h', rlen) + ('_' * rlen)
            if self.zero_bytes_forwarded:
                read = self.secret + read
                self.zero_bytes_forwarded = False
        elif self.mode == '2': # verify and remove the secret key
            if self.zero_bytes_forwarded:
                if read.startswith(self.secret):
                    read = read[len(self.secret):]
                    self.zero_bytes_forwarded = False
                else:
                    read = ''
            read = self.detect_and_remove_framing(read)
        # print '%04i -->'%len(read)
        self.from_remote_buffer += read

    def writable(self):
        return (len(self.to_remote_buffer) > 0)

    def handle_write(self):
        sent = self.send(self.to_remote_buffer)
        # print '%04i <--'%sent
        self.to_remote_buffer = self.to_remote_buffer[sent:]

    def handle_close(self):
        self.close()
        if self.sender:
            self.sender.close()

class sender(asyncore.dispatcher):
    def __init__(self, receiver, remoteaddr, remoteport, client_ip):
        asyncore.dispatcher.__init__(self)
        self.receiver=receiver
        receiver.sender=self
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((random.choice(remoteaddr), remoteport))

    def handle_connect(self):
        pass

    def handle_read(self):
        """Read from TCP server."""
        read = self.recv(4096)
        # print '<-- %04i'%len(read)
        self.receiver.to_remote_buffer += read

    def writable(self):
        return (len(self.receiver.from_remote_buffer) > 0)

    def handle_write(self):
        sent = self.send(self.receiver.from_remote_buffer)
        # print '--> %04i'%sent
        self.receiver.from_remote_buffer = self.receiver.from_remote_buffer[sent:]

    def handle_close(self):
        # when the buffer has not yet fully been written to the client, don't close quite yet.
        # handle_close() will be automatically called again by asyncore
        if not self.receiver.to_remote_buffer:
            self.close()
            self.receiver.close()

if __name__=='__main__':
    import optparse
    parser = optparse.OptionParser()

    parser.add_option(
            '-l','--local-ip',
            dest='local_ip',default='127.0.0.1',
            help='Local IP address to bind to (for listening socket)')
    parser.add_option(
            '-p','--local-port',
            type='int',dest='local_port',
            help='Local port to bind to')
    parser.add_option(
            '-P','--remote-port',
            type='int',dest='remote_port',
            help='Remote port to connect to')
    parser.add_option(
            '-r','--remote-hosts',
            type='string',dest='remote_hosts',default='127.0.0.1',
            help='Remote host(s) to connect to, comma-separated')
    parser.add_option(
            '-m','--mode',
            type='string',dest='mode',
            help='Operating mode ("0" for not using a secret key, ' + \
                    '"1:<secret>" for using/inserting the specified secret key, ' + \
                    '"2:<secret>" for verifying/stripping the specified secret key')
    options, args = parser.parse_args()
    alladdresses = {}
    for h in options.remote_hosts.split(','):
        (name, aliaslist, addresslist) = socket.gethostbyname_ex(h)
        for a in addresslist:
            alladdresses[a] = None
    if options.mode is None:
        (mode, secret) = (None, None)
    else:
        (mode, secret) = options.mode.split(':', 1)
        if len(secret) < 32:
            raise Exception('secret specified in -m option needs to be at least 32 characters long')
    #x = asyncake.AsynCake()
    forwarder(options.local_ip, options.local_port, alladdresses.keys(), options.remote_port, mode, secret)
    #x.loop()
    asyncore.loop()

Use it like this on the client: ./tcprelay-secret-exp.py -p 5000 -P 5001 -m 1:$secret

And on the server: ./tcprelay-secret-exp.py -p 5003 -P 5004 -m 2:$secret