从波浪号~1到资产清单:IIS短文件名扫描与8.3短名枚举实战
简介IIS_shortname_Scanner.rar 是一份面向 Web 安全初学者、渗透测试人员及运维人员的 IIS 短文件名泄露检测工具包主要用于验证目标站点是否存在 IIS 8.3 短文件名枚举风险并配套提供了 Python 2.7 64 位环境包的下载入口。包内共 32 个文件以 class、java、xml、md、py、jar 及脚本类文件为主兼顾 Java 与 Python 两套实现既有可直接运行的扫描程序也保留源码、配置与构建说明压缩包约 87KB体积轻便、便于在本地快速部署和二次研究。现有 250 人学习下载适合在授权测试环境中对照使用读者可借助 Java 版快速执行检测或通过 Python 脚本理解短文件名探测逻辑并结合说明文档完成环境准备、参数调整与结果核验为漏洞排查和加固验证提供可复用的工具与排错参考。1. 一个波浪号暴露整份目录IIS 短文件名扫描到底在扫什么iis短文件名扫描不是什么新手法但每次做内网资产梳理它仍然是我第一批运行的检查项。一个开着 8.3 别名的 IIS 站点请求里带上~1就可能把没写进 robots.txt、首页也没有挂链接的备份文件、上传目录、测试页面一个个问出来——攻击面图上本来空白的一块忽然有了轮廓。这个扫描包干的就是这件事给它一个 URL 或一串 URL它按 6 位前缀、波浪号编号、扩展名逐段试探把看不见的名字还原成字符串。适合两类人做授权测试、需要快速摸清目录结构的从业者以及自己维护 IIS、想确认短名别名到底关没关的运维。前提只有一个——目标是你自己的或者你手里有书面授权。2. 8.3 短名是怎么生成的波浪号后面的数字不是随机的想把扫描结果读明白得先知道那些AAAAAA~1是怎么来的。很多人拿到输出第一反应是「这堆乱码是什么」实际上每一位都有生成规则理解规则之后你能从结果里反推出目录的创建顺序、扩展名长度甚至判断某个命中是不是误报。2.1 长名截断成 NAME~1.EXT 的推导过程NTFS 为了兼容早期只认 8.3 格式的程序会为不符合 8.3 规范的长文件名额外分配一个别名。规则大致是这样取长文件名去掉空格和非法字符后的前 6 个有效字符作为主名加一个~再加序号从 1 开始扩展名取最后一段的前 3 个字符。举几个常见例子长名生成的短名说明BackupFilesBACKUP~1无扩展名主名取前 6 位database_2023.bakDATABA~1.BAK主名截断扩展名只留 3 位web.config.bakWEBCON~1.BAK中间的点被吃掉扩展名取最后一段uploadupload本身已符合 8.3不生成别名关键在序号~后面的数字不是哈希也不是随机值而是同一目录内「前缀冲突」的排队号。目录里如果已经有一个BACKUP~1新建的同前缀文件就是BACKUP~2以此类推。这意味着两点第一序号通常很小1 到 9 覆盖了绝大多数场景扫描器一般枚举到 9 就够第二序号反映的是创建先后不是字母序所以你不能靠~1、~2推断文件的排列。非 ASCII 文件名会让这套规则退化。中文名每个字符占两个字节截断后前缀可能只保留两三个汉字扫描器用 ASCII 字符集去猜就基本命中不了。这也是为什么同一个站点英文命名习惯的目录往往能扫出一大片中文命名的目录却干干净净——不是没文件是猜不出来。2.2 哪些目录天生就没有别名这条比规则本身更重要直接决定你扫描结果的解读方式。有几种情况文件明明存在短名却压根不存在站点根目录通常不生成 8.3 别名。IIS 的根路径对应的是卷上的一个目录但通过 Web 根访问时很多实现不会为根本身分配别名所以扫描必须从子路径开始。我见过有人直接把 URL 填成http://host/跑完一片空白就断定目标不存在短名问题——实际是起点选错了换成http://host/upload/立刻有反应。本身就是 8.3 格式的名字不会再生成别名。img、css、js、a.aspx、b.aspx这类名字天生合规NTFS 不会为它额外分配短名扫描器自然问不出来。所以扫描结果里缺失的项不代表文件不存在只是「这个名字不需要别名」。8dot3 生成开关可以被关闭。卷级别可以关掉短名生成关掉之后新建的文件就不再产生别名。但注意关闭不影响历史别名——之前已经生成的~1依然留在目录索引里依然可以被问出来。这是运维侧最容易误判的一点以为关了就干净了实际只是不再新增。系统目录自带一堆别名。_vti_开头的前端扩展目录、bin、App_Data、aspnet_client这些在很多 IIS 部署里默认存在扫描结果里出现它们是常态别当成战果往报告里写。2.3 手工发两个请求把机制验证清楚在跑工具之前我习惯先用最笨的方式确认机制是活的。原理不复杂往路径里塞一个非法的通配字符*IIS 在判定「这个 URL 不合法」之前会先拿它去匹配 8.3 名。名字存在就会走到「非法字符」那一支名字不存在直接 404。返回码的差异就是信号。# 目标换成你自己的靶机或授权范围内的地址 HOSThttp://10.0.0.5:8080 # 试探是否存在以 backup 开头的 8.3 名 curl -s -o /dev/null -w %{http_code}\n $HOST/backup*~1*/.aspx # 对照随便编一个大概率不存在的前缀 curl -s -o /dev/null -w %{http_code}\n $HOST/zzzzzz*~1*/.aspx # 顺手确认服务器类型和版本特征 curl -sI $HOST/ | grep -Ei server|x-powered-by|x-aspnet-version-o /dev/null丢掉响应体-w %{http_code}只打印状态码-s静默进度条。三条命令分别回答三个问题短名机制是否生效、对照组是否确实返回 404、目标是不是 IIS 以及大致版本。读返回码要有点耐心不同版本的 IIS 表现不完全一致。常见对照如下返回码含义备注400短名存在通配字符被拒之前先完成了名字解析404短名不存在最常见也是主要判据403存在但无权访问同样是命中信号别当失败丢掉200名字存在且请求被当真实路径处理少见要结合响应体长度判断500解析异常多半是 URL 编码或路径拼接写错了提示手工验证只发两三个请求成本极低但能省掉后面一大截「工具跑完没结果到底是谁的问题」的排查时间。我现在的习惯是每换一个目标域名先跑这三条。3. Scanner 上手跑通第一次枚举确认机制活着再谈工具。这一章按「环境 → 参数 → 跑一次 → 收结果」的顺序走每一步都能直接照抄。3.1 运行环境和前置检查先在本地把靶场搭起来别拿线上环境练手。Windows 上开 IIS、建一个测试站点、往目录里丢几个长名文件比如backup_2023.zip、config_old.bak确认 8dot3 没被关掉这是最省事的验证环境。跑通之后再对着授权目标复现。工具侧按包内入口分两种情况如果是编译好的可执行程序Windows 上直接双击或命令行带参数跑注意某些安全软件会拦截这类行为测试机上建议先加白名单如果是脚本形态先建虚拟环境再装依赖别往全局 Python 里灌包。# 脚本形态的通用起手式 python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install requests urllib3 # 先看帮助把参数拼写和版本确认清楚 python scanner.py --help虚拟环境的作用是隔离依赖版本。这类工具往往依赖requests的某个区间版本全局环境里如果已经装了别的版本很容易出现ImportError或者 TLS 握手异常排查起来很费时间。--help一定要先跑不同打包版本的参数拼写差异不小下面表格里的写法只是常见形态。注意只在你自己的靶机或有书面授权的目标上执行。扫描行为会在对方 IIS 日志里留下成片的 400特征非常明显别抱侥幸心理。3.2 参数怎么设一张表看懂常用开关参数常见写法作用建议值-u/--url单个目标地址带 scheme 和端口末尾不要漏斜杠-f/--file批量目标清单一行一个先去掉重复和不可达的-t/--threads并发线程数816内网可到 20公网别超 10--timeout单请求超时秒5跨机房链路给到 8-o/--output结果落盘统一.txt方便后续 grep-e/--ext扩展名字典先跑asp,aspx,zip,rar,bak,sql,config-r/--recursive是否递归子目录授权范围内再开容易放大请求量--level前缀枚举深度默认 6短名前缀超过 6 位没有意义线程数这一项值得单独说。这类扫描不是带宽密集型是延迟密集型瓶颈在 RTT 不在吞吐。线程开太高目标端的请求队列堆积返回码开始出现 500 和超时误判率立刻上去。我一般从 8 起步观察一轮的状态码分布再往上加看到 500 变多就立刻回调。3.3 跑一次完整枚举# 单目标先跑最小集合确认链路和判定都正常 python scanner.py -u http://10.0.0.5:8080/ -t 8 --timeout 5 -e aspx,zip,bak -o run1.txt # 看到结果之后再扩扩展名和深度 python scanner.py -u http://10.0.0.5:8080/ -t 12 --timeout 5 \ -e aspx,asp,zip,rar,bak,sql,config,old -r --level 6 -o run2.txt # 批量目标先去重、去不可达再喂给工具 sort -u targets.txt | while read -r h; do code$(curl -s -o /dev/null -m 3 -w %{http_code} $h) [ $code ! 000 ] echo $h done targets.alive.txt第一轮的作用是校准不是出成果。扩展名只放三个最常见观察命中率、平均响应时间、有没有 500 冒出来。第二轮才放大范围-r开递归的时候要盯着请求量一个中等规模的站点递归下去几万个请求很正常。最后的批量预处理容易被跳过但很值。curl -w %{http_code}返回000表示连接失败这类目标扔给扫描器只会白白消耗线程和时间先过滤掉。sort -u去重是因为资产清单里同一个域名经常出现多行不同端口、不同来源导出重复跑毫无意义。3.4 把输出接进后续流程扫描结果一般长这样http://host:8080/BACKUP~1.ZIP。直接交货没意义得做二次加工——去掉系统目录、合并大小写重复项、按扩展名分类。# 去掉常见的系统目录噪音合并大小写重复 grep -viE _vti_|aspnet_client|/bin/|/app_data/ run2.txt \ | tr A-Z a-z | sort -u run2.clean.txt # 按扩展名分组方便决定下一步验证哪个 awk -F. {print $NF} run2.clean.txt | sort | uniq -c | sort -rn # 只留高价值扩展名进验证清单 grep -iE \.(zip|rar|bak|sql|config|old)$ run2.clean.txt verify.listgrep -viE里的-v是反选-i忽略大小写这几个系统目录在几乎所有 IIS 站点都会命中留着只会稀释注意力。tr A-Z a-z是为合并大小写重复——同一个名字可能因为响应差异被记录成两种形式。awk -F. {print $NF}取最后一段当作扩展名uniq -c数频次一眼就能看出这个站点的备份习惯如果.zip和.bak占了多数说明运维有定时备份习惯后面重点验证这几类。4. 枚举算法拆解一个字符一个字符地问出来工具用起来简单但结果准不准全看判定逻辑和收敛策略。这一章把黑匣子拆开讲清楚它每一轮在问什么、为什么这么问。4.1 前缀枚举为什么按位递进而不是一次全猜短名前缀最多 6 位字符集如果取 36 个小写字母加数字6 位的组合数是 36 的 6 次方二十多亿。全量爆破不可能所以所有实现都用同一个思路逐位递进。先测 1 位a*~1*、b*~1*……留下命中的再在命中的前缀后面接一位继续测直到 6 位或者某一轮全灭。import requests from concurrent.futures import ThreadPoolExecutor BASE http://10.0.0.5:8080/ CHARS abcdefghijklmnopqrstuvwxyz0123456789_- HEADERS {User-Agent: Mozilla/5.0, Accept: */*} def probe(prefix: str, timeout: float 5.0) - bool: # * 必须原样出现在路径里这是判定成立的前提 url f{BASE}{prefix}*~1*/.aspx try: r requests.get(url, headersHEADERS, timeouttimeout, allow_redirectsFalse) except requests.RequestException: return False # 网络问题一律当未命中避免污染结果 return r.status_code in (400, 403) def enum_prefix(length: int 6, workers: int 16): alive [] # 上一轮命中的前缀集合 for _ in range(length): cands [p c for p in alive for c in CHARS] with ThreadPoolExecutor(max_workersworkers) as pool: flags list(pool.map(probe, cands)) alive [c for c, ok in zip(cands, flags) if ok] print(fdepth{len(cands[0])} candidates{len(cands)} hits{len(alive)}) if not alive: # 全灭就停省掉后面几万次无意义请求 break return aliveallow_redirectsFalse是刻意的跟随跳转会把 301 隐藏掉而 301 有时恰恰说明短名映射到了真实目录。ThreadPoolExecutor的workers取 16 是经验值内网延迟 12ms 时可以拉到 24跨机房就降到 8。probe里把异常直接判为未命中看起来粗暴但能避免网络抖动把结果带偏——代价是漏报收益是结果可复现。真要提高召回可以加一次重试但别超过一次。这个方案的请求量是这样第一轮 38 次之后每一轮的量取决于上一轮命中数。如果站点命中的前缀有 5 个第二轮就是 190 次六轮下来几千次请求比全量爆破省了六个数量级。4.2 编号和扩展名的判定顺序前缀确定之后接下来是两件事验证~N的编号以及猜扩展名。编号验证相对确定从~1测到~9就够。之所以上限是 9是因为同一目录同前缀的文件很少超过十个超过十个的目录多半是上传目录那类场景靠编号枚举反而不如直接看上传接口的命名规律。扩展名要麻烦一些判定模板换成PREFIX~1.EXT的形态。常见扩展名有上百个全量测代价太大实际做法是分两档第一档是高频的十来个aspx、asp、zip、rar、bak、sql、config、old、txt、xml第二档才按站点技术栈补.NET 站点补dll、pdbPHP 站点补php、inc。分档的依据是响应时间第一档跑完如果命中率是零第二档基本不用试说明这个目录下要么没有别名要么扩展名全在 3 位以内。还有一种情况得单独处理短名没有扩展名。有些备份文件就叫backup没有后缀这时候~1后面不跟点形如/BACKUP~1。扫描器如果不测这种形态就会漏掉一批。4.3 并发、超时和重试的取舍这三个参数是绑在一起的单独调一个没意义。超时设 5 秒、并发设 8意味着最坏情况下每秒只有 1.6 个请求能返回一批六千个请求要跑一小时。超时降到 2 秒、并发拉到 20同样的量十分钟跑完但超时误判会上升——本来该 400 的请求因为响应慢了被判成未命中。我的做法是分段先用timeout3、workers8跑一小批比如两百个候选统计超时率和 500 率。超时率低于 1%就放宽到workers16、timeout5超时率高于 5%说明目标端扛不住或者链路有问题先降并发再加超时。这个标定过程听着麻烦实际跑两次就有数了比盲目拉参数省时间。重试要慎用。一次重试能把偶发丢包救回来但也会把误判放大——如果目标端对非法字符的处理本身不一致有些 WAF 会随机返回 400重试反而会造出假命中。我一般只在ConnectionError上重试状态码类的问题不重试宁可漏。4.4 误报抑制几种会把 400 当成命中的情况400 是主要判据但不是所有 400 都是短名命中。以下几种情况会污染结果WAF 拦截。很多 WAF 看到路径里有*或~直接返回 400和短名无关。识别方法是看响应体——WAF 的 400 通常带一个明显的拦截页面长度远大于 IIS 的默认错误页。加了拦截特征之后命中数会掉一截但剩下的都是真的。路径规范化。有的中间件会把*编码掉或者直接拒绝返回 400 但和短名毫无关系。识别方法是拿一个绝对不可能存在的前缀做对照如果对照也 400说明这一路全是噪音整个目标可以直接放弃。URL 编码差异。%2A和*在不同版本上表现不同扫描器如果做了编码转换判定基准就变了。稳妥做法是固定用未编码的*别自作聪明。提示正式输出之前随机抽 5 个命中项手工复验一遍。命中率高不代表准我吃过一次亏——报告里八十多条结果手工一验三分之一是 WAF 拦出来的假阳性。5. 避坑与排查五类翻车现场下面这些是跑这类工具时最常撞上的按「现象 → 原因 → 解决」记遇到时直接对号入座。5.1 判定类问题现象整站零命中手工发请求却是 400。原因通常是扫描器请求的 URL 模板和你手工测的不一样——工具可能在*后面自动补了扩展名或者把路径末尾的斜杠吃掉了导致 IIS 走的是另一条解析分支。解决方法是抓一次工具的原始请求--debug或者本地起个抓包看逐字符和你手工那条对比把模板对齐。这类问题我遇到三次两次都是模板末尾少了斜杠。现象命中一大堆全是_VTI_、BIN、ASPNET_CLIENT。原因不是误报是这些系统目录确实有别名而且前缀固定、必然命中。解决靠过滤grep -viE一次性排掉别指望工具自己识别——它不知道你的业务上下文判断不了哪些目录值得关注。过滤规则建议固化成一个脚本每次跑完自动执行。现象报告交上去被反馈「这个文件不存在」。原因是短名映射到了真实文件但真实文件的权限、处理程序映射不一样直接访问短名可能 404 或 403。短名存在只证明目录索引里有这条记录不证明文件内容可读。解决办法是在报告里把「短名存在」和「文件可访问」分开写别混成一件事前者是信息泄露后者才是文件泄露。5.2 环境与流程类问题现象靶场上跑得好好的换到真实目标全部超时。原因多半是出站限制或者目标前有负载均衡扫描请求被分散到不同节点有的节点响应快有的慢超时设得紧就大面积失败。解决方法是先跑 20 个候选做标定看 P95 响应时间把超时设到 P95 的两倍以上并发相应降下来。现象同一份清单跑两次结果不一样。原因是并发下的时序问题——目标端在高压下返回码不稳定同一路径这次 400 下次 404。解决办法是把并发降到 8 以下重跑一遍取两次结果的交集作为可信集。交集之外的项单独列出来标注「待复验」别直接写进结论。注意扫描结论一旦进了正式报告就要经得起复现。凡是跑两次结果不一致的项宁可不写。6. 进阶把~1变成可核对的清单拿到短名只是起点能不能变成可核对的结论靠的是后面这几步。验证的第一步是拿短名反查真实名。请求http://host/UPLOAD~1/时留意响应如果返回 301Location 里往往带着真实目录名这一步的收益极高——UPLOAD~1直接变成uploads_2023比猜一百次字符都准。# 拿短名反查真实名重点看 Location 头和状态码 curl -sI http://10.0.0.5:8080/UPLOAD~1/ | grep -Ei ^(HTTP|location) # 短名直接访问文件看是否 200 且长度合理 curl -s -o /dev/null -w %{http_code} %{size_download}\n \ http://10.0.0.5:8080/BACKUP~1.ZIP第二步是做交叉印证。短名枚举出来的扩展名和站点技术栈对不上的要打问号一个纯静态站点扫出.aspx多半是历史遗留或者误判扫描器报~1但编号跳到了~5说明中间几个编号的文件已被删除这个目录有过清理动作值得单独看一眼。第三步是把结果整理成能过复核的格式。我习惯用三列短名、推断的真实名、验证状态。推断真实名靠三样东西——短名前缀 6 位、扩展名、以及同目录下其他文件的命名规律比如都带日期后缀。验证状态只有三个值已确认短名反查到了真实名、部分确认文件可访问但真实名未知、未验证。短名推断真实名验证状态BACKUP~1.ZIPbackup_2023.zip已确认UPLOAD~1uploads_old已确认CONFIG~2.BAK—部分确认最后说一个我踩过的坑。早期我特别喜欢追命中数量一个站扫出两百条就很有成就感报告里堆得满满当当。后来被反馈问了一次「这些路径哪些是真能访问的」才发现两百条里能落地的不到二十条——大量的系统目录噪音、WAF 假阳性、以及早就被删掉只剩索引记录的空壳。从那以后我每次跑完都强制走一遍「过滤 → 抽样手工复验 → 只留可核对项」这三步宁可结果清单短一点也不要一份自己都不敢复现的报告。这套动作现在固定在脚本里扫完自动执行。希望帮到你。本文还有配套的精品资源点击获取