Web システムの脆弱性を開発者の視点に立ってわかりやすく説明する、その1です。
今回解説するのは以下の内容となります。
・不正なリクエストデータに対する脆弱性
・不正な SQL 文を埋め込む攻撃
・不正な OS コマンドを埋め込む攻撃
・不正な HTML タグや JavaScript を埋め込む攻撃
・不正なパス名を埋め込む攻撃
・不十分なバリデーションチェック
・信頼できないデータの復元
・XML ドキュメントによる不正な外部エンティティ参照
参考: 認証の欠如による脆弱性
参考: 機密情報・認証情報の漏えいによる脆弱性・不適切なパーミッションによる脆弱性
参考: 不正なメモリ領域へのアクセスによる脆弱性
参考: Web システムの脆弱性の分類
不正なリクエストデータに対する脆弱性
GET や POST パラメータなどに悪意のあるコードを仕込む方法は、システムの脆弱性をつく一般的な攻撃方法です。
通常アクセスログには POST の中身が表示されず、一見すると通常のリクエストに見えるため、痕跡も残りにくく検知が難しいです。
入力データが安全かどうかしっかり確認することや、悪意のあるコードをサニタイズ(無効化)する対処が必要となります。
単体テストの段階からこのような観点を盛り込んで漏れなく検証を行いましょう。
WAF【Web Application Firewall】を利用することでこの類いの攻撃はかなり防ぐことができるため、導入を検討しても良いかと思います。
不正な SQL 文を埋め込む攻撃
・SQL インジェクション(CWE-89 [jvndb])
[対象となるシステム]
データベースを利用するシステム。
[脆弱性・攻撃方法]
リクエストデータの中に SQL 文として解釈されるような文字列を紛れ込ませることでデータベースを不正に操作し、本来見ることのできないデータベースの内容を参照したり、触ることのできないデータの更新や削除を行う攻撃方法です。
以下は POST データから SQL 文を組み立てる場合のシンプルな PHP のプログラム例です。
// SQL文生成プログラム
$name = $_POST['name'];
$sql = "select * from TABLE where name='".$name."' order by date";
POST データに SQL 文を仕込んでみましょう。
よくある例としては、OR 1=1
などを追記することでWhere
条件を無効にしたり、;
でプログラムが生成する本来の SQL 文を強制的に終わらせ、攻撃用の SQL 文を追記した後に最後にコメント構文(--
や#
など)をつけることにより、以降に付随する SQL 文を無効化します。
// Where条件の改ざん
$_POST['name'] = "xxx' or 1=1 --"; // OR 1=1を追記することでwhere条件を無効化
↓
// 実際に発行されるSQL文
select * from TABLE where name='xxx' or 1=1 -- order by date'
// 別のSQL文を実行
$_POST['name'] = "xxx'; delete from TABLE; --'"; // ;の後に悪意のあるSQL文を追記、以降を--でコメント化
↓
// 実際に発行されるSQL文
select * from TABLE where name='xxx'; delete from TABLE; -- order by date'
[対処方法]
上記のように文字列連結により、自身のプログラム上で SQL 文を生成する場合は、以下の2点に注意します。
・数値リテラルは数値以外の文字を受け付けない。
・文字列リテラルはクオートで囲み、適切なエスケープを行う。
以下は MySQL の場合の対処サンプルプログラムです。mysqli
にはreal_escape_string()
というエスケープ用のメソッドが用意されています。
ただしこのメソッドは設定されている文字セットに影響されるため、set_charset()
で文字セットを設定しておきます。
※PostgresSQL の場合は、pg_escape_string()
というメソッドが用意されています。
$mysqli = new mysqli($host, $user, $pass, $dbname);
$mysqli->set_charset('utf8mb4');
// SQL文生成プログラム
$name = $_POST['name'];
$sql = "select * from TABLE where name=".myfunc_escape($mysqli, $name)." order by date";
// クエリ実行
$res = $mysqli->query($sql);
if($res !== false){
while($rows = $res->fetch_assoc()){
$col1 = $rows['col1'];
$col2 = $rows['col2'];
}
}
$res->free();
$mysqli->close();
// エスケープを行う関数
function myfunc_escape($mysqli, $value, $force=false){
$sql = $value;
if($value === null){ // null
$sql = 'null';
}else if((!is_numeric($value))||($force)){ // 文字列 or 強制的に
$sql = "'".$mysqli->real_escape_string($value)."'";
}
return $sql;
}
プレースホルダの利用
プレースホルダが利用できる場合は、これを利用することで上記のような自前の対処が不要になります。
プレースホルダとは、SQL 文本体(Prepared Statement)と設定する値の部分(バインド変数)を明確に切り分け、設定値のエスケープ処理をデータベースエンジンやライブラリ側で行う仕組みです。
PHP では、PDO
やmysqli
がプレースホルダを提供しています。
以下はmysqli
のプレースホルダを利用したサンプル例です。
// SQL文生成プログラム
$mysqli = new mysqli($host, $user, $pass, $dbname);
$mysqli->set_charset('utf8mb4');
// SQL文の設定(プレースホルダの設定)
$name1 = $_POST['name1'];
$name2 = $_POST['name2'];
// ?の部分が実際の値に置き換わる
$sql = "select * from TABLE where name1=? and name2=? order by date";
$stmt = $mysqli->prepare($sql);
// 値をバインドして実行
$stmt->bind_param('ss', $name, $name2); // i:int d:double s:string b:blob
$stmt->execute();
$res = $stmt->get_result();
if($res !== false){
while($rows = $res->fetch_assoc()){
$col1 = $rows['col1'];
$col2 = $rows['col2'];
}
}
$res->free();
$mysqli->close();
不正な OS コマンドを埋め込む攻撃
・OS コマンドインジェクション(CWE-78 [jvndb])
・コマンドインジェクション(CWE-77 [jvndb])
[対象となるシステム]
リクエストデータを元にしてプログラム上から外部プログラム(コマンド)を実行するシステム。
PHP であれば、exec()
やsystem()
などを利用しているプログラム。
[脆弱性・攻撃方法]
リクエストデータの中に OS コマンドとして解釈されるような文字列を紛れ込ませることで、サーバ上にて不正なコマンドを実行されてしまう攻撃方法です。
サーバ内のファイルの改ざん、OS のシャットダウン、ユーザアカウントの変更、ウィルス感染、バックドアの設置など様々な攻撃が可能であり、被害は深刻なものとなります。
以下は POST データから該当のユーザのディレクトリの中身を取得する PHP のプログラム例です。
$userName = $_POST['userName'];
$command = 'ls -l /home/' . $userName;
system($command);
POST データに OS コマンドを仕込んでみましょう。
よくある例としては、;
などのコマンドセパレータを使用してプログラムが生成する本来のコマンドを強制的に終わらせ、攻撃用の OS コマンドを追記します。
// ユーザの指定
$_POST['userName'] = ";rm -rf /"; // rmコマンドが実行されます
↓
// 実際に発行されるコマンド
ls -l /home/;rm -rf /
[対処方法]
入力データに「数値のみ」などの厳格な条件を持たせることにより、これ以外の文字の組み合わせを許可しないようにプログラム側で判定します。
例えば上記の例でいうと、ユーザ名をそのまま入力させるのではなく、ユーザ名の一覧を表示しリスト番号から選択させる方法をとることで入力値を数値のみに限定することができます。
特に コマンドセパレータや悪用されやすい記号文字、|
、<
、>
、#
等は入力データに含まれていないか確認を行う必要があるでしょう。
基本的に記号は受け付けないようにした方が無難です。
不正な HTML タグや JavaScript を埋め込む攻撃
・クロスサイトスクリプティング(XSS)(CWE-79 [jvndb])
[対象となるシステム]
リクエストデータを元にしてページの表示内容を動的生成するシステム。
ほぼすべてのシステムに当てはまるかと思います。
[脆弱性・攻撃方法]
リクエストデータの中に HTML のタグや JavaScript のコードとして解釈されるような文字列を紛れ込ませることで、それらのデータが含まれる Web ページを表示した際に意図しないものが表示されたり、不正な JavaScript が実行されてしまう攻撃方法です。
システムの表示内容を改ざんできるため、例えばフィッシングサイトへ誘導するメッセージやリンクを表示させたり嘘の情報を表示させたりと様々な罠を仕込むことができます。
また、JavaScript を利用することで、画面上に表示された個人情報を読み取って攻撃者のサーバへ転送したり、Cookie からセション ID を取得してログインのなりすましを行うこともできます。
以下は POST データを利用して Web ページを生成する PHP のプログラム例です。
$userName = $_POST['userName'];
$url = $_POST['url'];
echo '<div class="header"> Welcome, '.$userName.' <a href="'.$url.'">Link</a></div>';
POST データに不正なコードを仕込んでみましょう。
よくある例としてはハイパーリンクを含む HTML タグや JavaScript をリクエストデータの中に紛れ込ませます。
POST データに HTML タグや JavaScript を入力してそのまま表示される、あるいは JavaScript が動作するようでしたらこれを無効化する対処が必要です。
// HTMLタグ
$_POST['userName'] = '<h2>お知らせ</h2><p>必ず確認してください!</p><a href="fishing_site.com/">重要事項</a>';
// JavaScript
$_POST['userName'] = '<script>alert("test!");</script>';
// Aタグのhref
$_POST['url'] = 'javascript:alert("test!");';
[対処方法]
基本的には HTML タグやスクリプトタグの入力は許可しないようにすべきです。
たとえタグが入力されたとしても、ブラウザが HTML ドキュメントとして解釈しないようエスケープして表示します。
また、入力された URL をA
タグのハイパーリンクにして表示する場合は、不正な URL でないかチェックする必要があります。
$userName = myfunc_getSafeText($_POST['userName']);
if(!myfunc_isURL($url)) $url = '#';
$url = myfunc_getSafeText($_POST['url']);
echo '<div class="header"> Welcome, '.$userName.' [<a href="'.$url.'">Link</a>]</div>';
// サニタイジングされた文字列の取得
function myfunc_getSafeText($text, $hasBr=false){
$safeStr = htmlspecialchars($text);
if($hasBr) $safeStr = nl2br($safeStr);
return $safeStr;
}
// URLのチェック
function myfunc_isURL($url){
$pattern = 'a-zA-Z0-9\-_.!~#%:?\/@$&+=';
if(preg_match('/^(http|https):\/\/['.$pattern.']+$/u',$url)) return true;
return false;
}
また、ページの文字エンコードをきちんと明示しておくことで、想定外の文字エンコード解釈によるサニタイジングのすり抜けが起きないようにします。
<meta charset="utf-8">
どうしてもタグの入力を許可せざるを得ないシステムでは、最低でもスクリプトに該当する文字列の無効化を行うべきかと思います。
WAF の導入も検討すべきでしょう。
すべてのブラウザが対応しているわけではありませんが、JavaScript から cookie を参照・更新できないようにすることもできます。
例えば PHP では、php.ini
やini_set()
等で以下の設定を有効(1にする)する、あるいは cookie を設定する際にsetcookie()
でhttponly
をtrue
にします。
session.cookie_httponly = 1
ini_set('session.cookie_httponly', 1);
setcookie($name, $value, $expires, $path, $domain, $secure, $httponly);
不正なパス名を埋め込む攻撃
・パス・トラバーサル(CWE-22 [jvndb])
[対象となるシステム]
リクエストデータを元にしてパス名(ファイル名)を生成し、当該ファイルに対してなんらかの処理を行うシステム。
[脆弱性・攻撃方法]
パス名の構成に関する入力があったとき、不正なパス名(例えば相対パスなど)を入力するなどしてシステムが意図しないファイルにアクセスしようとする攻撃方法です。
以下は POST データを利用して該当するユーザのプロファイルの内容を出力する PHP のプログラム例です。
// ユーザ名指定
$userName = $_POST['userName'];
$path = '/users/profiles/'.$userName;
// 当該ユーザのプロファイルテキストにアクセスしファイルの中身を表示
$fp = fopen($path, 'r');
while(($line = fgets($fp))){
echo $line;
}
fclose($fp);
POST データに不正なパス名を仕込んでみましょう。
よくある例としては、相対パスを使用して本来アクセスしてはいけないファイルへアクセスするよう仕向けます。
// ユーザ名指定
$_POST['userName'] = '../../etc/passwd';
↓
// 実際に読み込むファイル
$path = '/users/profiles/../../etc/passwd'; // = /etc/passwd
[対処方法]
まず入力内容がパス名(ファイル名)と直接結びつくような実装は避けるべきです。
例えば上記の例でいうと、ユーザ名をそのまま入力させるのではなく、ユーザ名の一覧を表示しリスト番号から選択させる方法をとることで入力内容がパス名に直接結びつかないようにすることができます。
特にディレクトリセパレータや悪用されやすい記号文字、/
、../
、..\
等の OS のパス名解釈でディレクトリを指定できる文字列は入力データに含まれていないか確認を行う必要があるでしょう。
基本的に記号は受け付けないようにした方が無難ですが、それが不可能な場合には以下のように様々な方法で安全にファイルにアクセスできるように心がけます。
// ユーザ名指定
$userName = basename($_POST['userName']); // ディレクトリを排除
$path = '/users/profiles/'.MY_PREFIX.'_'.$userName.'.prop'; // 入力内容を構成要素のごく一部に
// 指定されたユーザが存在するか確認
if(!myfunc_getUser($userName)){
throw new Exception('User not found.', -2);
}
// 指定されたファイルにアクセス可能か事前に確認
if(is_readable($path)){
throw new Exception('Access denied.', -13);
}
また、Web サーバ内のディレクトリやファイルに適切なパーミッションを設定することで、万が一想定外のファイルにアクセスされても大丈夫なようにしておくことも重要です。
実際のファイルへのアクセスは、apache や nginx といった Web サーバの実行ユーザで行うので、この実行ユーザの権限を考慮してパーミッションを設定します。
不十分なバリデーションチェック
・不適切な入力確認(CWE-20 [jvndb])
・危険なタイプのファイルの無制限アップロード(CWE-434 [jvndb])
[対象となるシステム]
すべてのシステム。
[脆弱性・攻撃方法]
入力内容にあらかじめ定められた形式や数値の範囲といった制約が存在する場合、その制約条件から外れるデータをはじく、もしくは無効化する必要があります。
プログラムロジックの組み方にもよりますが、適切な確認を怠ると入力内容によっては非常に危険な動作を引き起こす可能性があります。
この脆弱性は意図的な攻撃のみならず、入力者の誤解やタイプミスなどからも問題を引き起こします。
結果的にセキュリティ的な情報が漏れてしまう場合があるというだけで、どちらかというと単なるバグですね。
以下は不十分なチェックによる危険なコードの例です。
入力として幅と高さ(m
、n
)を受け取ります。MAX_DIM
でmalloc
が大きくならないようチェックしていますが、m
とn
に負の数が入るとこのチェックは意味をなさなくなります。
if(m > MAX_DIM || n > MAX_DIM){
die("Value too large: Die evil hacker!\n");
}
board = (board_square_t*)malloc(m * n * sizeof(board_square_t));
以下はアップロードされた画像をドキュメントルート配下のimages
に配置する PHP のプログラム例です。
$target = "images/".basename($_FILES['uploadedfile']['name']);
if(move_uploaded_file($_FILES['uploadedfile']['tmp_name'], $target)){
echo "The picture has been successfully uploaded.";
}else{
echo "There was an error uploading the picture, please try again.";
}
アップロードされたファイルのタイプをチェックしていないため、攻撃者は以下のようなプログラムをアップロードする可能性があります。
このファイル名は「.php」で終わるため、Web サーバ上で実行できます。
<?php
system($_GET['cmd']);
?>
攻撃者は次のような URL を使用してこのファイルを実行させます。
これにより「ls -l
」コマンドをサーバ上で走らせることができます。
https://server.example.com/images/malicious.php?cmd=ls%20-l
[対処方法]
個々のシステムや機能によって確認すべき点は異なりますので一概にこれと言えるものはありませんが、例えば数値であれば境界値分析、文字列であれば禁則文字を正しく理解して、エラーにする、あるいは無効化するようにします。
単体テストの段階からこのような観点を盛り込んで、バリデーションチェックが正しくなされているか検証を行いましょう。
また、アップロードされるファイルについては、意図しないファイルを排除するよう拡張子やコンテントタイプ、ファイルサイズ、文字コードなどを十分にチェックしましょう。
参考: 単体テストのテスト項目の観点
参考: 入力チェックのバリデーションで注意すべき点
信頼できないデータの復元
・信頼できないデータのデシリアライゼーション(CWE-502 [jvndb])
[対象となるシステム]
シリアライズ(直列化)されたオブジェクトを受け取って処理するシステム。
[脆弱性・攻撃方法]
ディープラーニングの普及により、例えば学習済モデルの受け渡しや保存のためにオブジェクトを直列化(シリアライズ)、あるいはそれを復元(デシリアライズ)する実装を持つプログラムが増えつつあります。
ここで注意すべきは、取得したデータをそのまま信用してデシリアライズしてはいけないということです。
デシリアライズしたオブジェクトは実行可能な任意のコードを持っているからです。
以下は Python の標準ライブラリ pickle を利用したプログラム例です。
pickle の脆弱性は有名であり、公式ドキュメントにも警告文の記述があります。
参考: pickle - Python object serialization
import os
import cPickle
# Exploit that we want the target to unpickle
class Exploit(object):
def __reduce__(self):
return (os.system, ('ls',))
shellcode = cPickle.dumps(Exploit())
cPickle.loads(shellcode) # lsが実行されます。
デシリアライズを行うcPickle.loads()
は__reduce__()
から返される関数を実行します。
したがって本オブジェクトをデシリアライズした途端に OS コマンドのls
が実行されます。
[対処方法]
厳格なルールやホワイトリストを作成するなどして、とにかく信頼できるものだけをデシリアライズの対象にすべきです。
信頼できないデータを処理する場合は、JSON などのより安全なシリアル化形式の方が適切な場合があります。
以下のコードはpickle.Unpickler
を継承し、find_class()
をオーバライドしてホワイトリスト safe_builtins
にあるもの以外のloads()
はエラーを返すように機能拡張したサブクラスです。
import builtins
import io
import pickle
safe_builtins = {
'range','complex','set','frozenset','slice'
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" (module, name))
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
XML ドキュメントによる不正な外部エンティティ参照
・XML 外部エンティティ参照の不適切な制限(XXE)(CWE-611 [jvndb])
[対象となるシステム]
データのやり取りなどに XML ドキュメントを利用するシステム。
[脆弱性・攻撃方法]
XML ドキュメントの DTD(ドキュメントタイプ定義)に不正な外部エンティティ参照を仕込む攻撃方法です。
以下はfile://
を使用して通常アクセスできないローカルファイルを読み取る例です。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxeattack SYSTEM "file:///etc/passwd">
]>
<xxx>&xxeattack;</xxx>
以下はプラベートアドレスを使用して外部に公開されていないサーバへアクセスする例です。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxeattack SYSTEM "http://192.168.0.1">
]>
<xxx>&xxeattack;</xxx>
[対処方法]
外部エンティティ参照は使用しない方が得策ですが、どうしても利用したい場合には参照する場所に制限を設けるなどのルールを定める必要があります。
以下のコードは外部エンティティ参照を使用しない場合の PHP の実装例です。
// libxml2.9.0以降では、エンティティの置換はデフォルトで無効になっているため不要です。
libxml_set_external_entity_loader(null);
$dd = new DOMDocument();
$r = $dd->loadXML($xml);
続きます。
Web システムの脆弱性を開発者の視点に立ってわかりやすく説明する その2
Web システムの脆弱性を開発者の視点に立ってわかりやすく説明する その3
Web システムの脆弱性を開発者の視点に立ってわかりやすく説明する その4
コメント