搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

IIS 配置与 W3C 日志

IIS 取证有两个必须先解决的问题:这台服务器上有哪些站点、各自对应磁盘上哪个目录;以及这些站点的访问日志实际记录了哪些字段、按什么顺序排列。

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

一、概述

IIS 取证有两个必须先解决的问题:这台服务器上有哪些站点、各自对应磁盘上哪个目录;以及这些站点的访问日志实际记录了哪些字段、按什么顺序排列。

第二个问题是新手最容易翻车的地方。W3C 日志看起来是"空格分隔的一行文本",很容易让人写死列索引去取字段。

但 IIS 的字段集合与顺序是可配置的——不同版本、不同管理员、不同时间点都不一样。

任何写死下标的解析器遇到非默认配置都会静默给出错误结果:把 IP 当成用户名、把状态码当时延。更糟的是它不报错,错误会一路流进取证报告。

换句话说,「这条日志说了什么」这个问题,在 IIS 上必须先答「这份日志的字段表是什么」才能回答。

在取证流程中的位置:本篇属于分析环的解析基座层,产出两件东西:站点拓扑表与可复用的日志解析器。

它不产出结论,而是决定后面每一篇的统计数字是否成立。

上游依赖 Windows 服务器取证工件地图 提供的工件边界与 事件日志深度分析 的通道检查;

下游直接接上 Web 访问日志深度分析 的归因统计与 Web 攻击痕迹还原 的主机侧印证。

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

形态 路径 / 字段 取证意义
IIS 7+ 全局配置 %windir%\system32\inetsrv\config\applicationHost.config(XML 明文) ★★★★★ 站点拓扑的唯一权威来源
配置备份 applicationHost.config.bak ★★★★★ 改配置前的版本,diff 即篡改证据
IIS 6 配置 %windir%\system32\inetsrv\MetaBase.xml + MBSchema.xml ★★★★★ 已经是 XML 明文,不是二进制
IIS 4/5 配置 MetaBase.bin ★★ 二进制且已不含有效配置,仅占位
站点配置 C:\inetpub\wwwroot\<site>\Web.config、App_Offline.htm ★★★★★ 含 machineKey,攻防焦点
访问日志 C:\inetpub\logs\LogFiles\W3SVC<n>\u_ex*.log ★★★★★ 逐请求记录
#Fields 行 文件头部 ★★★★★ 解析的唯一依据,绝不能写死列索引
关键字段 sc-status/sc-substatus/sc-win32-status 三元组、time-taken、cs(User-Agent) ★★★★★
自定义字段 #Fields 末尾多出的 x-forwarded-for 一列,文件名带 _x ★★★★ 穿透 CDN 的取证手段

★ 站点 ID 的三个要点:① 日志目录 W3SVC2 里的 2 就是 id="2",与站点名无关;② ID 由创建顺序决定,站点删过会留下 ID 空洞(1、3 存在而 2 不存在),这本身就是站点被删过的线索,且空洞站点的历史日志常常仍留在磁盘上;③ logFile/directory 可配到任意路径,很多生产环境改到数据盘。不读配置就按 C:\inetpub\logs 找,会完全找不到日志。

IIS 版本与配置格式(MetaBase.xml 还是 applicationHost.config);哪个站点 ID 对应哪个域名与物理路径;日志实际记录了哪些字段、按什么顺序;某次请求的真实状态是"未找到"还是"MIME 拒绝"(三元组才能区分)。

自定义字段是否存在——这一条决定你能否穿透 CDN 取到真实客户端 IP。某个 W3SVC<n> 目录里是否藏着已删站点的历史日志,也在答案里。

请求体与响应体内容(W3C 不记录);某次请求是否真的执行了后端代码(200 也可能是错误页);time-taken 里有多少是网络耗时(它含在内)。

IIS 4/5 的二进制 MetaBase.bin 里的历史配置也拿不到——不含有效数据,不要在这上面浪费时间。至于 sc-substatus 之外的静默失败路径,日志根本没记。

读者前提:需要 HTTP 请求/响应结构、XML 基础、Windows 服务与配置路径。

时区是本篇最容易被忽略的硬前提:W3C 的 date/time 是 UTC,事件日志是本地时间。UTC+8 环境直接比对会整体错 8 小时,足以把"先登录后访问"颠倒过来,详见 00 篇的 4.2 时间基准。

建议严格按 3.1 第 1 步 → 3.2 第 2 步 → 3.3 第 3 步 → 3.4 第 4 步 的顺序执行。跳步会让后面所有统计静默失真。

二、核心原理

2.1 IIS 版本差异:配置体系的两代更替

这是 IIS 取证第一个必须确认的事实——配置在哪、什么格式,完全取决于版本。

版本 配置文件 格式 站点标识
IIS 6.0(Win2K3) %windir%\system32\inetsrv\MetaBase.xml + MBSchema.xml XML 明文 数字 Site ID
IIS 4/5.0 %windir%\system32\inetsrv\MetaBase.bin 二进制(难解析) 数字 Site ID
IIS 7.0+(Win2008+) %windir%\system32\inetsrv\config\applicationHost.config XML 明文 字符串 W3SVC<n>
IIS 7.0+ 缓存 .../config\apphost.config 二进制缓存 —

三条关键推论:

  1. IIS 6.0 已经是 XML 明文(IIS 4/5 的 MetaBase.bin 才是二进制)。国内仍有大量 Win2K3 + IIS 6 老系统,见到 MetaBase.xml 不要惊讶。
  2. IIS 6.0 目录里可能还残留 MetaBase.bin,但它不含有效配置——那是向后兼容占位文件。"备份 MetaBase.bin 即可"是错的,必须备份整个 inetsrv 目录。
  3. IIS 7+ 引入自动配置备份:每次改配置,旧版本存为 applicationHost.config.bak。取证必须一并提取。

两代配置读站点映射的关键属性完全不同。

IIS 6 的站点是一个 locsection,节点名 W3SVC/1 里的 1 就是 Site ID,绑定、目录、日志路径分别存在 ServerBindings / Root / LogFileDirectory 三个 property 里。

IIS 7+ 则是一个 site 元素(注意 logFormat 是 logFile 的属性而非子元素):

<site name="digiforensics-site" id="2" serverAutoStart="true">
  <application path="/">
    <virtualDirectory path="/" physicalPath="C:\inetpub\wwwroot\digiforensics-site" />
  </application>
  <binding protocol="https" bindingInformation="*:443:digiforensics.example.com" />
  <logFile logFormat="W3C" enabled="true">
    <directory>D:\IISLogs\W3SVC2</directory>
  </logFile>
</site>

对应的 IIS 6 写法是 <locsection name="W3SVC/1"> 内含 <property name="Root" type="string" value="C:\inetpub\wwwroot\digiforensics-site" /> 等(完整解析见 3.3)。

这三种属性的名字两代完全不通用。写解析脚本时不要试图用同一段逻辑兼容。

★ 站点 ID 的三个要点:

  1. 日志目录名 W3SVC2 里的 2 就是 id="2",与站点名无关——id="2" 完全可能叫 Finance。
  2. ID 由创建顺序决定,站点删过会留下 ID 空洞(1、3、4 存在而 2 不存在),这本身就是站点被删过的线索。
  3. logFile/directory 可配到任意路径,很多生产环境改到数据盘。不读配置就按 C:\inetpub\logs 找,会完全找不到日志。

2.2 ★ W3C 日志的文件结构:先读 #Fields

这是本篇最核心、也最容易出错的知识点。

W3C 日志是 ASCII 文本,一行一条 HTTP 请求记录,文件开头有几行以 # 开头的指令行。

#Software:(软件版本,判断 IIS 版本)、#Version:(格式版本,通常 1.0)、#Date:(判断是否被轮转/重建)、#Remark:(任意注释,应忽略),以及最关键的 #Fields:(字段名称与顺序,解析日志的唯一依据)。

#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2024-11-14 00:00:00
#Fields: date time s-computername s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken cs-bytes cs-host
2024-11-14 03:12:07 DIGIFORENSICS-WEB 10.10.0.5 GET /uploads/up.aspx t=1 443 - 185.220.101.34 - - 200 0 0 1178 66 digiforensics.example.com

★ 第一条硬规则:任何解析器都必须先读 #Fields 行,绝不能写死列索引。 IIS 管理器里勾选"选择字段"就能改集合,管理员、运维脚本、模板站点都可能改过。

2.3 ★★ IIS 7+ 的默认字段顺序(本文核心)

IIS 7 及以上全新安装的 W3C 日志,默认选中字段的顺序是:

#Fields: date time s-computername s-ip cs-method cs-uri-stem cs-uri-query s-port
         cs-username c-ip cs(User-Agent) cs(Referer)
         sc-status sc-substatus sc-win32-status time-taken cs-bytes cs-host

细节一:cs-username 之后直接是 c-ip,中间没有 cs-domain。(… s-port cs-username c-ip cs(User-Agent) …)

很多资料(尤其从 IIS 6 时代文档转述来的)会让人以为 cs-username 后面是 cs-domain(客户端域名),然后才是 c-ip。

事实是:W3C 格式的字段列表里没有 cs-domain 这个标准字段。

按 cs-username → cs-domain → c-ip 去解析,会把 c-ip 的值错认成域名,并且后续所有字段整体错位一列。

若日志里真出现 cs-domain,那一定是管理员配置的自定义字段,不是标准字段。

细节二:字段前缀的四段式含义。 s- = server(服务器侧)、c- = client(客户端侧)、cs- = client sent(客户端发给服务器的)、sc- = sent to client(服务器返回给客户端的)。括号形式 cs(User-Agent)、cs(Referer)、cs(Cookie) 同样遵循此规则。

细节三:IIS 6 的字段顺序不同。 IIS 6.0 默认字段与 IIS 7+ 基本一致但结尾无 cs-host;而在最简配置下(只勾必需项)会短得多:

#Fields: date time cs-username c-ip cs-method cs-uri-stem cs-uri-query sc-status sc-substatus sc-win32-status

绝不能跨版本套用解析器:IIS 6 最简配置里 sc-status 在索引 7,IIS 7+ 默认配置里在索引 14。同一解析器在两种日志上结果完全不同,且不报错。

2.4 关键字段的判读要点

字段 判读要点
date / time ★ 是 UTC,不是服务器本地时间。国内服务器常为 UTC+8,与事件日志比对会整体错 8 小时
cs-uri-stem / cs-uri-query 请求路径(不含查询串,部分版本把 / 记成 \)/ 查询串(不带前导 ?,无则 -)
cs-username 认证用户,匿名为 -。出现非常规账号是提权信号
c-ip 客户端 IP。在 CDN/反代后面这是代理 IP,不是真实来源
cs(User-Agent) 空格被替换为 +,非打印字符也替换为 +。还原时 + → 空格
sc-status HTTP 状态码(详见 03)
sc-substatus IIS 细分子状态。404.0=资源不存在,404.1=站点不存在,404.3=MIME 类型限制
sc-win32-status Windows 系统错误码,0 = 成功。2=找不到文件、3=找不到路径、5=拒绝访问
time-taken ★ 单位是毫秒,从收到第一个字节到最后发送完,含网络耗时
cs-bytes / sc-bytes 收/发字节数,默认不勾选,未开启时不存在这两列

time-taken 的两个坑:单位是毫秒不是秒(1841 = 1.8 秒);它包含网络耗时——大文件下载时反映的是客户端网速。判断应用性能应只看动态请求(.aspx/.ashx/API)。

2.5 自定义字段与 _x 后缀

管理员可添加自定义字段(Custom Field),从请求头/响应头/服务器变量取值,最常见用途是记录 X-Forwarded-For 以穿透 CDN。

配了之后 #Fields 末尾会多出 x-forwarded-for 一列。

事项 说明
文件名会变 配置自定义字段后,IIS 写出的新日志文件名加 _x 后缀(u_ex241114_x.log),老文件保持原样
追加在末尾 自定义字段追加在 #Fields 列表末尾,因此不会挤掉 cs-username→c-ip 的相对顺序
不配置就不存在 没配 cs(Cookie)、cs-host、cs-bytes 时,这些字段根本不在 #Fields 里
★ 同一文件内可能变更 IIS 日志有缓冲写入(flushByEntryCountW3CLog 按 64K 条目批量刷新)。若管理员当天改了配置,同一文件里会出现两行 #Fields,前后段顺序不同

★ 解析器必须每遇到一次 #Fields 就重新读取,不能只在文件开头读一次。 这正是本文解析器的核心设计。

2.6 日志轮转与 W3SVC<n> 目录

默认按天轮转,文件名 u_exYYMMDD.log(YYMMDD = 两位年 + 月 + 日):

项目 说明
目录名 C:\inetpub\logs\LogFiles\W3SVC<站点ID>\(可配到别处)
IIS 7+ 文件名 u_ex241114.log(配自定义字段时为 u_ex241114_x.log)
IIS 6 文件名 ex241114.log(无 u_ 前缀)
轮转粒度 Daily(默认)/ Hourly / Weekly / Monthly / MaxSize(按大小,默认 20 MB)
IIS 7.5+ 旧格式 单个滚动文件 u_ex.log(不按天分);部分部署另有 .gz 归档
IIS 6 默认路径 %systemroot%\system32\LogFiles\W3SVC<n>\

★ 取证必须用通配符 *.log* 而非 u_ex*.log:会漏掉 IIS 6 的 ex*.log、带 _x 的自定义字段文件、.gz 归档,以及被改到非默认目录的日志。

W3C 日志只覆盖 HTTP 请求这一条线。

站点被改配置、被停用、服务进程崩溃这些动作落在 evtx 里。

按事件 ID 抽取并与访问日志合并成同一条时间线的方法见事件日志深度分析。


三、操作步骤

3.1 第 1 步:确认 IIS 版本与配置格式

Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\InetStp\Components' | Select-Object MajorVersion, MinorVersion
Get-ChildItem C:\Windows\System32\inetsrv\config\applicationHost.config*,
             C:\Windows\System32\inetsrv\MetaBase.* | Select-Object FullName, Length, LastWriteTimeUtc
# 最可靠:直接从日志的 #Software 行确认(升级后残留旧日志时以它为准)
Get-Content D:\IISLogs\W3SVC2\u_ex241114.log -TotalCount 3

验证:#Software 版本与注册表 MajorVersion 不一致时(升级残留常见),以日志自身版本为准——解析行为由产生它的版本决定。

3.2 第 2 步:读 #Fields,建立字段映射

这一步是全部后续分析的前提。

# 2.1 每个日志文件的 #Fields(★ 同一文件可能有多行,配置变更会产生新的)
Get-ChildItem D:\IISLogs\W3SVC2\*.log | ForEach-Object {
  Select-String -Path $_.FullName -Pattern '^#Fields:' |
    Select-Object @{n='File';e={$_.Filename}}, LineNumber, Line } | Format-List

# 2.2 人工核对列数与关键字段索引
$fields = ((Select-String -Path $log -Pattern '^#Fields:').Line -replace '^#Fields:\s*','') -split '\s+'
$data   = (Get-Content $log | Where-Object { $_ -notmatch '^#' } | Select-Object -First 1)
"#Fields=$($fields.Count) 数据行=$(($data -split '\s+').Count) " +
"cs-username=$([array]::IndexOf($fields,'cs-username')) c-ip=$([array]::IndexOf($fields,'c-ip'))"

验证:数据列数必须等于 #Fields 列数。不等就是两条线索之一——日志被截断(见 04),或该行正好在字段配置变更的边界上。

3.3 第 3 步:还原站点拓扑

安全验证 当前请求需要先完成一次滑块验证。