0x01 效果演示

都是在社区版下展示,可以看到功能跟专业版无差别(可能会有小bug)

30560-kmcq2g9fg1.png

b站视频地址: https://www.bilibili.com/video/BV1bwWWeAEqk/

源码下载地址: https://pan.quark.cn/s/2b460d59f88b

0x02 使用

首先来到应用商店已安装然后选择php点击参数按钮

31053-z1g67ccw6r.png

点击编辑

99979-9m4ggc39rsr.png

点击高级设置->编辑 compose 文件

41083-58vu0e0c0tl.png

然后在挂载文件处加上

- /opt/1panel/apps/openresty/openresty/1pwaf/data/db:/www/db

82429-n8cpe7147f.png

然后确定重建即可

78288-54pbk4zvxl4.png

来到容器

46316-1h4i7yz8c87.png

进入终端

14065-sac509cmmzo.png

ls可以看到db目录然后给db目录权限

36008-3snnpcnznh5.png

chmod 777 -R db/

然后就可以在网站里面添加php文件,文件名随意内容如下,为了方便所以把内容都写在了一个文件,代码比较简陋有需要的大佬可以自己在改改(也不是不能用)

<?php

/**
 * 1panel-WAF数据查看接口API文件
 *
 * @author Juneha juneha@qq.com
 * @version 1.0
 * @website https://blog.mo60.cn/
 * @created 2024-08-19
 */


error_reporting(0);
// error_reporting(E_ALL);
// ini_set('display_errors', 1);
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE");
header("Access-Control-Allow-Headers: X-Requested-With, Content-Type");
header("Access-Control-Allow-Credentials: true");

class SQLite
{
    function __construct($file)
    {
        try
        {
            @$this->connection=new PDO('sqlite:'.$file);
        }
        catch(PDOException $e)
        {
            try
            {
                $this->connection=new PDO('sqlite2:'.$file);
            }
            catch(PDOException $e)
            {
                exit('error!');
            }
        }
    }
 
    function __destruct()
    {
        $this->connection=null;
    }
 
    function query($sql)
    {
        return $this->connection->query($sql);
    }
 
    function getlist($sql)
    {
        $stmt = $this->query($sql);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
 
    function Execute($sql)
    {
        return $this->query($sql)->fetch();
    }
 
    function RecordArray($sql)
    {
        return $this->query($sql)->fetchAll();
    }
 
    function RecordCount($sql)
    {
        return count($this->RecordArray($sql));
    }
 
    function RecordLastID()
    {
        return $this->connection->lastInsertId();
    }
}

function getDocumentRoot() {
    $scriptPath = __FILE__;
    $documentRoot = $_SERVER['DOCUMENT_ROOT'];
    while (true) {
        $parentDir = dirname($scriptPath);
        if ($parentDir === $documentRoot) {
            break;
        }
        if (is_dir($parentDir . '/index.php') || is_dir($parentDir . '/public')) {
            $documentRoot = $parentDir;
        }
        $scriptPath = $parentDir;
    }
    return $documentRoot;
}

function doubleSingleQuotes($string) {
    $result = str_replace("'", "''", $string);
    return $result;
}

function getParameterValue($key, $default = '')
{
    return isset($_GET[$key]) ? doubleSingleQuotes(trim($_GET[$key])) : $default;
}

function translateWAFParams($param) {
    $translations = [
        'urlDefense' => 'URL 规则',
        'urlHelper' => '禁止访问的 URL',
        'dirFilter' => '目录过滤',
        'sqlInject' => 'SQL 注入',
        'xss' => 'XSS',
        'phpExec' => 'PHP 脚本执行',
        'oneWordTrojan' => '一句话木马',
        'appFilter' => '应用危险目录过滤',
        'webshell' => 'Webshell',
        'protocolFilter' => '协议过滤',
        'javaFileter' => 'Java 危险文件过滤',
        'scannerFilter' => '扫描器过滤',
        'escapeFilter' => '转义过滤',
        'customRule' => '自定义规则',
        'httpMethod' => 'HTTP 方法过滤',
        'fileExt' => '文件上传限制',
        'fileExtHelper' => '禁止上传的文件扩展名',
        'fiveSeconds'=>  "5 秒验证",
        'acl'=>  "ACL",
        'captcha'=>  "人机验证",
        'uaBlack' =>  "User-Agent 黑名单",
        'urlBlockList' =>  "URL 黑名单",
        'ipBlack'=> "IP 黑名单",
        'unknownWebsite'=>  "未授权域名访问",
        'geoRestrict'=>  "地区访问限制",
        'attackCount'=> "攻击频率限制",
        'notFoundCount'=> "404 频率限制",
        'cookieDefense'=>  "Cookie 规则",
        'headerDefense'=>  "Header 规则",
        'defaultUaBlack'=> "User-Agent 规则",
        'argsDefense'=>  "参数规则",
        'httpRule'=>"HTTP 规则",
        'defaultUrlBlack'=> "URL 规则",
        'defaultIpBlack'=> "恶意 IP 组",
        'fileExtCheck'=> "文件上传限制",
    ];

    return isset($translations[$param]) ? $translations[$param] : $param;
}

function buildInClause($data) {
    if (empty($data)) {
        return "IN ('')";
    }
    
    if (!is_array($data)) {
        $data = array($data);
    }

    $escaped_values = array_map(function($value) {
        return "'" . doubleSingleQuotes($value) . "'";
    }, $data);

    $in_clause = "IN (" . implode(', ', $escaped_values) . ")";

    return $in_clause;
}


$rootPath = getDocumentRoot();
$onepwafPath=$rootPath.'/../../../db/1pwaf.db';
$req_logPath=$rootPath.'/../../../db/req_log.db';

if (!file_exists($onepwafPath)){
    die("1pwaf.db file does not exist!");
}

if (!file_exists($req_logPath)){
    die("req_log.db file does not exist!");
}

$onepwafDB=new SQLite($onepwafPath);
$req_logDB=new SQLite($req_logPath);

$sql='';
$action = @$_GET['action'];
switch ($action) {
    case 'wafstat':
        $wafData = $onepwafDB->getlist("SELECT * FROM main.waf_stat order by id desc LIMIT 1");
        exit(json_encode($wafData[0]));
        break;
    case 'sitelist':
        $webList=$req_logDB->getlist("SELECT DISTINCT website_key FROM main.req_logs  where website_key <> 'unknown' order by time desc");
        exit(json_encode($webList));
        break;
    case 'list':
        $page = intval(getParameterValue('page', 1));
        $size = intval(getParameterValue('size', 10));
        $websiteKey = getParameterValue('website_key');
        $exec_rule = $_GET['exec_rule'];
        $uri = getParameterValue('uri');
        $ip = getParameterValue('ip');
        $offset = ($page - 1) * $size;
        
        $conditions = [];
        if (!empty($websiteKey)) {
            $conditions[] = "website_key='{$websiteKey}'";
        }
        if (!empty($exec_rule)) {
            $conditions[] = "( exec_rule ".buildInClause($exec_rule)." or rule_type ".buildInClause($exec_rule)." )";
        }
        if (!empty($uri)) {
            $conditions[] = "uri LIKE '%{$uri}%'";
        }
        if (!empty($ip)) {
            $conditions[] = "ip='{$ip}'";
        }
        $sql = "SELECT * FROM main.req_logs WHERE 1=1";
        if (!empty($conditions)) {
            $sql .= " AND " . implode(' AND ', $conditions);
        }
        
        if (!empty($conditions)) {
            $total=$req_logDB->RecordCount($sql);
         }else{
            $total=$req_logDB->RecordCount("SELECT * FROM main.req_logs");
         }

        $sql .= " ORDER BY time DESC LIMIT {$offset}, {$size}";
        $req_logData['data'] = $req_logDB->getlist($sql);
        $req_logData['total'] = $total;
        // var_dump($sql );
        for($i=0;$i<count($req_logData['data']);$i++){
            $req_logData['data'][$i]['rule_type']=translateWAFParams($req_logData['data'][$i]['rule_type']);
            $req_logData['data'][$i]['exec_rule']=translateWAFParams($req_logData['data'][$i]['exec_rule']);
        }
        exit(json_encode($req_logData));
        break;

    case 'clearlogs':
        $status = $req_logDB->query("DELETE FROM main.req_logs");
        if($status){
            exit(json_encode(['status'=>1]));
        }else{
            exit(json_encode(['status'=>0]));
        }
        break;
    default:
        header("HTTP/1.1 404 Not Found");
        exit;
        break;
}

?>

前端代码如下,也是找朋友 @n1l.cn 花了半天手搓的,也是为了方便全部放在了单页里面,需要修改API_BASE_URL 为自己部署的api地址



<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>拦截记录</title>
    <script src="https://unpkg.com/vue@3"></script>
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
    <link rel="icon" type="image/png" href="https://1panel.cn/img/favicon.png">
    <script src="https://unpkg.com/element-plus"></script>
    <script src="https://unpkg.com/@element-plus/icons-vue"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/javascript/javascript.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/nginx/nginx.min.js"></script>
    <style>
        body {
            padding: 0;
            margin: 0;
            font-family: Arial, sans-serif;
        }

        .centered-container {
            margin-top: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
            min-height: 100vh;
        }

        .el-card {
            width: 90%;
            margin-bottom: 20px;
        }

        .home-card .header span:before {
            position: absolute;
            top: 4px;
            left: -14px;
            width: 4px;
            height: 14px;
            content: "";
            background: var(--el-color-primary);
            border-radius: 10px;
        }

        .home-card .header span {
            position: relative;
            font-size: 16px;
            font-weight: 500;
            margin-left: 18px;
        }

        .el-row {
            margin-bottom: 20px;
        }

        .el-row:last-child {
            margin-bottom: 0;
        }

        .el-col {
            border-radius: 4px;
            text-align: center;
        }

        .el-col span {
            color: #646a73;
            font-size: 14px;
        }

        .count {
            margin-top: 10px;
        }

        .count span {
            font-size: 25px;
            color: #005eeb;
            font-weight: 500;
            line-height: 32px;
        }

        .host {
            width: 240px;
            margin-right: 10px;
        }

        .execrule {
            width: 380px;
        }

        .form-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 10px;
        }

        .select-container,
        .search-container {
            display: flex;
            gap: 10px;
        }

        .search-container>.el-button {
            flex-shrink: 0;
        }

        .rounded-input {
            margin-right: 10px;
        }

        .pag {
            margin-top: 20px;
            display: flex;
            justify-content: flex-end;
        }

        tbody {
            font-size: 14px;
            color: #646a73;
        }

        .el-drawer__header {
            margin-bottom: 0;
            padding-bottom: 14px;
            border-bottom: 1px solid #f2f2f2;
        }

        .margin-top {
            margin-top: 20px;
        }

        .ad-container {
            position: fixed;
            top: 20px;
            right: 20px;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
            overflow: hidden;
            z-index: 9999;
        }

        .ad-image {
            display: block;
            width: 180px;
            height: auto;
        }

        .close-btn {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0, 0, 0, 0.5);
            color: white;
            border: none;
            border-radius: 50%;
            width: 30px;
            height: 30px;
            text-align: center;
            line-height: 30px;
            font-size: 20px;
            cursor: pointer;
        }

        .close-btn:hover {
            background: rgba(0, 0, 0, 0.7);
        }

        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-thumb {
            background-color: #b1b3b8;
            border-radius: 4px;
            border: 2px solid #ffffff;
        }

        ::-webkit-scrollbar-track {
            background-color: #f5f7fa;
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background-color: #909399;
        }

        ::-webkit-scrollbar-track:hover {
            background-color: #e4e7ed;
        }



        @media (max-width: 992px) {
            .form-container {
                flex-direction: column;
                align-items: flex-start;
            }

            .select-container,
            .search-container {
                width: 100%;
            }

            .search-container>.el-button {
                margin-top: 10px;
            }
        }

        @media (max-width: 768px) {
            .host {
                width: 100%;
                margin-right: 10px;
            }

            .execrule {
                width: 100%;
            }

            .form-container {
                padding: 0 10px;
            }

            .select-container,
            .search-container {
                flex-direction: column;
                align-items: stretch;
                gap: 5px;
            }

            .select-container>.el-select,
            .search-container>.el-input,
            .search-container>.el-button {
                width: 100%;
            }

            .search-container>.el-input {
                margin-right: 0;
            }

            .search-container>.el-button {
                margin-top: 10px;
            }
        }
    </style>
