3.5K Views
November 03, 16
スライド概要
PHPカンファレンス2016における講演資料
安全なPHPアプリケーションの作り方2016 HASH コンサルティング株式会社 徳丸 浩
徳丸浩の自己紹介 • 経歴 – 1985年 京セラ株式会社入社 – 1995年 京セラコミュニケーションシステム株式会社(KCCS)に出向・転籍 – 2008年 KCCS退職、HASHコンサルティング株式会社設立 • 経験したこと – 京セラ入社当時はCAD、計算幾何学、数値シミュレーションなどを担当 – その後、企業向けパッケージソフトの企画・開発・事業化を担当 – 1999年から、携帯電話向けインフラ、プラットフォームの企画・開発を担当 Webアプリケーションのセキュリティ問題に直面、研究、社内展開、寄稿などを開始 – 2004年にKCCS社内ベンチャーとしてWebアプリケーションセキュリティ事業を立ち上げ • 現在 – HASHコンサルティング株式会社 代表 http://www.hash-c.co.jp/ – 独立行政法人情報処理推進機構 非常勤研究員 http://www.ipa.go.jp/security/ – 著書「体系的に学ぶ 安全なWebアプリケーションの作り方」(2011年3月) 「徳丸浩のWebセキュリティ教室 」(2015年10月) – 技術士(情報工学部門) Copyright © 2016 HASH Consulting Corp. 2
最近のPHP関連の脆弱性の話題 Copyright © 2016 HASH Consulting Corp. 3
Joomla! の権限昇格脆弱性 CVE-2016-8869、CVE-2016-8870 Copyright © 2016 HASH Consulting Corp. 4
CVE-2016-8869等はどのような問題か? • どのような問題か? – ユーザー登録時に、管理者権限を設定されてしまう(CVE-2016-8869) – ユーザー登録を許可していない設定でもユーザー登録ができてしまう(CVE-2016-8870) • なにが原因だったか? – Joomla!内にユーザー登録のメソッドが2つ存在した • UsersControllerRegistration::register() • UsersControllerUser::register() – UsersControllerUser::register() の方は、ユーザー登録許可の設定を確認していない! – 同じく、外部から権限を示すコードを設定可能 – UsersControllerUser::register()を外部から呼び出す経路が存在した • どう対策したか? – UsersControllerUser::register()の削除 • 利用者はどうすればよいか? – Joomla!のバージョンアップ Copyright © 2016 HASH Consulting Corp. Demo 5
CVE-2016-8869等から得られる教訓 • 使わなくなったコードは速やかに削除しよう • 脆弱性診断の古典的な観点ではあるが… – ソースを追わないと判らない場合が多い – 内部構造を熟知していないと、短期間の脆弱性診断では指摘できない • 脆弱性診断で、「使わないコードを削除する」ように勧めている理由 – 古いコードには脆弱性があるかもしれない – 拡張を.bak等に変更しているとソースコードが閲覧できてしなう – デバッグ機能等の場合は、バックドアとして悪用される可能性 • 「古いコード」がこれほどまでに「悪用可能」な例は珍しく、ちょっ と興奮した ← 違法ではないが一部不適切 • やはり、古いコードは速やかに削除しよう Copyright © 2016 HASH Consulting Corp. 6
Joomla!には類似脆弱性の”前科”がある • Joomla! 2.5.2以前に存在した脆弱性 • ユーザー登録時に、権限情報を付与し、 かつバリデーションエラーを発生させる ことで権限昇格が可能。 • NICT(情報通信研究機構)の研究者のサ イトが改ざんされる事件に悪用された https://developer.joomla.org/security-centre/39520120303-core-privilege-escalation.html より引用 Copyright © 2016 HASH Consulting Corp. 7
Joomla2.5.2の権限昇格脆弱性 攻撃の流れ 1. 2. 3. 4. 会員登録時にパスワードを不整合にしておく ユーザ登録時に jforms[groups][]=7 をPOSTパラメータに追加 バリデーションでエラー発生 再入力に備えてリクエストのパラメータをすべてセッションに 保存(コントローラ) 5. モデル側で、セッションの中味をすべて取り込み 6. 2.で追加したgroupsが取り込まれる Copyright © 2016 HASH Consulting Corp. 8
Joomla2.5.2の権限昇格脆弱性
components/com_users/controllers/registration.php register()関数
$data = $model->validate($form, $requestData);
// Check for validation errors.
バリデーションエラーの場合、リクエストデータを
まるごとセッション変数に放り込んでいる
if ($data === false) {
権限の情報も含まれている
// Save the data in the session.
$app->setUserState('com_users.registration.data', $requestData);
// Redirect back to the registration screen.
$this->setRedirect(JRoute::_('index.php?option=com_users&view=registration', false));
return false;
// 中略
// バリデーションが正常の場合
// Flush the data from the session.
$app->setUserState('com_users.registration.data', null);
Copyright © 2016 HASH Consulting Corp.
9
components/com_users/models/registration.php getData() 関数内
$temp = (array)$app->getUserState('com_users.registration.data', array());
foreach ($temp as $k => $v) {
$this->data->$k = $v; // セッションのデータをモデルに放り込んでいる
}
【中略】
セッション汚染、Trust Boundary Violation
と呼ばれる問題
$this->data->groups = isset($this->data->groups) ?
array_unique($this->data->groups) : array();
// $this->data->groups = array(); 2.5.3でこのように修正
Copyright © 2016 HASH Consulting Corp.
10
セッション汚染脆弱性とは?
• セッション変数を外部から変更できる脆弱性
• 例: phpMyAdmin CVE-2011-2505
libraries/auth/swekey/swekey.auth.lib.php 266行目以降
if (strstr($_SERVER['QUERY_STRING'],'session_to_unset') != false)
{
parse_str($_SERVER['QUERY_STRING']);
parse_str関数に外部入力を与え、
session_write_close();
第二引数なしで使うと、
session_id($session_to_unset);
register_globals以上に危険。
session_start();
セッション変数の書き換えが可能
$_SESSION = array();
session_write_close();
session_destroy();
exit;
}
Copyright © 2016 HASH Consulting Corp.
11
セッション汚染でセッション変数にオブジェクトを突っ込む • これができると危険だが… – 脆弱性(PHPあるいはアプリケーション)がないとできない • Joomla!みたいにRequestをごそっとセッション変数に代入したり、 parse_str関数を使っていても、オブジェクトは挿入できない • 以下のようなケース – PHPの脆弱性を使う • CVE-2015-6835 (Joomla! に対するゼロデイ攻撃で悪用された) • CVE-2016-7125 (後述) – 入力値をunserialize関数を通している • phpMyAdminの CVE-2011-2505 (前述) – その他、eval等(オブジェクトインジェクション以前に危険だが…) • オブジェクトインジェクション以前の問題として危険 Copyright © 2016 HASH Consulting Corp. 12
CVE-2016-7125によるオブジェクト・インジェクション脆弱性
<?php
require_once('Logger.php');
session_start();
?><form action="" method="POST">
name: <input name=name value=<?php ex($_SESSION['name']); ?>><br>
name: <input name=mail value=<?php ex($_SESSION['mail']); ?>><br>
<input type=submit><br>
// ex()はHTMLエスケープして表示
</form><?php
if (isset($_POST) && ! validate($_POST)) { // バリデーションエラーの場合は
foreach($_POST as $key => $value) {
// セッション変数にリクエスト値を退避
$_SESSION[$key] = $_POST[$key];
}
}
Copyright © 2016 HASH Consulting Corp.
13
脆弱なサンプル(続き)
<?php // Logger.php
class Logger {
const LOGDIR = '/tmp/';
private $filename = '';
private $log = '';
// ログ出力ディレクトリ
// ログファイル名
// ログバッファ
public function __construct($filename) {
// ファイル名を指定
if (! preg_match('/\A[a-z0-9\.]+\z/i', $filename)) {
throw new Exception(‘Logger: ファイル名は英数字とドットで…');
}
$this->filename = $filename; // ファイル名
$this->log = '';
// ログバッファ
}
public function add($log) {
$this->log .= $log;
// ログ出力
// バッファに追加するだけ
}
}
Copyright © 2016 HASH Consulting Corp.
14
脆弱なサンプル(続き)
public function __destruct() { // デストラクタではバッファの中身をファイルに書き出し
$path = self::LOGDIR . $this->filename; // ファイル名組み立て(局所的な脆弱性)
$fp = fopen($path, 'a');
if ($fp === false) {
die('Logger: ファイルがオープンできません' . htmlspecialchars($path));
}
if (! flock($fp, LOCK_EX)) {
// 排他ロックする
die('Logger: ファイルのロックに失敗しました');
}
fwrite($fp, $this->log); // ログの書き出し
fflush($fp);
// フラッシュしてからロック解除
flock($fp, LOCK_UN);
fclose($fp);
}
}
Copyright © 2016 HASH Consulting Corp.
15
CVE-2016-7125の攻撃
• POSTリクエストに以下を追加
&_SESSION=foo|O:6:"Logger":2:{s:16:"%00Logger%00filename";s:31:"../../../../../var
/www/evil.php";s:11:"%00Logger%00log";s:20:"<?php+phpinfo();+?>%0a";}
• 上記は、Loggerオブジェクトをシリアライズしたもの
– ファイル名: ../../../../../var/www/evil.php
– ログの中身: <?php phpinfo(); ?>
• すなわち、/var/www/evil.phpに、<?php phpinfo(); ?>を書き込む
• 当然ながら、任意のPHPコードが書き込める
• Loggerクラスには、局所的なディレクトリトラバーサル脆弱性がある
ことも原因で、それを対策しておくと攻撃を緩和できる
Copyright © 2016 HASH Consulting Corp.
16
先のサンプルはオブジェクト・インジェクション以前の問題 • 本質的な問題はSession Poisoning • 以下のような攻撃 • http://example.jp/?userid=yamada&login=1 – $_SESSION[‘userid’] === ‘yamada’ – $_SESSION[‘login’] === ‘1’ (TRUEと評価され得る) – → セッションハイジャックができてしまう • すなわち、オブジェクト・インジェクション以前の問題として、セッ ション汚染単体で問題となる可能性が高い Copyright © 2016 HASH Consulting Corp. 17
CVE-2016-7125の対策 • PHPを最新版にする – RHEL / CentOSではパッチが出ていないことに注意 https://access.redhat.com/security/cve/cve-2016-7125 より引用 – Debian / Ubuntu / Fedora のサポート中のバージョンは問題ない • そもそもセッション汚染の状態を避ける Copyright © 2016 HASH Consulting Corp. 18
インジェクション系脆弱性の話題 • • • • OSコマンドインジェクション 正規表現インジェクション SQLインジェクション XSS(JavaScriptリテラルのエスケープ) Copyright © 2016 HASH Consulting Corp. 19
ケータイキット for MovableTypeの OSコマンドインジェクション脆弱性 Copyright © 2016 HASH Consulting Corp. 20
http://itpro.nikkeibp.co.jp/atcl/news/16/042301210/ より引用 21
ケータイキット for Movable Type の脆弱性 (CVE-2016-1204) に関する注意喚起 各位 JPCERT-AT-2016-0019 JPCERT/CC 2016-04-26(新規) 2016-05-06(更新) <<< JPCERT/CC Alert 2016-04-26 >>> ケータイキット for Movable Type の脆弱性 (CVE-2016-1204) に関する注意喚起 https://www.jpcert.or.jp/at/2016/at160019.html I. 概要 アイデアマンズ株式会社のケータイキット for Movable Type には、OS コマンドインジェクションの脆弱性 (CVE-2016-1204) があります。この 脆弱性を悪用された場合、当該製品が動作するサーバ上で任意のOS コマンドを実行される可能性があります。 本脆弱性や影響の詳細については、以下を参照してください。 Japan Vulnerability Notes JVNVU#92116866 ケータイキット for Movable Type に OS コマンドインジェクションの脆弱性 https://jvn.jp/vu/JVNVU92116866/ なお、本脆弱性を悪用した攻撃活動が確認されているとの情報があります。 https://www.jpcert.or.jp/at/2016/at160019.html より引用 22
日本テレビ 個人情報不正アクセスに関する調査報告書 より引用 http://www.ntv.co.jp/oshirase/20160714.pdf 23
脆弱なソースコード(Ver 1.641)
$wallpaper = isset($_GET['mtkk_wallpaper'])? unserialize($_GET['mtkk_wallpaper']): null;
// 中略
$cl = (isset($wallpaper['left']) && $wallpaper['left'])? $wallpaper['left']: 0;
// 中略
$options['left'] = $cl;
// 中略
if($options['left'] != 0 || $options['top'] != 0 || $options['width'] != $id['w']
|| $options['height'] != $id['h']) {
$dest_tmp = "$dest-crop.bmp";
$option = " -crop{$options['width']}x{$options['height']}+{$options['left']}+{$options['t
op']}";
$execute = "$convert $option $src $dest_tmp";
if($this->debug_mode) {
// 中略
} else {
exec($execute);
}
24
対策版(Ver 1.65)
$wallpaper = isset($_GET['mtkk_wallpaper'])? unserialize($_GET['mtkk_wallpaper']): null;
// 中略
$cl = (isset($wallpaper['left']) && $wallpaper['left'])? $wallpaper['left']: 0;
// 中略
$options['left'] = $cl;
// 中略
if($options['left'] != 0 || $options['top'] != 0 || $options['width'] != $id['w']
|| $options['height'] != $id['h']) {
$dest_tmp = "$dest-crop.bmp";
$option = " -crop " . escapeshellarg("{$options['width']}x{$options['height']}+{$options
['left']}+{$options['top']}");
$execute = "$convert $option " . escapeshellarg($src) . " " . escapeshellarg($dest_tmp);
if($this->debug_mode) {
// 中略
} else {
exec($execute);
}
25
OSコマンドインジェクション対策に関して…徳丸の苦悩 • PHPには、シェル経由のコマンド実行関数しか用意されていない • そのため、安全なコマンド実行には、パラメータのエスケープ処理が 必須となる • 本当にPHPに用意されたシェルエスケープ関数は安全なのだろうか? • 試してみよう! – なんと、escapeshellcmd関数には脆弱性がある! http://www.tokumaru.org/d/20110101.html#p01 – escapeshellarg関数の方は大丈夫そう… • 「徳丸本」にはエスケープも含め4種類の対策を書くことに • エスケープは4種中第4位で、心から信頼しているわけではない Copyright © 2016 HASH Consulting Corp. 26
徳丸本に学ぶOSコマンドインジェクション対策 1. OS コマンド呼び出しを使わない実装方法を選択する 2. シェル呼び出し機能のある関数の利用を避ける(PHPでは選択でき ない) 3. 外部から入力された文字列をコマンドラインのパラメータに渡さな い 4. OS コマンドに渡すパラメータを安全な関数によりエスケープする Copyright © 2016 HASH Consulting Corp. 27
エスケープは「めんどうくさい」 できれば避けたい たとえば、エスケープを避ける Copyright © 2016 HASH Consulting Corp. 28
もっと良い方法があった!
• ポイントはこれだけです
1.
2.
3.
シェルスクリプトは静的な文字列として定義する。
パラメーターは環境変数で渡す。
エスケープ不要!
• 要は SQL のプリペアードステートメントみたいなものです。
#!/usr/bin/ruby
def getent_egrep_sed(db, pattern, script)
env = {
'db' => db,
'pattern' => pattern,
'script' => script,
}
system(env, 'getent -- "$db" |egrep -- "$pattern" |sed -- "$script"')
end
getent_egrep_sed(*ARGV[0..2])
https://fumiyas.github.io/2013/12/21/dont-use-shell.sh-advent-calendar.html より引用
29
PHPによるサンプルコード
$descriptorspec = array(
0 => array("pipe", "r"), // stdin
1 => array("pipe", "w"), // stdout
2 => array("file", "/tmp/error-output.txt", "a") // stderr .. temporary file
);
$arg2 = "; cat /etc/passwd #\ntest test test";
$env = array('e_arg1' => 'aeiou; "xxx"', 'e_arg2' => $arg2);
$process = proc_open('./argdump "$e_arg1" "$e_arg2"', $descriptorspec, $pipes, getcwd(), $env);
if (is_resource($process)) {
fclose($pipes[0]);
while(!feof($pipes[1]))
echo fread($pipes[1], 10);
fclose($pipes[1]);
$return_value = proc_close($process);
echo "command returned $return_value\n";
}
Copyright © 2016 HASH Consulting Corp.
30
ふみやす方式による"安全なsystem関数"
function e_system($cmd, ...$args) {
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error-output.txt", "a"));
$cmdline = $cmd;
$env = [];
foreach ($args as $key => $value) {
$argname = 'e_arg_' . $key;
// これは本当はよろしくない。局所的な脆弱性
$cmdline .= ' "$' . $argname . '"';
$env[$argname] = $value;
}
$process = proc_open($cmdline, $descriptorspec, $pipes, getcwd(), $env);
if (is_resource($process)) {
fclose($pipes[0]);
while(!feof($pipes[1]))
echo fread($pipes[1], 10);
fclose($pipes[1]);
return proc_close($process);
}
return FALSE;
}
e_system('/usr/sbin/sendmail', '[email protected]; cat /etc/passwd'); // パラメータは別個の引数として指定
Copyright © 2016 HASH Consulting Corp.
31
改善方針1
function e_system($cmd, ...$args) {
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error-output.txt", "a"));
$cmdline = $cmd;
$env = [];
for ($i = 0; $i < count($args); $i++) {
// わかりやすく for文にしてみました
$argname = 'e_arg_' . $i;
// これは本当はよろしくない。局所的な脆弱性
$cmdline .= ' "$' . $argame . '"';
$env[$argname] = $args[$i];
}
$process = proc_open($cmdline, $descriptorspec, $pipes, getcwd(), $env);
if (is_resource($process)) {
fclose($pipes[0]);
while(!feof($pipes[1]))
echo fread($pipes[1], 10);
fclose($pipes[1]);
return proc_close($process);
}
return FALSE;
}
e_system('/usr/sbin/sendmail', '[email protected]; cat /etc/passwd'); // パラメータは別個の引数として指定
Copyright © 2016 HASH Consulting Corp.
32
改善方針2 (for文を使うと死んでしまう人向け)
function e_system($cmd, ...$args) {
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error-output.txt", "a"));
$cmdline = $cmd;
$env = [];
foreach (array_values($args) as $key => $value) { // array_values関数でキー文字列を削除
$argname = 'e_arg_' . $key;
// これは本当はよろしくない。局所的な脆弱性
$cmdline .= ' "$' . $argname . '"';
$env[$argname] = $value;
}
$process = proc_open($cmdline, $descriptorspec, $pipes, getcwd(), $env);
if (is_resource($process)) {
fclose($pipes[0]);
while(!feof($pipes[1]))
echo fread($pipes[1], 10);
fclose($pipes[1]);
return proc_close($process);
}
return FALSE;
}
e_system('/usr/sbin/sendmail', '[email protected]; cat /etc/passwd'); // パラメータは別個の引数として指定
Copyright © 2016 HASH Consulting Corp.
33
正規表現インジェクション phpMyAdmin: CVE-2013-3238 Copyright © 2016 HASH Consulting Corp. 34
phpMyAdmin: CVE-2013-3238
case 'replace_prefix_tbl':
$current = $selected[$i];
$newtablename = preg_replace("/^" . $from_prefix . "/", $to_prefix, $current);
$from_pref = "/e\0“;
$to_prefix = "phpinfo();”;
preg_replace("/^/e\0/", "phpinfo();", "test");
PHP5.4.3以前では、\0以降は無視される
preg_replace("/^/e", "phpinfo();", "test");
Copyright © 2016 HASH Consulting Corp.
35
脆弱性が混入した要因 • preg_replaceに渡す正規表現をエスケープしていなかった – 最低限、/ をエスケープする必要がある preg_replace(“/^” . $from_prefix . “/”, … ↓ reg_replace("/^" . preg_quote($from_prefix, '/') . "/", … • えーっと、preg_quoteって、マルチバイト対応だっけ? – Shift_JIS以外では問題ない? • そもそも、正規表現を外部から(信頼境界を超えて)渡す実装は 避けるべき(実際にもその方向で改修された) Copyright © 2016 HASH Consulting Corp. 36
phpMyAdmin 4.6.2 libraries\transformations.lib.php
function PMA_Transformation_globalHtmlReplace($buffer, $options = array())
{
if (! isset($options['string'])) {
$options['string'] = '';
}
if (isset($options['regex']) && isset($options['regex_replace'])) {
$buffer = preg_replace(
正規表現中の @ を \@ にエ
'@' . str_replace('@', '\@', $options['regex']) . '@si',
スケープしているが不完全
$options['regex_replace'],
$buffer
);
}
// Replace occurrences of [__BUFFER__] with actual text
$return = str_replace("[__BUFFER__]", $buffer, $options['string']);
return $return;
}
Copyright © 2016 HASH Consulting Corp.
37
正規表現インジェクションの対策の考え方 • PHPには、正規表現をエスケープする関数 preg_quote が用意されて いるが… • そもそも、正規表現を外部から指定できる状況は、極力避けるべき • 【原則】外部由来の値は、正規表現として利用しない • phpMyAdminの正規表現インジェクション脆弱性も、正規表現を避け る形で実装された Copyright © 2016 HASH Consulting Corp. 38
SQLインジェクション ZEND Framekwork 1 のSQLインジェクション(昨年の続き) Copyright © 2016 HASH Consulting Corp. 39
Zend Framework のSQLインジェクション (CVE-2014-4914) 去年のおさらい及び続き Copyright © 2008-2015 HASH Consulting Corp. 40
Zend Frameworkとは? • PHPの心臓部であるZend Engineを開発しているZend Technologies社 が開発したアプリケーションフレームワーク • 柔軟な構造であり自由な使い方ができる • 依存関係が弱くコンポーネントとして利用が容易 • PHPのオブジェクト指向を活用している • … • 要はZend謹製のフレームワーク Copyright © 2008-2015 HASH Consulting Corp. 41
Zend_Dbの使い方
require_once 'Zend/Db.php';
$params = array('host' => 'localhost',
'username' => DBUSER,
'password' => DBPASSWD,
'dbname'
=> DBNAME);
$db = Zend_Db::factory('PDO_MYSQL', $params);
$select = $db->select()
->from('products')
->order('name'); // 列 nameでソート
$result = $db->fetchAll($select);
// 生成されるSQL文
SELECT `products`.* FROM `products` ORDER BY `name` ASC
Copyright © 2008-2015 HASH Consulting Corp.
42
orderメソッドあれこれ // 単純 order('name') ORDER BY `name` ASC // 降順 order('name desc') ORDER BY `name` DESC // 識別子のエスケープ order('na`me') ORDER BY `na``me` ASC // 配列による複数ソートキー指定 order(array('name', 'id')) ORDER BY `name` ASC, `id` ASC // 式も書けるよ order('(name + id)') ORDER BY (name + id) ASC Copyright © 2008-2015 HASH Consulting Corp. 43
ここで一つ疑問ががが
• 識別子はクォートとエスケープがされる
– name →
– na`ma →
`name`
`na``me`
• 式はそのまま
– (name + id)
→
(name + id)
• どうやって識別子と式を区別しているの?
• ソースを見よう!
if (preg_match('/\(.*\)/', $val)) {
$val = new Zend_Db_Expr($val);
}
// ( と ) があれば
// 式とみなす
Copyright © 2008-2015 HASH Consulting Corp.
44
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 45
CVE-2014-4914 (Zend Framework 1.12.6以前) • orderの引数文字列に ( と ) がありさえすれば式とみなされエスケープ 対象外となる • 1 ; 攻撃文字列 -- () とかでも おk SELECT `products`.* FROM `products` ORDER BY 1; 攻撃文字列 -- () ASC • 公表されたPoCは以下の通り order('MD5(1); drop table products --') ↓ 生成されるSQL文 SELECT `products`.* FROM `products` ORDER BY MD5(1); drop table products -ASC • 参考: http://framework.zend.com/security/advisory/ZF2014-04 Copyright © 2008-2015 HASH Consulting Corp. 46
Zend Framework 1.12.7 での修正
• 式の判定が以下のように修正された
// 1.12.6以前
if (preg_match('/\(.*\)/', $val)) {
$val = new Zend_Db_Expr($val);
}
// 1.12.7
if (preg_match('/^[\w]*\(.*\)$/', $val)) {
$val = new Zend_Db_Expr($val);
}
// 英数字0文字以上に続けて ( があり、末尾に ) があれば式とみなす
Copyright © 2008-2015 HASH Consulting Corp.
47
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 48
Zend Framework 1.12.7 に対する攻撃 • 従来のPoC order('MD5(1); drop table products --') ↓ 生成されるSQL文 SELECT `products`.* FROM `products` ORDER BY `MD5(1); drop table products --` ASC // order by 以降が ` で囲まれて識別子となる • 新しいPoC order('MD5(1); drop table products -- )') ↓ 生成されるSQL文 SELECT `products`.* FROM `products` ORDER BY MD5(1); drop table products -- ) ASC // 式とみなされる条件を満たすので「そのまま」SQL文に Copyright © 2008-2015 HASH Consulting Corp. 49
Zend Framework 1.12.8 での修正
• 式の判定が以下のように修正された
// 1.12.7
if (preg_match('/^[\w]*\(.*\)$/', $val)) {
$val = new Zend_Db_Expr($val);
}
// 英数字0文字以上に続けて ( があり、末尾に ) があれば式とみなす
// 1.12.8
if (preg_match('/^[\w]*\([^\)]*\)$/', $val)) {
$val = new Zend_Db_Expr($val);
}
// 英数字0文字以上に続けて ( があり、途中は ) 以外が続き、
// 末尾に ) があれば式とみなす
Copyright © 2008-2015 HASH Consulting Corp.
50
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 51
これはフレームワークの脆弱性なのか? • Ruby on Railsの場合、orderメソッドに指定する文字列は「式」と決 まっているので、アプリケーション側のバリデーション等で対策する ことが求められる • Zend Frameworkの場合は、文字列の内容により識別子か式かが決ま るので、責任境界があいまい • 本来、識別子用のメソッドと式用のメソッドは、名前などで明確に区 別するべし • つまり、Zend Frameworkの仕様の問題である Copyright © 2008-2015 HASH Consulting Corp. 52
これで終わったと徳丸は思っていた Copyright © 2016 HASH Consulting Corp. 53
だが、終わりではなかった Copyright © 2016 HASH Consulting Corp. 54
Zend Framework 1.12.10 での修正
• Zend Framework 1.12.10で関数呼び出し(括弧)のネストを許容した
• 式の判定が以下のように修正された
// 1.12.10
if (preg_match('/^([\w]*\(([^\)]|(?1))*\))$/', (string) $val)) {
$val = new Zend_Db_Expr($val);
}
https://regexper.com/ を利用
Copyright © 2008-2015 HASH Consulting Corp.
55
Zend Framework 1.12.10 での修正(続き) • この修正により、以下のようなORDER BY句が許容された – ORDER BY ((list_price - discount_price) * quantity) • だが、穴はないのか? • Zend Framework 1.12.10で混入した脆弱性 – MD5("(");DELETE FROM p2; #) https://regexper.com/ を利用 Copyright © 2008-2015 HASH Consulting Corp. 56
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 57
Zend Framework 1.12.19 での修正
• 式の判定が以下のように修正された
// 1.12.19
if (preg_match'/^([\w]+\s*\(([^\(\)]|(?1))*\))$/', (string) $val)) {
$val = new Zend_Db_Expr($val);
}
禁止文字に開き括
弧が追加された
https://regexper.com/ を利用
Copyright © 2008-2015 HASH Consulting Corp.
58
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 59
Zend Framework 1.12.19への攻撃 • 穴はないのか? • Zend Framework 1.12.19に対する攻撃 – MD5(“a(");DELETE FROM p2; #) https://regexper.com/ を利用 Copyright © 2008-2015 HASH Consulting Corp. 60
Zend Framework 1.12.20 での修正
• ORDER BY句のチェックに先立ち、SQLコメントを取り除く処理が追
加された
const REGEX_SQL_COMMENTS
(([\'"]).*?[^\\\]\2)
|(
(?:\#|--).*?$
|
/\*
(?: [^/*]
|/(?!\*)
|\*(?!/)
|(?R)
)*
\*\/
)\s*
|(?<=;)\s+
@msx';
=
#
#
#
#
#
#
#
#
#
#
#
#
#
'@
$1 : Skip single & double quoted expressions
$3 : Match comments
- Single line comments
- Multi line (nested) comments
. comment open marker
. non comment-marker characters
. ! not a comment open
. ! not a comment close
. recursive case
. repeat eventually
. comment close marker
Trim after comments
Trim after semi-colon
Copyright © 2008-2015 HASH Consulting Corp.
61
(;´Д`) Copyright © 2008-2015 HASH Consulting Corp. 62
Zend Framework 1.12.20 への攻撃はできないのか? • 実はまだ穴は残っている (;´Д`) • Zend Framework 1 は2016年9月28日でEnd of Life(EOL)となった https://framework.zend.com/blog/2016-06-28-zf1-eol.html • すなわち、もう改修の見込みはない • ダメ元で報告したけど、「アプリケーション側で対応せよ」という回答だった • 実は僕もそう思う Copyright © 2008-2015 HASH Consulting Corp. 63
対策 • Zend Framework 1 をお使いの方は… – 最新のZend Framework 1.12.20 に移行する かつ – orderメソッドの引数をバリデーション • できるだけ早期にZend Framework 2 または 3、あるいはその他のフ レームワークに移行しましょう ※ Zend Framework 2 にはこの問題はありません Copyright © 2008-2015 HASH Consulting Corp. 64
XSS(JavaScriptリテラルのエスケープ) Copyright © 2016 HASH Consulting Corp. 65
JavaScriptの文字列リテラルのエスケープは難しい • イベントハンドラ onxxxx= のエスケープは、 – まず文字列をJavaScript文字列としてエスケープして、 – HTMLの属性値としてエスケープする • script要素の中身のエスケープは、 – JavaScript文字列としてエスケープするだけでよいが、 – </script>を注入されることによる攻撃の対策が必要となる – 以下のようなケース <script> foo("☆ここを動的生成☆"); </script> Copyright © 2016 HASH Consulting Corp. 66
まちがった例 <script> foo("☆ここを動的生成☆"); </script> ◎HTMLエスケープした場合 "); alert("1 を注入 <script> foo(""); alert("1"); // ← alert("1") が注入される </script> ◎最低限のJavaScript文字列エスケープした場合 </script><script>alert(1) // を注入 <script> foo("</script><script>alert(1) //"); </script> ※ 攻撃に " ' \ は使えない Copyright © 2016 HASH Consulting Corp. 67
script要素内の文字列リテラル動的生成アプローチ
• 過剰エスケープ
– 徳丸本が紹介している方法の一つ
– 英数字以外を全てユニコードエスケープ \uXXXX とエスケープする
– 実はあまり好きではない
• HTMLノードとして文字列を生成して、JavaScriptから参照する
– 古典的にはhiddenフィールド(徳丸本で紹介している方法の一つ)
– 最近だと id="hoge" data-foo="<% bar %>" しておいて $("#hoge").data('foo')
でとりだすのが主流かと思います。
http://b.hatena.ne.jp/entry/blog.ohgaki.net/javascript-string-escape
• 奥一穂氏の提唱する方法(Inline JSONP)
– ひとことでいうと、JSONPと同形式の呼出をサーバサイドで生成しSCRIPTタグ
として埋め込む、という手法を採るべきだと思います。
http://d.hatena.ne.jp/kazuhooku/20131106/1383690938
Copyright © 2016 HASH Consulting Corp.
68
それぞれの方式の出力結果
• 過剰エスケープ
<script>
foo('\u003c\u002fscript\u003e\u003cscript\u003ealert\u00281\u0029\u002f\u002f');
</script>
• data-*方式
<div id="hoge" data-foo="</script><script>alert(1)//">何か</div>
<script>
foo(document.getElementById("hoge").dataset.foo);
</script>
• Inline JSONP
<script>
foo({"name":"\u003C\/script\u003E\u003Cscript\u003Ealert(1)\/\/"});
</script>
Copyright © 2016 HASH Consulting Corp.
69
なぜJavaScriptの動的生成を避けるべきか? • コードとデータは分離すべき • HTMLエスケープとJSONのエスケープは、テンプレートエンジンや JSONライブラリが勝手にやってくれるが、JavaScriptのエスケープは、 必ずしもそうではない • JavaScriptのエスケープは、ややこしすぎて人知を超えている • よって、HTMLノード経由にするか、Inline JSONPを採用するのが良い と考えます Copyright © 2016 HASH Consulting Corp. 70
安全なアプリケーションの作り方 Copyright © 2016 HASH Consulting Corp. 71
安全なウェブアプリケーションのための原則 • • • • • • わかりやすく書こう、うますぎるプログラムはいけない 局所的に脆弱性を解消する 防御的プログラミングを実践する 単体テストを徹底する コード、命令に対して、外部からの値を持ち込まない 危険な機能の利用を極力避ける Copyright © 2016 HASH Consulting Corp. 72
わかりやすく書こう-うますぎるプログラムはいけない*1
• こなれた機能を使ってシンプルに実装すること
• 「危険な機能」は原則として避ける
– eval、system、call_user_func …
– 複雑な正規表現も避けた方が良い
const REGEX_SQL_COMMENTS
(([\'"]).*?[^\\\]\2)
|(
(?:\#|--).*?$
|
/\*
(?: [^/*]
|/(?!\*)
|\*(?!/)
|(?R)
)*
\*\/
)\s*
|(?<=;)\s+
@msx';
=
#
#
#
#
#
#
#
#
#
#
#
#
#
'@
$1 : Skip single & double quoted expressions
$3 : Match comments
- Single line comments
- Multi line (nested) comments
. comment open marker
. non comment-marker characters
. ! not a comment open
. ! not a comment close
. recursive case
. repeat eventually
. comment close marker
Trim after comments
Trim after semi-colon
Zend Framework 1.12.20 に出て来る正規表現
Copyright © 2016 HASH Consulting Corp.
*1 プログラム書法、 B.カーニハン著、木村泉訳より引用
73
防御的プログラミング • 関数等の引数が想定したものかをモジュール側で確認する • 関数等の戻り値が想定したものかどうかを呼び出し側で確認する 「防御的プログラミング」とはプログラミングに対して防御的になること、つまり「そうなるはずだ」と決め付けないこと である。この発想は「防御運転」にヒントを得たものだ。防御運転では、他のドライバーが何をしようとするかまったくわ からないと考える。そうすることで、他のドライバーが危険な行動に出たときに、自分に被害が及ばないようにする。たと え他のドライバーの過失であっても、自分の身は自分で守ることに責任を持つ。同様に、防御的プログラミングの根底にあ るのは、ルーチンに不正なデータが渡されたときに、それが他のルーチンのせいであったとしても、被害を受けないように することだ。もう少し一般的に言うと、プログラムには必ず問題があり、プログラムは変更されるものであり、賢いプログ ラマはそれを踏まえてコードを開発する、という認識を持つことである。 CODE COMPLETE 第2版 第8章 から引用 防衛的プログラミング [Defensive programming] 潜在的なエラーに対抗するため、すべてのモジュールに膨大な一貫性チェックを行わせる手法(たとえ供給者と顧客の双方 によって冗長な検査が行われるとしても気にしない)。契約による設計の精神には反する。(同書 P713) 契約による設計[Design by Contract] システムのコンポーネント同士が、互いに厳密な契約を介して協調するように設計するソフトウェア構築手法。防衛的プロ グラミングも見よ。(同書 P707) バートランド・メイヤー著、オブジェクト指向入門第二版 方法論・実践 より引用 74
安全なウェブアプリケーションのための原則 • コード、命令に対して、外部からの値を持ち込まない – プログラム(JavaScript含む)、SQL文、シェルコマンド、ファイル名、正規表現 – HTMLとJSONは適当な方法がないのでエスケープで… • 局所的に脆弱性を解消する • 「ややこしいことが起きがちな」機能を避ける – eval、system、call_user_func … – 複雑な正規表現も避けた方が良い – デストラクタ等に複雑な処理を書かない • 防御的プログラミングを実践する • 単体テストを徹底する Copyright © 2016 HASH Consulting Corp. 75