Writing a port scanner in Bash shell

A primitive port scanner can be constructed using solely the bash shell via its /dev/tcp virtual file. The script will scan common ports, and if an open port is found, it will display a base64 encoded output of the servers response.

What is /dev/tcp

In Unix, there is a convention to abstract the system as file objects, such as /dev for hardware, and /proc for applications.

Bash shell allows the network to be accessed as though it was a file via /dev/tcp. /dev/tcp is not actually a real /dev resource. Instead, when bash sees a redirection to /dev/tcp/$address/$port, it will open a tcp connection, and redirect the I/O as though it were a file.

A very simple use of /dev/tcp is to send the output of commands or files to servers. To demonstrate this, I use an ad-hoc server I brought up with nc, and redirect the uname command to it

$ nc -l 127.0.0.1 31415 >nc.out &
$ uname > /dev/tcp/127.0.0.1/31415 
$ cat nc.out
> Linux

Using /dev/tcp to check if a port is open/closed

portisopen(){ 
timeout 0.5s /bin/bash -c "echo EOF > /dev/tcp/$1/$2" 2>/dev/null||(echo closed >/dev/stderr; return 1 );
}

When redirecting to /dev/tcp, there is notification of write failures (as with any other file). If bash is unable to establish connectivity with the server (within a relatively large amount of time), it will return failure and print an error message. A port scanner needs to be quick, so the timeout command is used to ensure the operation takes no longer than 0.1s. For the timeout command to work properly,

Banner grabbing

bannergrab(){
bash -c "exec 3<>/dev/tcp/$1/$2; echo EOF>&3; cat<&3"
};

Once a port has found to be open, it's often useful to query a default response from the server. This can often provide useful information about the service, protocol and host. In order to read information from /dev/tcp, an exec call is required, redirecting the terminal to /dev/tcp. If something goes awry, it can crash your current session, so bash -c is used to create a seperate session to run the command in. First EOF is sent to /dev/tcp, terminating the client request. cat <&3 is used to query the response from the server.

Creating the scanner

scan_common_ports(){
for port in 21 22 23 25 53 80 443 2222 8080 8443 9090 3306 10000 ; do portisopen $1 $port 2>/dev/null && (echo "<open port=$port host=\"$1\">";bannergrab $1 $port|base64; echo '</open>'; );done
};

The above functions can be put together to create a simple port-scanner. Bash checks that the port is open, redirecting stderr (2) to null, so that the output isn't cluttered by errors from closed ports.

If the port is open, it prints some XML, along with a base64 encoded output of the bannergrab. Base64 is used to sanitize outputs, to deal with target that output some ugly non-printable characters

Trying out the scanner


[rohan@desktop ~]$ scan_common_ports etherarp.net
<open port=80 host="etherarp.net">
SFRUUC8xLjEgNDAwIEJhZCBSZXF1ZXN0DQpEYXRlOiBTdW4sIDIzIERlYyAyMDE4IDEyOjQ0OjAz
IEdNVA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWwNCkNvbnRlbnQtTGVuZ3RoOiAxNzENCkNvbm5l
Y3Rpb246IGNsb3NlDQpTZXJ2ZXI6IGNsb3VkZmxhcmUNCkNGLVJBWTogLQ0KDQo8aHRtbD4NCjxo
ZWFkPjx0aXRsZT40MDAgQmFkIFJlcXVlc3Q8L3RpdGxlPjwvaGVhZD4NCjxib2R5IGJnY29sb3I9
IndoaXRlIj4NCjxjZW50ZXI+PGgxPjQwMCBCYWQgUmVxdWVzdDwvaDE+PC9jZW50ZXI+DQo8aHI+
PGNlbnRlcj5jbG91ZGZsYXJlPC9jZW50ZXI+DQo8L2JvZHk+DQo8L2h0bWw+DQo=
</open>
<open port=443 host="etherarp.net">
SFRUUC8xLjEgNDAwIEJhZCBSZXF1ZXN0DQpTZXJ2ZXI6IGNsb3VkZmxhcmUNCkRhdGU6IFN1biwg
MjMgRGVjIDIwMTggMTI6NDQ6MDMgR01UDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbA0KQ29udGVu
dC1MZW5ndGg6IDE3MQ0KQ29ubmVjdGlvbjogY2xvc2UNCkNGLVJBWTogLQ0KDQo8aHRtbD4NCjxo
ZWFkPjx0aXRsZT40MDAgQmFkIFJlcXVlc3Q8L3RpdGxlPjwvaGVhZD4NCjxib2R5IGJnY29sb3I9
IndoaXRlIj4NCjxjZW50ZXI+PGgxPjQwMCBCYWQgUmVxdWVzdDwvaDE+PC9jZW50ZXI+DQo8aHI+
PGNlbnRlcj5jbG91ZGZsYXJlPC9jZW50ZXI+DQo8L2JvZHk+DQo8L2h0bWw+DQo=
</open>
<open port=8080 host="etherarp.net">
SFRUUC8xLjEgNDAwIEJhZCBSZXF1ZXN0DQpEYXRlOiBTdW4sIDIzIERlYyAyMDE4IDEyOjQ0OjA0
IEdNVA0KQ29udGVudC1UeXBlOiB0ZXh0L2h0bWwNCkNvbnRlbnQtTGVuZ3RoOiAxNzENCkNvbm5l
Y3Rpb246IGNsb3NlDQpTZXJ2ZXI6IGNsb3VkZmxhcmUNCkNGLVJBWTogLQ0KDQo8aHRtbD4NCjxo
ZWFkPjx0aXRsZT40MDAgQmFkIFJlcXVlc3Q8L3RpdGxlPjwvaGVhZD4NCjxib2R5IGJnY29sb3I9
IndoaXRlIj4NCjxjZW50ZXI+PGgxPjQwMCBCYWQgUmVxdWVzdDwvaDE+PC9jZW50ZXI+DQo8aHI+
PGNlbnRlcj5jbG91ZGZsYXJlPC9jZW50ZXI+DQo8L2JvZHk+DQo8L2h0bWw+DQo=
</open>
<open port=8443 host="etherarp.net">
SFRUUC8xLjEgNDAwIEJhZCBSZXF1ZXN0DQpTZXJ2ZXI6IGNsb3VkZmxhcmUNCkRhdGU6IFN1biwg
MjMgRGVjIDIwMTggMTI6NDQ6MDQgR01UDQpDb250ZW50LVR5cGU6IHRleHQvaHRtbA0KQ29udGVu
dC1MZW5ndGg6IDE3MQ0KQ29ubmVjdGlvbjogY2xvc2UNCkNGLVJBWTogLQ0KDQo8aHRtbD4NCjxo
ZWFkPjx0aXRsZT40MDAgQmFkIFJlcXVlc3Q8L3RpdGxlPjwvaGVhZD4NCjxib2R5IGJnY29sb3I9
IndoaXRlIj4NCjxjZW50ZXI+PGgxPjQwMCBCYWQgUmVxdWVzdDwvaDE+PC9jZW50ZXI+DQo8aHI+
PGNlbnRlcj5jbG91ZGZsYXJlPC9jZW50ZXI+DQo8L2JvZHk+DQo8L2h0bWw+DQo=
</open>