《bash网络安全运维》 第七章

数据分析

命令

例:
file1.txt

12/05/2017 192.168.10.14 test.html
12/30/2017 192.168.10.185 login.html

sort

sort 命令用于将文本文件按照数字和字母顺序重新排列。
常见命令选项:

  1. -r:按降序排列
  2. -f:忽略大小写
  3. -n:使用数值排序,这样 1、2、3 都排在 10 之前。
  4. -k:根据一行中的数据子集进行排序,字段由空格分隔。

示例:
要按文件名列对 file1.txt 进行排序并忽略 IP 地址列:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ sort -k 2 file1.txt
12/05/2017 192.168.10.14 test.html
12/30/2017 192.168.10.185 login.

对字段的子集进行排序。按 IP 地址的第二个字节排序:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ sort -k 1.5,1.7 file1.txt
12/30/2017 192.168.10.185 login.html
12/05/2017 192.168.10.14 test.html

uniq

uniq 命令过滤掉彼此相邻的重复数据行。要删除文件中的所有重复行,请确保在使用 uniq 之前对其进行重新排序。
常见的命令选项:

  1. -c:打印出一行重复的次数。
  2. -f:在比较之前忽略指定的字段数。例如,-f3 将忽略每一行中的前三个字段。字段使用空格分隔。
  3. -i:忽略字母大小写。

Web 服务器访问日志

例:
access.log 中的一行

192.168.0.11 - - [12/Nov/2017:15:54:39 -0500] "GET /request-quote.html HTTP/1.1" 200 7326 "http://192.168.0.35/support.html" "Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0"

Apache Web 服务器组合日志格式字段

字段 描述 字段序号
192.168.0.11 请求页面的主机的 IP 地址 1
- RFC 1413 Ident 协议标识符 2
- 通过 HTTP 身份验证的用户 ID 3
[12/Nov/2017:15:54:39 -0500] 日期、时间和格林尼治时间偏移量 4-5
GET /request-quote.html 请求的页面 6-7
HTTP/1.1 HTTP协议版本 8
200 web 服务器返回的状态代码 9
7326 以字节为单位返回的文件大小 10
http://192.168.0.35/support .html 引用页面 11
Mozilla/5.0 (Windows NT 6.3; Win64……) 标识浏览器的用户代理 12+

HTTP 状态代码

代码 描述
200 OK
401 未经授权的
404 页面未找到
500 内部服务器错误
502 无效网关

Apache 访问日志的第二种类型称为公共日志格式。该格式与组合日志格式相同,只是它不包含引用页面和用户代理的字段。

数据排序和整理

要控制数据的排列和显示,可以使用管道末端的 sort、head和 tail 命令:

……| sort -k 2.1 -rn | head -15

上述管道将脚本的输出传输到 sort 命令中,然后将 sort 的输出通过管道传输到 head 指令中以打印前 15 行内容。这里 sort 命令使用第 2 个字段的第一个字符(2.1)作为排序键(-k),并以数字方式(-n)进行逆向排序(-r)。

统计数据出现频次

值得关注的情况:

  1. 大量请求返回特定页面 404 状态代码;这可以表示断开的超链接。
  2. 从一个 IP 地址发出的大量请求返回 404 状态代码;这很可能是探测活动,用于寻找隐藏的或未链接的页面。
  3. 返回 401 状态代码的大量请求,特别是来自相同 IP 地址的请求,这表示很可能有人试图绕过身份验证,比如强行猜测密码。

要检测上述行为,我们需要提取关键字段,比如源 IP 地址,并计算它们在文件中出现的次数。
countem.sh

#!/bin/bash -
#
# Cybersecurity Ops with bash
# countem.sh
#
# Description: 
# Count the number of instances of an item using bash
#
# Usage:
# countem.sh < inputfile
#

declare -A cnt        # assoc. array             # <1>
while read id xtra                               # <2>
do
    let cnt[$id]++                               # <3>