</head>

<body>
    <div id="app" class="centered-container">
        <el-card class="home-card">
            <div class="header">
                <span>今日状态</span>
            </div>
            <div style="margin-top: 20px;">
                <el-row :gutter="20">
                    <el-col :span="6">
                        <span>请求</span>
                        <div class="count">
                            <span>{{ req_count }}</span>
                        </div>
                    </el-col>
                    <el-col :span="6">
                        <span>拦截</span>
                        <div class="count">
                            <span>{{ attack_count }}</span>
                        </div>
                    </el-col>
                    <el-col :span="6">
                        <span>4xx数量</span>
                        <div class="count">
                            <span>{{ count4xx }}</span>
                        </div>
                    </el-col>
                    <el-col :span="6">
                        <span>5xx数量</span>
                        <div class="count">
                            <span>{{ count5xx }}</span>
                        </div>
                    </el-col>
                </el-row>
            </div>
        </el-card>
        <el-card>
            <div class="form-container">
                <div class="select-container">
                    <el-select v-model="website_key" clearable="" placeholder="请选择" @change="handleChange" class="host">
                        <template #prefix="">
                            <span>网站</span>
                        </template>
                        <el-option v-for="item in siteList" :key="item.value" :label="item.label" :value="item.value" />
                    </el-select>
                    <el-select v-model="execRuleList" multiple="" clearable="" collapse-tags="" placeholder="请选择"
                        @change="handleChange" class="execrule">
                        <template #prefix="">
                            <span>命中规则</span>
                        </template>
                        <el-option v-for="item in execRuleOptions" :key="item.value" :label="item.label"
                            :value="item.value" />
                    </el-select>
                </div>
                <div class="search-container">
                    <el-input v-model="searchUrl" class="rounded-input" placeholder="请输入 URL,支持模糊搜索"
                        clearable=""></el-input>
                    <el-input v-model="searchIP" class="rounded-input" placeholder="请输入 IP" clearable=""></el-input>
                    <el-button type="primary" plain="" @click="handleChange">
                        搜索
                    </el-button>
                </div>
            </div>
        </el-card>
        <el-card>
            <div class="card-header" style="display: flex; align-items: center;">
                <span style="font-weight: bold;">拦截记录
                    <el-divider direction="vertical" />
                </span>
                <el-button type="primary" plain="" @click="clearlogs">
                    清空日志
                </el-button>
                <div style="margin-left: auto;">
                    <el-button type="primary" plain="" @click="listData">
                        刷新
                    </el-button>
                </div>
            </div>
            <el-table :data="tableData" style="margin-top: 20px; width: 100%;">
                <el-table-column prop="ip" label="IP" min-width="130"></el-table-column>
                <el-table-column prop="host" label="域名" min-width="250"></el-table-column>
                <el-table-column prop="uri" label="url" :show-overflow-tooltip="true" min-width="300"></el-table-column>
                <el-table-column prop="ipLocation" label="IP归属地" width="150">
                    <template #default="scope">
                        {{ scope.row.ip_country_zh }} {{ scope.row.ip_province_zh }}
                    </template>
                </el-table-column>
                <el-table-column prop="action" label="动作" min-width="100">
                    <template #default="scope">
                        <el-tag v-if="scope.row.action === 'deny'" type="error">
                            禁止
                        </el-tag>
                        <el-tag v-else="" type="success">
                            允许
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="exec_rule" label="命中规则" min-width="150"></el-table-column>
                <el-table-column prop="rule_type" label="类型" min-width="100"></el-table-column>
                <el-table-column prop="localtime" label="时间" min-width="180">
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="100">
                    <template #default="scope">
                        <el-button style="color: #005eeb;" link="" @click="handleClick(scope.row)">
                            详情
                        </el-button>
                    </template>
                </el-table-column>
            </el-table>
            <div class="pag">
                <el-pagination v-if="!smallPagination" :page-sizes="[5, 10, 20, 50, 100]" size="default"
                    layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange"
                    @current-change="handleCurrentChange">
                </el-pagination>
                <el-pagination v-else size="small" background layout="prev, pager, next" :total="total" class="mt-4"
                    @size-change="handleSizeChange" @current-change="handleCurrentChange" />
            </div>
        </el-card>
        <!-- 抽屉 -->
        <el-drawer v-model="drawer" :size="drawerSize" @close="handleDrawerClose">
            <template #header>
                <el-page-header @back="drawer = false" title="返回">
                    <template #content>
                        <span class="text-large font-600 mr-3"> 详情 </span>
                    </template>
                </el-page-header>
            </template>
            <template #default>
                <div>
                    <el-descriptions title="攻击详情" :column="1" border>
                        <el-descriptions-item label="攻击 IP">
                            {{ selectedRow.ip }}
                        </el-descriptions-item>
                        <el-descriptions-item label="时间">
                            {{ selectedRow.localtime }}
                        </el-descriptions-item>
                        <el-descriptions-item label="动作">
                            {{ selectedRow.action === 'deny' ? '禁止' : '允许'}}
                        </el-descriptions-item>
                        <el-descriptions-item label="命中规则">
                            {{ selectedRow.exec_rule }}
                        </el-descriptions-item>
                        <el-descriptions-item label="匹配值">
                            {{ decodeBase64(selectedRow.match_value) }}
                        </el-descriptions-item>
                    </el-descriptions>
                    <el-descriptions class="margin-top" title="HTTP">
                    </el-descriptions>
                    <textarea id="nginx-log-editor"></textarea>
                </div>
            </template>
        </el-drawer>
        <!-- 广告 -->
        <div v-show="adDisplay" class="ad-container">
            <a href="https://www.lxware.cn/?code=ifconfig" target="_blank">
                <img src="https://src.sjtu.edu.cn/media/images/2024/08/20/b76df827-e1bb-4e79-b728-ea5c37fe1f0c-moDBtzpW.png" alt="Advertisement" class="ad-image">
            </a>
            <button class="close-btn" @click="adDisplay = false">&times;</button>
        </div>
    </div>
    <script>
        const { ElMessageBox, ElMessage } = ElementPlus;

        const API_BASE_URL = '';
        const App = {
            data() {
                return {
                    req_count: 0,
                    attack_count: 0,
                    count4xx: 0,
                    count5xx: 0,
                    website_key: '',
                    execRuleList: [],
                    searchUrl: '',
                    searchIP: '',
                    currentPage: 1,
                    pageSize: 10,
                    siteList: [],
                    drawer: false,
                    smallPagination: false,
                    selectedRow: {},
                    execRuleOptions: [
                        { value: 'urlDefense', label: 'URL 规则' },
                        { value: 'urlHelper', label: '禁止访问的 URL' },
                        { value: 'dirFilter', label: '目录过滤' },
                        { value: 'sqlInject', label: 'SQL 注入' },
                        { value: 'xss', label: 'XSS' },
                        { value: 'phpExec', label: 'PHP 脚本执行' },
                        { value: 'oneWordTrojan', label: '一句话木马' },
                        { value: 'appFilter', label: '应用危险目录过滤' },
                        { value: 'webshell', label: 'Webshell' },
                        { value: 'protocolFilter', label: '协议过滤' },
                        { value: 'javaFileter', label: 'Java 危险文件过滤' },
                        { value: 'scannerFilter', label: '扫描器过滤' },
                        { value: 'escapeFilter', label: '转义过滤' },
                        { value: 'customRule', label: '自定义规则' },
                        { value: 'httpMethod', label: 'HTTP 方法过滤' },
                        { value: 'fileExt', label: '文件上传限制' },
                        { value: 'fileExtHelper', label: '禁止上传的文件扩展名' },
                        { value: 'fiveSeconds', label: '5 秒验证' },
                        { value: 'acl', label: 'ACL' },
                        { value: 'captcha', label: '人机验证' },
                        { value: 'uaBlack', label: 'User-Agent 黑名单' },
                        { value: 'urlBlockList', label: 'URL 黑名单' },
                        { value: 'ipBlack', label: 'IP 黑名单' },
                        { value: 'unknownWebsite', label: '未授权域名访问' },
                        { value: 'geoRestrict', label: '地区访问限制' },
                        { value: 'attackCount', label: '攻击频率限制' },
                        { value: 'notFoundCount', label: '404 频率限制' },
                        { value: 'cookieDefense', label: 'Cookie 规则' },
                        { value: 'headerDefense', label: 'Header 规则' },
                        { value: 'defaultUaBlack', label: 'User-Agent 规则' },
                        { value: 'argsDefense', label: '参数规则' },
                        { value: 'httpRule', label: 'HTTP 规则' },
                        { value: 'defaultUrlBlack', label: 'URL 规则' },
                        { value: 'defaultIpBlack', label: '恶意 IP 组' },
                        { value: 'fileExtCheck', label: '文件上传限制' }
                    ],
                    total: 0,
                    tableData: [],
                    editor: null,
                    drawerSize: '40%',
                    adDisplay: true,
                };
            },
            methods: {
                async fetchData() {
                    try {
                        const { data } = await axios.get(API_BASE_URL, { params: { action: 'wafstat' } });
                        Object.assign(this, {
                            req_count: data.req_count,
                            attack_count: data.attack_count,
                            count4xx: data.count4xx,
                            count5xx: data.count5xx,
                        });
                    } catch (error) {
                        console.error("API 请求失败:", error);
                    }
                },
                async siteListData() {
                    try {
                        const { data } = await axios.get(API_BASE_URL, { params: { action: 'sitelist' } });
                        this.siteList = data.map(item => ({
                            value: item.website_key,
                            label: item.website_key,
                        }));
                    } catch (error) {
                        console.error("API 请求失败:", error);
                    }
                },
                async listData() {
                    try {
                        const { data } = await axios.get(API_BASE_URL, {
                            params: {
                                action: 'list',
                                page: this.currentPage,
                                size: this.pageSize,
                                website_key: this.website_key,
                                uri: this.searchUrl,
                                ip: this.searchIP,
                                exec_rule: this.execRuleList
                            }
                        });
                        this.total = data.total;
                        this.tableData = data.data;
                    } catch (error) {
                        console.error("API 请求失败:", error);
                    }
                },
                async clearlogs() {
                    try {
                        await ElMessageBox.confirm('清空日志将无法恢复,是否继续?', '清空日志', {
                            confirmButtonText: '确定',
                            cancelButtonText: '取消',
                        });

                        const { data } = await axios.get(API_BASE_URL, { params: { action: 'clearlogs' } });
                        if (data.status === 1) {
                            ElMessage({ message: '日志清空成功', type: 'success' });
                            this.listData();
                        } else {
                            ElMessage({ message: '日志清空失败', type: 'error' });
                        }
                    } catch (error) {
                        if (error === 'cancel') {
                            ElMessage({ type: 'info', message: '清空日志已取消' });
                        } else {
                            console.error("API 请求失败:", error);
                            ElMessage({ message: '请求失败,请稍后重试', type: 'error' });
                        }
                    }
                },
                handleClick(row) {
                    this.selectedRow = row;
                    this.drawer = true;
                    this.$nextTick(this.initializeCodeMirror);
                },
                handleChange() {
                    this.listData();
                },
                handleSizeChange(size) {
                    this.pageSize = size;
                    this.listData();
                },
                handleCurrentChange(page) {
                    this.currentPage = page;
                    this.listData();
                },
                initializeCodeMirror() {
                    const editorElement = document.getElementById('nginx-log-editor');
                    this.editor = CodeMirror.fromTextArea(editorElement, {
                        lineNumbers: true,
                        mode: 'nginx',
                        readOnly: true,
                        theme: 'default',
                    });
                    this.editor.setValue(this.selectedRow.nginx_log || '暂无日志数据');
                },
                handleDrawerClose() {
                    if (this.editor) {
                        this.editor.toTextArea();
                    }
                },
                decodeBase64(encodedString) {
                    try {
                        const decodedString = atob(encodedString);
                        return decodeURIComponent(escape(decodedString));
                    } catch (error) {
                        console.error('Base64 解码失败:', error);
                        return null;
                    }
                },
                updateDrawerSize() {
                    const width = window.innerWidth;

                    if (width > 1200) {
                        this.drawerSize = '40%';
                    } else if (width > 768) {
                        this.drawerSize = '60%';
                    } else {
                        this.drawerSize = '80%';
                        this.smallPagination = true;
                        this.adDisplay = false;
                    }
                },
            },
            async mounted() {
                await Promise.all([this.fetchData(), this.siteListData(), this.listData()]);
                this.updateDrawerSize();
                window.addEventListener('resize', this.updateDrawerSize);
            },
            beforeUnmount() {
                window.removeEventListener('resize', this.updateDrawerSize);
            },
        };

        const app = Vue.createApp(App);
        app.use(ElementPlus);
        app.mount("#app");
    </script>
</body>

</html>

0x03 原理 and 文档

1panel waf的日志都是保存在openresty容器里面的db文件,目录也有映射出来,我们通过php容器也去挂载一下即可读取到db文件然后读取即可

下面是简易的api接口文档

  1. wafstat
  • 说明:获取 WAF 状态的最新记录。
  • 请求方式:GET
  • 请求参数:无
  • 返回格式:JSON
  • 返回示例:
json
{
    "id": 5,
    "day": "2024-08-20",
    "req_count": 2085,
    "attack_count": 65,
    "count4xx": 917,
    "count5xx": 5,
    "create_date": "2024-08-19 16:06:47"
}

2.sitelist

  • 说明:获取所有已记录的网站列表。
  • 请求方式:GET
  • 请求参数:无
  • 返回格式:JSON
  • 返回示例:
json
[
  { "website_key": "example.com" },
  { "website_key": "testsite.com" }
]

3.list

  • 说明:获取请求日志列表,支持分页和过滤。
  • 请求方式:GET
  • 请求参数:
  • page:页码(默认值为 1)
  • size:每页条数(默认值为 10)
  • website_key:网站(可选)
  • exec_rule:命中规则(可选)
  • uri:请求 URI(可选)
  • ip:请求 IP(可选)
  • 返回格式:JSON
  • 返回示例:
json
{
    "data": [
        {
            "id": "17b7d444-9631-4bd2-a09e-8f7a4d03f6b6",
            "ip": "205.210.31.16",
            "ip_iso": "US",
            "ip_country_zh": "美国",
            "ip_country_en": "United States",
            "ip_province_zh": "",
            "ip_province_en": "",
            "ip_longitude": "-97.822",
            "ip_latitude": "37.751",
            "localtime": "2024-08-20 03:26:14",
            "time": 1724124372.559,
            "server_name": "8.222.178.70",
            "website_key": "8.222.178.70:8080",
            "host": "8.222.178.70",
            "method": "GET",
            "uri": "/",
            "user_agent": "Expanse, a Palo Alto Networks company, searches across the global IPv4 space multiple times per day to identify customers' presences on the Internet. If you would like to be excluded from our scans, please send IP addresses/domains to: scaninfo@paloaltonetworks.com",
            "exec_rule": "恶意 IP 组",
            "rule_type": "",
            "match_rule": "",
            "match_value": "MjA1LjIxMC4zMS4xNg==",
            "nginx_log": "GET  / HTTP/1.0\nUSER-AGENT: Expanse, a Palo Alto Networks company, searches across the global IPv4 space multiple times per day to identify customers' presences on the Internet. If you would like to be excluded from our scans, please send IP addresses/domains to: scaninfo@paloaltonetworks.com\nACCEPT: */*\n",
            "blocking_time": 0,
            "action": "deny",
            "is_block": 0,
            "is_attack": 1
        }
    ],
    "total": 13
}

4.clearlogs

  • 说明:清除所有请求日志。
  • 请求方式:GET
  • 请求参数:无
  • 返回格式:JSON
  • 返回示例:
json
{
  "status": 1
}

status:操作状态,1 表示成功,0 表示失败。

Last modification:August 20, 2024
  • 本文作者:Juneha
  • 本文链接:https://blog.mo60.cn/index.php/archives/1panel_waf_log_use.html
  • 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。
  • 法律说明:
  • 文章声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任,本人坚决反对利用文章内容进行恶意攻击行为,推荐大家在了解技术原理的前提下,更好的维护个人信息安全、企业安全、国家安全,本文内容未隐讳任何个人、群体、公司。非文学作品,请勿过度理解,根据《计算机软件保护条例》第十七条,本站所有软件请仅用于学习研究用途。
如果觉得我的文章对你有用,请随意赞赏,可备注留下ID方便感谢