Call: +44 (0)7759 277220 Call
PeteFinnigan.com Limited Products, Services, Training and Information
Blog

Pete Finnigan's Oracle Security Weblog

This is the weblog for Pete Finnigan. Pete works in the area of Oracle security and he specialises in auditing Oracle databases for security issues. This weblog is aimed squarely at those interested in the security of their Oracle databases.

Can we Block EXECUTE ANY PROCEDURE for our API?

I did a five part series on the security of AUDSYS.AUD$UNIFIED and showed how it works at a functional level and how it might be secured by Oracle and how we might design a similar system using standard database license free components. In the last part I tested my implementation of such a system to show it is possible but with the caveat that because this is not built in to the database like Database Vault a DBA could still turn it off.

BUT, the get out clause is that its a free solution or a solution that can be done in a Standard Edition of Oracle; in other words we don't use things like Database Vault or Row Level Security so its open to everyone. Because of that a DBA or someone with access to the schema could bypass it ot turn it off

The example design was to have a table in a schema and then protect that table and allow read through a READ ONLY view. Then allow specific DELETE only via an API and INSERT only via an API and no UPDATE. In a way this is similar to AUDSYS.AUD$UNIFIED.

I know there is a gap that a user with SELECT ANY TABLE could read the base table directly and there is no easy way with standard security features to stop this as the Oracle database does not provide a SELECT TRIGGER.

One other area that could be better is that we created an API that allows INSERT and DELETE on the secured table and we can grant access to that API direct to any users that need it, OR not. BUT a user can also execute the API without a direct grant or ROLE grant if they have EXECUTE ANY PROCEDURE system privilege.

Access can be granted via a role rather than a direct grant on the PL/SQL. I appreciate that roles are disabled inside DEFINER rights code but we will come back to that later.

I use this whole example in my Secure Coding in PL/SQL Class so decided to extend this blog series to discuss how we may block EXECUTE ANY PROCEDURE.

Like SELECT ANY TABLE or indeed READ ANY TABLE there is no EXECUTE trigger we can use to detect the use of EXECUTE ANY PROCEDURE. There is at some level in the we can set auditing and react to an audit entry as fast as possible BUT the session will have succeeded with the READ before we can block it so its not an option for blocking but can be an option to audit non standard access to the data. Back to EXECUTE ANY PROCEDURE....

Whilst there is no EXECUTE trigger to react in real time and block access that is not authorised we can only ever be inside PL/SQL when EXECUTE ANY PROCEDURE is used so we can block this by modifying the PL/SQL API to detect this and allow or not the access.

To do this we need two pieces of information inside the PL/SQL:
  • We need to know the name of the caller
  • We need to know if the caller has EXECUTE ANY PROCEDURE
.

How can we do this? first lets create a sample schema and package that be used as a test bed:

SQL> create user atk_test identified by atk_test default tablespace users;

User created.

SQL> grant create session, create procedure to atk_test;

Grant succeeded.

SQL>

We do not strictly need CREATE PROCEDURE for this example as I will create the code as SYS in the ATK_TEST schema. Lets create a dummy package procedure to test with:

SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 begin
8 dbms_output.put_line('Hello from atk_test.exe.test()');
9 end;
10* end;
11 .
SQL> @exe

Package created.


Package body created.

SQL>

And running it gives:

SQL> set serveroutput on
SQL> exec atk_test.exe.test;
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

So, it works. Now we need to see if we can find the caller. We can add some debug code to the procedure to see if we can find this:

SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 lv_user varchar2(100);
8 lv_username varchar2(100);
9 lv_curr varchar2(100);
10 lv_sess varchar2(100);
11 lv_scm varchar2(100);
12 begin
13 --
14 select 'USER: '||user userr,
15 'Username: '||username username,
16 'Current User: '||sys_context('userenv','current_user') curr,
17 'Session User: '||sys_context('userenv','session_user') sess,
18 'Current Schema: '||sys_context('userenv','current_schema') scm
19 into lv_user,lv_username,lv_curr,lv_sess,lv_scm
20 from user_users;
21 dbms_output.put_line('user =['||lv_user||']');
22 dbms_output.put_line('username =['||lv_username||']');
23 dbms_output.put_line('Current User =['||lv_curr||']');
24 dbms_output.put_line('Session User =['||lv_sess||']');
25 dbms_output.put_line('Current Schema=['||lv_scm||']');
26 --
27 dbms_output.put_line('Hello from atk_test.exe.test()');
28 --
29 end;
30* end;
31 .
SQL> @exe

Package created.


Package body created.

SQL>

And running it gives:

SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[USER: SYS]
username =[Username: ATK_TEST]
Current User =[Current User: ATK_TEST]
Session User =[Session User: SYS]
Current Schema=[Current Schema: ATK_TEST]
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

This solved the first part of the puzzle as the USER and Session User are SYS which is the caller. We should therefore focus on USER as session user could be spoofed BUT we could check both. Next lets see a list of roles and privileges that are granted and also enabled roles. Lets make more changes to the code:

SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 lv_user varchar2(100);
8 lv_username varchar2(100);
9 lv_curr varchar2(100);
10 lv_sess varchar2(100);
11 lv_scm varchar2(100);
12 begin
13 --
14 select 'USER: '||user userr,
15 'Username: '||username username,
16 'Current User: '||sys_context('userenv','current_user') curr,
17 'Session User: '||sys_context('userenv','session_user') sess,
18 'Current Schema: '||sys_context('userenv','current_schema') scm
19 into lv_user,lv_username,lv_curr,lv_sess,lv_scm
20 from user_users;
21 dbms_output.put_line('user =['||lv_user||']');
22 dbms_output.put_line('username =['||lv_username||']');
23 dbms_output.put_line('Current User =['||lv_curr||']');
24 dbms_output.put_line('Session User =['||lv_sess||']');
25 dbms_output.put_line('Current Schema=['||lv_scm||']');
26 --
27 for cc_priv in (
28 select 'SESSION ROLES' typ,role grants from session_roles
29 union
30 select 'GRANTED ROLE' typ,granted_role grants from user_role_privs
31 union
32 select 'GRANTED PRIVILEGE' typ,privilege grants from user_sys_privs
33 order by typ) loop
34 --
35 dbms_output.put_line(cc_priv.typ||chr(9)||cc_priv.grants);
36 --
37 end loop;
38 --
39 dbms_output.put_line('Hello from atk_test.exe.test()');
40 --
41 end;
42* end;
43 .
SQL> @exe

Package created.


Package body created.

SQL>

And now running gives:

SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[USER: SYS]
username =[Username: ATK_TEST]
Current User =[Current User: ATK_TEST]
Session User =[Session User: SYS]
Current Schema=[Current Schema: ATK_TEST]
GRANTED PRIVILEGE CREATE PROCEDURE
GRANTED PRIVILEGE CREATE SESSION
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

This does not help us as these privileges listed are the ones we granted to ATK_TEST not to the caller SYS. So, we have a number of paths we can follow to protect this solution:
  • We can look for any caller that has EXECUTE ANY PROCEDURE directly granted
  • We can look for any caller that has EXECUTE ANY PROCEDURE granted via any ROLE
  • We can check for direct grants on the API and disallow anyone else which would also disallow EXECUTE ANY PROCEDURE
  • We could check for the direct granting of a role that we design and allow execution

Querying the chain of roles and roles and roles and privileges is time consuming (not massively BUT if the procedure were used a lot it could be noticeable. If we were to look for EXECUTE ANY PROCEDURE to exclude we would need to check direct grants and grants via any number of layers of roles and similarly if we checked for grants on the API direct or via roles we would have the same issue.

So a better idea is to use a ROLE as a flag. If a user has been granted the role directly then we allow the code to continue if not we don't. This means that a user would need direct grants on the API or EXECUTE ANY PROCEDURE but they would need the role as a security flag. This means we can forcefully control access to the code/API by presence of the ROLE. Roles are turned off in definer rights PL/SQL BUT in this case we are going to check if it is granted.

So next lets create a role and by default because SYS creates it then the role will be granted to SYS:

SQL> create role ATK_ROLE
2 /

Role created.

SQL>

Check who has the role:

SQL> @sc_who_has_role
Enter value for output_method: S
old 144: lv_file_or_screen:= upper('&&output_method');
new 144: lv_file_or_screen:= upper('S');
Enter value for role_to_find: ATK_ROLE
old 145: write_op('Investigating Role => '||upper('&&role_to_find')||' (PWD = '
new 145: write_op('Investigating Role => '||upper('ATK_ROLE')||' (PWD = '
old 146: ||role_pwd(upper('&&role_to_find'))||') which is granted to =>');
new 146: ||role_pwd(upper('ATK_ROLE'))||') which is granted to =>');
old 148: get_role(upper('&&role_to_find'));
new 148: get_role(upper('ATK_ROLE'));
Investigating Role => ATK_ROLE (PWD = NO) which is granted to =>
====================================================================
User => SYS (ADM = YES)

PL/SQL procedure successfully completed.

SQL>

So, SYS created the role and its granted with ADMIN rights to SYS as the creator so it can be granted on. This is a security issue that causes grants of roles to users that do not need them BUT that is a different story not for now.

We also need to grant select on DBA_ROLE_PRIVS to the schema so that it can check for role membership:

SQL> grant select on dba_role_privs to atk_test;

Grant succeeded.

SQL>


Lets fix up the API to determine if the caller has the role:

SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 lv_user varchar2(100);
8 lv_username varchar2(100);
9 lv_curr varchar2(100);
10 lv_sess varchar2(100);
11 lv_scm varchar2(100);
12 lv_role number:=0;
13 begin
14 --
15 select 'USER: '||user userr,
16 'Username: '||username username,
17 'Current User: '||sys_context('userenv','current_user') curr,
18 'Session User: '||sys_context('userenv','session_user') sess,
19 'Current Schema: '||sys_context('userenv','current_schema') scm
20 into lv_user,lv_username,lv_curr,lv_sess,lv_scm
21 from user_users;
22 dbms_output.put_line('user =['||lv_user||']');
23 dbms_output.put_line('username =['||lv_username||']');
24 dbms_output.put_line('Current User =['||lv_curr||']');
25 dbms_output.put_line('Session User =['||lv_sess||']');
26 dbms_output.put_line('Current Schema=['||lv_scm||']');
27 --
28 for cc_priv in (
29 select 'SESSION ROLES' typ,role grants from session_roles
30 union
31 select 'GRANTED ROLE' typ,granted_role grants from user_role_privs
32 union
33 select 'GRANTED PRIVILEGE' typ,privilege grants from user_sys_privs
34 order by typ) loop
35 --
36 dbms_output.put_line(cc_priv.typ||chr(9)||cc_priv.grants);
37 --
38 end loop;
39 --
40 select count(*)
41 into lv_role
42 from dba_role_privs
43 where granted_role='ATK_ROLE'
44 and grantee=lv_user;
45 dbms_output.put_line('ATK_ROLE count=['||lv_role||']');
46 --
47 dbms_output.put_line('Hello from atk_test.exe.test()');
48 --
49 end;
50* end;
51 .
SQL> @exe

Package created.


Package body created.

SQL>

And running it to see if we have the role gives:

SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[USER: SYS]
username =[Username: ATK_TEST]
Current User =[Current User: ATK_TEST]
Session User =[Session User: SYS]
Current Schema=[Current Schema: ATK_TEST]
GRANTED PRIVILEGE CREATE PROCEDURE
GRANTED PRIVILEGE CREATE SESSION
ATK_ROLE count=[0]
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

This did not work as we did not find the role. It should work as the code run separately shows:

SQL> select count(*) from dba_role_privs where granted_role='ATK_ROLE' and grantee='SYS';

COUNT(*)
----------
1

SQL>

OK, that is a stupid mistake on my part as the user is USER: SYS as returned from the database as a debug line. Lets fix that and try again:

SQL> @cs
Connected.
USER is "SYS"
SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 lv_user varchar2(100);
8 lv_username varchar2(100);
9 lv_curr varchar2(100);
10 lv_sess varchar2(100);
11 lv_scm varchar2(100);
12 lv_role number:=0;
13 begin
14 --
15 select user userr,
16 username username,
17 sys_context('userenv','current_user') curr,
18 sys_context('userenv','session_user') sess,
19 sys_context('userenv','current_schema') scm
20 into lv_user,lv_username,lv_curr,lv_sess,lv_scm
21 from user_users;
22 dbms_output.put_line('user =['||lv_user||']');
23 dbms_output.put_line('username =['||lv_username||']');
24 dbms_output.put_line('Current User =['||lv_curr||']');
25 dbms_output.put_line('Session User =['||lv_sess||']');
26 dbms_output.put_line('Current Schema=['||lv_scm||']');
27 --
28 for cc_priv in (
29 select 'SESSION ROLES' typ,role grants from session_roles
30 union
31 select 'GRANTED ROLE' typ,granted_role grants from user_role_privs
32 union
33 select 'GRANTED PRIVILEGE' typ,privilege grants from user_sys_privs
34 order by typ) loop
35 --
36 dbms_output.put_line(cc_priv.typ||chr(9)||cc_priv.grants);
37 --
38 end loop;
39 --
40 select count(*)
41 into lv_role
42 from dba_role_privs
43 where granted_role='ATK_ROLE'
44 and grantee=lv_user;
45 dbms_output.put_line('ATK_ROLE count=['||lv_role||']');
46 --
47 dbms_output.put_line('Hello from atk_test.exe.test()');
48 --
49 end;
50* end;
51 .
SQL> @exe

Package created.


Package body created.

SQL>

If we now run this lets check we can detect the caller having our role ATK_ROLE:

SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[SYS]
username =[ATK_TEST]
Current User =[ATK_TEST]
Session User =[SYS]
Current Schema=[ATK_TEST]
GRANTED PRIVILEGE CREATE PROCEDURE
GRANTED PRIVILEGE CREATE SESSION
ATK_ROLE count=[1]
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

Great, this now works so lets now convert the ATK_TEST.EXE.TEST() procedure to block any user who does not have the ATK_ROLE and install it:

SQL> get exe
1 create or replace package atk_test.exe as
2 procedure test;
3 end;
4 /
5 create or replace package body atk_test.exe as
6 procedure test is
7 lv_user varchar2(100);
8 lv_role number:=0;
9 atk_exe exception;
10 pragma exception_init(atk_exe,-20011);
11 begin
12 --
13 select user userr
14 into lv_user
15 from user_users;
16 dbms_output.put_line('user =['||lv_user||']');
17 --
18 select count(*)
19 into lv_role
20 from dba_role_privs
21 where granted_role='ATK_ROLE'
22 and grantee=lv_user;
23 dbms_output.put_line('ATK_ROLE count=['||lv_role||']');
24 --
25 if(lv_role=0) then
26 raise atk_exe;
27 end if;
28 --
29 dbms_output.put_line('Hello from atk_test.exe.test()');
30 --
31 end;
32* end;
33 .
SQL> @exe

Package created.


Package body created.

SQL>

So, finally lets try as SYS and see if we can run the procedure as SYS has the ATK_ROLE:

SQL> sho user
USER is "SYS"
SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[SYS]
ATK_ROLE count=[1]
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

Works, great, lets create a user with EXECUTE ANY PROCEDURE, CREATE SESSION and the ATK_ROLE:

SQL> create user atk1 identified by atk1;

User created.

SQL> grant create session, execute any procedure to atk1;

Grant succeeded.

SQL> grant atk_role to atk1;

Grant succeeded.

SQL>

Now try and run it:

SQL> connect atk1/atk1@//192.168.56.33:1539/xepdb1
Connected.
SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[ATK1]
ATK_ROLE count=[1]
Hello from atk_test.exe.test()

PL/SQL procedure successfully completed.

SQL>

Works!!

Now revoke the ATK_ROLE from ATK1 and test again:

SQL> @cs
Connected.
USER is "SYS"
SQL> revoke atk_role from atk1;

Revoke succeeded.

SQL> connect atk1/atk1@//192.168.56.33:1539/xepdb1
Connected.
SQL> set serveroutput on
SQL> exec atk_test.exe.test;
user =[ATK1]
ATK_ROLE count=[0]
BEGIN atk_test.exe.test; END;

*
ERROR at line 1:
ORA-20011:
ORA-06512: at "ATK_TEST.EXE", line 22
ORA-06512: at line 1


SQL>

This works as planned.

BUT, there are still some issues; there always are when we use standard features, we need to go down the rabbit hole.

If we wanted to block EXECUTE ANY PROCEDURE from other peoples code then we can not initially unless we have the source code. One option is replace the original procedure with a thin veneer that does the security first and then the original that has been renamed is called. There is a flaw with this also as the original can be executed direct with EXECUTE ANY PROCEDURE. In some cases we can fix even this. We can edit the original file and add an ACCESSIBLE BY clause to force access via our wrapper and security so it cannot be called direct anymore.

There is a second more obvious problem. We decided to control the security via the membership of the role ATK_ROLE; so again down the security rabbit hole we must go as we must also now think about stopping anyone from granting that role as if they did it would bypass our security mechanism. We can do this in two ways; the first to stop people having the grants to grant roles - this is not practical in any database; the second is to create a new DDL trigger that specifically blocks the granting of this role; maybe unless its an ATK security DBA.


In summary we need layers and layers of security to implement something with standard database features. We need to implement the security and then security of that security and maybe security of security of security and we must not forget to add audit events!

#oracleace #sym_42 #oracle #security #audit #trail #audittrail #databreach #hacking #grants #protect #readonly #table

Testing a READONLY table and sometimes WRITE and DELETE

This is the next part of the series looking at the AUDSYS schema and AUD$UNIFIED table that Oracle has created and protected. In the first part we explored what AUDSYS and AUD$UNIFIED looks like in terms of security; in part 2 we explored how Oracle might have created this security and in part 3 we designed at a high level how we might implement similar security in the database. In the last part 4 I implemented a security design to match as best we could the AUDSYS and AUD$UNIFIED set up functionally from Oracle.

In this short post I want to now see if I can abuse abuse my set up and see how it performs.

First lets do the basics and add data to the table and delete from it via the API as designed:

SQL> exec atk_sec.atk_sec_pack.del;

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.ins(3,4);

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.ins(5,6);

PL/SQL procedure successfully completed.

SQL>

This works as planned but what about trying direct deletes, inserts and updates on the table as SYSDBA:

SQL> delete from atk_sec.my_tab;
delete from atk_sec.my_tab
*
ERROR at line 1:
ORA-20006:
ORA-06512: at "ATK_SEC.ATK_DML", line 20
ORA-04088: error during execution of trigger 'ATK_SEC.ATK_DML'


SQL>
SQL> insert into atk_sec.my_tab(col01,col02) values (1,2);
insert into atk_sec.my_tab(col01,col02) values (1,2)
*
ERROR at line 1:
ORA-20006:
ORA-06512: at "ATK_SEC.ATK_DML", line 20
ORA-04088: error during execution of trigger 'ATK_SEC.ATK_DML'


SQL>
SQL> update atk_sec.my_tab set col01=3;
update atk_sec.my_tab set col01=3
*
ERROR at line 1:
ORA-20006:
ORA-06512: at "ATK_SEC.ATK_DML", line 20
ORA-04088: error during execution of trigger 'ATK_SEC.ATK_DML'


SQL>

So, these all work as planned. We can insert records into the table only via the API and delete only via the API. Also I was logged on as SYSDBA and using UPDATE ANY TABLE and DELETE ANY TABLE and INSERT ANY TABLE and these were blocked so we fixed the issue of ATK_SEC not being a dictionary protected user.

Now, what if we try and truncate the table:

SQL> truncate table atk_sec.my_tab;

Table truncated.

SQL>

Hmm, that is not what we want. Lets fix that with a TRUNCATE DDL trigger:

SQL> get trunc
1 create or replace trigger atk_sec_trun
2 before truncate on database
3 declare
4 atk_trunc exception;
5 pragma exception_init(atk_trunc,-20009);
6 begin
7 if(ora_dict_obj_owner = 'ATK_SEC' and ora_sysevent='TRUNCATE') then
8 raise atk_trunc;
9 end if;
10* end;
SQL> @trunc
SQL> create or replace trigger atk_sec_trun
2 before truncate on database
3 declare
4 atk_trunc exception;
5 pragma exception_init(atk_trunc,-20009);
6 begin
7 if(ora_dict_obj_owner = 'ATK_SEC' and ora_sysevent='TRUNCATE') then
8 raise atk_trunc;
9 end if;
10 end;
11 /

Trigger created.

SQL>

And if try and truncate the table again:

SQL> truncate table atk_sec.my_tab;
truncate table atk_sec.my_tab
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_TRUN'
ORA-00604: error occurred at recursive SQL level 1
ORA-20009:
ORA-06512: at line 6


SQL>

Fixed!

What if we try and create a table or procedure in ATK_SEC:

SQL> create table atk_sec.tab2(col1 number);
create table atk_sec.tab2(col1 number)
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_ALTER'
ORA-00604: error occurred at recursive SQL level 1
ORA-20004:
ORA-06512: at line 6


SQL> create procedure atk_sec.my_proc is begin null; end;
2 /
create procedure atk_sec.my_proc is begin null; end;
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_ALTER'
ORA-00604: error occurred at recursive SQL level 1
ORA-20004:
ORA-06512: at line 6


SQL>

Also covered, if an attacker is able to create objects in the schema we protected then maybe they can find a way to bypass security.

Lets try and login as ATK_SEC:

SQL> connect atk_sec/atk_sec@//192.168.56.33:1539/xepdb1
ERROR:
ORA-28000: The account is locked.


Warning: You are no longer connected to ORACLE.
SQL>

OK, lets try and add a password and unlock:

SQL> alter user atk_sec identified by atk_sec;
alter user atk_sec identified by atk_sec
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_ALTER'
ORA-00604: error occurred at recursive SQL level 1
ORA-20002:
ORA-06512: at line 7


SQL>

Or can we unlock the account or make it proxiable:

SQL> alter user atk_sec account unlock;
alter user atk_sec account unlock
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_ALTER'
ORA-00604: error occurred at recursive SQL level 1
ORA-20002:
ORA-06512: at line 7


SQL> alter user atk_sec grant connect through aud4;
alter user atk_sec grant connect through aud4
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_ALTER'
ORA-00604: error occurred at recursive SQL level 1
ORA-20002:
ORA-06512: at line 7


SQL>

NO, what about trying to grant CREATE SESSION?

SQL> grant create session to atk_sec;
grant create session to atk_sec
*
ERROR at line 1:
ORA-04088: error during execution of trigger 'SYS.ATK_SEC_GRANT'
ORA-00604: error occurred at recursive SQL level 1
ORA-20003:
ORA-06512: at line 16


SQL>

We did capture some audit events with the standard events of PFCLATK

ID TIMESTAMP DBNAME PAYLOAD ERRORTEXT P
----- -------------------------- ------ -------------------------------- ------------------------------------------------------------------------------------------------------------------------ -
36 22-JUL-2025 15:16:50:21385 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_CREATE} using IP {192.168.56.1} with error code {0} N
36 22-JUL-2025 15:16:50:21385 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_DROP} using IP {192.168.56.1} with error code {0} N
36 22-JUL-2025 15:16:50:21385 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_GRANT} using IP {192.168.56.1} with error code {0} N
37 22-JUL-2025 15:40:43:12690 XE EVE_1_24:ERROR-LIMIT Someone on IP Adress {192.168.56.1} has has generated {9} errors for the user {SYS} in the last 30 minutes N
38 22-JUL-2025 15:40:54:44356 XE EVE_1_24:ERROR-LIMIT Someone on IP Adress {192.168.56.1} has has generated {10} errors for the user {SYS} in the last 30 minutes N
39 22-JUL-2025 15:42:02:70611 XE EVE_1_24:ERROR-LIMIT Someone on IP Adress {192.168.56.1} has has generated {11} errors for the user {SYS} in the last 30 minutes N
40 22-JUL-2025 15:46:50:11734 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {ALTER TABLE} used by {SYS} on {ATK_SEC.MY_TAB} using IP {192.168.56.1} with error code {4088} N
40 22-JUL-2025 15:46:50:11734 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_TRUN} using IP {192.168.56.1} with error code {0} N
SQL>



This is a pretty good version of the same type of security as Oracles AUDSYS and AUD$UNIFIED.

YES, I know it can be bypassed by turning off system triggers or disabling triggers but if we have a good audit trail and can detect very quickly that this has occurred then the danger is that an attacker has update or deleted records from the table. If we know it happened quickly we can get those records back via flashback or Log Miner so yes its possible to bypass but the damage does by the attacker can be found and fixed quickly.

We can also quickly test the structure of our security every time its used and react if it has changed.

The solution is not perfect BUT we can do it with non additional cost options

#oracleace #sym_42 #oracle #security #audit #trail #audittrail #databreach #hacking #grants #protect #readonly #table

Implement a Test System to Create a Readonly and Sometimes Insert / Delete Table

This is the next part (4th part) of the series exploring the AUDSYS schema and AUD$UNIFIED table that is READONLY with a lot of INSERTING and sometimes deleting.

In the first part we explored the AUDSYS schema and the AUD$UNIFIED table and what we can and cannot do. In the second part we started to explore how Oracle may have implemented this security by ruling out everything we could think of so that we came to the conclusion that Oracle must use internal policies or hard coded in C rules in the database engine. In the third part we discussed how we might design a similarly secured table using standard features of the database.

In this 4th part I am going to implement a simple example where I want a table that can be read by anyone granted access to it but no UPDATES and DELETE and where INSERT is only done via an API. I will also shadow AUDSYS and stop direct logging into my schema account.

So, lets first create the schema only account and locked; i.e. it has no password and is locked:

SQL> create user atk_sec no authentication account lock default tablespace users;

User created.

SQL>

Now remove the INHERIT privileges:

SQL> revoke inherit privileges on user atk_sec from public;

Revoke succeeded.

SQL>

Set up a quota on USERS:

SQL> alter user atk_sec quota unlimited on users;

User altered.

SQL>

Notice that we also did not grant CREATE SESSION to ATK_SEC to add one more hurdle to logging in as ATK_SEC.

Now, create a table that we will protect:

SQL> create table atk_sec.my_tab(col01 number, col02 number);

Table created.

SQL>

There will be no grants on this table of course. Now add a couple of rows just to test the view in a minute:

SQL> create table atk_sec.my_tab(col01 number, col02 number);

Table created.

SQL> insert into atk_sec.my_tab(col01,col02) values (1,2);

1 row created.

SQL> insert into atk_sec.my_tab(col01,col02) values (3,4);

1 row created.

SQL> commit;

Commit complete.

SQL>

We created the table that is to be protected as SYS not as ATK_SEC as we do not want any logons to ATK_SEC even via proxy which I would normally recommend a way to access a schema for maintenance.

Now create the READONLY view that will have access granted to users that need to read the data:

SQL> create or replace view atk_sec.my_view as select * from atk_sec.my_tab with read only;

View created.

SQL>

We now have a view that will allow access to the data and not allow DML through the view. This view access can then be granted to users who need the access. We will do this here as an example via a role BUT if the access was needed via client PL/SQL then it would need to be direct or the PL/SQL would need to use a granted role.

SQL> create role atk_sec_admin;

Role created.

SQL> grant select on atk_sec.my_view to atk_sec_admin;

Grant succeeded.

SQL>

OK, we have the basic data set up. Lets create a logon trigger to prevent login as ATK_SEC:

SQL> @cs
Connected.
USER is "SYS"
SQL> get trig
1 create or replace trigger atk_sec_logon
2 after logon on database
3 declare
4 atk_log exception;
5 pragma exception_init(atk_log,-46370);
6 begin
7 if(user='ATK_SEC') then
8 raise atk_log;
9 end if;
10* end;
SQL> @trig

Trigger created.

SQL>

Lets now kill a few birds with one stone and block an attempt to allow proxy through ATK_SEC and also stop adding a password or unlocking:

SQL> get alter
1 create or replace trigger atk_sec_alter
2 before alter on database
3 declare
4 atk_alter exception;
5 pragma exception_init(atk_alter,-20002);
6 begin
7 if(ora_dict_obj_type = 'USER') then
8 if(ora_dict_obj_name='ATK_SEC') then
9 raise atk_alter;
10 end if;
11 end if;
12* end;
SQL> @alter

Trigger created.

SQL>

This means that we did not really need a logon trigger to prevent logging in as ATK_SEC because it should not be possible to add proxy or change the password or unlock. We could add additional triggers for proxy and we could have written three separate ALTER triggers to prevent each action as that would potentially make it harder to remove the triggers separately and more interestingly it would create more noise for forensics in trying.

Next we need to create the PL/SQL API to allow insert into the table and delete on the table; these are simple for this demo and do not contain any internal security checks BUT they easily could:

SQL> get pack
1 create or replace package atk_sec.atk_sec_pack as
2 procedure ins(pv_col1 in number, pv_col2 in number);
3 procedure del;
4 end;
5 /
6 create or replace package body atk_sec.atk_sec_pack as
7 procedure ins(pv_col1 in number, pv_col2 in number)
8 is
9 begin
10 insert into atk_sec.my_tab(col01,col02) values (pv_col1, pv_col2);
11 commit;
12 end;
13 procedure del is
14 begin
15 delete from atk_sec.my_tab;
16 commit;
17 end;
18* end;
19 .
SQL> @pack

Package created.


Package body created.

SQL>

And testing quickly:

SQL> exec atk_sec.atk_sec_pack.ins(5,6);

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.del;

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.ins(5,6);

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.ins(7,8);

PL/SQL procedure successfully completed.

SQL> exec atk_sec.atk_sec_pack.ins(1,2);

PL/SQL procedure successfully completed.

SQL>

Now we need to create the DML trigger on the base table and allow the package ATK_SEC.ATK_SEC_PACK to be used but direct INSERT, UPDATE and DELETE to not be used:

SQL> get dml
1 create or replace trigger atk_sec.atk_dml
2 before insert or update or delete
3 on atk_sec.my_tab
4 declare
5 lv_depth number;
6 lv_ins boolean:=false;
7 lv_del boolean:=false;
8 atk_dml exception;
9 pragma exception_init(atk_dml,-20006);
10 begin
11 lv_depth:=utl_call_stack.dynamic_depth;
12 for i in 1 .. lv_depth loop
13 if(utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(i))='ATK_SEC_PACK.INS') then
14 lv_ins:=true;
15 elsif(utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(i))='ATK_SEC_PACK.DEL') then
16 lv_del:=true;
17 end if;
18 end loop;
19 -- dbms_output.put_line('lv_del=['||sys.diutil.bool_to_int(lv_del)||']');
20 -- dbms_output.put_line('lv_ins=['||sys.diutil.bool_to_int(lv_ins)||']');
21 if(inserting and (lv_ins=false)) then
22 raise atk_dml;
23 elsif(deleting and (lv_del=false)) then
24 raise atk_dml;
25 elsif(updating) then
26 raise atk_dml;
27 end if;
28* end;
29 .
SQL> @dml;

Trigger created.

SQL>

This will now allow the API to do inserts of records and the API to delete records but will block direct inserts and deletes and block updates all together.

Finally we can also add a CREATE trigger to prevent any additional objects being added to the ATK_SEC schema:

SQL> create or replace trigger atk_sec_create
2 before create on database
3 declare
4 atk_create exception;
5 pragma exception_init(atk_create,-20004);
6 begin
7 if(ora_dict_obj_owner = 'ATK_SEC' and ora_sysevent='CREATE') then
8 raise atk_create;
9 end if;
10 end;
11 /

Trigger created.

SQL>

This will stop any objects being created in the ATK_SEC schema that could be used to try and hack the schema and change the data or similar. One final thing we can do is add a GRANT trigger:

SQL> create or replace trigger atk_sec_grant
2 before grant on database
3 declare
4 lv_num pls_integer;
5 lv_grantee ora_name_list_t;
6 lv_atk boolean:=false;
7 atk_grant exception;
8 pragma exception_init(atk_grant,-20003);
9 begin
10 lv_num:=ora_grantee(lv_grantee);
11 for i in 1 .. lv_num loop
12 if(lv_grantee(i)='ATK_SEC') then
13 lv_atk:=true;
14 end if;
15 end loop;
16
17 if(lv_atk=true) then
18 raise atk_grant;
19 end if;
20 end;
21 /

Trigger created.

SQL>

This will detect any changes to the schema by an attacker trying to get around the security that involves adding more privileges such as GRANT CREATE SESSION needed to try and log in.

One final final thing we can do is also add a DROP system trigger to prevent dropping of any of the security we have added:

SQL> create or replace trigger atk_sec_drop
2 before drop on database
3 declare
4 atk_drop exception;
5 pragma exception_init(atk_drop,-20008);
6 begin
7 if(ora_dict_obj_owner = 'ATK_SEC' and ora_sysevent='DROP') then
8 raise atk_drop;
9 end if;
10 end;
11 /

Trigger created.

SQL>

This will prevent dropping of objects in the ATK_SEC schema

One final final final thing is to consider that we cant block some things like turning off system triggers with ALTER SYSTEM but we can audit this action and also audit all no standard actions or access on this solution. I leave that as a future example. I have PFCLATK installed in this database and it already has policies for most of these events and more. Some audit generated whilst creating this set up is here:

27 22-JUL-2025 10:24:56:46504 XE EVE_1_5:USER-CHANGES User Privilege {ALTER USER} used by {SYS} using IP {192.168.56.1} with error code {0} N
28 22-JUL-2025 10:46:50:05447 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TABLE} used by {SYS} on {ATK_SEC.MY_TAB} using IP {192.168.56.1} with error code {0} N
29 22-JUL-2025 10:46:50:06530 XE EVE_1_17:CHANGES-TO-EXTERNALS Externals Change {CREATE TABLE} used by {SYS} on {ATK_SEC.MY_TAB} using IP {192.168.56.1} with error code {0} N
30 22-JUL-2025 11:14:49:21530 XE EVE_1_19:CHANGES-TO-PARAMTERS Alter system/session command {LOGON} used by {ATK_SEC} using IP {192.168.56.1} with error code {0} N
31 22-JUL-2025 11:16:50:19369 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_LOGON} using IP {192.168.56.1} with error code {0} N
31 22-JUL-2025 11:16:50:19369 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE VIEW} used by {SYS} on {ATK_SEC.MY_VIEW} using IP {192.168.56.1} with error code {0} N
32 22-JUL-2025 12:46:50:06414 XE EVE_1_5:USER-CHANGES User Privilege {ALTER USER} used by {SYS} using IP {192.168.56.1} with error code {4088} N
33 22-JUL-2025 12:46:50:06976 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {SYS.ATK_SEC_ALTER} using IP {192.168.56.1} with error code {0} N
34 22-JUL-2025 14:16:50:16473 XE EVE_1_9:CHANGES-TO-EXTERNALS System Privilege {CREATE TRIGGER} used by {SYS} on {ATK_SEC.ATK_DML} using IP {192.168.56.1} with error code {0} N
35 22-JUL-2025 14:16:50:17548 XE EVE_1_17:CHANGES-TO-EXTERNALS Externals Change {EXECUTE} used by {SYS} on {.} using IP {192.168.56.1} with error code {904} N
SQL>


When creating security like this we have to be careful what order we add the security protections, as adding each layer can stop the next thing from working. The same idea/restriction applies to other built in tools such as Database vault.

Remember the same ideas can be used to secure anything such as a need to allow passwords to be changed by the helpdesk; so we can create a schema that has ALTER USER and we then protect a schema that has ALTER USER and expose the privilege via an API. We could do many other things as well using this design pattern.

Remember it is not perfect as we have used just standard features and it could be disabled by a DBA BUT we augment the design with audit policies to ensure that we catch any changes to the security.

#oracleace #sym_42 #oracle #security #audit #trail #audittrail #grants #protect #readonly #table

Build a readonly table like AUD$UNIFIED

In the two parts of this series on the security of AUDSYS.AUD$UNIFIED we looked at the main security features of the AUDSYS user and the AUD$UNIFIED table so that we could imagine using these same features ourselves.

I have taught a design pattern and used it for years that is similar to what Oracle have done with AUDSYS and AUD$UNIFIED where we create a schema to own something dangerous and then lock that schema to prevent access to it and also then create an API that allows only the access that we need to allow and add extra protections to prevent bypass or removal of the security.

As we found out with AUDSYS and the unified audit trail we don't know for sure exactly how Oracle does it as it is likely hard coded or some sort of internal security policy implemented in the C or simply flags we cannot set ourselves easily or even if we could we could not access these flags in our code.

If we use any number of standard database features to do the same thing then there are two risks; the first is that the owner of the object could manipulate the object itself if someone were to access the schema; and second that a DBA can disable enough of the security to allow access to the object or schema.

This is the big issue with standard features and why Oracle brought in Database vault to allow this type of security to be added to the database where the DBA cannot simply disable the Database Vault settings themselves. The problem then shifts to the database vault administrators as they could then remove the security or enough of it to allow access again to the object. Yes, its better than standard features as its harder to get around but shows how a problem with security in a database is the number of users accessing it and protecting it and the complexity of needed solutions.

As an anecdote I have seen many times where companies have implemented Database Vault and the same people have access to the DV admin/owner accounts, Oracle OS account and sometimes root and also of course access to SYS/SYSTEM etc. If you do this you are wasting the money you paid for database vault.

Even with standard features we could remove the threat of a DBA from un-doing the security if we didn't allow the DBA to access SYSDBA or any other powerful role or privilege that would allow that security to be bypassed. Oracle Autonomous Database does something along these lines already where the tenant of the ATP or ADW database does not have access to the CDB or SYS or standard Oracle roles. So yes a solution could be done provided we can change the way people work.

As part of this discussion we are not going to use cost options like Database Vault or Label Security or Enterprise Edition features such as Row Level Security. This means anyone can use these ideas with the caveats from above; i.e. reduce access from powerful users and schemas.


NOTE: what I said above, these ideas are not perfect and if someone does have access to SYSDBA or a DBA like privileges they could get around this solution. But it is better than doing nothing!! and if we combine with audit events we can minimise the risks by reacting in real or semi-time if someone disables our security. Its the best we can do without access to internal c code and without cost options


What do we want to achieve here as an example. I want to have a table in as schema that cannot be changed or DML performed on it outside of a fixed API. I want to have an API that is allowed to delete blocks of rows of data and also new rows can be added but only through the API. I want anyone allowed access to be able to read the data in this table.

This is essentially what AUDSYS and AUD$UNIFIED do now. To do this I need to do or consider these things:
  • We should create a schema that owns the table
  • The schema should have no password and be locked
  • Create the table that we will protect in the schema
  • No grants to be made on the table
  • Create a READ ONLY view based on the table and allow grants for READ to be made on this view
  • Create the API that will allow insert and delete on the table
  • It should not be possible to log into the schema
  • We should not allow the schema to be proxied to
  • We can create DML triggers for INSERT, UPDATE and DELETE
  • UPDATE should be prevented completely
  • INSERT should only be allowed via the API
  • DELETE should only be allowed via the API

This will give us the basis of protecting a table. Now we need to think about how we might protect the schema and the table protection mechanisms. When we create a security mechanism in Oracle we need to now protect the security mechanism. This is the same by the way if we used Database Vault. If we used Database vault then we need to protect all of the code and schemas and user used in Database Vault otherwise someone could bypass it.

In general we need security of security and we need audit of security. So we could add the following features to our design:
  • Create a DDL/ALTER trigger to prevent changing of the schema password
  • Create a DDL/ALTER trigger to prevent unlocking the schema
  • Create a LOGON trigger to prevent logon as the schema
  • Create a LOGON trigger to prevent logon via proxy to the schema
  • Create a DDL trigger to prevent creation of any object in the schema
  • Create a GRANT trigger to stop any grants to ATK_SEC
  • In the DML triggers ensure that no one can do DML unless the call comes from the specific API
  • Detect if any trigger is dropped or disabled either through another DDL trigger or audit trails
  • Because someone can turn off system triggers with ALTER SYSTEM and this is not caught by a DDL trigger, create an audit event
  • Create audit events for non standard access to the table
  • Create audit events for changes to any part of the security
  • Create audit events for any changes to the security settings

We could even go further and detect changes to the above detections and add auditing ad-infinitum.

In summary we need to detect if someone tried to change the security and block it if possible and if someone succeeds in changing security then create an audit event to detect that fact and we can also verify all the security is valid by checking settings on a regular basis - if not raise an audit alert.

Also think about performance; we should not do things that would slow down the system noticeably.

In summary we can create a similar mechanism to AUDSYS and AUD$UNIFIED using standard features of the database; it is not as perfect as Oracles solution but we can come close.

#oracleace #sym_42 #oracle #security #audit #unified #protect #audittrail #databreach #readonly #grants #protection

What is a Schema in Oracle?

In Oracle a user is the same as a schema - well not 100% true - so lets explain a bit.

Firstly at a logical high level in Oracle a user is an account used by a real person to connect to the database and do work on other users (I know) data. A schema is an account that owns objects such as tables, views, procedures etc.

BUT, a user intended to be a logical user, used by a real person to connect and do things in the database can also have objects. A schema intended to only own the objects of the application

In Oracle when you create a user or schema you use the CREATE USER command. For instance:

SQL> create user pete_u identified by pete_u;

User created.

SQL> create user pete_s identified by pete_s;

User created.

SQL>

I create a user and a schema. For the user i can grant privileges to login and to access other objects or what is needed. For the schema I also create a user. The intention with the schema is to not log in; even for creating objects and for the schema to own objects that are needed and make grants on those objects to other users and schemas.

From an Oracle perspective a user is a schema and a schema is a user BUT, each user has a schema created at the same time as the user and each schema has a user created at the same time. We cannot separate the two except for making a user/schema a schema only account; i.e. it cannot be logged into. More likely we need to manage our accounts and never log into schemas and never create objects in users.

Oracle does not really have a true separation between a schema and a user.

From 18c we can create a schema only account but in truth this could simply be altered and a password added. A schema only account is created as follows:

SQL> create user pete_o no authentication;

User created.

SQL>

As we can see below the password is just zeros:

SQL> select password,spare4 from sys.user$ where name='PETE_O';

PASSWORD
--------------------------------------------------------------------------------
SPARE4
--------------------------------------------------------------------------------

S:0000000000000000000000000000000000000000FAD4D632D13FC635FD36;T:000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000FA258C558BB96EE9061BA13F6547AC98


SQL>

It was possible before 18c to simply create a user with the undocumented IDENTIFIED BY VALUES clause to achieve the same thing and indeed I have done this for over 20 years and advised others the same.

The key to this design is to also use proxy to connect to the schema. So that no one ever directly logs into the schema but proxies to it to manage it and create objects. We are still connected in a sense to our own user (logged in) and accessing the schema without authenticating to the schema.

So, In Oracle a user is a schema and a schema is a user.

Oracle also supports a CREATE SCHEMA statement BUT this does not create a schema without a user or add a schema to an existing user. We cannot separate the two. The CREATE SCHEMA command can be used to add multiple objects at the same time to an existing user in a sense creating a schema. Or if the user does not exist then it creates the user account. Lets try all the possible types of CREATE SCHEMA. First lets try creating a schema for an existing user:

SQL> create schema pete_o create table test1 (col1 number);
create schema pete_o create table test1 (col1 number)
*
ERROR at line 1:
ORA-02420: missing schema authorization clause


SQL>

OK, so what if we change that to not use an existing user:

SQL> create schema pete_x create table test1 (col1 number);
create schema pete_x create table test1 (col1 number)
*
ERROR at line 1:
ORA-02420: missing schema authorization clause


SQL>

Still does not work; lets create a schema PETE_X and authenticate via the user PETE_O:

SQL> create schema pete_x authorization pete_o create table test1 (col1 number);
create schema pete_x authorization pete_o create table test1 (col1 number)
*
ERROR at line 1:
ORA-02420: missing schema authorization clause


SQL>

Still does not work. If we just authorise in PETE_O which exists

SQL> create schema authorization pete_o create table test1 (col1 number);
create schema authorization pete_o create table test1 (col1 number)
*
ERROR at line 1:
ORA-02421: missing or invalid schema authorization identifier


SQL>

Hmm, should work. The account PETE_O exists and we are in essence adding objects via the CREATE SCHEMA command. What does this error mean?

ORA-02421: missing or invalid schema authorization identifier
02421. 00000 - "missing or invalid schema authorization identifier"
*Cause: the schema name is missing or is incorrect in an authorization
clause of a create schema statement.
*Action: If the name is present, it must be the same as the current
schema.

So, I think we can do two things. First grant create table and quota to PETE_O so it has the right to create a table and second set the current_schema to PETE_O:

SQL> grant create table to pete_o;

Grant succeeded.

SQL> grant unlimited tablespace to pete_o;

Grant succeeded.

SQL> alter session set current_schema=pete_o;

Session altered.

SQL> select sys_context('userenv','current_schema') from dual;

SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
--------------------------------------------------------------------------------
PETE_O

SQL> create schema authorization pete_o create table test1 (col1 number);
create schema authorization pete_o create table test1 (col1 number)
*
ERROR at line 1:
ORA-02421: missing or invalid schema authorization identifier


SQL>

Still does not work

So, it would seem that alter session set current_schema does not work as we actually need to be connected as the user who CREATE SCHEMA is executed for not pretending to be that user:

SQL> alter user pete_o grant connect through pete_u;

User altered.

SQL> grant create session to pete_o;

Grant succeeded.

SQL> grant create session to pete_u;

Grant succeeded.

SQL>

Now connect to PETE_O and try again:

SQL> connect pete_u[pete_o]/pete_u@//192.168.56.33:1539/xepdb1
Connected.
SQL> create schema authorization pete_o create table test1 (col1 number);

Schema created.

SQL>

OK, we finally get CREATE SCHEMA to work. What a faff.

This is not necessary. We should just proxy to PETE_O in my case as the schema only account and simply create the tables, views, procedures we need. We still need separate grants to the schema for each object type. Schema is not a separate thing.

The Oracle documentation for the CREATE SCHEMA says:

This statement does not actually create a schema. Oracle Database automatically creates a schema when you create a user (see CREATE USER). This statement lets you populate your schema with tables and views and grant privileges on those objects without having to issue multiple SQL statements in multiple transactions.
Oracle 18c CREATE SCHEMA

All of the statements for multiple tables, views etc still need to be executed. The only saving is a semi-colon after each one. Not worth the hassle; just create users and if there are no objects it is a user and if there are it is a schema. For schemas set an impossible password or make it schema only (same thing). Do not mix the two.

#oracleace #sym_42 #oracle #database #security #create #schema

How does Oracle protect AUDSYS and AUD$UNIFIED

In the recent blog first part of this series on the security of AUDSYS.AUD$UNIFIED we looked at the main features of the AUDSYS user and its ability or design to stop anyone from randomly deleting or updating or doing DDL on the audit trail held in AUD$UNIFIED.

As I said at the end of that post I am interested because I have been teaching in my Secure Coding in PL/SQL Class a design pattern to protect things in an Oracle database such as the use of a privilege or access to a resource or external access or access to a database object such as a table or data.

This design pattern is something I want to do for my customers. There are many ways we could achieve this BUT Oracle have done it with AUD$UNIFIED so i would like to get more details and see if we can do the same.

If we wanted to make AUD$UNIFIED readonly then we could just make the table READONLY and if any DDL or DML was attempted it would result in a ORA-12081 - Update operation not allowed on table "schema"."table". This feature stops DML and also some DDL on the table, so it is is similar to the security of AUD$UNIFIED. This feature is turned on with:

SQL> alter table orablog.bof_pay_details read only;

And turned off with:

SQL> alter table orablog.bof_pay_details read write;

This is OK as a solution to stop changes to data that should not change BUT this would not work with AUD$UNIFIED as the table needs to be written to on a continuous basis and also needs to be deleted from periodically via DBMS_AUDIt_MGMT. So, we could flip between READ ONLY and READ WRITE but one session could make it READ WRITE to insert a record BUT another session could then jump in whilst its READ WRITE and delete audit records.

Another option would be a READ ONLY view with the WITH READ ONLY syntax. This would allow read but not DML so we would get an Oracle error such as ORA-01733 if we tried an INSERT. Again for the same reasons as above this would not work for the AUD$UNIFIED case.

Another option could be a READ ONLY TABLESPACE in a similar way to above we can alter a tablespace and make it read only or tread write. But this has the same problems as above for the AUD$UNIFIED case. We cannot simply switch a tablespace between read only and read write as each time we switch to read only all existing read / write transactions need to complete. This is not an option for this security case.

Yes another option is to use a constraint such as the following:

SQL> @cs
Connected.
USER is "SYS"
SQL> alter table orablog.bof_pay_details add constraint bof_pay_details_read_only check(1=1) disable validate;

Table altered.

SQL>

Now test it

SQL> select id,name_on_card from orablog.bof_pay_details;

ID
----------
NAME_ON_CARD
--------------------------------------------------------------------------------
1
Mr David Bentley

2
Mr Martin Chisholm


SQL> update orablog.bof_pay_details set last_four='1111';
update orablog.bof_pay_details set last_four='1111'
*
ERROR at line 1:
ORA-25128: No insert/update/delete on table with constraint
(ORABLOG.BOF_PAY_DETAILS_READ_ONLY) disabled and validated


SQL> delete from orablog.bof_pay_details;
delete from orablog.bof_pay_details
*
ERROR at line 1:
ORA-25128: No insert/update/delete on table with constraint
(ORABLOG.BOF_PAY_DETAILS_READ_ONLY) disabled and validated


SQL>

Clearly this stops DML but again for the AUD$UNIFIED it would not work to allow the fine grained DML whilst being essentially READ ONLY for everyone else. The constraints on AUDSYS.AUD$UNIFIED confirm this:

SQL> set serveroutput on
SQL> @sc_print 'select * from dba_constraints where table_name=''''AUD$UNIFIED'''''
old 32: lv_str:=translate('&&1','''','''''');
new 32: lv_str:=translate('select * from dba_constraints where table_name=''AUD$UNIFIED''','''','''''');
Executing Query [select * from dba_constraints where table_name='AUD$UNIFIED']
OWNER : AUDSYS
CONSTRAINT_NAME : SYS_C005872
CONSTRAINT_TYPE : C
TABLE_NAME : AUD$UNIFIED
SEARCH_CONDITION : "ENTRY_ID" IS NOT NULL
SEARCH_CONDITION_VC : "ENTRY_ID" IS NOT NULL
R_OWNER :
R_CONSTRAINT_NAME :
DELETE_RULE :
STATUS : ENABLED
DEFERRABLE : NOT DEFERRABLE
DEFERRED : IMMEDIATE
VALIDATED : VALIDATED
GENERATED : GENERATED NAME
BAD :
RELY :
LAST_CHANGE : 17-AUG-21
INDEX_OWNER :
INDEX_NAME :
INVALID :
VIEW_RELATED :
ORIGIN_CON_ID : 3
-------------------------------------------
OWNER : AUDSYS
CONSTRAINT_NAME : SYS_C005873
CONSTRAINT_TYPE : C
TABLE_NAME : AUD$UNIFIED
SEARCH_CONDITION : "STATEMENT_ID" IS NOT NULL
SEARCH_CONDITION_VC : "STATEMENT_ID" IS NOT NULL
R_OWNER :
R_CONSTRAINT_NAME :
DELETE_RULE :
STATUS : ENABLED
DEFERRABLE : NOT DEFERRABLE
DEFERRED : IMMEDIATE
VALIDATED : VALIDATED
GENERATED : GENERATED NAME
BAD :
RELY :
LAST_CHANGE : 17-AUG-21
INDEX_OWNER :
INDEX_NAME :
INVALID :
VIEW_RELATED :
ORIGIN_CON_ID : 3
-------------------------------------------
OWNER : AUDSYS
CONSTRAINT_NAME : SYS_C005874
CONSTRAINT_TYPE : C
TABLE_NAME : AUD$UNIFIED
SEARCH_CONDITION : "EVENT_TIMESTAMP" IS NOT NULL
SEARCH_CONDITION_VC : "EVENT_TIMESTAMP" IS NOT NULL
R_OWNER :
R_CONSTRAINT_NAME :
DELETE_RULE :
STATUS : ENABLED
DEFERRABLE : NOT DEFERRABLE
DEFERRED : IMMEDIATE
VALIDATED : VALIDATED
GENERATED : GENERATED NAME
BAD :
RELY :
LAST_CHANGE : 17-AUG-21
INDEX_OWNER :
INDEX_NAME :
INVALID :
VIEW_RELATED :
ORIGIN_CON_ID : 3
-------------------------------------------
OWNER : AUDSYS
CONSTRAINT_NAME : SYS_C005875
CONSTRAINT_TYPE : C
TABLE_NAME : AUD$UNIFIED
SEARCH_CONDITION : "ACTION" IS NOT NULL
SEARCH_CONDITION_VC : "ACTION" IS NOT NULL
R_OWNER :
R_CONSTRAINT_NAME :
DELETE_RULE :
STATUS : ENABLED
DEFERRABLE : NOT DEFERRABLE
DEFERRED : IMMEDIATE
VALIDATED : VALIDATED
GENERATED : GENERATED NAME
BAD :
RELY :
LAST_CHANGE : 17-AUG-21
INDEX_OWNER :
INDEX_NAME :
INVALID :
VIEW_RELATED :
ORIGIN_CON_ID : 3
-------------------------------------------
OWNER : AUDSYS
CONSTRAINT_NAME : SYS_C005876
CONSTRAINT_TYPE : C
TABLE_NAME : AUD$UNIFIED
SEARCH_CONDITION : "RETURN_CODE" IS NOT NULL
SEARCH_CONDITION_VC : "RETURN_CODE" IS NOT NULL
R_OWNER :
R_CONSTRAINT_NAME :
DELETE_RULE :
STATUS : ENABLED
DEFERRABLE : NOT DEFERRABLE
DEFERRED : IMMEDIATE
VALIDATED : VALIDATED
GENERATED : GENERATED NAME
BAD :
RELY :
LAST_CHANGE : 17-AUG-21
INDEX_OWNER :
INDEX_NAME :
INVALID :
VIEW_RELATED :
ORIGIN_CON_ID : 3
-------------------------------------------

PL/SQL procedure successfully completed.

SQL>

So, we know Oracle has not used these ideas because they are not workable. But, how does Oracle make AUD$UNIFIED work in the way that it does?

Lets jump back a step for a short while. We know Oracle also does not allow a connection to AUDSYS and the cynical me would suggest that is because if we can connect direct to AUDSYS then maybe we can delete the audit trail or we can disable the security if we can find it. If we compare AUDSYS with my user ATK as follows:

SQL> @sc_print 'select * from sys.user$ where name=''''AUDSYS'''''
old 32: lv_str:=translate('&&1','''','''''');
new 32: lv_str:=translate('select * from sys.user$ where name=''AUDSYS''','''','''''');
Executing Query [select * from sys.user$ where name='AUDSYS']
USER# : 8
NAME : AUDSYS
TYPE# : 1
PASSWORD :
DATATS# : 1
TEMPTS# : 3
CTIME : 17-AUG-21
PTIME : 10-JUL-25
EXPTIME :
LTIME :
RESOURCE$ : 0
AUDIT$ :
DEFROLE : 1
DEFGRP# :
DEFGRP_SEQ# :
ASTATUS : 0
LCOUNT : 0
DEFSCHCLASS : DEFAULT_CONSUMER_GROUP
EXT_USERNAME :
SPARE1 : 384
SPARE2 :
SPARE3 : 16382
SPARE4 : S:
;T:
SPARE5 :
SPARE6 :
SPARE7 :
SPARE8 :
SPARE9 : 3
SPARE10 : 0
SPARE11 :
-------------------------------------------

PL/SQL procedure successfully completed.

SQL>

And

SQL> @sc_print 'select * from sys.user$ where name=''''ATK'''''
old 32: lv_str:=translate('&&1','''','''''');
new 32: lv_str:=translate('select * from sys.user$ where name=''ATK''','''','''''');
Executing Query [select * from sys.user$ where name='ATK']
USER# : 668
NAME : ATK
TYPE# : 1
PASSWORD :
DATATS# : 5
TEMPTS# : 3
CTIME : 10-JUL-25
PTIME : 10-JUL-25
EXPTIME : 06-JAN-26
LTIME :
RESOURCE$ : 0
AUDIT$ :
DEFROLE : 1
DEFGRP# :
DEFGRP_SEQ# :
ASTATUS : 0
LCOUNT : 0
DEFSCHCLASS : DEFAULT_CONSUMER_GROUP
EXT_USERNAME :
SPARE1 : 0
SPARE2 :
SPARE3 : 16382
SPARE4 :
S:8F8A89214078675093CE2F62F3E90083A8164FB1FDBD6F7FCF7CEDBAB323;T:62B8A47574C8771
0A37D1E93209788A315BB92B6857875E029D31B144A9BDE3EBC14629C9BAF7C1AC66BC703E6675B6
A8153315B86127C09DD36DE0BA334CE7F4E0DB06BD7F4421FF8CB75B7364DD56A
SPARE5 :
SPARE6 : 10-JUL-25
SPARE7 :
SPARE8 :
SPARE9 : 3
SPARE10 : 0
SPARE11 :
-------------------------------------------

PL/SQL procedure successfully completed.

SQL>


The only difference is spare1 being set to 384 for AUDSYS and 0 for ATK. What other values are there for SPARE1 across the database:

SQL> col name for a30
SQL> col spare1 for 9999999999
SQL> col type# for 99
SQL> select name,spare1,type# from sys.user$ where spare1!=0;

NAME SPARE1 TYPE#
------------------------------ ----------- -----
SYS 384 1
PUBLIC 400 0
CONNECT 384 0
RESOURCE 384 0
DBA 384 0
PDB_DBA 384 0
AUDIT_ADMIN 384 0
AUDIT_VIEWER 384 0
AUDSYS 384 1
SYSTEM 384 1
SELECT_CATALOG_ROLE 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
EXECUTE_CATALOG_ROLE 384 0
CAPTURE_ADMIN 384 0
SYSBACKUP 65920 1
SYSDG 65920 1
SYSKM 65920 1
SYSRAC 65920 1
OUTLN 65920 1
EXP_FULL_DATABASE 384 0
IMP_FULL_DATABASE 384 0
AVTUNE_PKG_ROLE 384 0
REMOTE_SCHEDULER_AGENT 65920 1

NAME SPARE1 TYPE#
------------------------------ ----------- -----
CDB_DBA 384 0
APPLICATION_TRACE_VIEWER 384 0
ACCHK_READ 384 0
LOGSTDBY_ADMINISTRATOR 384 0
DBFS_ROLE 384 0
GSMUSER_ROLE 384 0
GSMROOTUSER_ROLE 384 0
GSMADMIN_INTERNAL 65920 1
GSMUSER 65920 1
DIP 65920 1
AQ_ADMINISTRATOR_ROLE 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
AQ_USER_ROLE 384 0
DATAPUMP_EXP_FULL_DATABASE 384 0
DATAPUMP_IMP_FULL_DATABASE 384 0
ADM_PARALLEL_EXECUTE_TASK 384 0
PROVISIONER 384 0
XS_SESSION_ADMIN 384 0
XS_NAMESPACE_ADMIN 384 0
XS_CACHE_ADMIN 384 0
XS_CONNECT 384 0
XS$NULL 65920 1
HS_ADMIN_EXECUTE_ROLE 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
DBSFWUSER 65920 1
GATHER_SYSTEM_STATISTICS 384 0
OPTIMIZER_PROCESSING_RATE 384 0
DBMS_MDX_INTERNAL 384 0
ORACLE_OCM 65920 1
BDSQL_ADMIN 384 0
BDSQL_USER 384 0
RECOVERY_CATALOG_OWNER 384 0
RECOVERY_CATALOG_OWNER_VPD 384 0
RECOVERY_CATALOG_USER 384 0
EM_EXPRESS_BASIC 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
EM_EXPRESS_ALL 384 0
SYSUMF_ROLE 384 0
SYS$UMF 65920 1
MAINTPLAN_APP 384 0
SCHEDULER_ADMIN 384 0
PPLB_ROLE 384 0
DGPDB_INT 65920 1
HS_ADMIN_SELECT_ROLE 384 0
SODA_APP 384 0
HS_ADMIN_ROLE 384 0
GLOBAL_AQ_USER_ROLE 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
OEM_ADVISOR 384 0
OEM_MONITOR 384 0
DBSNMP 65920 1
APPQOSSYS 65920 1
GSMADMIN_ROLE 384 0
GSM_POOLADMIN_ROLE 384 0
GDS_CATALOG_SELECT 384 0
GSMCATUSER 65920 1
GGSYS 65920 1
GGSYS_ROLE 384 0
XDB 65920 1

NAME SPARE1 TYPE#
------------------------------ ----------- -----
ANONYMOUS 65920 1
XDBADMIN 384 0
XDB_SET_INVOKER 384 0
AUTHENTICATEDUSER 384 0
XDB_WEBSERVICES 384 0
XDB_WEBSERVICES_WITH_PUBLIC 384 0
XDB_WEBSERVICES_OVER_HTTP 384 0
OLAPSYS 65920 1
DATAPATCH_ROLE 384 0
WMSYS 65920 1
WM_ADMIN_ROLE 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
JAVAUSERPRIV 384 0
JAVAIDPRIV 384 0
JAVASYSPRIV 384 0
JAVADEBUGPRIV 384 0
EJBCLIENT 384 0
JMXSERVER 384 0
DBJAVASCRIPT 384 0
OJVMSYS 65920 1
JAVA_ADMIN 384 0
CTXSYS 65920 1
CTXAPP 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
ORDSYS 65920 1
ORDDATA 65920 1
ORDPLUGINS 65920 1
SI_INFORMTN_SCHEMA 65920 1
ORDADMIN 384 0
OLAP_XS_ADMIN 384 0
DVSYS 65920 1
DV_SECANALYST 384 0
OLAP_DBA 384 0
OLAP_USER 384 0
MDSYS 65920 1

NAME SPARE1 TYPE#
------------------------------ ----------- -----
MDDATA 65920 1
RDFCTX_ADMIN 384 0
LBACSYS 65920 1
LBAC_DBA 384 0
DVF 65920 1
DV_MONITOR 384 0
DV_ADMIN 384 0
DV_OWNER 384 0
DV_ACCTMGR 384 0
DV_PATCH_ADMIN 384 0
DV_STREAMS_ADMIN 384 0

NAME SPARE1 TYPE#
------------------------------ ----------- -----
DV_GOLDENGATE_ADMIN 384 0
DV_XSTREAM_ADMIN 384 0
DV_GOLDENGATE_REDO_ACCESS 384 0
DV_AUDIT_CLEANUP 384 0
PDBADMIN 2097152 1
DV_DATAPUMP_NETWORK_LINK 384 0
DV_POLICY_OWNER 384 0

128 rows selected.

SQL>

It seems that only the users SYS, SYSTEM and AUDSYS have a SPARE1 of 384, the rest are roles. These values such as 384 are flags so some with higher values may also contain the same flags as AUDSYS. If we look at the dsec.bsq file we can see that 384 = 128+256 which are the flags for Oracle Maintained and CDB Common User. So nothing that explains how AUDSYS cannot be logged into.

A review of AUD$UNIFIED in DBA_TABLES and also SYS.TAB$ does not reveal any significant flags or values in a quick look that could explain how it works the way it does.

A check of dependencies shows that only the view UNIFIED_AUDIT_TRAIL depends on AUD$UNIFIED and no upwards dependencies.

What is Odd is the fact that if you search all the scripts in $ORACLE_HOME/rdbms/admin there is nothing obvious that enables security on AUDSYS or AUD$UNIFIED. BUT, also its obvious that the user and table are created (using SYS) and then altered - i.e. violating the no DML and no DDL protection. So, we would have to assume that because this would be done in non-live status the security is off; i.e. the database is being built.

OK, can we try another option to connect as AUDSYS:

SQL> alter user audsys grant connect through atk;
alter user audsys grant connect through atk
*
ERROR at line 1:
ORA-28154: Proxy user may not act as client 'AUDSYS'


No, another internal security policy captures this. What privileges does AUDSYS have:

SQL> @cs
Connected.
USER is "SYS"
SQL> @find_all_privs



find_all_privs: Release 1.0.7.0.0 - Production on Sat Jul 12 19:51:16 2025
Copyright (c) 2004 PeteFinnigan.com Limited. All rights reserved.

NAME OF USER TO CHECK [ORCL]: AUDSYS
OUTPUT METHOD Screen/File [S]:
FILE NAME FOR OUTPUT [priv.lst]:
OUTPUT DIRECTORY [DIRECTORY or file (/tmp)]:

User => AUDSYS has been granted the following privileges
====================================================================
SYS PRIV => ALTER SESSION grantable => NO
SYS PRIV => CREATE JOB grantable => NO
SYS PRIV => CREATE TABLE grantable => NO
SYS PRIV => SELECT ANY DICTIONARY grantable => NO
SYS PRIV => SET CONTAINER grantable => NO
TABLE PRIV => DELETE object => SYS.DAM_CLEANUP_JOBS$ grantable => NO
TABLE PRIV => DELETE object => SYS.DAM_CONFIG_PARAM$ grantable => NO
TABLE PRIV => DELETE object => SYS.DAM_LAST_ARCH_TS$ grantable => NO
TABLE PRIV => EXECUTE object => SYS.AUD_PDB_LIST grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_ASSERT grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_AUDIT_MGMT_SUPPORT grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_INTERNAL_LOGSTDBY grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_LOCK grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_PDB_EXEC_SQL grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_SCHEDULER grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_SESSION grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_SQL grantable => NO
TABLE PRIV => EXECUTE object => SYS.DBMS_STATS grantable => NO
TABLE PRIV => EXECUTE object => SYS.GET_AUD_PDB_LIST grantable => NO
TABLE PRIV => INSERT object => SYS.DAM_CLEANUP_JOBS$ grantable => NO
TABLE PRIV => INSERT object => SYS.DAM_CONFIG_PARAM$ grantable => NO
TABLE PRIV => INSERT object => SYS.DAM_LAST_ARCH_TS$ grantable => NO
TABLE PRIV => READ object => SYS.DBA_XS_AUDIT_POLICY_OPTIONS grantable => NO
TABLE PRIV => READ object => SYS.GV_$INSTANCE grantable => NO
TABLE PRIV => READ object => SYS.GV_$UNIFIED_AUDIT_TRAIL grantable => NO
TABLE PRIV => READ object => SYS.VW_X$AUD_XS_ACTIONS grantable => NO
TABLE PRIV => READ object => SYS.V_$CONTAINERS grantable => NO
TABLE PRIV => READ object => SYS.V_$DATABASE grantable => NO
TABLE PRIV => READ object => SYS.V_$INSTANCE grantable => NO
TABLE PRIV => READ object => SYS.V_$OPTION grantable => NO
TABLE PRIV => READ object => SYS.V_$VERSION grantable => NO
TABLE PRIV => UPDATE object => SYS.DAM_CLEANUP_JOBS$ grantable => NO
TABLE PRIV => UPDATE object => SYS.DAM_CONFIG_PARAM$ grantable => NO
TABLE PRIV => UPDATE object => SYS.DAM_LAST_ARCH_TS$ grantable => NO

PL/SQL procedure successfully completed.

For updates please visit http://www.petefinnigan.com/tools.htm

SQL>

Interesting, well maybe a couple of things. AUDSYS can do ALTER SESSION but we do not know specifically why and it also has SELECT ANY DICTIONARY rather than specific direct grants on other SYS objects but it has a lot of direct grants anyway.

What objects does AUDSYS own:

SQL> col owner for a30
SQL> col object_name for a30
SQL> col object_type for a30
SQL> set lines 220
SQL> l
1* select object_name,object_type from dba_objects where owner='AUDSYS'
SQL> /

OBJECT_NAME OBJECT_TYPE
------------------------------ ------------------------------
DBMS_AUDIT_MGMT PACKAGE
AUD$UNIFIED TABLE
SYS_IL0000019880C00097$$ INDEX
SYS_LOB0000019880C00097$$ LOB
SYS_IL0000019880C00031$$ INDEX
SYS_LOB0000019880C00031$$ LOB
SYS_IL0000019880C00030$$ INDEX
SYS_LOB0000019880C00030$$ LOB
UNIFIED_AUDIT_TRAIL VIEW
CDB_UNIFIED_AUDIT_TRAIL VIEW
DBA_XS_AUDIT_TRAIL VIEW
CDB_XS_AUDIT_TRAIL VIEW
DBMS_AUDIT_MGMT_LIB LIBRARY
DBMS_AUDIT_MGMT PACKAGE BODY
DBMS_AUDIT_MGMT_LSBY PACKAGE
DBMS_AUDIT_MGMT_LSBY PACKAGE BODY
DV$CONFIGURATION_AUDIT VIEW
DV$ENFORCEMENT_AUDIT VIEW
AUD$UNIFIED TABLE PARTITION
SYS_IL0000019880C00097$$ INDEX PARTITION
SYS_LOB0000019880C00097$$ LOB PARTITION
SYS_IL0000019880C00031$$ INDEX PARTITION

OBJECT_NAME OBJECT_TYPE
------------------------------ ------------------------------
SYS_LOB0000019880C00031$$ LOB PARTITION
SYS_IL0000019880C00030$$ INDEX PARTITION
SYS_LOB0000019880C00030$$ LOB PARTITION
AUD$UNIFIED TABLE PARTITION
SYS_IL0000019880C00097$$ INDEX PARTITION
SYS_LOB0000019880C00097$$ LOB PARTITION
SYS_IL0000019880C00031$$ INDEX PARTITION
SYS_LOB0000019880C00031$$ LOB PARTITION
SYS_IL0000019880C00030$$ INDEX PARTITION
SYS_LOB0000019880C00030$$ LOB PARTITION
AUD$UNIFIED TABLE PARTITION
SYS_IL0000019880C00097$$ INDEX PARTITION
SYS_LOB0000019880C00097$$ LOB PARTITION
SYS_IL0000019880C00031$$ INDEX PARTITION
SYS_LOB0000019880C00031$$ LOB PARTITION
SYS_IL0000019880C00030$$ INDEX PARTITION
SYS_LOB0000019880C00030$$ LOB PARTITION

39 rows selected.

SQL>

Nothing stands out except the two Database Vault tables but a quick look at those shows no rows and does not reveal anything related to protecting the audit trail.

We should try the obvious and grant DELETE to PUBLIC on the audit table:

SQL> grant delete on audsys.aud$unified to public;

Grant succeeded.

SQL> delete from audsys.aud$unified;
delete from audsys.aud$unified
*
ERROR at line 1:
ORA-46385: DML and DDL operations are not allowed on table
"AUDSYS"."AUD$UNIFIED".


SQL>

Of course this does not work. Lets check some other basics. Are there are triggers on the audit trail:

SQL> select * from dba_triggers where table_name='AUD$UNIFIED';

no rows selected

SQL>

No, any Database Vault:

SQL> select realm_name,owner,object_name,object_type
2 from dba_dv_realm_object where object_name='AUD$UNIFIED';

no rows selected

SQL>

No, any VPD:

SQL> select * from dba_policies where object_name='AUD$UNIFIED';

no rows selected

SQL>

No, any label security:

SQL> select * from all_sa_schema_policies where schema_name='AUDSYS';

no rows selected

SQL>

SQL> select * from all_sa_table_policies where schema_name='AUDSYS';

no rows selected

SQL>

No, any RAS:

SQL> select * from dba_xs_objects where owner='AUDSYS';

no rows selected

SQL>

There could be parameters that control this behaviour but a quick look at normal parameters and also hidden ones did not reveal anything. Also Oracle may use events to turn this on or off but a quick search of the /rdbms/admin folder did not reveal anything obvious BUT there could be an event that turns on security for these rules

In summary this is only a quick investigation and i might be wrong BUT it does look like Oracle use internal security policies that we cannot access or use ourselves or they are not immediately obvious or it is simply hard coded in C. It would be great if we could use the same facilities BUT that would also open up the possibility of turning them off again.

If we use some of the core database features such as READ ONLY tables or constraints then the owner or a DBA could turn them off. That is the problem as why Oracle really needs to implement things like this in a stronger way

PS: Since I posted the first part of this blog Christoph posted a message on my twitter account that he was able to truncate the AUDSYS.AUD$UNIFIED but only by using gdb to change a value in memory (in the SGA but not exposed via an x$ table). So, I was right that this mechanism is internal by setting a flag. How that flag is set, I do not know. It is likely the C code checks the flag and if set allows the action (truncate, logon,...). How would I guess the flag is set?, an event or perhaps a hidden parameter but more likely an event. But, i do not know

#oracleace #sym_42 #oracle #security #audit #unified #protect #audittrail #databreach

New PL/SQL Unwrapper Available

I was emailed by Cameron overnight to tell me that he has written a new unwrapper for PL/SQL.

There have been no public unwrappers for 9ir2 and lower available on the internet for a long time. There were a number of unwrappers available at some points in the past from Russia, China and others. These all disappeared a long time ago and at least one of them, if you downloaded it would infect your PC.

Unwrapping 10g and higher is much simpler than pre 10g as the process is a combination of a substitution cypher, unix compress and base64. There are plenty of free 10g and higher unwrapper tools available.

I first created an unwrapper for PL/SQL over 20 years ago and I have spoken and written about unwrapping PL/SQL many times and I also spoke at Black Hat in las Vegas in 2006 about the same subject. Some examples of some posts and a link to my talk at Black Hat:

The unwrapper for 9i and lower released by Cameron is a massive peice of work and is GPL and released on his GitHub - PL/SQL Unwrapper. Have a look and if you still want me to unwrap lost source code then please contact me as our unwrapper is written in C and runs on the command line and does not need access to the database or installing the PL/SQL code in another database or the unwrapper to be installed in the database.

#oracleace #sym_42 #oracle #plsql #unwrap #reverse #engineering #security