Tuesday, April 6, 2010

Episode #89: Let's Scan Us Some Ports

Ed mulls:

I'm sure it happens all the time. One of our readers is sitting at a command shell pondering their activities for the day, when the urge suddenly hits them -- "I wanna port scan something... just because." I know I feel that urge a lot, which I often sate with the wonderful Nmap port scanner. But, what if you don't have Nmap handy, and aren't allowed to install it? How can you do a TCP port scan using only built-in tools? Maybe you're on a very restricted penetration test, and you've just popped a box to get shell access, but aren't allowed to install any other software on the machine. After your shell happy-dance is complete, how can you do a port scan from your newly conquered territory against other hosts?

At first blush, you'd probably think about using the telnet client, which can make a connection to arbitrary ports. I'll walk you through the logic and approach I used to try to make the Windows telnet client bend to our will, so you can see how I approach these kind of problems. It's ugly, but perhaps the steps will interest you. After we show the utter failure and heap of ruin the completely awful Windows telnet client is, I'll then show you a technique that'll actually work.

Ahhh... but the telnet client was removed from Vista and Win7, now wasn't it? Well, while telnet.exe isn't there on the machine ready to run, the install package for it is included in most versions of Vista and Win7 without a further download. The telnet client is there, just waiting for you to install it via the package manager command:

C:\> pkgmgr /iu:"TelnetClient"

The /iu there stands for "install update". Also, on most versions of Windows, you have to put the quotes around "TelnetClient" and make sure your T & C are caps. Later, if you want to remove this package, you can run:

C:\> pkgmgr /uu:"TelnetClient"

That command uninstalls update (uu). Again, remember that the telnet client software is already on the box -- no download happens here. You are just applying the package and then removing it.

So, with our telnet client ready to rock, we can test individual ports easily. Let's try port 3 on target machine

C:\> telnet 3
Connecting To not open connection to the host, on port 3: Con
ect failed

Ahh, this looks promising, right? Well, actually, it turns out it isn't, as we can see when we wrap it into a FOR loop to try to hit a range of ports to scan ports 1 through 1024:

C:\> for /L %i in (1,1,1024) do telnet %i

This looks nice on the surface, but when you reach an open port, it just hangs. And, it'll hang for as long as the remote port keeps the connection open, which could be forever. Hmmm... How can we get around that? Well, let's echo nothing into telnet and see what happens:

C:\> for /L %i in (1,1,1024) do echo | telnet %i

Ahh... that's better. Now, when it encounters an open port, it clears the screen, and prints:
Welcome to Microsoft Telnet Client

Escape Character is 'CTRL+]'

Almost there, right? Uhh... You wish! We've got another problem. We've gotta record our result somewhere. Unfortunately, if you try to do anything with Standard Output when using the Microsoft telnet client, the telnet client refuses to connect! It's strange... it's almost like the telnet client is psychic and can peek ahead in your command line to see that you are doing something with its Standard Output, so it refuses to run. Thus, you can't pipe or redirect that output anywhere. We've got to consider other ways.

How about checking the error condition? Yeah, we can try the little && or || to conditionally execute a command after the telnet client, which will tell us if telnet succeeded or not. Let's try that:
C:\> for /L %i in (1,1,1024) do @echo | telnet %i && echo Port %i is open
Connecting To not open connection to the host, on port 1: Conn
ect failed
Port 1 is open
Connecting To not open connection to the host, on port 2: Conn
ect failed
Port 2 is open
Connecting To
Welcome to Microsoft Telnet Client

Escape Character is 'CTRL+]'

Port 3 is open

Bah! Infernal Microsoft... you don't properly set the error condition here so our && trick doesn't work. Telnet always succeeds, even when it fails. Unlike the Vista marketing campaign, but I digress.

Ok... let's give it one more shot, shall we? How about we activate logging in the telnet client (with -f logfile). We can just log the status of each of our connection attempts into a file for each port called portN.txt. That will surely tell us if it can connect or not, right? Let's try:

C:\> for /L %i in (1,1,1024) do @echo | telnet -f port%i.txt %i && echo
Port %i is open

