前言:
这篇文章的灵感来源于一次安全测试的复盘。
当时评估一套医院电子病历系统,本来只想验证自己的就诊记录能不能正常读取。
结果发现,只要改一下链接里的数字,就能看到别的患者的病历。
顺着这条线一路挖下去,从水平越权、垂直提权,到 JWT 防线、目录穿越,最后落到文件名枚举,把一套完整的越权与文件下载攻防链路完整走了一遍。
这篇文章就是这次评估的完整记录。
文中所有代码仅用于安全研究与防御学习,严禁用于任何非法用途。
个人主页:艺杯羹
1. 水平越权:改一个数字看遍全院病历
1.1. 越权是怎么发生的:链接里的那段数字
登录医院电子病历系统,点击查看自己的就诊记录。
页面正常加载,屏幕上出现了自己的检查和用药信息。
但细心观察会发现,当前病历所在网页的链接里,藏着一串数字。
这串数字不是别的,正是当前患者的 ID。
1.2. 篡改 ID:把 19 改成 18
既然链接里带着 ID,那把这个数字改掉会怎样?
把 19 改成 18,回车,屏幕上瞬间出现了另一位患者的病历。
图片说明:患者 ID 直接暴露在链接中,篡改 ID 即可读取其他同权限患者的病历。
再改成 17、16、15……一路点下去,几乎能翻遍整家医院所有普通患者的就诊记录。
这种攻击,就叫做水平越权。
1.3. 为什么叫"水平":只能看到同权限的
水平越权之所以叫"水平",是因为它只能横向触达与自己权限相同的数据。
普通患者只能看到其他普通患者的病历。
而权限更高的人,比如管理员、科室主任,普通患者是看不到的。
对比项 |
可见范围 |
正常访问 |
仅本人病历 |
水平越权 |
所有同权限患者的病历 |
越权边界 |
无法触达更高权限的数据 |
也就是说,水平越权只能横向扩散,无法向上突破。
2. 垂直越权:把角色字段改成 admin
2.1. 登录抓包:发现请求里的角色字段
水平越权只能看普通患者,权限更高的数据依旧无法触及。
于是回到登录界面,重新输入用户名和密码。
在点击登录的瞬间抓包,发现登录请求里不仅带着用户名和密码。
还额外携带了一个角色字段:role=user。
2.2. 篡改角色:把 user 改成 admin
把请求里的 user 改成 admin,重新发包。
登录成功后,系统管理员的管理界面直接出现在眼前。
这一次,连权限最高的数据也尽收眼底。
图片说明:登录请求中的角色字段被篡改,从普通用户提权为管理员,突破权限等级边界。
这种从低权限直接提升到高权限的攻击,就叫做垂直越权。
2.3. 水平越权与垂直越权的区别
对比维度 |
水平越权 |
垂直越权 |
攻击方向 |
横向(同级) |
纵向(跨级) |
篡改对象 |
数据 ID |
角色 / 权限字段 |
触达范围 |
同权限数据 |
更高权限数据 |
危害程度 |
中 |
高 |
水平越权是"看别人",垂直越权是"当领导"。
一次是横向扩散,一次是纵向突破。
3. 越权的本质:一个不可调和的悖论
两种越权攻击看似手法不同,背后却指向同一个根因。
这个根因,本质上是一个不可调和的悖论。
3.1. 悖论的两端:必须发 ID,又怕被改 ID
一方面,用户在执行操作时,必须把自己的 ID 发送给服务器。
服务器需要在后台解析用户身份,才能据此授予对应的操作权限。
另一方面,一旦用户有权限把自己的 ID 发给服务器,他就能把这个 ID 篡改成别人的。
发送 ID 与篡改 ID,在同一个请求里完成了闭环。
3.2. 悖论的核心:身份凭据不可信
悖论面向 |
具体表现 |
必须发 ID |
服务器需要身份才能鉴权 |
能发就能改 |
发出去的 ID 可能被篡改 |
结果 |
越权漏洞天然存在 |
问题不在用户,而在"用户提交的身份信息本身不可信"。
只要服务器完全信任客户端提交的身份字段,越权就永远防不住。
4. JWT 防线:给身份加一道不可篡改的签名
4.1. 破解悖论:让身份信息"改不动"
要解决悖论,关键在于让用户没法篡改身份信息。
登录时,服务器不再依赖客户端提交的角色字段。
而是直接根据用户名,在后台数据库自动匹配用户角色。
匹配成功后,服务端把用户 ID、用户角色等信息签名成一个 token 返回给用户。
用户之后的每一次请求,都必须携带这个 token。
4.2. JWT 的三段结构:签名就是护城河
这个 token 就是JWT(JSON Web Token),目前最常见的防越权手段之一。
JWT 由三段组成,中间用点号分隔:
Header.Payload.Signature
JWT 分段 |
内容 |
作用 |
Header |
算法类型(如 HS256) |
声明签名算法 |
Payload |
用户 ID、角色、过期时间 |
承载用户身份信息 |
Signature |
用密钥对前两段签名 |
防篡改核心 |
图片说明:用户 ID 与角色被签名进 JWT,缺少服务端密钥的用户无法篡改身份信息。
4.3. 为什么无法篡改:没有密钥就签不了名
由于 JWT 是加密的,且用户无法获得对应的密钥。
所以用户一旦篡改了用户 ID 或角色,签名就会立刻失效。
服务端校验签名失败,直接拒绝请求。
import hmac import hashlib import base64 SECRET = "server-side-secret-key" # 密钥只保存在服务端 def verify_token(token: str) -> bool: """ 校验 JWT 签名是否有效 任何对 Header/Payload 的篡改都会导致签名不匹配 """ header, payload, signature = token.split(".") expected = base64.urlsafe_b64encode( hmac.new(SECRET.encode(), f"{header}.{payload}".encode(), hashlib.sha256).digest() ).decode().rstrip("=") return hmac.compare_digest(expected, signature) # 篡改 Payload 里的角色后,签名必然校验失败 token = "header.eyJyb2xlIjoiYWRtaW4ifQ.fake-signature" print("签名有效" if verify_token(token) else "篡改被拒绝")
用户改不了 ID,也改不了角色,越权这条路被从根上堵死。
5. 目录穿越:下载文件时偷走系统文件
JWT 守住了身份这一关,但攻击者并没有放弃。
在医院系统里,还有一个功能引起了注意:下载病历报告。
5.1. 下载功能的文件名参数
每次点击下载按钮,浏览器都会跳出一个新的链接。
链接的末尾往往跟着要下载的文件名。
比如:download?file=202608_10019_report.pdf。
5.2. 把文件名换成系统文件
看到文件名跟着链接走,攻击者灵机一动。
如果把文件名替换成系统文件会怎么样?
无论是在 Windows 还是 Linux 系统里,都有一些系统文件存在于固定路径下。
比如 /etc 目录下的 passwd 文件,保存着 Linux 系统的用户密码信息。
把链接里的文件名直接替换成 /etc/passwd,回车。
图片说明:文件名参数未校验,被替换为系统路径后,直接下载了服务器上最敏感的隐私文件。
系统里重要的隐私文件,就这样被下载了下来。
这种通过文件名穿越目录、访问任意文件的技术,就叫做目录穿越。
6. 第一轮修复:过滤特殊字符与限定目录
工程师紧急修复了这个漏洞。
6.1. 过滤掉特殊字符
第一招,不允许文件名里传递斜杠、点之类的特殊字符。
/、\、.. 这些路径穿越的"武器"统统被拦截。
6.2. 限制只能访问网站内部目录
第二招,限制网站应用只能访问网站内部的文件和目录。
即使构造出路径,也无法跳出应用目录去读取系统文件。
修复手段 |
说明 |
过滤特殊字符 |
拦截 |
限定访问目录 |
只能读取应用内部文件 |
校验文件类型 |
只允许指定扩展名 |
但攻击者立刻发现,这套修复存在一个漏洞。
7. 第二轮博弈:文件名规律照样能枚举
7.1. 发现文件命名规律
攻击者注意到,病历报告的文件名存在明显规律。
文件名由**月份 + 工号(患者 ID)**组成。
比如 202608_10019_report.pdf,中间的 10019 就是患者 ID。
7.2. 替换工号:依旧能下载他人文件
既然文件名有规律,那只需要把自己的工号替换成别人的工号。
依旧可以下载获得这个文件。
图片说明:过滤特殊字符后,攻击者又利用"月份+工号"的命名规律枚举下载他人文件;最终随机重命名才彻底封堵。
目录穿越虽然被堵住了,但文件却依然能被"猜"出来。
工程师这下没招了。
7.3. 终极修复:随机字符串重命名
工程师不得不使用系统生成的随机字符串来重新命名文件。
文件名变成了没有规律的一串乱码,无法再被推测。
这样一来,攻击者再也无法获得其他人的病历文件了。
防御阶段 |
文件命名 |
能否枚举 |
修复前 |
月份 + 工号 |
能,规律明显 |
修复后 |
随机字符串 |
不能,不可预测 |
8. 攻防演进全景总结
把整条链路串起来,可以看到一个清晰的攻防演进过程:
回合 |
攻击手段 |
防御手段 |
攻防结果 |
第 1 回合 |
修改 ID 水平越权 |
服务端校验身份 |
攻击者转向提权 |
第 2 回合 |
篡改角色垂直提权 |
JWT 签名防线 |
身份越权被杜绝 |
第 3 回合 |
目录穿越下载文件 |
过滤特殊字符 + 限定目录 |
系统文件被隔离 |
第 4 回合 |
文件名规律枚举 |
随机字符串重命名 |
文件无法再枚举 |
图片说明:从水平越权、垂直提权到 JWT 防线,再到目录穿越与文件名枚举,攻防双方围绕身份可信与文件可猜持续博弈。
每一次防御升级,都逼迫攻击者另寻出路。
而每一次攻击进化,又催生出更严密的防御。
这就是安全领域最核心的规律:攻防永远是一场没有终点的螺旋博弈。
9. 写在最后
本文用医院电子病历系统这个场景,完整推演了从水平越权到文件名枚举的越权与下载攻防链路。
这些知识点并不局限于医疗系统,它们广泛存在于电商、金融、政务、企业内网等各类业务系统中。
理解越权的本质,才能真正构建起有效的身份鉴权体系。
对开发者而言,服务端鉴权、JWT 签名、文件名白名单与随机化,是企业级应用的基本功。
技术本身没有善恶,但越界使用技术有代价。
希望这篇文章能提供一个清晰的越权攻防思维框架