Web 访问日志深度分析

W3C 访问日志是 IIS 上唯一的逐请求记录,也是全案密度最高、最容易被噪声淹没的证据。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:W3C 日志、sc-status、sc-substatus、sc-win32-status、风险打分、SQL 注入特征、User-Agent 异常 难度:进阶 前置知识:HTTP 协议状态码语义、IIS W3C 日志结构、正则表达式 相关文章:IIS 配置与 W3C 日志、Web 攻击痕迹还原

一、概述

W3C 访问日志是 IIS 上唯一的逐请求记录,也是全案密度最高、最容易被噪声淹没的证据。

它不含任何人工行为推断,只有服务器工作线程按固定格式吐出的请求事实:谁、什么时候、请求了什么、结果如何、耗时多久。

事件日志记的是系统级事件,注册表记的是配置现状——只有访问日志能证明"这个具体的攻击请求确实到达过这台服务器,并且得到了这样的响应"。

但这份证据的密度与分析要求完全不匹配。

中等规模站点日均请求量在几十万条量级,逐条人眼看不动;而真正有意义的信息——"哪一批是自动化攻击、哪一次返回 200 说明落地成功"——埋在几十万条 200 0 0 里。

难点在于把噪声压下去。

本篇给出的三层判读方法:

  1. 状态码三元组——sc-status / sc-substatus / sc-win32-status 的组合语义。这是 IIS 独有的信息维度,比单看状态码精确得多,能区分"文件真不存在"和"路径语法错了"这种看似同为 404 的情况。
  2. 攻击特征的日志指纹——SQL 注入、路径穿越、上传落地、目录扫描、凭据爆破、脚本调用,各自对应不同的字段组合模式。
  3. 风险打分脚本——把上述判读固化成可运行、可复现、可调阈值的自动化流程,输出 Top N 可疑请求并附命中原因。

在取证流程中的位置:本篇属于分析环的归因层,输入是已解析的 W3C 结构化记录,输出是"哪些请求可疑"的候选集。

它的产出是候选,不是结论——每个候选都要在 3.3 第 3 步 人工复核,并在主机侧印证后才进入报告。

上游依赖 IIS 配置与 W3C 日志——本篇假设你已读完它并能正确解析(字段表可信、时区已换算、状态码两层含义已分清,这三条没做到,每个统计数字都是错的而且不会报错);下游接上 Web 攻击痕迹还原 的主机侧印证。

涉及的证据形态(具体到字段,不是"访问日志"这种大类):

维度 字段 判读价值
状态码三元组 sc-status + sc-substatus + sc-win32-status ★★★★★ 本篇最核心的判据
组合判读 404/1 = 未找到站点 ★★★★★ 虚拟主机扫描的强信号
404/7 = 扩展名被拒 ★★★★★ 攻击被 Request Filtering 挡下的直接证据
404/14 = URL 过长 ★★★★ 缓冲区溢出类探测前兆
sc-win32-status=2 vs 3 vs 5 ★★★★★ 文件不存在 / 路径不可达 / 存在但被拒
耗时 time-taken(毫秒) ★★★★★ 时间盲注的唯一抓手
查询串 cs-uri-query:' OR 1=1--、UNION SELECT、SLEEP(5) ★★★★★ 注入指纹
请求路径 cs-uri-stem:../、%2e%2e/、%252e(双重编码) ★★★★ 路径穿越
方法 + 路径 PUT/PATCH 到 /upload* 且 .aspx ★★★★★ 落地证据,响应 201
UA sqlmap、Nikto、nuclei、python-requests ★★ 只是佐证,不能单独定性
时间分布 间隔 P90−P10 抖动 < 5% ★★★★★ 机器节拍,脚本发出的确证
客户端 IP c-ip 全是同一个 → 那是 CDN/代理 ★★★★ 归因前的必查项

★ 本篇最重要的纠错:大量中文资料把 404.3 说成"Win32 找不到路径"。这是错的——sc-substatus=3 是 MIME 类型限制;"路径找不到"是 sc-win32-status=3(ERROR_PATH_NOT_FOUND),"文件找不到"才是 sc-win32-status=2。混用会得出"文件存在只是路径错了"这种反向结论。

