我突然有一个用 Bash 来编写 TCP 端口扫描器的想法。Bash 支持可读写的特殊文件 /dev/tcp/host/port ,往这个文件写内容可以让 bash 打开一个 TCP 连接到 host:port ,如果写文件成功则表示此端口是打开的,否则说明该端口没有打开。
因此我们先简单的写一个测试脚本:
1 |
for port in {1..65535}; do |
2 |
echo >/dev/tcp/google.com/$port && |
3 |
echo "port $port is open" || |
4 |
echo "port $port is closed" |
5 |
done |
该脚本将扫描 google.com 服务器端口,从 1 到 65535。当然,如果端口没打开的话是没法工作的,bash 花了 2 分钟时间意识到这点。
为了解决这个问题我们需要一些类似 alarm(2) 的方法来中断 bash,而 bash 没有内置的 alarm 函数,因此我们用 Perl 语言写了一个:
01 |
alarm () { |
02 |
perl -e ' |
03 |
eval { |
04 |
$SIG {ALRM} = sub { die }; |
05 |
alarm shift ; |
06 |
system ( @ARGV ); |
07 |
}; |
08 |
if ($@) { exit 1 } |
09 |
' "$@" ; |
10 |
} |
这个 alarm 函数需要两个参数:alarm 调用的秒数和要执行的代码,如果执行的代码没有在指定的时间内执行完毕则该函数调用失败。
有了这个 alarm 函数,我们就可以修改上面的代码如下:
1 |
for port in {1..65535}; do |
2 |
alarm 1 " echo >/dev/tcp/google.com/$port && |
3 |
echo \ "port $port is open\"" || |
4 |
echo "port $port is closed" |
5 |
done |
这个终于可以运行了,当扫描到某个端口是关闭的, bash 将在 1 秒后执行下一个端口的扫描。
然后我们将这些代码封装到一个 scan 函数中:
01 |
scan() { |
02 |
if [[ -z $1 || -z $2 ]]; then |
03 |
echo "Usage: $0 <host> <port, ports, or port-range>" |
04 |
return |
05 |
fi |
06 |
07 |
local host=$1 |
08 |
local ports=() |
09 |
case $2 in |
10 |
*-*) |
11 |
IFS=- read start end <<< "$2" |
12 |
for ((port=start; port <= end; port++)); do |
13 |
ports+=($port) |
14 |
done |
15 |
;; |
16 |
*,*) |
17 |
IFS=, read -ra ports <<< "$2" |
18 |
;; |
19 |
*) |
20 |
ports+=($2) |
21 |
;; |
22 |
esac |
23 |
24 |
25 |
for port in "${ports[@]}" ; do |
26 |
alarm 1 " echo >/dev/tcp/$host/$port && |
27 |
echo \ "port $port is open\"" || |
28 |
echo "port $port is closed" |
29 |
done |
30 |
} |
这样就可以在 shell 中使用 scan 函数,需要的参数包括要扫描的主机地址、端口列表(可以时端口组合和端口范围,或者是某个特定端口)
下面是扫描 google.com 服务器的 78 - 82 端口:
1 |
$ scan google.com 78-82 |
2 |
port 78 is closed |
3 |
port 79 is closed |
4 |
port 80 is open |
5 |
port 81 is closed |
6 |
port 82 is closed |
如果你想测试 UDP 端口,只需要将前面提及的 /dev/tcp 改为 /dev/udp/