I let that run for 7 ports, and then I stopped it. Port number 3 was listening on the target while I ran it. Let's look at what our telnet client actually logged:

C:\> dir port*
Volume in drive C has no label.
Volume Serial Number is E008-2D0C

Directory of C:\

04/03/2010 03:59 AM 0 port1.txt
04/03/2010 03:59 AM 0 port2.txt
04/03/2010 03:59 AM 0 port3.txt
04/03/2010 03:59 AM 0 port4.txt
04/03/2010 03:59 AM 0 port5.txt
04/03/2010 03:59 AM 0 port6.txt
04/03/2010 03:59 AM 0 port7.txt
7 File(s) 0 bytes
0 Dir(s) 696,209,408 bytes free

Remember, port 3 was open, the others are closed. But, it created an empty file for every port! There is no way to differentiate from this output which ports are open or closed.

The built-in Windows telnet client is absolute garbage... really one of the worst written tools ever. Microsoft should be ashamed of itself.

So, we'll have to find something else that can make a TCP connection for us so we can do our little scan. We need a simple command-line tool that's built-in to Windows that can make a TCP connection to an arbitrary port. How about the FTP client! Yeah, everyone loves the FTP client. Let's try it on port 3:

C:\> ftp 3

Transfers files to and from a computer running an FTP server service
(sometimes called a daemon). Ftp can be used interactively.

FTP [-v] [-d] [-i] [-n] [-g] [-s:filename] [-a] [-w:windowsize] [-A] [host]

Oh wait... that's just displaying the help of the ftp command. To make the FTP client connect to a given port, we can't just invoke it to connect to an IP Address and a port number. But, notice that -s:filename option. We could make the ftp client connect to port 3 by creating a file called 3.txt containing:
open 3

Then, we invoke the ftp client to simply read its command from this file:

C:\> ftp -s:3.txt
ftp> open 3
Connected to

Nice! We've got something here... Let's write a loop that will create a file called ftp.txt, which contains our connection commands (open IPaddr portnum followed by quit). We'll store Standard Error from our FTP command, because that'll give us an indication of which ports are closed, by saying "Connection closed by remote host." Here is our command:

C:\> for /L %i in (1,1,1024) do @echo Checking Port %i: >> ports.txt & echo 
open %i > ftp.txt & echo quit >> ftp.txt & ftp -s:ftp.txt 2>>ports.txt

Now, when the scan is done, we can look at ports.txt. Wherever it shows the text "Connection closed by remote host", we know that port is open. You see, there was a connection, which was then closed. So, the port was open. By the way, interestingly enough, when it does find an open port, the ftp client will hang for about 30 seconds. Then, the client drops the connection (not the remote host, as is erroneously reported in the error message!), and the client prints the message "Connection closed by remote host." on Standard Error, which we capture.

Sadly, there is one more little wrinkle here, though. The command we just showed works great on XP and 2003. But, Microsoft, in its infinite wisdom, decided to change the FTP client in Windows Vista and Windows 2008 server. In those two versions of our beloved OS, the FTP client doesn't display its error or success messages on Standard Error any more. Nor does it display them on Standard Output! I'm sure they did this in response to a lot of customer requests. I can picture it now: "Hello, Microsoft? Yeah, I want you to change the built-in FTP client so that its connection messages aren't displayed on Standard Error. Oh, and I don't want them on Standard Output either. Yes... Yes... just display them on the screen so that we can't capture them. Uh huh... I want you to change this 15 year old tool to make it even more cumbersome and less usable than ever. That's what I'm paying you for, isn't it, Microsoft? Think about MS Word as an example." I'm sure there was such an outcry for this change, Microsoft bent to the will of its customers. Still, despite this little change from Microsoft, our port scanner will function on Win Vista and 2008 server... it just won't store its results into a file for us.

Wanna hear something even weirder? When I originally wrote the previous paragraph, I had included Win7 among the versions with the lame change to ftp.exe. I tested it in Win Vista and Win2008, and figured Windows 7 would have the same behavior. But, our crack team of Bodacious Research Assistants here at CLKF labs, working diligently through the night (we do feed them now, though, by order of court) mentioned to me that on Window 7, the ftp client actually does display the "Connection closed by remote host." on Standard Error, so our little command works well on Win 7, diligently recording results. So, our port scanner works great on XP, 2003, and Win7, storing its results. But, on Vista and 2008, it doesn't. Yet another reason for confining Vista to the ash heap of history, and just avoiding doing port scans from a Win2K8 box using FTP.