done
# now display what we counted
# for each key in the (key, value) assoc. array
for id in "${!cnt[@]}"                           # <4>
do    
	printf '%s %d\n'  "$id"  "${cnt[$id]}"       # <5>
done
  1. 因为不知道具体的 IP 地址,所以我们将使用一个关联数组,这里用 -A 选项声明,这样我们就可以使用读取的任何字符串作为索引。
  2. 虽然我们只用到了每行的第一个单词,但是我们将变量 xtra 放在那里以捕获该行中出现的任何其他单词。read 命令上的每个变量都从输入中取得相应的单词,而最后一个变量得到所有剩余的单词。另一方面,如果一行中输入的单词少于 read 命令中的变量,那么这些额外的变量就会被设置空字符串。
  3. 使用该字符串作为索引,并对该索引对应的值进行累加。在第一次使用索引时,其对应的值还没有设置,则该值将被置为零。
  4. 上述语法允许我们遍历所有索引值。但是请注意,由于取得索引值所采用的哈希算法的特性,并不能保证遍历顺序是按照字母顺序或者任何其他特定顺序。
  5. 在打印值和键是,我们将值放在引号中,这样我们总是为每个参数得到一个单独的值——即使这个值里面有一两个空格。

conuntem.awk

# Cybersecurity Ops with bash
# countem.awk
#
# Description: 
# Count the number of instances of an item using awk
#
# Usage:
# countem.awk < inputfile
#

awk '{ cnt[$1]++ }
END { for (id in cnt) {
        printf "%d %s\n", cnt[id], id
      }
    }'

两者都可以在一下命令管道中很好地工作:

cut -d' ' -f1 logfile | bash countem.sh

示例:
要计算每个 IP 地址发出的 HTTP 请求导致的 404 错误次数:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ awk '$9 == 404 {print $1}' access.log | bash countem.sh
1 192.168.0.36
2 192.168.0.37
1 192.168.0.11

可以从查看访问 Web 服务器的主机开始入手分析示例 access.log 文件:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ cut -d' ' -f1 access.log | bash countem.sh | sort -rn
111 192.168.0.37
55 192.168.0.36
51 192.168.0.11
42 192.168.0.14
28 192.168.0.26

接下来我们来深入研究请求最多的主机的活动:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ awk '$1 == "192.168.0.37" {print $0}' access.log | cut -d' ' -f7 | bash countem.sh
1 /uploads/2/9/1/4/29147191/31549414299.png?457
14 /files/theme/mobile49c2.js?1490908488
1 /_/cdn2.editmysite.com/images/editor/theme-background/stock/iPad.html
1 /uploads/2/9/1/4/29147191/2992005_orig.jpg
14 /files/theme/plugin49c2.js?1490908488
2 /uploads/2/9/1/4/29147191/32981bd4c.png?161
……

这个主机的活动并没有什么特别之处,似乎是标准的 Web 浏览行为。而如果我们查看下一个请求最多的主机,将能看到一些更有趣的东西:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ awk '$1 == "192.168.0.36" {print $0}' access.log | cut -d' ' -f7 | bash countem.sh

1 /files/theme/mobile49c2.js?1490908488
1 /uploads/2/9/1/4/29147191/31549414299.png?457
1 /_/cdn2.editmysite.com/images/editor/theme-background/stock/Coffee.html
1 /_/cdn2.editmysite.com/images/editor/theme-background/stock/iPad.html
1 /files/theme/plugin49c2.js?1490908488
1 /uploads/2/9/1/4/29147191/2992005_orig.jpg
1 /files/theme/images/light-checkboxaf0e.png?1509483497
1 /bcp.html
1 /uploads/2/9/1/4/29147191/32981bd4c.png?161
1 /files/theme/images/icon-bubbleaf0e.png?1509483497
1 /consulting.html
1 /uploads/2/9/1/4/29147191/principlesofencryption-nb_orig.png
……

主机 192.168.0.36 只访问了网站上的每个页面一次。这种行为通常是网站爬取或站点克隆。接下来看一下客户端提供的用户代理字符串,它会进一步验证这个结论:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ awk '$1 == "192.168.0.36" {print $0}' access.log | cut -d' ' -f12-17 | uniq
"Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)"

用户代理将自己标识为 HTTtrack,这是一个用于下载或克隆网站的工具。

统计数据总数

summer.sh

#!/bin/bash -
#
# Cybersecurity Ops with bash
# summer.sh
#
# Description: 
# Sum the total of field 2 values for each unique field 1
#
# Usage: ./summer.sh
#   input format: <name> <number>
#

declare -A cnt        # assoc. array
while read id count
do
  let cnt[$id]+=$count
done
for id in "${!cnt[@]}"
do
    printf "%-15s %8d\n"  "${id}"  "${cnt[${id}]}" 
done

脚本 summer.sh 读取两列数据。第一列由索引值(IP 地址)组成,第二列是一个数字(IP 地址发送的字节数)。当脚本在第一列中找到重复的 IP 地址时,就将第二列的值添加到该 IP 地址对应的总字节数中从而将该 IP 地址发送的字节数合计起来。
示例:
我们可以对示例 access.log 文件运行 summer.sh 脚本以得到每个主机的请求的数据总量:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ cut -d' ' -f1,10 access.log | bash summer.sh | sort -k 2.1 -rn
192.168.0.36     4371198
192.168.0.14     2876088
192.168.0.37     2575030
192.168.0.11     2537662
192.168.0.26      665693

这个结果识别出了与其他主机相比传输了异常大量数据的主机,是非常有用的。一个异常大的传输数据量表示可能存在数据窃取或数据泄漏。

用直方图显示数据

histogram.sh

#!/bin/bash -
#
# Cybersecurity Ops with bash
# histogram.sh
#
# Description: 
# Generate a horizontal bar chart of specified data
#
# Usage: ./histogram.sh
#   input format: label value
#

function pr_bar ()                            # <1>
{
    local -i i raw maxraw scaled              # <2>
    raw=$1
    maxraw=$2
    ((scaled=(MAXBAR*raw)/maxraw))            # <3>
    # min size guarantee
    ((raw > 0 && scaled == 0)) && scaled=1				# <4>

    for((i=0; i<scaled; i++)) ; do printf '#' ; done
    printf '\n'
    
} # pr_bar

#
# "main"
#
declare -A RA						# <5>
declare -i MAXBAR max
max=0
MAXBAR=50	# how large the largest bar should be

while read labl val
do
    let RA[$labl]=$val					# <6>
    # keep the largest value; for scaling
    (( val > max )) && max=$val
done

# scale and print it
for labl in "${!RA[@]}"					# <7>
do
    printf '%-20.20s  ' "$labl"
    pr_bar ${RA[$labl]} $max				# <8>
done
  1. 定义一个函数来绘制直方图的一个条形。
  2. 将所有这些变量声明为局部的,因为我们不希望它们干扰这个脚本其余部分中的变量名。通过 -i 选项将所有变量声明为整数。
  3. 计算是在双括号内完成的。在这些变量中,不需要使用 $ 来表示每个变量名的“值”。
  4. 这是一个 “if-less” 的 if 语句。当且仅当双圆括号内的表达式为 true 时,执行第二个表达式。这将保证当原始值为非 0 时 scaled 变量永远不会为 0 。这么做是因为希望取值非 0 时,直方图上不显示空。
  5. 声明了 RA 关联数组。
  6. 使用标签作为索引引用关联数组。
  7. 因为数组不是按数字索引的,所以我们不能通过整数作为索引来循环遍历它。这个 for 循环语法结构可遍历作为数组索引的所有字符串,每次一个。
  8. 再次使用标签作为索引来获取计数,并将其作为第一个参数传递给 pr_bar 函数。

这个脚本将使用第一个字段作为关联数组的索引,第二个字段作为该数组元素的值。然后,它遍历数组并打印一些 # 来表示计数,将列表中最大的计数缩放到 50 个 # 符号。

histogram_plain.sh

#!/bin/bash -
#
# Cybersecurity Ops with bash
# histogram_plain.sh
#
# Description: 
# Generate a horizontal bar chart of specified data without
# using associative arrays, good for older versions of bash
#
# Usage: ./histogram_plain.sh
#   input format: label value
#

declare -a RA_key RA_val                                 # <1>
declare -i max ndx
max=0
maxbar=50    # how large the largest bar should be

ndx=0
while read labl val
do
    RA_key[$ndx]=$labl                                   # <2>
    RA_value[$ndx]=$val
    # keep the largest value; for scaling
    (( val > max )) && max=$val 
    let ndx++
done

# scale and print it
for ((j=0; j<ndx; j++))                                  # <3>
do
    printf "%-20.20s  " ${RA_key[$j]}
    pr_bar ${RA_value[$j]} $max
done
  1. 变量名被声明为数组。小写 a 表示它们是数组,但不属于关联类型。
  2. 键和值对分别存储在单独的数组中,但位于相同的索引位置。
  3. 此处 for 循环与前面的脚本不同,它是一个从 0 到 ndx 的整数的简单计数。

这个脚本避免了使用关联数组。其中两个数组:一个用于存储索引值,另一个用于存储计数。因为它们是普通数组,所以必须使用整数索引,所以脚本中使用了变量 ndx 进行计数。

除了查看 IP 地址或主机传输的字节数外,基于日期和时间来观察数据通常也能发现很有趣的现象。
例:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ cut -d' ' -f4,10 access.log | cut -c2-
12/Nov/2017:15:52:59 2377
12/Nov/2017:15:52:59 4529
12/Nov/2017:15:52:59 1112
12/Nov/2017:15:52:59 503
12/Nov/2017:15:52:59 6933
12/Nov/2017:15:52:59 504
……

这里选项 -c2- 告诉 cut 按字符提取数据,从位置 2 开始,一直到行尾(-)。
或者:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ cut -d' ' -f4,10 access.log | tr -d '['
12/Nov/2017:15:52:59 2377
12/Nov/2017:15:52:59 4529
12/Nov/2017:15:52:59 1112
12/Nov/2017:15:52:59 503
12/Nov/2017:15:52:59 6933
……

接下来需要解决如何按时间界限进行分组:按天、月、年、小时等。
Apache 日志日期/时间字段的提取(仅适用于 Apache 日志文件)

日期/时间 提取的示例输出 Cut 选项
整个日期/时间 12/Nov/2017:19:26:09 -c2-
月、日、年 12/NOV/2017 -c2-12,22-
月、年 Nov/2017 -c5-12,22-
时间 19:26:04 -c14-
小时 19 -c14-15,22-
2017 -c9-12,22-

如果想查看某天和每小时检索的数据总量直方图,可以执行:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ awk '$4 ~ "12/Nov/2017" {print $0}' access.log | cut -d' ' -f4,10 | cut -c14-15,22- | bash summer.sh | bash histogram.sh
17                   ##
16                   ###########
15                   ############
19                   ##
18                   ################################################

发现数据的唯一性

pagereq.sh

# Cybersecurity Ops with bash
# pagereq.sh
#
# Description: 
# Count the number of page requests for a given IP address using bash
#
# Usage:
# pagereq <ip address> < inputfile
#   <ip address> IP address to search for
#

declare -A cnt                                             # <1>
while read addr d1 d2 datim gmtoff getr page therest
do
    if [[ $1 == $addr ]] ; then let cnt[$page]+=1 ; fi
done
for id in ${!cnt[@]}                                       # <2>
do
    printf "%8d %s\n" ${cnt[$id]} $id
done
  1. 将 cnt 声明为一个关联数组,这样就可以使用字符串作为数组的索引。
  2. $ {!cnt[@]}将输出所有不同索引值的集合。

bash 的早期版本没有关联数组,我们可以使用 awk 去实现这个功能。
pagereq.awk

# Cybersecurity Ops with bash
# pagereq.awk
#
# Description: 
# Count the number of page requests for a given IP address using awk
#
# Usage:
# pagereq <ip address> < inputfile
#   <ip address> IP address to search for
#

# count the number of page requests from an address ($1)
awk -v page="$1" '{ if ($1==page) {cnt[$7]+=1 } }                # <1>
END { for (id in cnt) {                                          # <2>
    printf "%8d %s\n", cnt[id], id
    }
}'
  1. 慈航有两个不同的 $1 变量。第一个 $1 是 shell 变量,他引用调用脚本时提供给该脚本的第一个参数。第二个 $1 是一个 awk 变量。它指的是每一行输入的第一个字段。第一个 $1 被分配给 awk 变量 page,以便与 awk 的每个 $1 进行比较。
  2. 利用变量 id 遍历 cnt 数组的索引值的值。

识别异常数据

我们可以维护一个已知用户代理字符串的列表来识别出特殊的用户代理。
例:
useragents.txt

Firefox
Chrome
Safari
Edge

下一步是读取 Web 服务器日志,并将每一行与列表中的每个用户代理进行比较,直到匹配为止。如果没有找到匹配,则应将其视为异常,并将与发出请求的系统 IP 地址一起打印到标准输出中。
useragents.sh

#!/bin/bash -
#
# Cybersecurity Ops with bash
# useragents.sh
#
# Description: 
# Read through a log looking for unknown user agents
#
# Usage: ./useragents.sh  <  <inputfile>
#   <inputfile> Apache access log
#


# mismatch - search through the array of known names
#  returns 1 (false) if it finds a match
#  returns 0 (true) if there is no match
function mismatch ()                                    # <1>
{
    local -i i                                          # <2>
    for ((i=0; i<$KNSIZE; i++))
    do
        [[ "$1" =~ .*${KNOWN[$i]}.* ]] && return 1      # <3>
    done
    return 0
}

# read up the known ones
readarray -t KNOWN < "useragents.txt"                      # <4>
KNSIZE=${#KNOWN[@]}                                     # <5>

# preprocess logfile (stdin) to pick out ipaddr and user agent 
awk -F'"' '{print $1, $6}' | \
while read ipaddr dash1 dash2 dtstamp delta useragent   # <6>
do
    if mismatch "$useragent"
    then
        echo "anomaly: $ipaddr $useragent"
    fi
done
  1. 此处定义了一个函数作为脚本核心。
  2. 将 for 循环索引声明为局部变量是一种好的习惯。
  3. 需要比较两个字符串:日志文件中的输入和已知用户代理列表中的一行。为了实现非常灵活的比较,我们使用 regex 比较操作符(=~)。.* 位于 $KNOWN 数组引用的任何一边,这意味着已知字符串可以在另一个字符串中的任何位置进行匹配。
  4. 文件的每一行都作为元素添加到指定的数组中,这构建起一个已知用户代理的数组。在 bash 中有两种完全相同的方法:readarray 或 mapfile 。这里使用 readarray。-t 选项从每一行读取中删除尾随的换行符,包含已知用户代理集的文件在此指定,可以根据需要进行修改。
  5. 计算数组的大小。它在 mismatch 函数中用于遍历数组。我们在循环之外计算一次,以避免在每次调用函数时重新计算它。
  6. 输入字符串是单词和引号的复杂组合。为了捕获用户代理字符串,我们使用双引号作为字段分隔符。然而,这样做意味着我们的第一个字段不仅仅包含 IP 地址。read 的最后一个参数接受所有剩余的单词,这样它就可以捕获用户代理字符串的所有单词。

useragents.sh 脚本将输出在 useragent.txt 文件中没有找到的任何用户代理字符串:

pwnki@LAPTOP-KETPO6R7:~/project/diqi$ bash useragents.sh < access.log
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
anomaly: 192.168.0.36 Mozilla/4.5 (compatible; HTTrack 3.0x; Windows 98)
……

内容来源

《bash网络安全运维》

posted @ 2021-01-23 14:26  PwnKi  阅读(11)  评论(0编辑  收藏