能回答什么:哪些请求是自动化攻击而非人工浏览;某次攻击探测是否真的到达了 IIS 并被哪一层挡下(404/7 比"没落地"更有价值);时间盲注藏在哪条记录里;某次 PUT ... 201 是否意味着上传成功;哪些 IP、路径、UA 组合构成一条完整的攻击序列;攻击窗口的起止时间。

不能回答什么:攻击是否利用成功——200 不等于代码执行成功,可能是错误页;请求体与响应体内容(W3C 不记录);攻击者的真实 IP(c-ip 在反代后面是代理,见 01 篇)。

主机的后续行为日志里也没有——日志到此为止,要看文件系统与事件日志。至于哪些请求被 sc-substatus 之外的静默失败路径吞掉,同样无从查证。

内容边界(本篇的红线):本篇处理的是日志侧的统计特征与攻击请求的识别,产出是"哪些请求可疑"这一候选集。它不提供任何攻击构造、载荷生成或利用步骤——所有特征描述都以"如何从字段组合中认出这类请求"的角度表述。

同理,看到 sqlmap UA 不等于可以断定"对方用 sqlmap 攻击了",它只是忘了改默认值的疏忽性证据。

读者前提:本篇硬依赖 IIS 配置与 W3C 日志——字段表、时区换算、状态码两层含义这三条前提没做到,本篇每一个统计数字都是错的,而且不报错。

事件 ID 基础与 Get-WinEvent 用法见事件日志深度分析。

另外需要 HTTP 状态码语义、正则表达式,以及对"标准差/分位数"这类统计量的直观理解(用来判读"间隔抖动 < 5%"这个判据)。

二、核心原理

2.1 ★ 状态码三元组:IIS 比 HTTP 多给了两路错误信息

标准 HTTP 只返回 404 这样一个状态码。

IIS 在此基础上额外记录两路独立的失败原因,这是它取证价值高于普通 Web 日志的根本原因:

字段 含义 取值空间 反映的问题
sc-status HTTP 状态码 100–599 协议层结论(对客户端说的话)
sc-substatus IIS 细分子状态 各状态码下自定义 IIS 内部哪一步没通过
sc-win32-status Win32 系统错误码 Windows 错误码 操作系统层失败原因

★★★ 本篇最重要的纠错:大量中文资料把 404.3 说成"Win32 找不到路径"。这是错的——sc-substatus=3 是 MIME 类型限制;"路径找不到"是 sc-win32-status=3(ERROR_PATH_NOT_FOUND),"文件找不到"才是 sc-win32-status=2。混用会得出"文件存在只是路径错了"这种反向结论。** sc-substatus 侧组合判读**:

组合 含义 判读
404.0 未找到,文件已移动或不存在 真 404。资源真的不在这个路径上
404.1 未找到站点 请求的 Host 头没有对应绑定 → 虚拟主机扫描的强信号
404.2 MIME 类型限制 / 路径语法问题 请求路径形式上无法映射到文件
404.3 MIME 类型限制 扩展名映射无效或未配置(不是路径找不到)
404.4 未配置处理程序 扩展名有 MIME 映射但没有对应 handler → 例如请求了 .php 但没装 PHP
404.7 文件扩展名被拒绝 命中了 Request Filtering 的扩展名黑名单 → 攻击探测被 IIS 挡下的直接证据
404.14 请求 URL 太长 缓冲区溢出类探测的前兆,或畸形超长请求
400.1 请求头过长 超长 Header 探测
值 Win32 常量 含义 取证解读
0 ERROR_SUCCESS 无系统级错误 失败发生在 IIS/应用层,不是文件系统问题
2 ERROR_FILE_NOT_FOUND 找不到文件 路径是对的,只是文件不在 → 真 404 的确认信号
3 ERROR_PATH_NOT_FOUND 找不到路径 路径本身不可达(中间目录不存在)
5 ERROR_ACCESS_DENIED 拒绝访问 权限或 Request Filtering 拦截
53 ERROR_BAD_NETPATH 网络路径不存在 指向了 UNC 或已卸载的共享

判读口诀:sc-win32-status=0 + 5xx = 应用层自己崩了,与文件无关;sc-win32-status=2 + 404 = 文件确实不存在;sc-win32-status=5 + 404 = 存在但被拒绝——这一条最容易被误判成"文件不存在",实际是被权限或过滤规则挡住,而文件可能就在那儿。

2.2 攻击特征在日志中的落点