Truth be told, it wasn't the Bodacious Research Assistants (and no, we don't ever convert that designation to an acronym, thank you very much... again by court order) that pointed out the change in Win7. It was Tim Medin himself, who, while Bodacious, is not a mere Research Assistant.

Diligent reader Matthew writes in about using netsh to check ports:

I was reading your recent post on port scanning from the command line and I like it a lot. In my job I have to do a lot from the Windows and Linux command line and one day while cursing at Windows for not having netcat I stumbled upon a great way to check whether a port was listening or not. You can use netsh. To check if a port is listening or not just run "netsh diag connect iphost ipaddr portnum". Here is the output when something is listening on the port.

IPHost (www.google.com)
IPHost = www.google.com
Port = 80
Server appears to be running on port(s) [80]

And here's what the command looks like when something isn't running on the port

IPHost (www.google.com)
IPHost = www.google.com
Port = 3389
Server appears to be running on port(s) [NONE]

It can't tell whether a port is open with nothing listening but for most purposes I think it works pretty good.

Awesome stuff, Matt. Thanks for the info! It should be noted that your commands work like a champ on XP and 2003 Server. However, Microsoft removed the diag context from netsh in Vista and Windows 7. You gotta love 'em. :)

Let's see what else Mr. Medin (whose last name is actually pronounced "Sally") has up his lab coat sleeve.

Tim joins the mess

While working as a starving BRA in the basement of <redacted by court order>'s house, I decided to get back at him by taking his work and one-upping him. I tried for countless hours to implement the Windows command line equivalent of "strings", but to no avail. I made attempts to solve other problems in Windows, such as lack of netcat, but there were no new breakthroughs. Before I collapsed from hunger, I decided to find a better way to port scan in Windows. Eureka! Ed did all the hard work, but there is a slightly shorter way of doing it. Finally, FOOD!

Ed's port scanning technique in Windows is really ingenious. The only problem is that annoying script file, but there is a way around it using some echo magic and the pipeline.

C:\> for /L %i in (1,1,1024) do @((echo open %i)&(echo quit)) |
ftp 2>&1 | find "host" && @echo %i is open

Connection closed by remote host.
3 is open
In this version, the commands to be executed by our ftp client are sent down the pipeline together, well, sort of. The two commands are bundled by wrapping them in parenthesis and using an ampersand between each group. Unfortunately, we can't send more than two strings down the pipeline (e.g. opening the connection, sending a username, and then a password) since the third and later commands are dropped. My guess is that the ftp client isn't expecting or can't handle commands that quickly.

Now to look at the results of our ftp commands. As Ed mentioned, an open port is found when "Connection closed by remote host" is shown on standard error. To find this message we need to first redirect standard error to standard out (2>&1). We can then use the Find command to filter for output that contains the word "host", which is a short version of "Connection closed by remote host."

Finally, we use a logical And (&&) before displaying the open port number. The use of the logical And ensures that the "port is open" text is only displayed if all of the previous commands, including Find, were successful.

No matter how you look at it, that is ugly. PowerShell is a bit better, but still not great. Here is the command in PowerShell:

C:\> PS C:\> 1..1024 | % { echo ((new-object Net.Sockets.TcpClient).Connect("",$_)) "$_ is open" } 2>$null
3 is open
We start off using the range operator to create an array of the ports we want to scan. The array is then piped into the ForEach-Object cmdlet. Inside the ForEach-Object cmdlet's script block is where the magic is done.

The connection attempt is made using the .NET TcpClient class. The Connect method requires an address and a port.

Now here is where we do a bit of Write-Output (alias echo) magic. You'll notice the echo command has been given two parameters, the connection attempt and the text that says the port is open. The trick is, if the first command fails then the output of the second command is not displayed either. Here is an example:

PS C:\> echo (1+1) (2+2)
If we replace the (1+1) with (1/0) then the only output is the error:

PS C:\> echo (1/0) (2+2)
Attempted to divide by zero.

The Connect method doesn't have any default output. If it fails it throws an error, if it works then there is no output and no error. We need a way to display the status, and we can use the technique above to our advantage. If the connection is successful then the "$_ is open" is displayed. If our connection fails, an error is thrown and we don't output the "$_ is open" portion, but we have an error messages to clean up. Clean up is done by redirecting standard error to $null.

This is much easier than the classic Windows shell, but still not super easy or clean. We could use something like nmap, but that isn't installed in Windows. Besides, wouldn't you rather impress your boss with this crazy Windows fu than something that looks easy?

Now that Ed and I have made a major mess of things let's see what Hal has for us.

Hal plays along

While I think it would actually be reasonable for me to claim that nmap is almost a de facto standard command in Unix-like operating systems these days, I'll play along with Ed's arbitrary little scenario. So in the spirit of Episode #61, I'm going to show you how to make a port scanner using just built-in bash primitives.

The main trick here is to use the /dev/tcp/... output redirection that I showed you way back in Episode #47:

$ echo >/dev/tcp/localhost/22
$ echo >/dev/tcp/localhost/23
bash: connect: Connection refused
bash: /dev/tcp/localhost/23: Connection refused

The general syntax is "/dev/tcp/<host|ip>/<port>". And as you can see, it's pretty easy to tell from the output whether the port is open or not. But rather than parsing the output, we can just use the short-cut "&&" operator to produce more useful output:

$ echo >/dev/tcp/localhost/22 && echo 22 open
22 open

However, to be clean about this, I'd also like to make the "Connection refused" messages disappear from the output so I just get a list of the open ports. Your first attempt might be to simply add a "2>/dev/null" before the "&&":

$ echo >/dev/tcp/localhost/23 2>/dev/null && echo 23 open
bash: connect: Connection refused
bash: /dev/tcp/localhost/23: Connection refused

Why didn't that work? The problem is that the "Connection refused" messages are the result of the attempted output redirection to "/dev/tcp/...". Your "2>/dev/null" is applying to the standard error output of the original echo command, so it's not going to suppress the error messages we want to go away.

The easiest work-around is to wrapper the original echo command and /dev/tcp/... output redirection in parentheses, forcing them into a sub-shell. We can then redirect the standard error output of the entire sub-process to /dev/null to remove the offending error messages:

$ (echo >/dev/tcp/localhost/23) 2>/dev/null && echo 23 open

Awesome! Now the only thing we need to do to turn this into a real port scanner is to wrap our command up inside a loop:

$ for ((i=1; $i < 65535; i++)); do 
(echo > /dev/tcp/localhost/$i) 2>/dev/null && echo $i open;

22 open
631 open
29754 open
46783 open

It takes about 90 seconds to sweep all 64K TCP ports on my local machine over the loopback interface. Scanning another host on the same LAN takes about twice as long. Of course, I could modify my loop so that I start several probes in parallel by backgrounding tasks, which would speed up the process. But really, if speed were a factor you'd be using nmap instead.

Jeff Haemer wrote in to point out that my loop would be much faster if I did away with the sub-shells and handled the standard error redirection outside of the loop:

$ for ((i=0; $i < 65535; i++)); do 
echo >/dev/tcp/localhost/$i && echo $i open;
done 2>/dev/null

22 open
631 open
29754 open
46783 open

This version takes under 10 seconds to scan all ports over the loopback interface and still gives us the clean output we're looking for. Thus we see the perils of just throwing a loop around a simple command line without considering the possibilities for optimization.

By th way, bash also supports similar syntax with /dev/udp/... Can we use this to create a UDP port scanner as well? Unfortunately not:

$ echo >/dev/udp/localhost/53 && echo 53 open
53 open
$ echo >/dev/udp/localhost/54 && echo 54 open
54 open

Note that I'm running the above commands on a machine that has a name server listening on 53/udp, but nothing bound to 54/udp. As you can see, the output from both is the same. Apparently bash doesn't distinguish the ICMP port unreachable messages that the second command is generating. Oh well.