一个 500 错误页泄露了源码、绝对路径和短信网关口令

2772 字
14 分钟
一个 500 错误页泄露了源码、绝对路径和短信网关口令

为什么盯上了一个”报错页”#

任务下来后,我对目标站点做的是常规的接口梳理:看有哪些业务页、哪些不需要登录、哪些明显是后期加上去的临时功能。

密码找回相关的页面在这类系统里通常是个薄弱点——它要调外部短信网关、要生成验证码、要写库,往往是外包补丁或者临时加的,代码质量和配置一致性都容易掉队。所以我先手工请求了一遍这几个页面,看它们的响应行为。

结果第一个页面就返回了 HTTP 500。

到此为止其实没什么稀奇的,500 太常见了。真正的问题是它返回了什么。

现象:一条 curl,一屏源码#

请求本身不能更简单,匿名、无 Cookie、无参数:

Terminal window
curl -sk -D hdr.txt -o body.html "https://<目标域名已脱敏>/PasswordService.aspx"
head -12 hdr.txt

响应头:

HTTP/1.1 500 Internal Server Error
Cache-Control: private
Content-Length: 38390
Content-Type: text/html; charset=gb2312
Server: Microsoft-IIS/7.5
X-XSS-Protection: 1
X-Content-Type-Options: nosniff
Content-Security-Policy: frame-ancestors 'self'
X-AspNet-Version: 0
X-Powered-By: WAF/2.0
Date: ...

正文不是自定义错误页,不是”系统繁忙”,而是 ASP.NET 原生的 Compilation Error 页面——业内俗称 YSOD(Yellow Screen of Death)。

关键片段(已把凭据与路径换成占位符,其余保持原文结构):

<title>Compilation Error</title>
...
<b> Compiler Error Message: </b>CS0246: The type or namespace name 'ChinaSms' could not be found
(are you missing a using directive or an assembly reference?)
<b>Source Error:</b>
Line 65: // 这里应该调用短信服务发送验证码
Line 66: // SendSms(phoneNumber, $"您的验证码是:{verificationCode}");
Line 67: ChinaSms sms = new ChinaSms("****", "****");
Line 69: bool re = sms.SingleSend("176****9396", "正在申请重置密码操作,您的验证码是:" + verificationCode + ",五分钟内有效");
<b>Source File:</b> e:\<站点目录>\PasswordService.aspx.cs
<b>Line:</b> 67

同一响应里还带着编译器命令行(csc.exe 的完整参数)、Temporary ASP.NET Files\root\<hash>\<hash>\... 临时程序集路径、已加载的第三方 DLL 清单、[HttpCompileException] 的 BuildManager 调用堆栈,以及一大段 ASP.NET 动态生成的构建源码(含数十处 #line N "E:\<站点目录>\PasswordService.aspx" 行指令)。

页面编码是 gb2312,这点后面会讲,它差点让我以为”啥也没泄露”。

泄露了什么#

按危害排序,一共四类。

一、硬编码的第三方短信网关账号口令

第 67 行把短信平台的账号和口令写成了字面量,第 69 行是真实调用的接收号码和短信模板。这意味着任何能访问这个 URL 的人,都拿到了一个可以直接调用第三方短信平台的身份。

这类风险和普通的”源码泄露”不在一个量级——源码泄露的损失是滞后的(要有人去挖下一个洞),而凭据泄露的损失是即时的、可以直接变现的。

二、服务器绝对路径与部署结构

e:\<站点目录>\ 这种盘符级路径,配合 Temporary ASP.NET Files\root\<hash>\<hash>\ 里的编译缓存目录,对攻击者的价值不在于”信息泄露”本身,而在于它是下一步攻击的坐标:

  • 任意文件读取类漏洞需要知道读哪里;
  • 文件上传类漏洞需要知道落点是否在 Web 根目录内;
  • 路径穿越需要知道目录层级。

很多人觉得路径泄露是”低危”,那是因为把它当成了独立漏洞。放在攻击链里,它是放大器。

三、第三方组件清单

编译器参数把 /R: 引用的程序集全列了出来,等于免费给了一份精确的技术栈清单:

MySql.Data.dll
AjaxPro.2.dll
DTcms.BLL.dll
Gnete.OpenVendorV34.dll
Spire.XLS.dll
CrystalDecisions.Shared.dll(13.0.2000.0)
Interop.CRQUERYENGINE.dll
Microsoft.mshtml.dll

其中 AjaxPro.2 是早已停止维护的老组件,历史版本上有过反序列化和未授权方法调用的风险;CrystalDecisions、Spire.XLS 这类老报表/Excel 组件也长期是解析类漏洞的重灾区。

正常情况要摸清一个站用了什么组件,得靠指纹识别去猜、去试。这里人家主动列了个清单,连版本号都给了(CrystalDecisions 13.0.2000.0、System.WorkflowServices v4.0_4.0.0.0),后续针对性验证的成本直接被压到很低。

四、框架版本与页面结构

页尾的 Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.34209,加上那几十处 #line 行指令还原出来的页面结构,足够支撑”针对这个框架版本、针对这个页面的控件布局”去构造载荷。

顺带说明一点:这个响应里没有泄露数据库连接串。我特意在本地证据里检索过 connectionString / Password= / User ID 等关键词,均为 0 命中。所以危害评估要实事求是——是凭据泄露 + 全量技术栈暴露,不是”数据库被拖”。

为什么会这样#

四个原因,缺一个这个洞都不成立。

1. customErrors 没对远程请求生效

生产环境的 web.config 里,<customErrors> 要么是 Off,要么根本没配。ASP.NET 的默认行为是把 HttpCompileException 的完整细节渲染成 YSOD 直接返回给客户端,只有在 customErrors mode="RemoteOnly" 时才会对非本机请求隐藏。

同时 <compilation debug="true"> 也没关。debug 模式会让错误页更啰嗦,还会带来额外的性能与信息暴露问题。

2. 生产站点带着编译错误在上线

CS0246: The type or namespace name 'ChinaSms' could not be found —— 这说明该页面的代码引用了 ChinaSms 这个类型,但所在项目的程序集引用或 using 缺失,页面在运行时编译阶段就挂了。

这一点很关键:错误是稳定复现的,不是偶发故障。同一个 URL 请求两次,两次都返回 500,响应长度分别是 38629 和 38390 字节(内容一致,长度差异来自日期等动态字段)。也就是说,这个页面从上线那天起就对所有人敞着,只是没人去看。

3. 凭据硬编码

短信账号、口令、测试号码、短信模板,全部以字面量写在业务代码里。这种写法在临时补丁和外包交付的代码里非常常见,因为”先跑通再说”。它的风险是双份的:源码一泄露凭据就泄露;而且凭据轮换要改代码重新发布,运维往往懒得换,于是一个泄露能存活好几年。

4. 敏感功能页直接暴露在公网

密码找回属于身份认证链路的一环,这类页面本该有访问控制、频控、来源校验。而这个页面是匿名可访问的裸奔状态。

修复建议#

按优先级排,前三条是 24–48 小时内必须做的。

立即处置

  1. 关掉详细错误。改 web.config:
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="/error.aspx" />
<compilation debug="false" targetFramework="4.0" />
<httpRuntime enableVersionHeader="false" />
</system.web>
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="500" />
<error statusCode="500" path="/error.aspx" responseMode="ExecuteURL" />
</httpErrors>
</system.webServer>

改完必须从外部网络验证一遍,本机访问看到详细错误是正常的(RemoteOnly 就是这么设计的),容易误判成”没生效”。

  1. 修掉编译错误。补齐 ChinaSms 所在的程序集引用和 using,或者回滚到可编译的版本。同时确认这个密码找回功能当前是不是一直处于故障状态——如果是,那说明业务早就坏了,只是没人报障。

  2. 轮换凭据。作废并更换短信网关账号口令;在服务商侧加来源 IP 白名单、签名校验、发送频率和额度上限;核查历史发送日志和账单,确认没有异常调用。这一条比改配置更重要,因为配置改的是未来,日志查的是过去。

中期加固

  1. 凭据迁出源码。用 aspnet_regiis -pe 加密配置节,或者接密钥管理服务;代码里只留配置项引用。顺手删掉调试用的测试号码。

  2. 统一错误处理。在 Application_Error 里把完整异常写服务端日志,只给客户端返回通用错误码 + 追踪 ID。

  3. 组件治理。盘一遍上面那份清单的真实版本,AjaxPro.2 这类停止维护的组件评估替换,同时核查页面上暴露的 [AjaxMethod] 面。

  4. 预编译部署。用 aspnet_compiler 预编译替代运行时编译,从根上消掉 Temporary ASP.NET Files 相关的路径泄露。

  5. 把”错误页”纳入上线基线。发布前对全站 .aspx 抽样请求,断言:不返回 YSOD、不回显堆栈、不回显绝对路径、不暴露组件清单。这个检查用一条 curl 加几个 grep 就能自动化,成本极低,收益很高。

踩坑与认知纠错#

复盘里最有价值的部分往往不是”我怎么打进去的”,而是”我一开始想错了什么”。

错判一:看到 X-Powered-By: WAF/2.0,以为站点有防护,错误页会被拦。

事实是 WAF 在这条路径上完全没起作用。WAF 的规则库通常针对注入、上传、命令执行这类”攻击特征”,而”访问一个正常的业务页面”不构成任何特征。有 WAF ≠ 有安全,WAF 管的是”看起来像攻击的请求”,不管”服务器主动把秘密说出来”。

错判二:第一次 grep 什么都没抓到,以为响应是空的。

页面 charset=gb2312,直接用 UTF-8 读会让中文变成乱码,而关键行(Source Error 段落里的中文注释和短信模板)恰好中英混排,正则匹配容易整段失效。正确做法是按 gb18030 解码并先做 HTML 实体反转义:

import re, html
h = open('body.html', 'rb').read().decode('gb18030', 'ignore') # 页面 charset=gb2312
t = html.unescape(h) # 源码里的引号是 &#39; 实体
for pat in [r'Compiler Error Message:.*', r'Source File:.{0,80}',
r'Line \d+:.*ChinaSms.*', r'Line \d+:.*SingleSend.*',
r'Version Information[\s\S]{0,120}']:
m = re.search(pat, t)
print(m.group(0) if m else 'NOT FOUND')

html.unescape 这一步不能省。源码行里的引号在 HTML 里是 &#39;,不反转义的话,想匹配 new ChinaSms("..." 这种带引号的模式永远匹配不上。

错判三:以为 500 意味着”功能不可用,所以不算漏洞”。

这个页面确实不可用,但它能正常返回内容。漏洞判定看的是”未授权者能拿到什么”,不是”业务能不能跑”。“功能坏了”和”坏的时候把秘密说出来了”是两件事,后者才是要报的那个。

方法论上的一条:稳定复现是定级的前提。

第一次看到 500 时我怀疑是偶发(比如刚好在发布、或者连接池打满)。所以隔了一会儿又请求了一次,两次响应结构完全一致,才敢下”配置缺陷、长期暴露”的结论。如果只抓一次就写报告,很可能被运维一句”那是临时故障”顶回来。

收尾#

整件事的教训其实很朴素:ASP.NET 的详细错误页是为开发者准备的,它就不该出现在生产环境的公网上。 一个 customErrors 配置项,加上”发布前从外网看一眼”,就能把这条链断掉。


文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
52
分类
20
标签
32
总字数
311,598
运行时长
0 天
最后活动
0 天前

文章目录