/ linux security

Writing a port scanner in Bash shell

This isn't actually particularly useful but it nicely illustrates an interesting feature of Bash, the /dev/tcp device file.

As an extreme example of the "everything is a file" philosophy of Unix, we can build TCP clients and servers by redirecting I/O to this special file.

/dev/tcp in action

For example, we can send HTTP requests without wget or any other external tools.

[rohan@localhost ~]$ HTTP_REQUEST="GET / HTTP/1.0\r\nHost: etherarp.net\r\nConnection: Close\r\n\r\n"
[rohan@localhost ~]$ exec 3<>/dev/tcp/etherarp.net/80; echo -e $HTTP_REQUEST >&3 && cat <&3
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sun, 17 Sep 2017 12:48:08 GMT
Content-Type: text/html
Content-Length: 178
Connection: close
Location: https://etherarp.net/

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
[rohan@localhost ~]$ 

How it works

/dev/tcp works through sending an exec call. When you execute a command line program, the input and output is redirected from the terminal to the program, and the output is sent back to you. This is precisely how /dev/tcp works. When using the exec() call, we get a success value.

In cases where the IP/port values are malformed, or if the connection is rejected or times out,
then we get a failure value returned much like when a program cannot be found.

We can take advantage of this using the logical OR || operator in bash.
If we have commandA || commandB then we execute the right-hand command only if commandA reports failure.

Quickly check if a port is open

$ (echo </dev/tcp/$host/$port)>/dev/null && echo "$port Open" || echo "$port Closed"

Banner grabbing

When we see an open port, a quick and sometimes quite effective way of identifying the service is to see the default response from the server. Not only can it identify the service, but it in some cases can provide useful information about the target machine.

For example, we find a server that has port 13769/tcp open.
Hmm, I wonder what that could be?

$ exec 3<>/dev/tcp/172.17.37.69/13769 && echo "">&3 && cat <&3
SSH-2.0-OpenSSH_7.5
Protocol mismatch

We now can be virtually certain SSH runs on that port. Even better, we know the version of the server.

Writing a script to "Scan & Grab"

So, let's write our scanner to do a bannergrab whenever it discovers a listening server. This is simple to do, when it discovers an active port, we print $ip and $port. Once these results have been printed, we can then use xargs to pass each line as an input field to netcat (or even in a pinch, our own functions).

After an afternoon of tinkering, this is my Bash script (I also wanted to practice bash). It's still a bit buggy, and sometimes it needs a manual ctrl+d so it doesn't hang. But still, it does achieve what I wanted. Remember, this is mostly to learn about the features of the shell, as it's vastly inferior to a real tool like Nmap


ports=( `cat ports.txt`)
[[ -n "$2" ]]; ip=$2 || false
#[[ -z "$ports" ]]; ports=$(seq 1 65534) || false

function scanport {
  (echo </dev/tcp/$1/$2) &>/dev/null && (echo -n "$2 ") || false
}
function openport {
  if [ -n "$1" ] && [ -n "$2" ]; then
    printf "<server ip=\"%s\" port=\"%s\">\n" $1 $2
    exec 3<>/dev/tcp/$1/$2; echo "\n">&3; cat <&3
    printf "</server>\n";
  fi
}

case "$1" in
"portscan")
  [[ -n "$ip" ]] && (>&2 echo "open ports on $ip... ") || false
  for port in ${ports[@]}; do  scanport $ip $port ;
  done
  ;;

"bannergrab")
    [[ -n "$2" ]] && [[ -n "$3" ]] && openport $2 $3 || false
  ;;

"scan")
  for port in ${ports[@]}; do
    scanport $ip $port &&  openport $ip $port;
  done
  ;;

"scanport")
  [[ -n "$2" ]] && [[ -n "$3" ]] && scanport $2 $3 &&  echo "open"|| echo "closed"
  echo
  ;;

*)
  echo -e "bash-scan.sh - shell based port scanner\n"
  echo -e "portscan) what ports are open on a host? (in the set ports.txt)\n"
  echo -e "scanport) is a particular port open?\n"
  echo -e "bannergrab) see the response from a server?\n"
  echo -e "scan) scan for open ports and then bannergrab what you find\n"
  ;;
esac

This is what the output looked like

80 <server ip="etherarp.net" port="80">
HTTP/1.1 400 Bad Request
Server: nginx
Date: Sun, 17 Sep 2017 15:02:34 GMT
Content-Type: text/html
Content-Length: 166
Connection: close

<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
</server>
443 <server ip="etherarp.net" port="443">
HTTP/1.1 400 Bad Request
Server: nginx
Date: Sun, 17 Sep 2017 15:02:35 GMT
Content-Type: text/html
Content-Length: 166
Connection: close

<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
</server>