6,720分でWebアプリケーションスキャナを作る方法

2014/12の[ssmjp](http://ssmjp.connpass.com/event/10320/)にて、「**Webアプリケーションスキャナの作り方**」と題して喋った。
本投稿はそのときの内容を纏めたものとなる。
#アジェンダ
0.なぜWebアプリケーションスキャナを自作するのか？
1.Webアプリケーション診断とは？
2.Webアプリケーションスキャナとは？
3.Webアプリケーションスキャナの基本動作
4.用意するもの
5.GUIの作り方
6.Proxy機能の作り方
7.診断リクエスト送信機能の作り方
8.脆弱性判定機能の作り方
9.動かしてみる
10.まとめ
#0.なぜWebアプリケーションスキャナを自作するのか？
私の場合は特に理由はない。ただの趣味。

といっては味気ないので、あえて尤もらしい理由を付けると、
自作を通してスキャナが脆弱性を検出する原理を理解することで、**Webアプリケーション診断の精度が向上する**からだ。

診断ではスキャナを使うことが多いが、スキャナの診断結果を鵜呑みにすることはない。
必ず診断結果の精査と呼ばれる作業を行う。
なぜならば、スキャナは機械的に脆弱性の有無を判定するため、誤検知を引き起こすことがあるからだ。
このため、診断担当者がスキャナの結果を精査し、誤検知か否かを判断できなければ、誤った診断結果をレポートすることになる。

ところで、正確な判断能力を養うためには長い年月が必要だ。
しかし、手っ取り早く判断能力を身につける方法がある。それが、スキャナを自作することだ。
前述したとおり、スキャナの動作原理が理解できれば、それが誤検知か否かを判断する知見を手に入れることができる。

#1.Webアプリケーション診断とは？
Webアプリケーションに潜む**脆弱性**(注1)を見つけ出す行為。
**診断員**(注2)と呼ばれる人が、診断対象のWebアプリに対して疑似攻撃を仕掛け、Webアプリの挙動を解析することで脆弱性の有無を判断する。
ブラックボックスのセキュリティテスト。

![img1.png](https://qiita-image-store.s3.amazonaws.com/0/76953/719c3772-bf70-8e2c-62df-8565f980282d.png)

診断はインターネット経由で行うことが多いが、インターネットからリーチできない場合は**オンサイト**で行うこともある(注3)。

>注1：脆弱性
情報漏洩やなりすましなどに悪用され得る安全上の欠陥。
詳しくはIPAの[安全なウェブサイトの作り方](http://www.ipa.go.jp/security/vuln/websecurity.html)を参照のこと。

>注2：診断員
診断を行う人。
特に資格は必要ないが、脆弱性診断士として[資格化](http://isog-j.org/output/2014/about-pentester-web-skillmap-201412.pdf)の動きがある。

>注3：オンサイト
データセンター内(ラックの前)で行うこともある。
過酷な環境下で診断を行うことで、腰痛や耳鳴りなどの症状が出る場合がある。

なお、診断では1つの**パラメータ**(注4)に対して300種類上の疑似攻撃を試すことがある。
仮にパラメータが10,000個あった場合、3,000,000回以上の疑似攻撃が必要になるが、これを全て手作業で行うのは現実的ではない。

そこで、診断では**Webアプリケーションスキャナ**(以下、スキャナ)をサポートツールとして**使用することが多い**(注5)。

>注4：パラメータ
HTTPリクエスト中のGET／POSTパラメータ、リクエストヘッダなど。
診断ではパラメータ毎に疑似攻撃を試していく。

>注5：使用することが多い
スキャナを使わない診断ベンダもある。
「診断＝スキャナを使用」ではないので誤解しないように。

#2.Webアプリケーションスキャナとは？
疑似攻撃と脆弱性判定を機械的に行い、診断の効率性を高めるためのツール。
検出できる脆弱性の種類・精度、有償・無償など様々なものが存在。
私が直ぐに思いつくスキャナは以下の通り。

* [AppScan](http://www-03.ibm.com/software/products/ja/category/application-security)
* [Burp Suite(Professional)](http://portswigger.net/burp/)
* [OWASP ZAP](https://www.owasp.org/index.php/OWASP_Zed_Attack_Proxy_Project)
* [skipfish](http://code.google.com/p/skipfish/)
* [VEX](https://www.ubsecure.jp/vex/vex.html)
* [WebInspect](http://www8.hp.com/jp/ja/software-solutions/webinspect-dynamic-analysis-dast/)

(アルファベット順)

スキャナの比較サイトもあるので、参考にして欲しい。
[Sec Tool Market](http://www.sectoolmarket.com/price-and-feature-comparison-of-web-application-scanners-unified-list.html)

#3.Webアプリケーションスキャナの基本動作
スキャナの動作をざっくり表したのが下図だ。

![img2.png](https://qiita-image-store.s3.amazonaws.com/0/76953/97dad6dc-a75d-1752-da30-a14bac1d14d7.png)

* 対象アプリから正常遷移時のログ(HTTPリクエスト/レスポンス)を取得(1,2)
* ログをDBなどに保管(3)
* ログ(HTTPリクエスト)に診断シグネチャを付与(診断リクエスト作成)(4)
* 診断リクエストを対象アプリに送信(5)
* それに対するレスポンスを受信(6)
* レスポンスを解析し、脆弱性の有無を判定(7)

スキャナの機能を分類すると、以下の**三機能**(注6)となる。

* Proxy機能
* 診断リクエスト送信機能
* 脆弱性判定機能

>注6：三機能
診断結果をレポートする機能などが備わっていることも多いが、今回は診断行為を行う機能に絞って話を進める。

###Proxy機能
クライアント-Webサーバ間のHTTP通信を捕捉。
通信のログ(HTTPリクエスト/レスポンス)をDBまたはファイルに保存。
###診断リクエスト送信機能
Proxy機能で保存したHTTPリクエストに、画面遷移設定や様々なシグネチャを付与して送信。
###脆弱性判定機能
診断リクエストに対するレスポンスを受信・解析し、脆弱性の有無を判定。

この三機能を簡単に作るための道具は次の通りだ。

#4.用意するもの
今回はWindowsフォームアプリ型のスキャナを作る。
開発言語はC#。

###Visual Studio 2013 Express Edition
C#の開発環境。
GUIの作成やデバッグが容易に行えるので、非常に開発効率がいい。
個人的には最強のIDEだと思っている。
###SQLite
DBMS。
ログの保管に使用。
動作速度が早く、アプリに組み込んで使用できるため、手軽に扱えて便利。
### Fiddler Core
.NET用のHTTP Proxyライブラリ。
Webデバッガの[Fiddler](http://www.telerik.com/fiddler)から、Proxy機能を切り出し.NET用のライブラリ化したもの。
ほぼこれでProxy機能を実現できる。
###HTML Agility Pack(HAP)
.NET用のHTMLパーサ。
診断リクエスト送信機能で使用。
レスポンスから特定のHTMLタグを容易に抽出可能。
###テキスト差分解析DLL
.NET用のテキスト差分解析ライブラリ。
脆弱性判定機能で使用。
二つのレスポンスの差分比較が容易。

これらを組み合わせることで、**6,720分**(注7)でスキャナを作ることができた。

>注7：6,720分
最低限度の診断機能を有したスキャナを開発するのに要した時間。
設計書などのドキュメントは作成していない。

以降、開発手順に沿って説明する。

#5.GUIの作り方
フォームやボタン、ツリービューなどのGUIを開発する。
が、Visual Studioを使えば簡単に作れるため、説明は割愛。

今回は以下のようなGUIを作った。

![img3.png](https://qiita-image-store.s3.amazonaws.com/0/76953/5dc6e82a-3600-b0e9-d6b2-9b0f164cc45d.png)


先ずは自分が理想とするGUI(モック)を作り、その後に機能毎に処理の作りこみを行う。
先にモックを作ると各機能の関連が分かるため、関連を考慮しながらコーディングすることができ、結果的に手戻りが少なくなる。

#6.Proxy機能の作り方
[**Fiddler Core**](http://www.telerik.com/fiddler/fiddlercore)(注8)を使うことで簡単に実現できる。

>注8：Fiddler Core
HTTP Proxyライブラリ。
HTTP/HTTPSトラフィックの表示やリクエスト／レスポンスの改竄ができる。
.NET標準の「HttpWebRequest」クラスをベースにしていると推測。

Fiddler Coreで主に使用するのは下記の三機能。

* HTTPリクエストセッションのハンドル
* HTTPレスポンスセッションのハンドル
* 送受信成功イベントのハンドル

以下、サンプルコードを示しながらFiddler Coreの使い方を説明する。

* Fiddlerパッケージの参照

Fiddler Coreを使うには、Visual Studioの参照設定からFiddlerCore.dllを参照させ、パッケージの宣言部でFiddlerをusingする。

```lang:パッケージ参照
//Visual StudioのプロジェクトからFiddlerCore.dllを参照しておくこと
using Fiddler;
```
これでFiddler Coreを使用することができる。

* Fiddler Coreイベントのハンドラ登録

Fiddler Coreを介してHTTP通信を行うと、三つのイベントが発生する。

* HTTPリクエスト送信前のイベント(BeforeRequest)
* HTTPレスポンス受信時のイベント(BeforeResponse)
* HTTPレスポンスをクライアントに返した後のイベント(AfterSessionComplete)

これらイベントを上手く処理することで、「リクエストを改竄してから送信」「受信したレスポンス内容をチェックし、気に入らなかったら破棄する(クライアントに返さない)」などの動作が実現できる。

イベントを捕捉するには、下記の通りイベントハンドラを登録しておけばOK。

```lang:イベントのハンドラ登録
//HTTPリクエストを送信する前に発生するイベント
Fiddler.FiddlerApplication.BeforeRequest
            += new Fiddler.SessionStateHandler(FiddlerApplication_BeforeRequest);

//HTTPレスポンス受信時に発生するイベント
Fiddler.FiddlerApplication.BeforeResponse
            += new Fiddler.SessionStateHandler(FiddlerApplication_BeforeResponse);

//HTTPレスポンスをクライアントに返した後のイベント
Fiddler.FiddlerApplication.AfterSessionComplete
            += new Fiddler.SessionStateHandler(FiddlerApplication_AfterSessionComplete);
```
なお、SessionStateHandlerの第一引数は、イベント処理用のメソッド名を指定する。

* Fiddler Coreの初期設定

Fiddler Coreでクライアント証明書を使用する場合は、以下のコードを書けばよい。
たったワンラインでOK。

```lang:クライアント証明書の設定
Fiddler.FiddlerApplication.oDefaultClientCertificate = X509Certificate.CreateFromCertFile(System.Environment.CurrentDirectory + "\\FiddlerCert.cer");
```
今回はMakecertで作った適当な証明書(FiddlerCert.cer)を設定した。
クライアント証明書を必要とするWebアプリにアクセスする場合は、正当な証明書を設定する必要があると**推測**(注9)。

>注9：推測
クライアント証明書が手に入らなかったため、検証していない。

次に、Fiddler Coreが使用するProxyポートやSSL周りの設定を行う。

```lang:Fiddlerの開始
//Proxyポートは自動選択。HTTPSに対応できるようにDecryptSSLを使用。
Fiddler.FiddlerApplication.Startup(0, FiddlerCoreStartupFlags.DecryptSSL | ~Fiddler.FiddlerCoreStartupFlags.ChainToUpstreamGateway);
```
Startupメソッドの第一引数を「0」にすると、Fiddler Coreがシステムの空きポートを自動的に探し、Proxyポートして割り当ててくれる。
ポート番号を明示的に設定したい場合は「8080」のようにポート番号を指定する。

Startupの第二引数に「DecryptSSL」を設定すると、Fiddler CoreでHTTPS通信を捕捉できるようになる。

最後に、Fiddler CoreにProxyポート番号とProxyアドレスをバインドする。
下記のコードでは、Proxyアドレスに「127.0.0.1」、ProxyポートにStartup時に自動取得したポート番号(Fiddler.FiddlerApplication.oProxy.ListenPort)をバインドしている。

```lang:ProxyアドレスとProxyポートの設定
Fiddler.URLMonInterop.SetProxyInProcess(string.Format("{0}:{1}", "127.0.0.1", Fiddler.FiddlerApplication.oProxy.ListenPort), "<local>");
```
これで、Fiddler Coreを使ってProxyできる準備は整った。
下記のコードは、Fiddler Coreを使ってブラウザとWebアプリ間でやり取りされるHTTPリクエストとHTTPレスポンスを捕捉し、コンソールに表示するものだ。
※”q + ENTER”でプログラムは終了。

```FiddlerSample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Fiddler;
using System.Security.Cryptography.X509Certificates;

namespace FiddlerCoreSampe
{
    class Program
    {
        static void Main(string[] args)
        {
            //HTTPリクエストを送信する前に発生するイベント
            Fiddler.FiddlerApplication.BeforeRequest 
                        += new Fiddler.SessionStateHandler(FiddlerApplication_BeforeRequest);

            //HTTPレスポンス受信時に発生するイベント
            Fiddler.FiddlerApplication.BeforeResponse
                        += new Fiddler.SessionStateHandler(FiddlerApplication_BeforeResponse);

            //HTTPレスポンスをクライアントに返した際に発生するイベント
            Fiddler.FiddlerApplication.AfterSessionComplete
                        += new Fiddler.SessionStateHandler(FiddlerApplication_AfterSessionComplete);

            //クライアント証明書を設定する
            Fiddler.FiddlerApplication.oDefaultClientCertificate = X509Certificate.CreateFromCertFile(System.Environment.CurrentDirectory + "\\FiddlerCert.cer");

            //ポートは自動選択。HTTPSに対応できるようにDecryptSSLを使用する
            Fiddler.FiddlerApplication.Startup(0, FiddlerCoreStartupFlags.DecryptSSL | ~Fiddler.FiddlerCoreStartupFlags.ChainToUpstreamGateway);

            //ユーザが設定した任意のアドレスとポート設定する
            Fiddler.URLMonInterop.SetProxyInProcess(string.Format("{0}:{1}", "127.0.0.1", Fiddler.FiddlerApplication.oProxy.ListenPort), "<local>");

            //以下はプログラム制御用のコード。Fiddler Coreとは関係ない。
            var CancelTokenSource = new CancellationTokenSource();

            Task.WhenAll(
                Task.Run(() => KeyEventLoop(CancelTokenSource)),
                Loop(CancelTokenSource.Token)
                ).Wait();
        }

        //HTTPリクエスト送信前のイベント処理
        static void FiddlerApplication_BeforeRequest(Fiddler.Session oSession)
        {
            //リクエストデータを組み立てる
            var ReqBodyString = oSession.GetRequestBodyAsString();
            var ReqBodyBytes = oSession.requestBodyBytes;
            var ReqHeaders = oSession.oRequest.headers.ToString();
            int intAt = ReqHeaders.IndexOf("\r\n");

            if (intAt < 0)
            {
                Console.WriteLine("リクエストの送信に失敗しました。");
            }
            else
            {
                string strMessage = ReqHeaders + "\r\n" +
                    (!string.IsNullOrEmpty(ReqBodyString) ? ReqBodyString + "\r\n" : string.Empty);
                Console.WriteLine(strMessage);
            }
        }

        //HTTPレスポンス受信時のイベント処理
        static void FiddlerApplication_BeforeResponse(Fiddler.Session oSession)
        {
            //レスポンスをクライアントに返す前に行う処理を記述する。
            //例えば、レスポンス内容を改竄してクライアントに返す、など。
        }

        //HTTPレスポンスをクライアントに返した際のイベント処理
        static void FiddlerApplication_AfterSessionComplete(Fiddler.Session oSession)
        {
            //レスポンスデータを組み立てる
            var ResBodyString = oSession.GetResponseBodyAsString();
            var ResBodyBytes = oSession.responseBodyBytes;
            var ResHeaders = oSession.oResponse.headers.ToString();
            int intAt = ResHeaders.IndexOf("\r\n");

            if (intAt < 0)
            {
                Console.WriteLine("レスポンスの受信に失敗しました。");
            }
            else
            {
                ResBodyString = ResBodyString.Replace("\n", "\r\n");

                string strMessage = ResHeaders + "\r\n" +
                    (!string.IsNullOrEmpty(ResBodyString) ? ResBodyString + "\r\n" : string.Empty);
                Console.WriteLine(strMessage);
            }
        }

        //1ms毎にループ
        private static async Task Loop(CancellationToken CancelToken)
        {
            while (!CancelToken.IsCancellationRequested)
            {
                await Task.Delay(1);
            }
        }

        //キーボードイベント処理用のループ
        static void KeyEventLoop(CancellationTokenSource CancelTokenSource)
        {
            while (!CancelTokenSource.IsCancellationRequested)
            {
                //キーボード入力のイベント発生を待つ
                string strline = Console.ReadLine();
                char chrEventCode = strline.Length == 0 ? '\0' : strline[0];

                //イベント処理
                switch (chrEventCode)
                {
                    //FiddlerCoreの終了処理を行い、プログラムを終了させる。
                    case 'q':
                        CancelTokenSource.Cancel();

                        //ブラウザに自動的に設定したProxy設定を外す
                        Fiddler.URLMonInterop.ResetProxyInProcessToDefault();

                        //FiddlerCoreの終了処理
                        Fiddler.FiddlerApplication.Shutdown();

                        break;
                    default:
                        break;
                }
            }
        }
    }
}
```
コード中の「Fiddler.Session oSession」は、Proxy時にFiddler Coreが取得したHTTP通信に関する各種情報が格納されている。
oSessionの各プロパティを参照することで、HTTP通信に関する情報を手に入れることができる。
以下、プロパティの一例。

* oSession.host(アクセス先のHost)
* oSession.fullUrl(アクセス先のフルURL)
* oSession.oRequest.headers(リクエストヘッダ)
* oSession.GetRequestBodyAsString(リクエストボディ)
* oSession.responseCode(レスポンスコード)
* oSession.oResponse.headers(レスポンスヘッダ)
* oSession.GetResponseBodyAsString(レスポンスボディ)

など。

このようにFiddler Coreを使うことで、非常に簡単にProxy機能が実現できる。
Telerik社に感謝だ。
※上述したが、.NET標準の「HttpWebRequest」クラスでもProxy機能は実現できそうだ。
　今後、自前のProxyライブラリを作ったら公開する予定。

#7.診断リクエスト送信機能の作り方
Proxy機能が片付いたので、次は診断リクエスト送信機能だ。
本機能は、Proxy機能で記録した**HTTPリクエストのログにシグネチャを付与**(注10)し、診断対象アプリに送信する役割を果たす。
本機能の流れを示したものが下図だ。

![img4.png](https://qiita-image-store.s3.amazonaws.com/0/76953/61c70450-72b7-72c3-6795-cb0da3122887.png)

>注10：HTTPリクエストのログにシグネチャを付与し
厳密には、HTTPリクエスト中のリクエストヘッダ、GETパラメータ、POSTパラメータ、Pathの末尾などにシグネチャを付与する。

1. Proxy機能で記録した生のHTTPリクエストを取り出す
2. 画面遷移設定を行う
3. 画面遷移設定を行ったHTTPリクエストに診断シグネチャを付与
4. 診断シグネチャを付与したHTTPリクエストを対象アプリに送信

2の画面遷移設定とは、診断時に診断対象アプリを正常に動かすための設定を指す。
通常Webアプリは、Cookieに格納したセッションIDでセッション管理を行っていたり、また、HTTPリクエストの正当性確認のためにワンタイムTokenをリクエスト中に含ませていたりする。
このため、Proxy機能で記録した生のHTTPリクエストに診断シグネチャを付与して送るだけでは、セッションIDやワンタイムTokenが古い(ログ採取時)ままなので、正常にWebアプリが動作しない。
すなわち、正常に診断が行えない。

そこで、診断シグネチャの付与に加えて、セッションIDやワンタイムTokenに新鮮な値を設定する必要がある。

以下、"とある"情報編集機能を例にして説明する。

![img5.png](https://qiita-image-store.s3.amazonaws.com/0/76953/633213f5-46f8-ccc3-ac59-12927f77de30.png)

"とある"情報編集機能では、

1. 編集内容を入力するフォーム(編集内容入力)
2. 入力した内容を確認する画面(編集内容確認)
3. 編集内容を確定させる処理(編集完了)

の画面遷移で編集処理が行われている。
ちなみに、緑色の矢印はHTTPリクエストを示している。

編集完了に向かうHTTPリクエストには、ワンタイムToken「TICKET」が含まれており、リクエストの都度TICKETに新鮮な値(Value)を設定しなければ、編集完了処理は実行できない。

![img6.png](https://qiita-image-store.s3.amazonaws.com/0/76953/d469f3b2-b594-a4b7-195e-cdf5cb539677.png)

どうやって新鮮なValueを設定するのか？
ありがたいことに、"とある"情報編集機能では編集内容確認のレスポンス中に新鮮なTICKETのValueが出力される。

![img7.png](https://qiita-image-store.s3.amazonaws.com/0/76953/f42280ef-11d9-0eb7-8499-f02ea354cee0.png)

よって、編集内容確認のレスポンスからTICKETのValueをリクエストの都度取得し、編集完了処理のパラメータ「TICKET」に設定すればよい。

レスポンスからお目当てのValueを簡単に取得できるのが[**HTML Agility Pack**](https://htmlagilitypack.codeplex.com/)(以下、HAP)だ(注11)。
HAPは.NET用のHTMLパーサで、XPATHやXSLTでHTMLを解析できる、かなり強力なパーサだ。

>注11
Nugetでインストールすることもできる。

HAPを使って、レスポンスボディからTICKETのValueを取得するコードを示す。

```lang:HAPSample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace HAPSample
{
    class Program
    {
        static void Main(string[] args)
        {
            //サンプルhtml(コマンドラインから第一引数でファイル名を渡す)の内容を抽出する。
            StreamReader StrmReader = 
                new StreamReader(System.Environment.CurrentDirectory + "\\" + args[0], Encoding.GetEncoding("Shift_JIS"));
            string strSampleHtml = StrmReader.ReadToEnd();
            StrmReader.Close();

            //サンプルhtmlをHAPに取り込む。
            HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument();
            doc.LoadHtml(strSampleHtml);

            //取り込んだhtmlから、inputタグの中身を根こそぎ取得する。
            HtmlAgilityPack.HtmlNodeCollection _nodes = doc.DocumentNode.SelectNodes("//input");

            foreach (HtmlAgilityPack.HtmlNode node in _nodes)
            {
                //「input type」となっているタグを対象とする。
                if (node.GetAttributeValue("type", "") != "" && node.GetAttributeValue("type", "") != null)
                {
                    //nameが「TICKET」となっているパラメータのValueを取得する。
                    if (node.GetAttributeValue("name", "") == "TICKET")
                    {
                        //TICKETのvalue値を出力する。
                        Console.WriteLine(node.GetAttributeValue("value", ""));
                    }
                }
            }
        }
    }
}
```
Visual StudioのプロジェクトからHtmlAgilityPackを参照させておくこと。

このように、面倒な処理を非常に簡単なコードで実現できる。
さらに、C#の正規表現と組み合わせることで、自由自在にお目当てのValueを取得できる。
とっても便利だ。

#8.脆弱性判定機能の作り方
診断リクエスト送信機能も片付いたので、最後に脆弱性判定機能について説明する。
私の経験上、脆弱性の判定方法は大きく3パターンであると考えている。

* 文字列マッチング
* 差分比較
* 応答時間計測

**文字列マッチング**とは、診断リクエストに対するレスポンスに**特定の文字列が含まれていたら**脆弱性有りと判定するもの。
XSSやSQLコマンドインジェクション、ディレクトリトラバーサルなど、幅広く使われる。
例)入力したJavaScriptが無害化されずにレスポンスに出力されていたらアウト！など。

**差分比較**とは、正常時と診断時の**レスポンスの差分が大きい**場合にアウトにするもの。
ブラインドSQLコマンドインジェクションなどの診断で使われる。
例)SQL文として正しい構文を入力した場合と、誤った構文を入力した場合の差分を比較。
　 差分が一定以上大きい場合はアウト！など。

**応答時間計測**とは、診断リクエストに対するWebアプリからの**応答時間が基準値と近しい**場合にアウトにするもの。
OSコマンドインジェクションやSQLコマンドインジェクションなどの診断で使われる。
例)”ping -n 10”などのOSコマンドを入力し、レスポンスが10秒後に帰ってきたらアウト！など。

今回は「差分比較」について説明する(注12)。

>注12：「差分比較」について説明する
差分比較は他の2パターンと比べると処理が難しいため。
文字列マッチングは、C#の正規表現を使うことで容易に実現できる。
応答時間計測は、ストップウォッチオブジェクト(System.Diagnostics.Stopwatch)を使用し、診断リクエストの送信からレスポンス受信までの時間を計測することで、容易に実現できる。

差分比較はフルスクラッチで書くと非常に面倒なため、便利なライブラリを使わせてもらう。
それが、NonSoftの「[テキスト差分解析DLL](http://homepage2.nifty.com/nonnon/Download/NonDiffDll/)(NonDiff)」だ。
NonDiffは二つのファイル内容を比較し、差分(Update,Delete,Addなど)を返す。
※文字コードSJIS/JIS/EUC/UNICODE/UTF7/UTF8、そしてバイナリにも対応。

下記のコードは、コマンドラインから渡された二つのテキストファイルを比較し、差分がある行数をコンソールに表示するものだ。

```lang:NonDiffSample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace NonDiffSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // テキスト差分オブジェクト
            NonDiffNet.NonDiffClass objDiff;

            // テキスト差分オブジェクト生成
            objDiff = new NonDiffNet.NonDiffClass();

            // テキストファイル読込
            StreamReader StrmReader =
                new StreamReader(System.Environment.CurrentDirectory + "\\" + args[0], Encoding.GetEncoding("Shift_JIS"));
            string strText1 = StrmReader.ReadToEnd();
            StrmReader.Close();

            StrmReader = null;
            StrmReader = new StreamReader(System.Environment.CurrentDirectory + "\\" + args[1], Encoding.GetEncoding("Shift_JIS"));
            string strText2 = StrmReader.ReadToEnd();
            StrmReader.Close();

            // テキスト差分解析
            String[,] dfList;
            dfList = objDiff.NonDiff(strText1, strText2, true, 5, 5, 10, "file0", "file1");

            // テキスト差分表示
            long i;
            int intCount = 0;
            for (i = 1; i <= dfList.GetLength(1) - 1; i++)
            {
                if (i == dfList.GetLength(1) - 1 && dfList[2, i] == "" && dfList[4, i] == "")
                {
                    continue;
                }

                // 表示データ変換(TABはスペースに変換)
                String[] strDt = new String[2];
                if (dfList[2, i] == null)
                {
                    dfList[2, i] = "";
                }
                if (dfList[4, i] == null)
                {
                    dfList[4, i] = "";
                }
                strDt[0] = dfList[2, i].Replace("\t", " ");
                strDt[1] = dfList[4, i].Replace("\t", " ");

                // 差分リスト設定(表示データ設定)
                String[] dsp = new String[7];
                dsp[1] = "" + i;        //通番
                dsp[2] = dfList[0, i];  //差分内容を示す識別子(更新…UPD、削除…DELなど)
                dsp[3] = dfList[1, i];  //テキスト1の行番号
                dsp[4] = strDt[0];      //テキスト1の内容
                dsp[5] = strDt[1];      //テキスト2の内容
                dsp[6] = dfList[3, i];  //テキスト2の行番号

                //識別子が設定された行をカウント(差分がある行をカウント)
                if (dsp[2] != string.Empty)
                {
                    intCount++;
                }
            }

            Console.WriteLine(string.Format("Diff line :{0}", intCount));
        }
    }
}
```
このように、NonDiffを使うことで、非常に簡単にファイルの差分比較を行うことができる。
なお、差分比較結果を格納している「dsp」をListViewに渡すことで、下記のようにグラフィカルに差分比較結果を表すこともできる。

![img8.png](https://qiita-image-store.s3.amazonaws.com/0/76953/a3b2a947-62f1-103c-1c45-da1cd5f4933b.png)

NonDiffメソッドの第4～6引数で差分比較の精度を調整できるらしいが、検証はしていない。
このように、面倒な差分比較もNonDiffを使えば簡単だ。

#9.動かしてみる
上述した手法を組み合わせ、簡易的なWebアプリスキャナを作ってみた。
以下、デモムービーだ。

本デモでは、[OWASP Broken Web Application](https://www.owasp.org/index.php/OWASP_Broken_Web_Applications_Project)のWebGoat.NETに対して診断を行っている。

[![デモムービー](http://img.youtube.com/vi/_FP7grU1AEM/3.jpg)](https://www.youtube.com/watch?v=_FP7grU1AEM)

診断の結果、いくつかXSS脆弱性を検出しているのが分かる。

#10.まとめ
6,720分でスキャナを作る方法を3行でまとめる。

* スキャナを幾つかの機能に分解する
* 分解した機能毎に、利用できそうな**ライブラリを探す**(注13)
* モックを先に作り、GUIを確定させてから処理の作りこみを行う

>注13：ライブラリを探す
自作のスキャナを商用目的で使う場合は、利用するライブラリのライセンスを確認すること。
Ms-PLやMIT Licenseは緩いので問題にならないが、GPLの場合は配布時にソースコードの公開を求められるため、注意が必要だ。

なお、大人の事情により、デモを行ったスキャナのソースコードは公開できない。
ただし、質問には極力お答えするので、興味があれば連絡ください。

以上