每类攻击都在特定字段上留下固定模式。把"攻击类型 → 字段模式"记住,比记正则表达式有用得多。

攻击类型 判读字段 特征模式 典型响应
SQL 注入 cs-uri-query ' OR 1=1--、id=1 UNION SELECT null,version()、id=1 AND SLEEP(5) 500(语法错)、200 + time-taken 异常大(盲注)、200(直接回显)
路径穿越 cs-uri-stem ../、..%2f、%2e%2e/、..%5c、%252e(双重编码) 404/2(无法映射)、200(读到了系统文件)
上传落地 cs-method + 路径 PUT/PATCH、路径在 /upload*、扩展名 .aspx 201 Created(成功)、405(不允许)
目录扫描 批量 404 同一 IP 短时间大量不同路径全 404,路径呈规律递增 全部 404
凭据爆破 sc-status 同一 IP 同一路径集中出现 401/403 401/403,末尾可能夹一个 302
脚本调用 cs-uri-query cmd=、exec=、shell=、eval= 等参数名 200 + 响应体极小
Web 扫描器 cs(User-Agent) sqlmap、Nikto、nessus、acunetix、nuclei 混合
反代穿透 c-ip 全是同一个 IP → 那是 CDN/代理,不是真实来源 —

★ 最有价值的单条判读:time-taken + SQL 注入。 时间盲注的注入请求返回 200,看起来完全正常——但 time-taken 会出现明显离群值。正常页面 time-taken 集中在几十毫秒,而 SLEEP(5) 那条是 5000+ 毫秒。在满是 200 的日志里,毫秒时延是唯一能把盲注揪出来的字段。 前提是别把 time-taken 当秒(它是毫秒)。

2.3 User-Agent 与时间维度:两个最容易被滥用的维度

UA 是最容易被滥用的字段——攻击者可随意伪造,它本身不构成任何证据;而自动化发包很难带上随机抖动(工具默认固定 sleep)。

这两点必须同时理解。

维度 判读 可靠性 说明
UA 命中已知工具名(sqlmap/Nikto) ★★★ 攻击者忘了改默认值,属疏忽性证据,须与查询串互证
为空 ★★ 脚本化常见;也可能是极简客户端或反代剥离
python-requests/curl ★★ 自动化客户端。但备份脚本、监控探针、CI 也是这个
正常浏览器 UA 但行为异常 ★★★★★ 最有价值——伪装正常,正因如此才要看查询串
时间 深夜突增(00:00–05:00 偏离历史基线) ★★★ 业务此时本无流量
间隔 P90−P10 抖动 < 5% ★★★★★ 机器节拍,脚本发出的确证
每分钟数百请求且全 404 ★★★ 目录扫描
沉寂数小时后单次请求 ★★ 人工介入或 C2 心跳

陷阱:看到 curl/8.4.0 就写"攻击工具"是错的——UA 只是佐证,不能单独定性。

★ 为什么"间隔均匀"比"用了扫描器"更强:工具 UA 可随便改、IP 可换代理,但发包节拍是工具库的实现细节。一条伪装成 Chrome、但 500 个请求间隔标准差极小的记录,比 100 条写着 sqlmap 的记录更能说明自动化。

2.5 为什么需要打分而不是单纯 grep

朴素做法是每个特征跑一个 grep 再人工去重排序,但有三个硬伤:

  1. 一次命中不等于可疑——union 也会出现在正常的技术文章页里;
  2. 强信号往往不带单条特征——自动化扫描的每一条单独看都是普通 404,只有聚合后才显出"几百个不同路径全是 404";
  3. 强信号常是弱特征的叠加——空 UA(3 分)+ 无 Referer(3 分)+ 深夜(2 分)单条都不起眼,加起来就超过一条真正的 SQL 注入。

打分脚本的价值就是把"弱信号叠加 + 跨行聚合"固化下来,并且输出命中原因——评审时能直接看到"为什么这条是 31 分",而不是一个黑箱数字。

三、操作步骤

3.1 第 1 步:确认分析目标与时间基准

从镜像提取 W3SVC* 下的 *.log*(完整命令见第七章),随后确认两件事:

head -4 /evidence/u_ex241114.log    # #Software 判版本,#Fields 判字段顺序
ls -l /evidence/W3SVC2/             # ★ 体积异常 = 截断线索(见 04 篇)

验证:若同一文件出现多行 #Fields,说明当天改过日志配置(见 02)。做任何统计之前先确认时区——W3C 的 date/time 是 UTC。

3.2 第 2 步:运行风险打分脚本

脚本仅用 Python 3.6+ 标准库,无需 pip install 任何东西。保存为 w3c_risk.py:

#!/usr/bin/env python3

-- coding: utf-8 --

"""IIS W3C 访问日志风险打分器:逐行规则匹配 + 4 个跨行聚合规则,输出 Top N 可疑请求。 用法: python3 w3c_risk.py <日志文件或目录...> [-n 20] [--tz +8] [--min 1] [-o risk.csv] 依赖: 仅 Python 3.6+ 标准库(无第三方包)""" import argparse, glob, os, re, sys from collections import defaultdict from datetime import datetime, timedelta, timezone from urllib.parse import unquote

===== 规则表 (规则码, 分值, 范围, 正则, 说明) =====

范围: t=解码后的"路径+查询串" tp=t 再把 '+' 还原为空格 s=路径 ua=User-Agent m=方法

RULES = [ # --- SQL 注入 --- ("SQLI-UNION", 10, "tp", r"union\s+(?:all\s+|distinct\s+)?select", "URL 中拼接 union select"), ("SQLI-BOOL", 10, "tp", r"(?:'|"|\b)\s*(?:or|and)\s+['"]?\w+['"]?\s*=\s*['"]?\w+", "恒真/恒假布尔条件"), ("SQLI-COMMENT", 8, "tp", r"(?:'|")?\s*(?:--|#|/*)\s*$|'\s*(?:--|#)", "SQL 注释截断后条件"), ("SQLI-TIME", 10, "tp", r"\b(?:sleep|benchmark|pg_sleep)\s*(|\bwaitfor\s+delay", "延时函数(盲注)"), ("SQLI-INFO", 8, "tp", r"information_schema|sysobjects|syscolumns|sqlite_master|@@version", "库表结构枚举"), # --- 路径穿越 / 远程执行 --- ("TRAVERSAL", 10, "tp", r"..[\/]|%2e%2e|..%2f|..%5c|%252e|boot.ini|win.ini", "路径穿越"), ("RCE-PARAM", 10, "tp", r"(?:^|[?&;])(?:cmd|exec|shell|command|eval|passthru|system|query)\s*=", "命令/执行类参数名"), ("RCE-SEP", 10, "tp", r"|\s*(?:cat|whoami|id|nslookup|ipconfig|tasklist)\b|%00|${jndi:", "管道命令/空字节截断/JNDI"), # --- 落地与扫描 --- ("WEBSHELL-EXT", 10, "s", r".(?:aspx|ashx|asmx|axd|config|php[0-9]?|jsp|asp|cer|asax)(?:$|.)", "可执行/可配置扩展名"), ("UPLOAD-PATH", 5, "s", r"/(?:upload|upfile|files|attachment|tmp|temp)/", "上传类路径"), ("SCAN-PATH", 5, "s", r"/(?:wp-admin|phpmyadmin|/admin\b|/manager/html|/WEB-INF|/.git|/.svn|/.env|/backup|/cgi-bin|/solr|jenkins|actuator)", "常见扫描目标路径"), # --- User-Agent --- ("UA-BLACK", 5, "ua", r"sqlmap|nikto|nessus|acunetix|wpscan|nuclei|masscan|nmap|dirbuster|gobuster|havij|arachni", "已知扫描/利用工具 UA"), ("UA-SCRIPT", 3, "ua", r"^(?:python-requests|python-urllib|curl|wget|go-http-client|okhttp|node-fetch|axios|apache-httpclient|libcurl)", "脚本化 HTTP 客户端 UA"), ("UA-EMPTY", 5, "ua", r"^$", "空 User-Agent"), # --- 方法 --- ("METHOD-WRITE", 5, "m", r"^(?:PUT|PATCH|DELETE|MKCOL|MOVE|COPY|OPTIONS|TRACE|DEBUG)$", "写入/配置类 HTTP 方法"), ]

===== 聚合阈值(经验值,须按站点历史基线调整)=====

BURST_404, BRUTE_N, DIR_ENUM, REGULAR_N = 200, 10, 50, 12

def _i(x):