はじめに
僕は仕事に迫られて数年前からPostgreSQLを触り始めた猫だ。名前はまだ決まってない。
にゃあ。
※某有名な猫にあやかって自己紹介してみたが当然ながらそんな技術力はない。予防線大事。
今日のテーマ(TL;DR)
初投稿にしてPostgreSQL初心者がアドベントカレンダーに挑戦する無謀なコーナーですが皆様いかがお過ごしでしょうか。
さて本日は上皇陛下の誕生日兼クリスマス・イブのイブと大変お日取りも良く、であるならば出たてほやほや?のv15ちゃんと戯れたいと馳せ参じた次第。早速うきうきとrelease noteをスクロールしていき…
はて???
- Remove PUBLIC creation permission on the public schema (Noah Misch)
- Change the owner of the public schema to be the new pg_database_owner role (Noah Misch)
意訳:いままでなあなあで済ませてたpublicスキーマの管理だけど、おめーらずさんだから引き締めるっぺ。まず手始めにオーナー以外はcreate権限はく奪な、あとちゃんとpg_datbase_ownerの持ち物しておくからな。
(https://www.postgresql.org/docs/current/release-15.html)
どうもこいつはPostgreSQL10.3で修正されたCVE-2018-1058への対策らしい。
A Guide to CVE-2018-1058: Protect Your Search Pathによると、昔PostgreSQLの関数、例えばnvlなどと同名の奴をpublicで作っちゃえば簡単に乗っ取りしていたずらできたらしい。
(https://wiki.postgresql.org/wiki/A_Guide_to_CVE-2018-1058%3A_Protect_Your_Search_Path)
ところが昔は優先度の高いpublicスキーマ使ってオーバーライドし放題だった関数やらオブジェクトたちも、今やきちんとpg_catalogに集約されて、そうそういじくれなくなったらしい。ちーん。
検証のコーナー
目標:何とかしてシステムカタログと同じ名前のオブジェクト作って乗っ取りをしてみたい。
- まずはv15の基本仕様の確認
CREATE USER dbowner PASSWORD 'test';
CREATE USER dbuser PASSWORD 'test';
CREATE database dbtest OWNER dbowner; --dbownerを持ち主としたdbtesデータベースを作る
- dbownerは持ち主なので大体なんでもできる
dbtest=> \dn
List of schemas
Name | Owner
--------+-------------------
public | pg_database_owner
(1 row)
dbtest=> CREATE TABLE test(id int, dt timestamp);
CREATE TABLE
dbtest=> INSERT into test VALUES(1,now());
INSERT 0 1
dbtest=> SELECT * FROM test;
id | dt
----+----------------------------
1 | 2022-12-23 19:23:33.036867
(1 row)
- 同じことを関係ないユーザdbuserでやってみる
dbtest=> \c dbtest dbuser;
You are now connected to database "dbtest" as user "dbuser".
dbtest=> CREATE table test2(id int,dt timestamp);
ERROR: permission denied for schema public
LINE 1: CREATE table test2(id int,dt timestamp);
^
パーミッションエラーが出てブロックされる、よしよし。
- では、持ち主なら本当に何でもできるのか
pg_stat_user_tablesという素敵なviewにターゲットを定める。
\c dbtest dbowner
\d pg_stat_user_tables;
View "pg_catalog.pg_stat_user_tables"
Column | Type | Collation | Nullable | Default
---------------------+--------------------------+-----------+----------+---------
relid | oid | | |
schemaname | name | | |
relname | name | | |
seq_scan | bigint | | |
seq_tup_read | bigint | | |
idx_scan | bigint | | |
idx_tup_fetch | bigint | | |
n_tup_ins | bigint | | |
n_tup_upd | bigint | | |
n_tup_del | bigint | | |
n_tup_hot_upd | bigint | | |
n_live_tup | bigint | | |
n_dead_tup | bigint | | |
n_mod_since_analyze | bigint | | |
n_ins_since_vacuum | bigint | | |
last_vacuum | timestamp with time zone | | |
last_autovacuum | timestamp with time zone | | |
last_analyze | timestamp with time zone | | |
last_autoanalyze | timestamp with time zone | | |
vacuum_count | bigint | | |
autovacuum_count | bigint | | |
analyze_count | bigint | | |
autoanalyze_count | bigint | | |
無理やり同名のオブジェクトを作ってみる。
dbtest=> CREATE VIEW pg_stat_user_tables AS SELECT 'Fake View!!' joke;
CREATE VIEW
おや、作れはする。
dbtest=> \d pg_stat_user_tables;
View "pg_catalog.pg_stat_user_tables"
(以下略)
あれ、作ったビューはいずこに?
dbtest=> \d public.pg_stat_user_tables;
View "public.pg_stat_user_tables"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
joke | text | | |
一応、スキーマまで指定すれば出てくるが、どうやら名前をかぶせると強制的にpg_catalogスキーマが優先されてしまうようだ。
ただ、dbownerは持ち物であるデータベースであれば、pg_catalogスキーマにも編集権限を持つのではないか?
- 無理やりpg_stat_user_tablesを改名してみる
dbtest=> ALTER view pg_stat_user_tables RENAME to pg_stat_user_tables2;
ERROR: must be owner of view pg_stat_user_tables
さすがに一筋縄ではいかない。
それもそのはず、pg_catalogのオブジェクトはpostgresユーザのものらしい
postgres=# SELECT relname,relnamespace::regnamespace,relowner::regrole FROM pg_class WHERE relname='pg_stat_user_tables';
relname | relnamespace | relowner
---------------------+--------------+----------
pg_stat_user_tables | pg_catalog | postgres
(1 row)
さすがにpostgresユーザを使えばどうにかなった。
You are now connected to database "dbtest" as user "postgres".
dbtest=# ALTER view pg_stat_user_tables RENAME to pg_stat_user_tables2;
ALTER VIEW
dbtest=# \d pg_stat_user_tables;
View "public.pg_stat_user_tables"
Column | Type | Collation | Nullable | Default
--------+------+-----------+----------+---------
joke | text | | |
dbtest=# \c dbtest dbowner;
You are now connected to database "dbtest" as user "dbowner".
dbtest=> SELECT * FROM pg_stat_user_tables;
joke
-------------
Fake View!!
(1 row)
結論
- CVE-2018-1058対策としてv15からセキュリティ強化されて、他のユーザからはpublicスキーマにオブジェクトが作れなくなっている。
- データベースの持ち主であれば、自由にオブジェクト作れるが、システムカタログとかぶせてもシステムカタログが優先される
- システムカタログの持ち主であるpostgresユーザで無理やり名前を変えてしまえば、一応乗っ取りは成立する。
ただし、すがにpostgresユーザのパスワードをほいほい漏らしてたらセキュリティホールとかそんな問題以前の話であるので、hook作ってソースをいじったりしない限り、おそらく旧来許されていたカタログレベルの乗っ取りは気にする必要がないだろう。