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
fastify.after(()=>{fastify.get('/',(req,res)=>{if(req.user)returnres.redirect('/notes');returnres.view('login');});fastify.post('/login',{schema: userSchema},async(req,res)=>{let{ username, password }=req.body;username=username.toLowerCase();letuser=awaitUser.findOne({where: { username }});if(user===null){returnres.status(400).send('User not found');}if(!(awaitargon2.verify(user.password,password))){returnres.status(400).send('Wrong password!');}req.session.set('user',user.username);returnres.redirect('/notes');});fastify.get('/register',(req,res)=>{returnres.view('register');});fastify.post('/register',{schema: userSchema},async(req,res)=>{let{ username, password }=req.body;username=username.toLowerCase();letuser=awaitUser.findOne({where: { username }});if(user){returnres.status(400).send('User already exists!');}awaitUser.create({
username,password: awaitargon2.hash(password)});req.session.set('user',username);returnres.redirect('/notes');});fastify.get('/notes',auth(async(req,res)=>{returnres.view('notes',{notes: req.user.notes,csrf: awaitres.generateCsrf()});}));fastify.get('/new',auth(async(req,res)=>{returnres.view('new',{csrf: awaitres.generateCsrf()});}));fastify.post('/new',{schema: noteSchema,preHandler: fastify.csrfProtection},auth(async(req,res)=>{let{ title, content }=req.body;awaitNote.create({
title,
content,userId: req.user.id});returnres.redirect('/notes');}));fastify.post('/delete',{schema: deleteSchema,preHandler: fastify.csrfProtection},auth(async(req,res)=>{let{ id }=req.body;letdeleted=false;for(letnoteofreq.user.notes){if(note.id===id){awaitnote.destroy();deleted=true;}}if(deleted){returnres.redirect('/notes');}else{res.status(400).send('Note not found!');}}));fastify.get('/report',auth(async(req,res)=>{returnres.view('report',{csrf: awaitres.generateCsrf()});}));fastify.post('/report',{schema: reportSchema,preHandler: fastify.csrfProtection},auth((req,res)=>{let{ url }=req.body;if(report.open){returnres.send('Only one browser can be open at a time!');}else{report.run(url);}returnres.send('URL has been reported.');}));})
bot 的程式碼:
constcrypto=require('crypto');constpuppeteer=require('puppeteer');asyncfunctionrun(url){letbrowser;try{module.exports.open=true;browser=awaitpuppeteer.launch({headless: true,pipe: true,args: ['--incognito','--no-sandbox','--disable-setuid-sandbox'],slowMo: 10});letpage=(awaitbrowser.pages())[0]awaitpage.goto('http://0.0.0.0:8080/register');awaitpage.type('[name="username"]',crypto.randomBytes(8).toString('hex'));awaitpage.type('[name="password"]',crypto.randomBytes(8).toString('hex'));awaitPromise.all([page.click('[type="submit"]'),page.waitForNavigation({waituntil: 'domcontentloaded'})]);awaitpage.goto('http://0.0.0.0:8080/new');awaitpage.type('[name="title"]','flag');awaitpage.type('[name="content"]',process.env.FLAG??'ctf{flag}');awaitPromise.all([page.click('[type="submit"]'),page.waitForNavigation({waituntil: 'domcontentloaded'})]);awaitpage.goto('about:blank')awaitpage.goto(url);awaitpage.waitForTimeout(7500);awaitbrowser.close();}catch(e){console.error(e);try{awaitbrowser.close()}catch(e){}}module.exports.open=false;}module.exports={open: false, run }
functionisCustomComponent(tagName: string,props: Object){if(tagName.indexOf('-')===-1){returntypeofprops.is==='string';}switch(tagName){// These are reserved SVG and MathML elements.// We don't mind this list too much because we expect it to never grow.// The alternative is to track the namespace in a few places which is convoluted.// https://w3c.github.io/webcomponents/spec/custom/#custom-elements-core-conceptscase'annotation-xml':
case'color-profile':
case'font-face':
case'font-face-src':
case'font-face-uri':
case'font-face-format':
case'font-face-name':
case'missing-glyph':
returnfalse;default:
returntrue;}}
這次有兩題 Web 比較難,解掉了一題,另一題解不開但解法超值得一看,照樣簡單寫個心得。
Noted
連結在這:https://play.picoctf.org/practice/challenge/282
簡單來說狀況是這樣的,這是一個常見的可以新增 note 的系統,在 /notes 頁面可以看到自己所有的 note,然後有個 self XSS,然後現在有個處於登入狀態下的 admin 會造訪你提供的 URL,要想辦法拿到 admin 的筆記內容。
程式碼:
bot 的程式碼:
題目敘述有說這題的 admin bot 沒有對外連線的功能,所以增加了一些額外的難度(不過其實有開就是了,看 discord 好像開賽前加回去了)。
不過這點先不討論,先來討論其他的。
這題有 csrf token 來防止 csrf,但登入的部分沒有,所以可以用 csrf 來登入,就可以順利在 admin 上執行 XSS。
如果 same site cookie 沒設定的話,其實就 iframe 一下很容易,像這樣:
先在頁面上面開好 iframe,裡面就會有 admin 的筆記,然後用 form csrf 一下,新開一個視窗出來。
接著只要在自己的帳號裡面弄出 XSS,用
window.opener.frames['f'].document.body
就可以拿到頁面的內容了, 雖然新開啟的頁面跟 window.opener 不同源,但因為跟window.opener.frames['f']
同源所以沒差,一樣可以存取到。不過這題的最大問題是 Chrome 預設的 default Lax,所以 iframe 不會帶 cookie,就沒辦法用了。
一個很直觀的解法是改用
window.open
來做,像這樣:但最大的問題是,在新開的視窗中沒辦法用
window.opener.win
來存取到開啟的 window,因為跟window.opener
不同源,所以不讓你存取上面的東西。如果兩個新開的 window 彼此間不能溝通,這該怎麼辦呢?
想了一陣子之後,突然靈機一動:「那就直接把
window.opener
變成要拿的頁面不就好了?」像這樣:
有種 race condition 的感覺,表單送出之後立刻跳轉頁面,這時候因為新的登入還沒完成,所以跳過去時還是 admin 的 session,因此在新的視窗登入完成後就可以直接拿
window.opener.document
的東西。如果有連網的話,就樣就完成了。在沒有對外連線的情況下,可以發現 admin bot 沒有檢查網址,所以可以傳
javascript:
或是data:
之類的東西給它,回傳 flag 的部分可以直接用新增筆記的方式。讓 admin bot 訪問底下這段,其實就只是用
data:text/html
載入 html 而已:XSS payload,這邊直接新開一個 window 去做就好,也可以用 iframe 來做
另一種解法
運用我前幾天在 iframe 與 window.open 黑魔法學到的招數,也就是「當開啟同名 window 時會拿到 reference,不會新開 window」,這樣兩個新開的 window 就可以互相溝通了。
XSS 的部份這樣寫:
這篇也用了類似的解法:https://github.com/Scoder12/ctf/blob/main/PicoCTF%202022/web_noted.md
Live Art
連結:https://play.picoctf.org/practice/challenge/277?page=1&search=live
這題我還沒有自己重現過,解法都是從這篇 picoCTF 2022 WriteUps 看來的,簡單記一下,主要的漏洞有兩個。
is
屬性攻擊 React第二點特別有趣,所以這邊特別講一下第二點。
請問底下的 React app 有什麼問題?
這邊就只是把網址列上的 query string 變成 object 後丟進 img 去,
URLSearchParams
預設的用法沒支援 array 跟 object,所以想產生dangerouslySetInnerHTML: { __html: '..'}
是不可能的。換句話說,今天如果可以控制 React render 中元素的 props,但是 value 只能是字串,可以做些什麼?
當 React 在設置屬性時,如果你寫
<img onError="alert(1)">
,會跳出錯誤訊息:改成小寫的版本,則是會跳 warning:
相關的檢查都在這邊:https://github.com/facebook/react/blob/v18.0.0/packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js#L275
而這邊可以注意到有個
isCustomComponent
的判斷,程式碼在這:https://github.com/facebook/react/blob/v18.0.0/packages/react-dom/src/shared/isCustomComponent.js只要 props 內的 is 是字串的話,就會是 true。
而 React 在設置屬性時也會有一些檢查:https://github.com/facebook/react/blob/v18.0.0/packages/react-dom/src/client/DOMPropertyOperations.js#L151
簡單來說,如果是 custom element(props.is 是個字串)的話,很多屬性都會直接設定上去。
所以,如果有底下的程式碼:
就可以觸發 XSS!因為 is 的緣故,讓 React 直接設置了
onerror
的屬性。我好興奮啊!寫 React 這麼久第一次知道這個特性,在攻擊 React app 上又多了一個攻擊面。
The text was updated successfully, but these errors were encountered: