监控提权命令之audit
利用了linux自带的audit审计。老实说,我对这个东西挺陌生的,这两天差点把我劝退。。。一开始想用inotifywait去做,发现只能记录root执行的提权命令,然后想到用户家目录下的.bash_history去拿最近日志,感觉还是不够audit靠谱,所以硬着头皮研究吧。
提下关键点,下面脚本很多都是ai写的(尤其它写的正则,强大),当然我也花了很多功夫帮它纠正代码逻辑,验证它写的内容,算是互相成就吧,哈哈哈~~不然自己写可能不止搞一天哦,本人代码能力比较弱。
1、预先设置含关键字的audit规则,然后才能记录到审计日志
centos 7 编辑这个文件 /etc/audit/rules.d/audit.rules ,规则内容如下(-k 标识记录日志的关键字,下面搜索会用到),标识记录普通用户运行sudo时的审计日志。
-a always,exit -F arch=b64 -S execve -F uid!=0 -F exe=/usr/bin/sudo -k sudo-exec -a always,exit -F arch=b64 -S execve -F uid!=0 -F exe=/bin/sudo -k sudo-exec -a always,exit -F arch=b64 -S execve -F uid!=0 -F exe=/bin/su -k sudo-exec -a always,exit -F arch=b64 -S execve -F uid!=0 -F exe=/usr/bin/su -k sudo-exec
设置完规则需要重启audit服务,还有如果写的规则有问题,会发现“auditctl -l” 是没有新添加的规则,所以最好写入这个文件前,命令行先验证规则是否正确,例如:
auditctl -a always,exit -F arch=b64 -S execve -C euid=0 -F euid!=0 -k sudo-exec
另一个问题是ai一直给我写条件规则:euid!=0来排除root用户,这个是错误的,需要用uid去判断
2、提取关于提权命令的日志
一开始我看/var/log/audit/audit.log,可读性根本不是人类能够适应的。然后找到工具ausearch,利用规则关键字来搜索日志记录,格式类似这样:“ausearch -k 关键字”
然后发现一运行,满屏都是日志,所以要加条件去限制,例如最近x分钟。目前找到的最小粒度是最近10分钟内的日志。写法是:
ausearch --start recent --end now -k 'sudo-exec'
老实说,用ausearch搜的日志比直接在audit.log可读性强太多,每个日志段用“----”分割,接着就是从每个日志段提取我们需要的字段内容。
3、日志分析
包括字段提取,和数组存值。每个日志段我们需要用对应数组去存(用户、具体命令、当前所在路径),因为后续要遍历进行钉钉告警。除此我们需要处理以下的点
(1)time->Tue Sep 12 20:44:34 2023,需要利用时间戳来转格式
(2)提取命令在argc{n}这列,需要拼接a0~an-1里面“=”的内容
(3)提取操作用户,根据uid的值,读取/etc/passwd 去匹配
4、钉钉告警用json拼接消息
这个大家直接看脚本,我以前那种写法报警不了,可能是因为操作命令里有空格导致的,这个有机会再研究
5、放到定时任务搜索日志内容为空
ausearch搜关键字日志在命令行执行可以重定向到日志文件,但是放到定时任务竟然为空,全靠前公司运维同事的醍醐灌顶,结合“--input-logs”管道去处理,真是万分感激。
1 #!/bin/bash 2 3 # 定义空数组来存储事件数据 4 timestamps=() 5 users=() 6 commands=() 7 cwds=() 8 9 ausearchlog=/root/ausearch.log 10 11 DATE=`date +%F_%T` 12 >$ausearchlog 13 # 找到10分钟前到现在的日志并重定向输出到文件 14 # 写不进去日志: /sbin/ausearch --start recent --end now -k 'sudo-exec' > $ausearchlog 2>&1 15 # 搜不到内容,可能跟时间差有关: /sbin/ausearch --start recent --end now -k 'sudo-exec' -f /var/log/audit/audit.log > $ausearchlog 16 ### 可用写法 17 /sbin/ausearch --start recent --end now -k 'sudo-exec' --input-logs |tee -a $ausearchlog 18 19 20 # 循环读取日志文件 21 while IFS= read -r line; do 22 # 提取时间: 23 if [[ $line =~ ([a-zA-Z]{3} [a-zA-Z]{3} [0-9]{1,2} [0-9:]{8} [0-9]{4}) ]]; then 24 timestamp="${BASH_REMATCH[1]}" 25 # 转换时间格式 26 formatted_timestamp=$(date -d "$timestamp" +%Y-%m-%d_%H:%M:%S) 27 timestamps+=("$formatted_timestamp") 28 echo "时间: $formatted_timestamp" 29 fi 30 31 # 提取操作用户所在目录: 32 if [[ $line == *"type=CWD"* ]]; then 33 cwd=$(echo "$line" | awk -F"cwd=" '{print $2}' | awk '{print $1}' | tr -d '"' ) 34 cwds+=("$cwd") 35 echo "用户当前所在目录:$cwd" 36 fi 37 38 # 提取操作命令: 39 if [[ $line =~ argc=([0-9]+) ]]; then 40 argc="${BASH_REMATCH[1]}" 41 command_args=() 42 43 # 使用循环提取 a0 到 an-1 的内容 44 for ((i = 0; i < argc; i++)); do 45 arg_name="a$i" 46 if [[ $line =~ $arg_name=\"([^\"]+)\" ]]; then 47 arg_value="${BASH_REMATCH[1]}" 48 command_args+=("$arg_value") 49 fi 50 done 51 52 # 将命令参数拼接,并以空格分隔 53 command="${command_args[*]}" 54 echo "运行的命令: $command" 55 56 # 添加命令到数组 57 commands+=("$command") 58 fi 59 60 # 提取操作用户: 61 if [[ $line == *"type=SYSCALL"* ]]; then 62 #userid=$(echo "$line" | awk -F"uid=" '{print $2}' | awk '{print $1}' ) 63 userid=$(echo "$line" | grep -o '\buid=[0-9]\+\b' | awk -F'=' '{print $2}') 64 echo "userid = $userid" 65 user=$(grep "$userid" /etc/passwd | cut -d: -f1) 66 users+=("$user") 67 echo "操作用户:$user" 68 fi 69 70 # 分隔符行,调试每个日志段时为了好看 71 if [[ $line == "----" ]]; then 72 echo "" 73 fi 74 done < $ausearchlog 75 76 # 统计事件数量 77 cnt=${#timestamps[@]} 78 echo "事件数量: $cnt" 79 80 DATE=`date +%F_%T` 81 TOKEN="钉钉机器人token" 82 PHONE="报警人电话" 83 84 # 输出事件信息 85 for ((i = 0; i < $cnt; i++)); do 86 timestamp="${timestamps[$i]}" 87 user="${users[$i]}" 88 command="${commands[$i]}" 89 cwd="${cwds[$i]}" 90 echo "(for循环)时间: $timestamp, 用户: $user, 命令: $command, 当前目录:$cwd" 91 92 93 #钉钉报警 --- 不行,以前我经常用这种方式 94 #curl -H "Content-Type:application/json" -X POST --data '{"msgtype":"text","text":{"content":"服务器:xxx\n当前时间:'$DATE'\n操作时间:'$timestamp'\n操作用户:'$user'\n操作命令:'${command}'\n当前用户所在目录:'$cwd'"} , "at": {"atMobiles": ['${PHONE}'], "isAtAll":false }}' ${TOKEN} > /dev/null 2>&1 95 96 # 构建 JSON 字符串并将结果保存到 json_msg 变量中,需要事先安装jq 97 json_msg=$(/usr/local/bin/jq -n \ 98 --arg DATE "$DATE" \ 99 --arg command "$command" \ 100 --arg PHONE "$PHONE" \ 101 '{ 102 "msgtype": "text", 103 "text": { 104 "content": "服务器:xxx\n当前时间:\($DATE)\n操作时间:'$timestamp'\n操作用户:'$user'\n操作命令:\($command)\n当前用户所在目录:'$cwd'" 105 }, 106 "at": { 107 "atMobiles": [($PHONE | tonumber)], 108 "isAtAll": false 109 } 110 }') 111 112 ## 使用 curl 发送 JSON 消息 113 curl -H "Content-Type: application/json" -X POST --data "$json_msg" "$TOKEN" > /dev/null 2>&1 114 115 done
一些想法
最后说下需要完善的地方,例如,如果当前用户在自己家目录上,运行“sudo 自己家目录下的目录或者查看家目录下的文件”,这种我们不应该去告警,所以告警里,为啥我会加个当前用户目录,是想着后续做排除的;其次是这个脚本放定时任务里,每10分钟执行一次,能不能再缩小搜索时间,例如搜索最近5分钟内,感觉10分钟有点久
另外,监控其他命令或者安全事件其实都可以从这个audit日志筛选,算是挺实时的,一运行完命令,ausearch就能搜到