長連接與短連接的安全差異討論


一、長連接與短連接的定義

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()
View Code

客戶端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()
View Code

 

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等標准協議)直接采用長連接,也因此滲透測試者也就難免會遇上長連接系統

而長連接的有狀態特性,就要求對於習慣於滲透短鏈接的滲透測試者,在滲透長連接系統時需要注意以下兩點:

一是在長連接系統中,如果截獲一個危險操作的數據包發現里面沒有任何鑒權字段,那也不能就認定該系統存在越權漏洞,需要查看連接剛建立時有沒有登錄認證機制,如果有登錄認證機制且該機制沒問題那是不存在越權漏洞的。

二是在長連接系統中,如果有登錄認證機制那么如果想直接對服務端口重放從別的連接截獲的數據包那是不可能成功的,需要先完成連接開頭的登錄認證。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM