长连接与短连接的安全差异讨论
一、长连接与短连接的定义
1.1 定义
在接解http的时候,我们或多或少都会听说过长连接和短连接的概念,有心点还会知道http默认是短连接如果要长连接可带上如下头,什么长连接节省性能从来都是不管的。
Connection: keep-alive
但其实除了性能,长连接和短连接在安全方面其实存在差异,这就使得渗透测试人员不得不认真了解什么是长连接什么是短连接。
而第一步就是要明确什么是长连接什么是短连接,学院派流行把简单的东西自我感觉良好地说到让人听不懂,学术派流行能力不足强行解释把简单的东西自我都感觉不良好地说到让人听不懂。
反正就是听不懂,所以还是得自己来理解一下。先举个例子,比如现在有url1和url2两个页面:
短连接的访问模式是:三次握手---url1----四次挥手,三次握手----url2----四次挥手。一次连接只承载一组http请求(一组而不是一个,是因为请求和响应肯定要在一个连接中完成;一组而不是一对,是因为当前页面通过url导入的其他元素如js文件css文件图片等的请求响应也是在同一个连接中完成)。
长连接的访问模式是:三次握手----url1----url2----urlx----四次挥手。一次连接承载多组http请求。
然后我们可以下定义:一个连接以三次握手开始四次挥手结束;如果在这个连接只传输一组应用层数据包那他就是短连接,如果能传输多组应用层数据包那他就是长连接。
(不过严谨地讲,现在的apache等http服务器都支持设置keep-alive的时长,所以时间长短还是传输多少组应用层数据包都比较难准确划分长连接短连接,但现在的系统都讲登录我们可以从登录角度去下一个定义:如果一个连接传输从在用户登录到用户退出中所有请求那他就是长连接,反之则是短连接。)
1.2 http等协议使用短连接的原因
从前面讨论可以看到短连接要频繁地建立和断开连接,每多一组请求就比长连接多一次握手和挥手,直觉上长连接比短连接有优势。但现实是众多应用层协议使用的是短连接而不是长连接,我们以http为例来分析其原因。
我们以访问和查看一个链接为一个时间单位----比如你查看这篇博客----从点击链接到现在这段时间其实也就只有加载页面那一组请求,查看内容这段时间是没有任何请求的,也就是说在这段时间中长连接确实比短连接节省了一个握手挥手过程,但也在整段时间内比短连接多耗保持连接需要的系统资源,而且用户查看内容的时间越长长连接耗费的资源就越大。
访问整个网站可以分拆成访问和查看一个个链接,从单个时间单位上看http使用长连接其实并不比短连接节省资源,所以整个来看http使用长连接也不会比短连接节省资源。
从上面讨论中,主要就是看是长连接减少的握手挥手过程节省的资源多,还是短连接不需要保存会话节省的资源多;或者叫,长连接的优势与服务时间内的请求组数成正比。
二、长连接与短连接的安全差异
在日常web渗透中我们的经验是,如果一个包返回了某个结果那我们用burpsuite再次发送时仍会得到同样的结果(不考虑防重放不考虑会话超时不考虑删除操作不要钻牛角尖)。
直到有一天我截获了一个没有鉴权的数据包,然后编写脚本重放时得到了迵然不同的结果,才意识到这个经验并不能用到长连接上。下面举例说明。
2.1 代码
服务端server_keep_alive.py代码如下:
import socket import threading # 线程实现类 class thread_socket (threading.Thread): def __init__(self, thread_id, client_addr, client_socket): threading.Thread.__init__(self) self.thread_id = thread_id self.client_addr = client_addr self.client_socket = client_socket def run(self): msg = self.client_socket.recv(1024).decode("utf-8") while msg != "exit": print(f"receive msg from client {self.thread_id}-{self.client_addr}:{msg}") msg_dict = msg.split(":") # 如果客户传过来的内容不能以:切分成2份那必然不是用户名密码,继续要求用户输入用户名密码 if len(msg_dict) != 2: msg = f"{self.thread_id}-{self.client_addr},sorry please enter correct username and password at first".encode("utf-8") self.client_socket.send(msg) # 如果是正确的用户名密码,那么允许客户端执行命令 elif msg_dict[0] == "admin" and msg_dict[1] == "password": msg = f"{self.thread_id}-{self.client_addr},congratulation, you have connect with server\r\nnow, you can execute your command".encode("utf-8") self.client_socket.send(msg) msg = self.client_socket.recv(1024).decode("utf-8") while msg != 'exit': print(f"receive msg from client {self.thread_id}-{self.client_addr}: command: {msg}") msg = f"{msg} execute finished".encode("utf-8") self.client_socket.send(msg) msg = self.client_socket.recv(1024).decode("utf-8") print(f'{self.thread_id}-{self.client_addr} now close connect') self.client_socket.close() # 如果是正确的用户名密码,继续要求用户输入用户名密码 else: msg = f"{self.thread_id}-{self.client_addr},sorry,please enter correct username and password at first".encode("utf-8") self.client_socket.send(msg) # self.client_socket.close() msg = self.client_socket.recv(1024).decode("utf-8") self.client_socket.close() # 服务端主类 class server_class : def build_listen(self): # 监听端口 server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server_socket.bind(('10.10.6.91',9999)) server_socket.listen(5) print(f"now server listen at 10.10.6.91:9999") thread_count = 0 threads = [] while True: # 每接收一个客户端连接,就新启动一个线程去交互 client_socket, client_addr = server_socket.accept() thread_name = thread_socket(thread_count, client_addr, client_socket) threads.append(thread_name) threads[thread_count].start() # threads[thread_count].join() thread_count += 1 if __name__ == "__main__": server = server_class() server.build_listen()
客户端client_keep_alive.py代码如下:
import socket class client_class: def send_hello(self): # 与服务端建立连接 client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client_socket.connect(('10.10.6.91',9999)) # 向服务器发送消息,打印服务器返回消息 msg = input("please enter your msg for send to server:") while msg != "close": # 向服务端发送消息 client_socket.send(msg.encode("utf-8")) # 接收服务端返回的消息 msg = client_socket.recv(1024).decode('utf-8') print(f"receive msg from server : {msg}\r\n") msg = input("please enter your msg for send to server:") client_socket.close() if __name__ == "__main__": client = client_class() client.send_hello()
2.2 运行过程
操作步骤如下:
第一步,运行server_keep_alive.py
第二步,运行client_keep_alive.py两次,实例化出两个客户端client0和client1
第三步,client0输入client0,client1输入client1;返回结果都是要求输入用户名密码
第四步,client0输入admin:password登录成功,client1输入admin:password未登录成功继续被要求输入用户名密码
第五步,client0输入whoami命令执行成功,client1输入whoami命令执行不成功继续被要求输入用户名密码
client0运行截图:
client1运行截图:
server运行截图:
总的意思就是长连接中client0登录成功,成功执行命令;client1登录未成功,企图直接模仿client0执行命令被拒绝了。
也就是说,在长连接中如果有登录认证机制,那么所有连接都需要独自完成这个认证过程,直接构造发送认证之后才接收的数据包服务端是不认的;或者说长连接能够记录每个连接是否已通过认证;或者说长连接是有状态的(http没有状态根本原因就是http使用的是短连接,http需要cookie的根本原因也是http使用的是短连接)。
三、渗透长连接系统的注意事项
现在随着计算机性能的长足进步,性能已逐渐被安全性易用性等超越沦为系统设计中的次要矛盾,在一些私有系统(相对百度等公共可以访问的系统)中尤为明显。由于长连接的有状态特性直接节省了会话保持设计,有很多的私有协议(相对http等标准协议)直接采用长连接,也因此渗透测试者也就难免会遇上长连接系统。
而长连接的有状态特性,就要求对于习惯于渗透短链接的渗透测试者,在渗透长连接系统时需要注意以下两点:
一是在长连接系统中,如果截获一个危险操作的数据包发现里面没有任何鉴权字段,那也不能就认定该系统存在越权漏洞,需要查看连接刚建立时有没有登录认证机制,如果有登录认证机制且该机制没问题那是不存在越权漏洞的。
二是在长连接系统中,如果有登录认证机制那么如果想直接对服务端口重放从别的连接截获的数据包那是不可能成功的,需要先完成连接开头的登录认证。