文章总结: 文章详述一次由头像上传SVG触发存储型XSS,再借localStorageJWT与CORS限制,最终用img标签绕过并窃取token实现账户接管的实战链。核心在于上传配置缺陷、token延迟写入、跨域外传技巧及多漏洞串联思路,提醒研究者对XSS深入挖掘、关注存储与CORS细节 综合评分: 92 文章分类: 渗透测试,WEB安全,红队,漏洞分析,实战经验
从图片上传到账户接管 —— 在一次真实渗透测试中串联上传、存储与 CORS 问题
haidragon haidragon
安全狗的自我修养
2026年1月22日 14:47 湖南
官网:http://securitytech.cc
I’ve always been fascinated by how small misconfigurations can lead to critical impact. In this post, I’ll walk you through a real-world vulnerability I discovered during a pentest — starting from a simple file upload and ending in full account takeover using a clever chain of XSS + storage + CORS bypass.
我一直对“小小的错误配置却能造成严重影响”这件事非常着迷。在这篇文章中,我将带你了解我在一次渗透测试中发现的真实漏洞 —— 从一个简单的文件上传开始,最终通过巧妙地串联 XSS + 存储 + CORS 绕过 实现完整的账户接管。
Recon: Looking at the Upload Feature
信息收集:查看上传功能
While reviewing the application, I noticed a profile image upload functionality. I tested whether it accepted other file types like .svg or .html — and to my surprise, SVG uploads were accepted and rendered as-is.
在审查这个应用时,我注意到有一个头像上传功能。我测试它是否接受其他文件类型,比如 .svg 或 .html —— 结果让我吃惊的是,SVG 文件居然可以上传并且被原样渲染。
Press enter or click to view image in full size 按回车或点击查看大图
Uploaded .svg image containing script tags — initial XSS test
上传包含 script 标签的 .svg 图片 —— 初始 XSS 测试
I opened the uploaded file in the browser, and — yesss!! — a popup appeared!
我在浏览器中打开上传的文件,然后 —— 太棒了!!—— 弹窗出现了!
Press enter or click to view image in full size 按回车或点击查看大图
XSS popup confirms the stored XSS vulnerability XSS 弹窗确认了这是一个存储型 XSS 漏洞
This is where things got interesting.
事情从这里开始变得有意思了。
Where’s the Session?
Session 在哪里?
Whenever we find an XSS, the next thought should always be:
每当发现 XSS 时,接下来第一反应应该是:
“Can I steal a session and prove account takeover?” “我能不能窃取会话并证明账户接管?”
So I immediately opened DevTools and checked document.cookie — but there was no session cookie, as the application was using JWT-based header authentication instead. That’s good from a defense perspective, but for an attacker, it’s time to dig deeper.
于是我立刻打开开发者工具查看 document.cookie —— 但没有任何 session cookie,因为这个应用使用的是 基于 JWT 的 Header 认证。从防御角度看这是好的,但对攻击者来说,就需要继续深挖。
Next stop: localStorage.
下一站:localStorage。
But… surprise! There was no token in **localStorage** when viewing the uploaded XSS file directly.
但是……惊喜来了!当直接访问上传的 XSS 文件时,localStorage 里并没有 token。
Press enter or click to view image in full size 按回车或点击查看大图
JWT missing in localStorage when viewing the XSS page 查看 XSS 页面时 localStorage 中缺少 JWT
The First Catch: Token Is Set Only After Visiting /profile
第一个关键点:只有访问 /profile 才会设置 Token
While exploring, I noticed that after logging in and navigating to the /profile endpoint, the frontend JavaScript dynamically fetched user data — and only then stored the JWT in localStorage.
在探索过程中我发现,在登录并访问 /profile 接口后,前端 JavaScript 会动态获取用户数据 —— 只有在这个时候才会把 JWT 存入 localStorage。
Press enter or click to view image in full size 按回车或点击查看大图
JWT appears in localStorage only after accessing /profile 只有访问 /profile 后 JWT 才会出现在 localStorage 中
So here’s the catch:
所以关键点在这里:
The XSS page doesn’t have the JWT. We have to force the browser to visit
**/profile**first, then wait for the JWT to be saved, and then extract it.XSS 页面本身没有 JWT。我们必须强制浏览器先访问
/profile,等待 JWT 被保存,然后再把它提取出来。
Crafting the Payload
构造 Payload
To automate this chain, I created an HTML payload that:
为了自动化这条攻击链,我构造了一个 HTML payload,用来:
- Loads
**/profile**silently in the background 在后台静默加载/profile - Waits a short delay 等待一小段时间
- Extracts the JWT from
***/localStorage***localStorage 从localStorage中提取 JWT - Sends it to my Burp Collaborator server 把它发送到我的 Burp Collaborator 服务器
Press enter or click to view image in full size 按回车或点击查看大图
Code injected to visit ***/profile*** and exfiltrate the token
注入的代码用于访问 /profile 并外传 token
Second Catch: CORS Hits Again
第二个关键点:再次遇到 CORS
The browser threw a CORS error when trying to send the token via fetch().
当使用 fetch() 发送 token 时,浏览器抛出了 CORS 错误。
Press enter or click to view image in full size 按回车或点击查看大图
CORS error when sending token to external server 向外部服务器发送 token 时出现 CORS 错误
No worries — I rewrote the exfiltration logic using an <img> tag instead of fetch(). This bypasses CORS because the browser doesn’t block image loads — even cross-origin.
不用担心 —— 我改用 <img> 标签重写了外传逻辑,而不是 fetch()。这样可以绕过 CORS,因为浏览器不会阻止跨域图片加载。
Press enter or click to view image in full size 按回车或点击查看大图
Modified payload using <img> for CORS-free exfiltration
使用 <img> 修改后的 payload,实现无 CORS 阻拦的数据外传
Once I tested it again… no error appeared in the console.
再次测试后……控制台里没有任何错误出现。
Press enter or click to view image in full size 按回车或点击查看大图
Clean console — no CORS error 干净的控制台 —— 没有 CORS 错误
Game Over: Token Stolen & Session Hijacked
游戏结束:Token 被盗,会话被劫持
My Collaborator server received the JWT successfully.
我的 Collaborator 服务器成功收到了 JWT。
Press enter or click to view image in full size 按回车或点击查看大图
JWT token successfully captured on my server JWT token 已成功在我的服务器上捕获
To confirm the account takeover, I used the following command in the browser console:
为了确认账户已经被接管,我在浏览器控制台中执行了下面的命令:
localStorage.setItem('token', 'PASTE_STOLEN_TOKEN_HERE');
Press enter or click to view image in full size 按回车或点击查看大图
Token set in localStorage Token 已设置进 localStorage
Then I refreshed the page… and I was logged in as the victim.
然后刷新页面……我已经以受害者身份登录成功。
Press enter or click to view image in full size 按回车或点击查看大图
Successfully hijacked victim session 成功劫持受害者会话
Final Thoughts
最后的想法
This vulnerability wasn’t just about XSS — it was about chaining multiple layers:
这个漏洞不仅仅是 XSS,而是多层问题的串联利用:
- File Upload Misconfiguration — letting SVG/XSS through 文件上传配置错误 —— 允许 SVG/XSS 通过
- Token Stored in
**localStorage**Instead of Cookie Token 存在localStorage而不是 Cookie - JWT only set after visiting
**/profile**JWT 只有访问/profile后才会设置 - CORS limitations during exfiltration 外传时受到 CORS 限制
- Bypassing CORS using creative tricks (like
**<img>**tags) 使用巧妙方式绕过 CORS(例如<img>标签)
If you ever find an XSS, don’t stop at an
*alert(1)*—*go deeper**:*如果你发现了 XSS,不要只停留在
alert(1)—— 要继续深入挖掘:
- If there are no cookies, check
localStorageandsessionStorage. 如果没有 cookie,就检查localStorage和sessionStorage。 - If you can’t access the token directly, check which page triggers it. 如果无法直接获取 token,看看是哪个页面触发它的。
- Use browser logic against itself — force navigation, wait, extract, send. 利用浏览器自身逻辑 —— 强制跳转、等待、提取、发送。
- And when CORS blocks your fetch — use
<img>,<script>, or even<form>! 当 CORS 阻止 fetch 时 —— 使用<img>、<script>,甚至<form>!
Thanks for Reading!
感谢阅读!
I hope this blog helped you understand how real-world XSS exploitation works — not just from a payload perspective, but from a strategic chaining mindset.
我希望这篇博客能帮助你理解真实世界中的 XSS 利用方式 —— 不只是 payload 层面,而是战略性的链式思维。
If you enjoyed it or have suggestions to improve, I’d love to hear your feedback! 如果你喜欢或者有改进建议,非常欢迎你的反馈!
- 公众号:安全狗的自我修养
- vx:2207344074
- http://gitee.com/haidragon
- http://github.com/haidragon
- bilibili:haidragonx
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全狗的自我修养 haidragon haidragon《从图片上传到账户接管 —— 在一次真实渗透测试中串联上传、存储与 CORS 问题》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论