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.

AI Comparison for Oracle Security Code Generation

I am running a Mac Book Pro M5 with 64gb of RAM and a 2tb SSD. I wanted to use local LLMs to generate Oracle security related PL/SQL to see how good these models are at generating PL/SQL code and also how they perform against each other in terms of elapsed time and also tokens per second. In AI we can measure the speed of the AI in elapsed time and also tokens per second. Tokens are the chunks of text that the LLM then processes. They can be words or parts of words.

I tested three models, qwen2.5-coder, qwen3-coder:30b and qwen3-coder-next.

I used the same prompt for each of the models. The prompt is:
please write a PL/SQL public package procedures and any supporting private procedures and functions where that public procedure can encrypt and decrypt data passed and base64 encode/decode the result

I ran qwen2.5-coder and it generated this text at the start:

Certainly! Below is a complete PL/SQL package named encrypt_decrypt_pkg that includes public procedures for encrypting and decrypting data, as well as base64 encoding and decoding. The package uses AES256 encryption with CBC mode and PKCS#5 padding.

and it went on to generate a PL/SQL package header, package body and some example use cases of the resultant code. The header generated is:

CREATE OR REPLACE PACKAGE encrypt_decrypt_pkg AS
-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
);

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
);
END encrypt_decrypt_pkg;
/

The package body generated is:

CREATE OR REPLACE PACKAGE BODY encrypt_decrypt_pkg AS
-- Private function to generate a key for encryption
FUNCTION generate_key(p_key IN VARCHAR2 DEFAULT NULL) RETURN RAW IS
v_key RAW(32);
BEGIN
IF p_key IS NOT NULL THEN
-- Use provided key and hash it to 256 bits
v_key := DBMS_CRYPTO.HASH(UTL_I18N.STRING_TO_RAW(p_key, 'AL32UTF8'), DBMS_CRYPTO.HASH_SH256);
ELSE
-- Generate a random key
v_key := DBMS_CRYPTO.RANDOMBYTES(32);
END IF;

RETURN v_key;
END generate_key;

-- Private function to base64 encode raw data
FUNCTION base64_encode_raw(p_data IN RAW) RETURN VARCHAR2 IS
v_encoded RAW(32767);
BEGIN
v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data, TRUE);
RETURN UTL_RAW.CAST_TO_VARCHAR2(v_encoded);
END base64_encode_raw;

-- Private function to base64 decode string to raw data
FUNCTION base64_decode_raw(p_data IN VARCHAR2) RETURN RAW IS
v_decoded RAW(32767);
BEGIN
v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_data));
RETURN v_decoded;
END base64_decode_raw;

-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_iv RAW(16); -- Initialization Vector for AES-256-CBC
v_encrypted_raw RAW(32767);
BEGIN
-- Generate encryption key
v_key := generate_key(p_key);

-- Generate a random initialization vector (IV)
v_iv := DBMS_CRYPTO.RANDOMBYTES(16);

-- Encrypt data using AES256 in CBC mode with PKCS#5 padding
v_encrypted_raw := DBMS_CRYPTO.ENCRYPT(
src => UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,
key => v_key,
iv => v_iv
);

-- Prepend the IV to the encrypted data and base64 encode it
p_encrypted_data := base64_encode_raw(v_iv || v_encrypted_raw);
END encrypt_and_encode;

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_iv RAW(16); -- Initialization Vector for AES-256-CBC
v_combined_raw RAW(32767);
v_decrypted_raw RAW(32767);
BEGIN
-- Generate decryption key
v_key := generate_key(p_key);

-- Base64 decode the input data
v_combined_raw := base64_decode_raw(p_encrypted_data);

-- Extract the IV from the beginning of the combined data
v_iv := UTL_RAW.SUBSTR(v_combined_raw, 1, 16);

-- Extract the encrypted data after the IV
v_decrypted_raw := UTL_RAW.SUBSTR(v_combined_raw, 17);

-- Decrypt the data using AES256 in CBC mode with PKCS#5 padding
p_decrypted_data := UTL_I18N.RAW_TO_STRING(
DBMS_CRYPTO.DECRYPT(
src => v_decrypted_raw,
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,
key => v_key,
iv => v_iv
),
'AL32UTF8'
);
END decode_and_decrypt;

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
) IS
v_raw_data RAW(32767);
BEGIN
-- Convert string to raw data
v_raw_data := UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8');

-- Base64 encode the raw data
p_encoded_data := base64_encode_raw(v_raw_data);
END base64_encode;

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
) IS
v_decoded_raw RAW(32767);
BEGIN
-- Base64 decode the input data
v_decoded_raw := base64_decode_raw(p_encoded_data);

-- Convert raw data back to string
p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');
END base64_decode;
END encrypt_decrypt_pkg;
/

The example usage code is:

-- Example 1: Encrypt and encode data
DECLARE
v_encrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.encrypt_and_encode(
p_data => 'Hello World',
p_encrypted_data => v_encrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Encrypted and encoded: ' || v_encrypted);
END;
/

-- Example 2: Decode and decrypt data
DECLARE
v_decrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.decode_and_decrypt(
p_encrypted_data => 'your_encrypted_string_here',
p_decrypted_data => v_decrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Decrypted data: ' || v_decrypted);
END;
/

-- Example 3: Simple base64 encoding
DECLARE
v_encoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_encode(
p_data => 'Hello World',
p_encoded_data => v_encoded
);
DBMS_OUTPUT.PUT_LINE('Base64 encoded: ' || v_encoded);
END;
/

-- Example 4: Simple base64 decoding
DECLARE
v_decoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_decode(
p_encoded_data => 'SGVsbG8gV29ybGQ=',
p_decoded_data => v_decoded
);
DBMS_OUTPUT.PUT_LINE('Base64 decoded: ' || v_decoded);
END;
/

No privileges are generated for DBMS_CRYPTO or UTL_ENCODE so we will do these manually at the end but first what did qwen3-coder:30b generate?

Here's a complete PL/SQL package for encryption/decryption with Base64 encoding/decoding. It generated the package header and body together unlike qwen2.5-coder:

CREATE OR REPLACE PACKAGE encrypt_decrypt_pkg AS
-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
);

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
);

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
);
END encrypt_decrypt_pkg;
/

CREATE OR REPLACE PACKAGE BODY encrypt_decrypt_pkg AS
-- Private function to generate a key for encryption
FUNCTION generate_key(p_key IN VARCHAR2 DEFAULT NULL) RETURN RAW IS
v_key RAW(32);
BEGIN
IF p_key IS NOT NULL THEN
-- Use provided key
v_key := DBMS_CRYPTO.HASH(p_key, DBMS_CRYPTO.HASH_SH256);
ELSE
-- Generate a random key
v_key := DBMS_CRYPTO.RANDOMBYTES(32);
END IF;

RETURN v_key;
END generate_key;

-- Private function to base64 encode raw data
FUNCTION base64_encode_raw(p_data IN RAW) RETURN VARCHAR2 IS
v_encoded VARCHAR2(32767);
BEGIN
v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data);
RETURN v_encoded;
END base64_encode_raw;

-- Private function to base64 decode string to raw data
FUNCTION base64_decode_raw(p_data IN VARCHAR2) RETURN RAW IS
v_decoded RAW(32767);
BEGIN
v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_data));
RETURN v_decoded;
END base64_decode_raw;

-- Public procedure to encrypt data and base64 encode it
PROCEDURE encrypt_and_encode(
p_data IN VARCHAR2,
p_encrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_encrypted_raw RAW(32767);
BEGIN
-- Generate encryption key
v_key := generate_key(p_key);

-- Encrypt data using AES256 in CBC mode with PKCS#5 padding
v_encrypted_raw := DBMS_CRYPTO.ENCRYPT(
src => UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8'),
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC + DBMS_CRYPTO.PKCS5,
key => v_key
);

-- Base64 encode the encrypted data
p_encrypted_data := base64_encode_raw(v_encrypted_raw);
END encrypt_and_encode;

-- Public procedure to decrypt base64 decoded data
PROCEDURE decode_and_decrypt(
p_encrypted_data IN VARCHAR2,
p_decrypted_data OUT VARCHAR2,
p_key IN VARCHAR2 DEFAULT NULL
) IS
v_key RAW(32);
v_decrypted_raw RAW(32767);
BEGIN
-- Generate decryption key
v_key := generate_key(p_key);

-- Base64 decode the input data
v_decrypted_raw := base64_decode_raw(p_encrypted_data);

-- Decrypt the data using AES256 in CBC mode with PKCS#5 padding
p_decrypted_data := UTL_I18N.RAW_TO_STRING(
DBMS_CRYPTO.DECRYPT(
src => v_decrypted_raw,
typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC + DBMS_CRYPTO.PKCS5,
key => v_key
),
'AL32UTF8'
);
END decode_and_decrypt;

-- Public procedure to base64 encode data
PROCEDURE base64_encode(
p_data IN VARCHAR2,
p_encoded_data OUT VARCHAR2
) IS
v_raw_data RAW(32767);
BEGIN
-- Convert string to raw data
v_raw_data := UTL_I18N.STRING_TO_RAW(p_data, 'AL32UTF8');

-- Base64 encode the raw data
p_encoded_data := base64_encode_raw(v_raw_data);
END base64_encode;

-- Public procedure to base64 decode data
PROCEDURE base64_decode(
p_encoded_data IN VARCHAR2,
p_decoded_data OUT VARCHAR2
) IS
v_decoded_raw RAW(32767);
BEGIN
-- Base64 decode the input data
v_decoded_raw := base64_decode_raw(p_encoded_data);

-- Convert raw data back to string
p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');
END base64_decode;
END encrypt_decrypt_pkg;
/

Some sample usage code was generated:

-- Example 1: Encrypt and encode data
DECLARE
v_encrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.encrypt_and_encode(
p_data => 'Hello World',
p_encrypted_data => v_encrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Encrypted and encoded: ' || v_encrypted);
END;
/

-- Example 2: Decode and decrypt data
DECLARE
v_decrypted VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.decode_and_decrypt(
p_encrypted_data => 'your_encrypted_string_here',
p_decrypted_data => v_decrypted,
p_key => 'my_secret_key'
);
DBMS_OUTPUT.PUT_LINE('Decrypted data: ' || v_decrypted);
END;
/

-- Example 3: Simple base64 encoding
DECLARE
v_encoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_encode(
p_data => 'Hello World',
p_encoded_data => v_encoded
);
DBMS_OUTPUT.PUT_LINE('Base64 encoded: ' || v_encoded);
END;
/

-- Example 4: Simple base64 decoding
DECLARE
v_decoded VARCHAR2(4000);
BEGIN
encrypt_decrypt_pkg.base64_decode(
p_encoded_data => 'SGVsbG8gV29ybGQ=',
p_decoded_data => v_decoded
);
DBMS_OUTPUT.PUT_LINE('Base64 decoded: ' || v_decoded);
END;
/

As with qwen2.5-coder this qwen3-coder also did not generate the grants needed to run this code.

OK, lets put the 2.5 and 3 code generated into scripts and see if this will compile and run. In both cases I first granted CREATE PROCEDURE, EXECUTE ON DBMS_CRYPTO and EXECUTE ON UTL_ENCODE and created sample users.

I created qwen25.sql and qwen3.sql and added the exact package headers and bodies generated by each LLM model. I have had to add code to create users in each case, either qwen25 or qwen3 and granted privileges to allow the packages to be installed and used. The code for qwen25 for instance to create the user and make grants is:

connect sys/oracle@//192.168.56.34:1521/freepdb1 as sysdba
drop user qwen25 cascade;
create user qwen25 identified by qwen25;
grant create session to qwen25;
grant create procedure to qwen25;
grant execute on dbms_crypto to qwen25;
grant execute on utl_encode to qwen25;

connect qwen25/qwen25@//192.168.56.34:1521/freepdb1
set serveroutput on

I also added show err for each of the package header and body creations for each code generated by the LLM. I also did not include the demos of each package as we are interested initially as to whether the code compiles or not.

Now let us try and run qwen25.sql

SQL> @qwen25
Connected.

User dropped.


User created.


Grant succeeded.


Grant succeeded.


Grant succeeded.


Grant succeeded.

Connected.

Package created.

No errors.

Warning: Package Body created with compilation errors.

Errors for PACKAGE BODY ENCRYPT_DECRYPT_PKG:

LINE/COL ERROR
-------- -----------------------------------------------------------------
21/9 PL/SQL: Statement ignored
21/22 PLS-00306: wrong number or types of arguments in call to
'BASE64_ENCODE'

50/9 PL/SQL: Statement ignored
52/32 PLS-00302: component 'AES256' must be declared
85/9 PL/SQL: Statement ignored
85/38 PLS-00302: component 'RAW_TO_STRING' must be declared
121/9 PL/SQL: Statement ignored
121/36 PLS-00302: component 'RAW_TO_STRING' must be declared
SQL>

The first error at line 21 is because the code shows:

v_encoded := UTL_ENCODE.BASE64_ENCODE(p_data, TRUE);

And UTL_ENCODE.BASE64_ENCODE() only accepts one parameter, the data itself. Even checking Oracle 11 this function did not have an extra parameter. The next error is line 52:

typ => DBMS_CRYPTO.AES256 + DBMS_CRYPTO.CBC_PKCS5,

The global constants are wrong; for instance DBMS_CRYPTO.AES256 should be DBMS_CRYPTO.ENCRYPT_AES256 and DBMS_CRYPTO.CBC_PKCS5 should probably be DBMS_CRYPTO.CHAIN_CBC + DBMS_CRYPTO.PAD_PKCS5.

The next two errors are for RAW_TO_STRING() for instance:

p_decoded_data := UTL_I18N.RAW_TO_STRING(v_decoded_raw, 'AL32UTF8');

The function RAW_TO_STRING() does not exist. It should probably be RAW_TO_CHAR() that returns a VARCHAR2.

OK, so a few errors, let us now try the qwen3 generated code by running my qwen3.sql script:

SQL> @qwen3
Connected.
drop user qwen3 cascade
*
ERROR at line 1:
ORA-01918: user 'QWEN3' does not exist



User created.


Grant succeeded.


Grant succeeded.


Grant succeeded.


Grant succeeded.

Connected.

Package created.

No errors.

Warning: Package Body created with compilation errors.

Errors for PACKAGE BODY ENCRYPT_DECRYPT_PKG:

LINE/COL ERROR
-------- -----------------------------------------------------------------
46/9 PL/SQL: Statement ignored
48/32 PLS-00302: component 'AES256' must be declared
72/9 PL/SQL: Statement ignored
72/38 PLS-00302: component 'RAW_TO_STRING' must be declared
107/9 PL/SQL: Statement ignored
107/36 PLS-00302: component 'RAW_TO_STRING' must be declared
SQL>

Almost the same errors but less; the AES256 constant needs to be changed as well as its compatriots around PKCS5 and CBV the same as the 2.5 model. Also the same RAW_TO_STRING() function needs to be changed.

I am not going to fix them as part of this demo as I have learned enough on this case This was an experiment to see if we can use 3 different LLM models to generate PL/SQL code to encrypt and decrypt data in an Oracle database. A reminder of the prompt I used is here
please write a PL/SQL public package procedures and any supporting private procedures and functions where that public procedure can encrypt and decrypt data passed and base64 encode/decode the result

I thought I was clear; but maybe not. I wanted to have an encrypt() and decrypt() function that privately converted the encrypted data to BASE64 and on decrypt accept BASE64 data and decode it first and then decrypt. In other words only two public functions; encrypt and decrypt that are public. There is a combined function BUT I would have left the BASE64 functions private.

So, we had three LLMs and one failed and fell over; this was qwen3-coder-next. This ran for 51 minutes before falling over completely in the open-webui interface and where it did not generate any code. So this is not included further here.

The qwen2.5-coder LLM took 3723 ms to run and complete generating the code; which is 3.73 seconds. The model was processing at around 24 tokens per second. Not shabby.

The qwen3-coder:30b LLM took 1023 ms which is 1.023 seconds and was processing 134 tokens per second.

In summary, qwen3-coder-next is not useful at least on my mac; it did do a lot of reasoning before it failed so is clearly a better LLM for deeper analysis BUT if it failed then this does not matter. Qwen3-coder:30b ran in time in one third of the time of qwen2.5-coder and in terms of tokens per second it was processing 5.58 tokens per second faster than the 2.5 model.

So, qwen3-coder:30b is clearly faster than qwen2.5-coder and its code that it generated is slightly less broken than the 2.5 model BUT both produced code that did not work. I could have fed the output from each back into the LLM and it may have helped but the purpose of this post was to show two things; 1) the speed of three models compared to each other to generate PL/SQL and the quality of the code.

As I have said in my last blog. I think these models are more useful if you already know what you are doing. They can be productivity assistants and get you some way along the process but the code in each case needed tweaking/fixing but certainly the qwen3-coder:30b is fast enough for daily use.

#oracleace #sym_42 #ai #llm #qwen3-coder #plsql #oracle #security #generate #code

AI

My day job is Oracle security and that covers a massive area of topics from performing security audits, helping customers secure data in their Oracle databases, forensics, building and designing audit trails and reports, secure coding in PL/SQL, encryption of data from TDE to custom native encryption in PL/SQL or using any manor of Oracle cost options such as Database Vault, Virtual Private database and even third party tools.

I love to code and I code in PL/SQL, C, C++, Lua and .net including VB and c++/cli as well as even developing in vba in Excel.

So, AI? - I still use documentation and search engines to find out a setting or command that I cannot remember when developing for Oracle or configuring and setting up something such as Database vault or audit trails but over the last few months I have been also testing and playing with AI; Chatgpt or google Gemini or Microsoft co-pilot. I want to understand what the AI says compared to what I find out myself on Google.

Standard models are good but for specific subjects such as Oracle they often come up with the wrong answers. I can think of some simple examples I have tried for fun. I asked one of the free AIs how to truncate the unified audit trail in Oracle and it said truncate table unified_audit_trail. hmmm. The AI says it with confidence. Instead of saying; i am not sure or I do not know it tells me to truncate the table. I asked another question on changing a database password to the previous one and it showed me using ALTER USER IDENTIFIED BY VALUES but it did not tell how to get the old password first and also used the very old syntax before spare4 was used. I asked it for fun (because I know already) how to connect with a proxy. It just made up the syntax which was wrong. I also asked is Database Vault available with 21c XE and it said no. I knew it was as I had the license pdf open. I corrected the AI and it said - You are right - Oracle Database Vault is included in Oracle 21c XE. My earlier statement was incorrect. I have many many more examples of incorrect answers gathered over a while.

if you know what you are looking for and know what roughly the answer should be then the AI is good because you can recognise if the answer is probably right or not; is it better than the docs or Google; difficult to say; probably not. I know there are better models that you have to pay for but I am from Yorkshire and we are as they say tight. I also know you can augment the AI in three ways to make it better. You can create a well crafted system prompt and add input data via RAG and also use skills.md files to target the right answers.

My fear is the inexperienced person completely relying on the free chat interfaces and then implementing what it tells them in real systems.

We can use AI such as chatgpt as a web based chat window but of course the power of AI is to be able to use things like agents and automate what you want to do; so instead of getting a sample command in a chat window and the using it yourself you can set up AI to run the command on your real systems.

If you do not really know what you are doing in the first place then you should be wary of AI and not simply use the answers.

There has been a lot of noise and chatter about AI for a long time now with some suggesting the industry is a bubble bigger than the dot com bubble and some talking about the huge costs of teaching models that looks unlikely to be recovered by paying customers. There is also the increases in price and shortages of the GPU hardware and the huge energy costs of the data centers.

In the last few days there have been some prominent news articles discussing AI sovereignty and focusing on building AI internally to retain all of the data / prompts passed to the AI. This is exactly paralleled with my Oracle security day job - protecting the your data. As I said in Oracle security in my trainings and here; it is not the job of Oracle to protect your data; it is your job to project your data using the standard features of the Oracle database and even cost options from Oracle or third party solutions. The same applies in AI. If you want to use AI you must protect your data that is fed into the AI .

Is a free on line tool better than a home run LLM (Large Language Model)? this depends on a lot of things; the hardware that you use, the sources of data that you can add to it via RAG or the context window and of course the model you download and use.

My experience so far with the free tools is that you get blocked quickly particularly if you upload files. Load 2 and you are fine, load three and it says come back tomorrow in 24/25 hours and try again.

I have been playing with a local LLM recently as well to test how good it is. I am feeding it skills files and sources via RAG and testing is in early stages but looks great so far. I m using a Mac Book Pro M5 with 64gb of RAM and a 2 TB SSD. There are so many models to use so I will be trying quite a few. I have tested qwen2.5 coder so far. My experience of setting up the local LLM was great, downloading the models takes the biggest time but setting up to run with a chat interface was simple.

More on this soon as I will report back here on my experience with a local LLM. Having a local LLM that does not have limits and is almost as fast as chatgpt and the answers are at a similar level so far is good.

#oracleace #sym_42 #ai #llm #localllm #oracle #security

Forensic Analysis for records in Oracle with no Timestamp

If we have an Oracle database table with no timestamp can we still get a date/time stamp when something happened?

For instance if a table has a date/time column then yes of course we can know when the records changed assuming the standard "last updated by" and "last updated when" columns like we see in applications such as EBS.

If we have auditing enabled on the table we are interested in and it captures the right events then yes, we can see when a record changed or was added or deleted in that table.

If the database is in archivelog mode and the archive logs are available then we can use the redo/archive logs and log miner to retrieve changes from the database that affect the table we are interested in.

BUT, inevitably there will be no column, audit or redo available so is there any other way we can find out what happened?

A good example would be system grants in the database. So for instance

SQL> grant create session to sec_auditor;

So, we can see grants in the DBA_SYS_PRIVS view and this has the following structure:

C:\>sqlplus sys/oracle@//192.168.56.34:1521/freepdb1 as sysdba

SQL*Plus: Release 19.0.0.0.0 - Production on Sun Jun 7 09:28:16 2026
Version 19.28.0.0.0

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


Connected to:
Oracle AI Database 26ai Free Release 23.26.0.0.0 - Develop, Learn, and Run for Free
Version 23.26.0.0.0

SQL> desc dba_sys_privs
Name Null? Type
----------------------------------------- -------- ----------------------------
GRANTEE VARCHAR2(128)
PRIVILEGE VARCHAR2(40)
ADMIN_OPTION VARCHAR2(3)
COMMON VARCHAR2(3)
INHERITED VARCHAR2(3)

SQL>

As we can see there is no date/timestamp for system privilege grants. I would like to be able to map them if possible as part of an Oracle database security audit opr as part of a forensic investigation of the database.

What is the base tables:

SQL> alter session set container=cdb$root;

Session altered.

SQL>

SQL> set long 1000000
SQL> select text from dba_views where view_name='DBA_SYS_PRIVS';

TEXT
--------------------------------------------------------------------------------
select u.name,spm.name,decode(min(mod(option$, 2)),1,'YES','NO'),
'NO', 'NO'
from sys.system_privilege_map spm, sys.sysauth$ sa, user$ u
where sa.grantee#=u.user# and sa.privilege#=spm.privilege
and bitand(nvl(option$, 0), 4) = 0
group by u.name,spm.name
union all
/* Commonly granted Privileges */
select u.name,spm.name,decode(min(bitand(option$, 16)),16,'YES','NO'),
'YES', decode(SYS_CONTEXT('USERENV', 'CON_ID'), 1, 'NO', 'YES')
from sys.system_privilege_map spm, sys.sysauth$ sa, user$ u

TEXT
--------------------------------------------------------------------------------
where sa.grantee#=u.user# and sa.privilege#=spm.privilege
and bitand(option$,8) = 8
group by u.name,spm.name
union all
/* Federationally granted Privileges */
select u.name,spm.name,decode(min(bitand(option$, 128)),128,'YES','NO'),
'YES',
decode(SYS_CONTEXT('USERENV', 'IS_APPLICATION_PDB'), 'YES', 'YES', 'NO')
from sys.system_privilege_map spm, sys.sysauth$ sa, user$ u
where sa.grantee#=u.user# and sa.privilege#=spm.privilege
and bitand(option$,64) = 64

TEXT
--------------------------------------------------------------------------------
group by u.name,spm.name


SQL>

Quite a complex union of 3 SQLs mapping the user names for grantee and also linking to the system_privilege_map and mainly sys.sysauth$. All the privilege records are basically stored in sysauth$. What is the structure of this table:

SQL> desc sysauth$
Name Null? Type
----------------------------------------- -------- ----------------------------
GRANTEE# NOT NULL NUMBER
PRIVILEGE# NOT NULL NUMBER
SEQUENCE# NOT NULL NUMBER
OPTION$ NUMBER

SQL>

No timestamp is available on this table so we do not have any native columns. The Oracle database uses SCNs (System Change Number) and whilst these are not a timestamp they are useful as they order records. There is a table in the database that is managed by SMON_SCN_TIME table that maintains a short window of mappings between SCN numbers and timestamps. So if the SCN is recent then if we have an SCN we can map to a timestamp. Sounds like it could be useful but there is a window of accuracy with the mapping of 3 seconds so the timestamp can be out by 3 seconds.

So, can we get the SCN from the SYSAUTH$ table for all records / rows?

SQL> select ora_rowscn,grantee#,privilege#,sequence#,option$ from sysauth$ order by sequence#;

ORA_ROWSCN GRANTEE# PRIVILEGE# SEQUENCE# OPTION$
---------- ---------- ---------- ---------- ----------
501880 0 -352 1 12
501880 0 2 2 28
501880 2 -5 3 12
501880 0 3 4 28
501880 3 -40 5 12
501880 3 -60 6 12
501880 3 -80 7 12
501880 3 -90 8 12
501880 3 -105 9 12
501880 3 -140 10 12
501880 3 -151 11 12

ORA_ROWSCN GRANTEE# PRIVILEGE# SEQUENCE# OPTION$
---------- ---------- ---------- ---------- ----------
501880 3 -172 12 12
501880 3 -180 13 12
...
4605568 237 -265 3826
4605568 237 -80 3827
4605568 237 -44 3828
4605568 237 -71 3829
4605568 237 -73 3830
4605568 241 -165 3835
4605568 243 241 3836
4605568 244 242 3837
4605568 243 240 3838
4605568 244 240 3839
4605568 237 239 3840

ORA_ROWSCN GRANTEE# PRIVILEGE# SEQUENCE# OPTION$
---------- ---------- ---------- ---------- ----------
4605568 237 240 3841

1332 rows selected.

SQL>

A few interesting points pop up. 1) There are 1332 rows in this table so you would assume currently there are 1332 system privilege grants:

SQL> select count(*) from dba_sys_privs;

COUNT(*)
----------
1089

SQL>

No, 1089 grants not 1332. We could analyse the difference but that is moving off topic for this discussion. 2) the last row sequence# is 3841 so the assumption here is that there are a very large number of rows missing if we assume one sequence per row and its incremented every time a row is added. Also a quick look shows that there are a lot of gaps in the sequence numbers so that supports the theory of deletions or more correctly revokes of grants.

The biggest issue for our investigation is that the SCN is stored at the block level not the row level. We can see from the output that there are few actual SCNs per row:

SQL> select count(*),ora_rowscn from sysauth$ group by ora_rowscn;

COUNT(*) ORA_ROWSCN
---------- ----------
379 501880
382 617063
368 995672
203 4605568

SQL>

As we can see this table currently uses 4 blocks in my database and from the numbers assigned to each SCN we can see the relative similar number of records for the full blocks as 368 - 382 and clearly the last block with just 203 records is not full yet. We can also see the block numbers in this SQL:

SQL> select count(*),dbms_rowid.rowid_relative_fno(rowid) AS file_no,dbms_rowid.rowid_block_number(rowid) AS block_no from sysauth$ group by dbms_rowid.rowid_relative_fno(rowid),dbms_rowid.rowid_block_number(rowid);

COUNT(*) FILE_NO BLOCK_NO
---------- ---------- ----------
382 0 1490
379 0 1489
368 0 1491
203 0 1492

SQL>

This proves the SCN is per block and each block has between 203 (latest and filling up) and 382 rows

Lets see the timestamp per block

SQL> select distinct scn_to_timestamp(ora_rowscn) from sysauth$;
select distinct scn_to_timestamp(ora_rowscn) from sysauth$
*
ERROR at line 1:
ORA-08181: specified number is not a valid system change number
ORA-06512: at "SYS.SCN_TO_TIMESTAMP", line 1


SQL>

The SCN to timestamp mapping is stated to have a precision of 3 seconds, and up to 120 hours of mapping if automatic undo management is used.- SCN to Timestamp Mapping.

The ORA-08181 message means that the mapping is not stored anymore as the database holds limited mapping

The mapping is in the table sys.smon_scn_time

SQL> desc smon_scn_time
Name Null? Type
----------------------------------------- -------- ----------------------------
THREAD NUMBER
TIME_MP NUMBER
TIME_DP DATE
SCN_WRP NUMBER
SCN_BAS NUMBER
NUM_MAPPINGS NUMBER
TIM_SCN_MAP RAW(1200)
SCN NUMBER
ORIG_THREAD NUMBER

SQL>

We can check the max time range in the table with:

SQL> select to_char(min(time_dp),'DD-MON-YYYY HH24:MI:SS'),to_char(max(time_dp),'DD-MON-YYYY HH24:MI:SS') from smon_scn_time;

TO_CHAR(MIN(TIME_DP),'DD-MON- TO_CHAR(MAX(TIME_DP),'DD-MON-
----------------------------- -----------------------------
20-OCT-2025 15:18:02 07-JUN-2026 08:54:39

SQL>

The range in my database is not realistic as this is a 26ai OVA and I installed it two weeks ago so the date range is in reality smaller

So, can we at least get a time for each block

SQL> select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1492;

TIM ROD
---------------------------------------- -----
05-JUN-26 02.44.52.000000000 PM 1492

SQL>

The other 3 older blocks give ORA-08181 error that the SCN to time map is too old.

SQL> select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1491;
select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1491
*
ERROR at line 1:
ORA-08181: specified number is not a valid system change number
ORA-06512: at "SYS.SCN_TO_TIMESTAMP", line 1


SQL> select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1490;
select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1490
*
ERROR at line 1:
ORA-08181: specified number is not a valid system change number
ORA-06512: at "SYS.SCN_TO_TIMESTAMP", line 1


SQL> select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1489;
select distinct scn_to_timestamp(ora_rowscn) tim,dbms_rowid.rowid_block_number(rowid) rod from sysauth$ where dbms_rowid.rowid_block_number(rowid)=1489
*
ERROR at line 1:
ORA-08181: specified number is not a valid system change number
ORA-06512: at "SYS.SCN_TO_TIMESTAMP", line 1


SQL>

We can also get the min and max timestamps and SCNs and also work out on average how many SCNs per second:

SQL> select to_char(min(time_dp),'DD-MON-YYYY HH24:MI:SS'),to_char(max(time_dp),'DD-MON-YYYY HH24:MI:SS'),min(scn),max(scn),max(scn)-min(scn) diff,(max(time_dp)-min(time_dp))*86400 sec, ((max(time_dp)-min(time_dp))*86400)/(max(scn)-min(scn)) from smon_scn_time where to_char(time_dp,'MM-YYYY')='06-2026';

TO_CHAR(MIN(TIME_DP),'DD-MON- TO_CHAR(MAX(TIME_DP),'DD-MON- MIN(SCN)
----------------------------- ----------------------------- ----------
MAX(SCN) DIFF SEC
---------- ---------- ----------
((MAX(TIME_DP)-MIN(TIME_DP))*86400)/(MAX(SCN)-MIN(SCN))
-------------------------------------------------------
01-JUN-2026 06:12:37 07-JUN-2026 09:24:41 4390900
4755799 364899 529924
1.45224843


SQL>

So 1.45 SCN per second are generated.

The previous blocks are not in the same scn range but we could calculate fir fun

1 - 4390900 - 995672 = 3395228 SCN / 1.45 = 2,341,536 seconds =

SQL> SELECT to_char(TO_DATE('01-JUN-2026 06:12:37','DD-MON-YYYY HH24:MI:SS')-(2341536/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('01-
--------------------
05-MAY-2026 03:47:01

SQL>

2 - 4390900 - 617063 = 3,773,837/1.45 = 2,602,646.2 =

SQL> SELECT to_char(TO_DATE('01-JUN-2026 06:12:37','DD-MON-YYYY HH24:MI:SS')-(2602646/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('01-
--------------------
02-MAY-2026 03:15:11

SQL>

3 - 4390900 - 501880 = 3889020/1.45=2682082 =

SQL> SELECT to_char(TO_DATE('01-JUN-2026 06:12:37','DD-MON-YYYY HH24:MI:SS')-(2682082/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('01-
--------------------
01-MAY-2026 05:11:15

SQL>

This is clearly wrong as the min time in the SCN map was October 2025 BUT this is a pre-built 26ai OVA and it was not run until 01-JUN-2026 by me. BUT the calculations show how you may get a very rough ball park for a system that is not an OVA that has been stopped and not used until someone starts to install and use it

We can get the last change at a block level - i.e. a date time for the last grant of a SYSTEM privilege but we cannot get timestamps for each grant done since I started to use the database unless we have an audit trail

BUT, often when an attack occurs we want to try and understand what happened and when and part of that is system grants

We can also repeat the calculation not from current SCN map but also for the older blocks with the last SCN map:


SQL> SELECT to_char(TO_DATE('20-OCT-2025 15:18:02','DD-MON-YYYY HH24:MI:SS')-(2341536/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('20-
--------------------
23-SEP-2025 12:52:26

SQL> SELECT to_char(TO_DATE('20-OCT-2025 15:18:02','DD-MON-YYYY HH24:MI:SS')-(2602646/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('20-
--------------------
20-SEP-2025 12:20:36

SQL> SELECT to_char(TO_DATE('20-OCT-2025 15:18:02','DD-MON-YYYY HH24:MI:SS')-(2682082/86400),'DD-MON-YYYY HH24:MI:SS') from dual;

TO_CHAR(TO_DATE('20-
--------------------
19-SEP-2025 14:16:40

SQL>

This is probably more realistic

What is database created date?

SQL> select to_char(created,'DD-MON-YYYY HH24:MI:SS') from v$database;

TO_CHAR(CREATED,'DD-MON-YYYYH
-----------------------------
20-OCT-2025 15:04:47

SQL>

And the date time of SYS

SQL> select to_char(ctime,'DD-MON-YYYY HH24:MI:SS'),ptime,exptime,ltime,spare6 from user$ where name='SYS';

TO_CHAR(CTIME,'DD-MON-YYYYHH2 PTIME EXPTIME LTIME SPARE6
----------------------------- --------- --------- --------- ---------
08-OCT-2025 22:00:58

SQL>

And all users

SQL> set lines 220
SQL> col name for a30
SQL> col ctime for a20
SQL> col ptime for a20
SQL> col exptime for a20
SQL> col ltime for a20
SQL> col spare6 for a20
SQL> select name,to_char(ctime,'DD-MON-YYYY HH24:MI:SS') ctime,to_char(ptime,'DD-MON-YYYY HH24:MI:SS') ptime,to_char(exptime,'DD-MON-YYYY HH24:MI:SS') exptime,to_char(ltime,'DD-MON-YYYY HH24:MI:SS') ltime,to_char(spare6,'DD-MON-YYYY HH24:MI:SS') spare6 from user$;

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
SYS 08-OCT-2025 22:00:58
PUBLIC 08-OCT-2025 22:00:58
CONNECT 08-OCT-2025 22:00:58
RESOURCE 08-OCT-2025 22:00:58
DBA 08-OCT-2025 22:00:58
PDB_DBA 08-OCT-2025 22:00:58
AUDIT_ADMIN 08-OCT-2025 22:00:58
AUDIT_VIEWER 08-OCT-2025 22:00:58
AUDSYS 08-OCT-2025 22:00:59 20-OCT-2025 15:13:12
SYSTEM 08-OCT-2025 22:00:59 20-OCT-2025 15:54:39
SELECT_CATALOG_ROLE 08-OCT-2025 22:00:59

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
EXECUTE_CATALOG_ROLE 08-OCT-2025 22:00:59
CAPTURE_ADMIN 08-OCT-2025 22:00:59
SYSBACKUP 08-OCT-2025 22:00:59 20-OCT-2025 15:13:12
SYSDG 08-OCT-2025 22:00:59 20-OCT-2025 15:13:12
SYSKM 08-OCT-2025 22:00:59 20-OCT-2025 15:13:12
SYSRAC 08-OCT-2025 22:00:59
OUTLN 08-OCT-2025 22:01:06 20-OCT-2025 15:13:12
EXP_FULL_DATABASE 08-OCT-2025 22:01:22
IMP_FULL_DATABASE 08-OCT-2025 22:01:22
AVTUNE_PKG_ROLE 08-OCT-2025 22:01:31
DATAPUMP_CLOUD_EXP 08-OCT-2025 22:33:17

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
VECSYS 08-OCT-2025 22:01:32 20-OCT-2025 15:13:12
BAASSYS 08-OCT-2025 22:01:33 20-OCT-2025 15:13:12
CDB_DBA 08-OCT-2025 22:31:55
APPLICATION_TRACE_VIEWER 08-OCT-2025 22:32:18
ACCHK_READ 08-OCT-2025 22:32:18
LOGSTDBY_ADMINISTRATOR 08-OCT-2025 22:32:50
DBFS_ROLE 08-OCT-2025 22:32:56
GSMUSER_ROLE 08-OCT-2025 22:32:57
GSMROOTUSER_ROLE 08-OCT-2025 22:32:57
GSMADMIN_INTERNAL 08-OCT-2025 22:32:57 20-OCT-2025 15:13:12
GSMUSER 08-OCT-2025 22:32:57 20-OCT-2025 15:13:12

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
GGSHAREDCAP 08-OCT-2025 22:32:59 20-OCT-2025 15:13:12
DIP 08-OCT-2025 22:33:07 08-OCT-2025 22:33:07
SAGA_ADM_ROLE 08-OCT-2025 22:33:12
SAGA_PARTICIPANT_ROLE 08-OCT-2025 22:33:12
SAGA_CONNECT_ROLE 08-OCT-2025 22:33:12
AQ_ADMINISTRATOR_ROLE 08-OCT-2025 22:33:14
AQ_USER_ROLE 08-OCT-2025 22:33:14
DATAPUMP_EXP_FULL_DATABASE 08-OCT-2025 22:33:17
DATAPUMP_IMP_FULL_DATABASE 08-OCT-2025 22:33:17
XS$NULL 08-OCT-2025 22:33:29 08-OCT-2025 22:33:29
DATAPUMP_CLOUD_IMP 08-OCT-2025 22:33:17

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
ADM_PARALLEL_EXECUTE_TASK 08-OCT-2025 22:33:27
PROVISIONER 08-OCT-2025 22:33:29
XS_SESSION_ADMIN 08-OCT-2025 22:33:29
XS_NAMESPACE_ADMIN 08-OCT-2025 22:33:29
XS_CACHE_ADMIN 08-OCT-2025 22:33:29
XS_CONNECT 08-OCT-2025 22:33:29
RECOVERY_CATALOG_USER 08-OCT-2025 22:36:16
REMOTE_SCHEDULER_AGENT 08-OCT-2025 22:33:34 20-OCT-2025 15:13:12
DBSFWUSER 08-OCT-2025 22:33:36 20-OCT-2025 15:13:12
SQL_FIREWALL_ADMIN 08-OCT-2025 22:33:38
SQL_FIREWALL_VIEWER 08-OCT-2025 22:33:38

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
OSAK_ADMIN_ROLE 08-OCT-2025 22:33:39
GATHER_SYSTEM_STATISTICS 08-OCT-2025 22:34:30
OPTIMIZER_PROCESSING_RATE 08-OCT-2025 22:34:30
DBMS_MDX_INTERNAL 08-OCT-2025 22:34:46
BDSQL_ADMIN 08-OCT-2025 22:35:37
BDSQL_USER 08-OCT-2025 22:35:37
RECOVERY_CATALOG_OWNER 08-OCT-2025 22:36:16
RECOVERY_CATALOG_OWNER_VPD 08-OCT-2025 22:36:16
OEM_MONITOR 08-OCT-2025 22:49:11
SYSUMF_ROLE 08-OCT-2025 22:44:53
SYS$UMF 08-OCT-2025 22:44:53 20-OCT-2025 15:13:12

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
MAINTPLAN_APP 08-OCT-2025 22:44:53
NOTIFICATIONS_USER 08-OCT-2025 22:44:55
NOTIFICATIONS_ADMIN 08-OCT-2025 22:44:55
SCHEDULER_ADMIN 08-OCT-2025 22:46:26
PPLB_ROLE 08-OCT-2025 22:46:37
DGPDB_ROLE 08-OCT-2025 22:46:37
DGPDB_INT 08-OCT-2025 22:46:37 20-OCT-2025 15:13:12
HS_ADMIN_SELECT_ROLE 08-OCT-2025 22:46:53
HS_ADMIN_EXECUTE_ROLE 08-OCT-2025 22:46:53
HS_ADMIN_ROLE 08-OCT-2025 22:46:53
GLOBAL_AQ_USER_ROLE 08-OCT-2025 22:46:56

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
OGG_CAPTURE 08-OCT-2025 22:49:10
OGG_APPLY 08-OCT-2025 22:49:10
OGG_APPLY_PROCREP 08-OCT-2025 22:49:10
OGG_SHARED_CAPTURE 08-OCT-2025 22:49:10
XSTREAM_CAPTURE 08-OCT-2025 22:49:10
XSTREAM_APPLY 08-OCT-2025 22:49:10
OEM_ADVISOR 08-OCT-2025 22:49:11
PGX_SESSION_GET_PUBLISHED_GRAP 08-OCT-2025 22:49:31
H

DBSNMP 08-OCT-2025 22:49:12 20-OCT-2025 15:13:12

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
APPQOSSYS 08-OCT-2025 22:49:12 20-OCT-2025 15:13:12
GSMCATUSER_ROLE 08-OCT-2025 22:49:14
GSMADMIN_ROLE 08-OCT-2025 22:49:14
GSM_POOLADMIN_ROLE 08-OCT-2025 22:49:14
GDS_CATALOG_SELECT 08-OCT-2025 22:49:14
SHARDED_SCHEMA_OWNER 08-OCT-2025 22:49:14
GSMCATUSER 08-OCT-2025 22:49:14 20-OCT-2025 15:13:12
GGSYS 08-OCT-2025 22:49:23 20-OCT-2025 15:13:12
GGSYS_ROLE 08-OCT-2025 22:49:23
DB_DEVELOPER_ROLE 08-OCT-2025 22:49:30
GRAPH_DEVELOPER 08-OCT-2025 22:49:31

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
GRAPH_ADMINISTRATOR 08-OCT-2025 22:49:31
GRAPH_USER 08-OCT-2025 22:49:31
PGX_SESSION_CREATE 08-OCT-2025 22:49:31
PGX_SERVER_GET_INFO 08-OCT-2025 22:49:31
PGX_SERVER_MANAGE 08-OCT-2025 22:49:31
PGX_SESSION_READ_MODEL 08-OCT-2025 22:49:31
PGX_SESSION_MODIFY_MODEL 08-OCT-2025 22:49:31
PGX_SESSION_NEW_GRAPH 08-OCT-2025 22:49:31
OJVMSYS 08-OCT-2025 23:00:15 20-OCT-2025 15:13:12
PGX_SESSION_COMPILE_ALGORITHM 08-OCT-2025 22:49:31
PGX_SESSION_ADD_PUBLISHED_GRAP 08-OCT-2025 22:49:31

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
H

PGX_SESSION_SET_IDLE_TIMEOUT 08-OCT-2025 22:49:31
XDB 08-OCT-2025 22:50:49 20-OCT-2025 15:13:12
ANONYMOUS 08-OCT-2025 22:50:49 20-OCT-2025 15:13:12
XDBADMIN 08-OCT-2025 22:50:49
XDB_SET_INVOKER 08-OCT-2025 22:51:06
AUTHENTICATEDUSER 08-OCT-2025 22:51:08
XDB_WEBSERVICES 08-OCT-2025 22:51:08
XDB_WEBSERVICES_WITH_PUBLIC 08-OCT-2025 22:51:08
XDB_WEBSERVICES_OVER_HTTP 08-OCT-2025 22:51:08

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
SODA_APP 08-OCT-2025 22:56:28
WMSYS 08-OCT-2025 22:58:20 20-OCT-2025 15:13:12
WM_ADMIN_ROLE 08-OCT-2025 22:58:24
JAVAUSERPRIV 08-OCT-2025 23:00:11
JAVAIDPRIV 08-OCT-2025 23:00:11
JAVASYSPRIV 08-OCT-2025 23:00:11
JAVADEBUGPRIV 08-OCT-2025 23:00:11
EJBCLIENT 08-OCT-2025 23:00:11
JMXSERVER 08-OCT-2025 23:00:11
DV_SECANALYST 08-OCT-2025 23:12:52
DVSYS 08-OCT-2025 23:12:46 20-OCT-2025 15:13:12

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
JAVA_ADMIN 08-OCT-2025 23:00:27
CTXSYS 08-OCT-2025 23:03:06 20-OCT-2025 15:13:12
CTXAPP 08-OCT-2025 23:03:08
OLAP_XS_ADMIN 08-OCT-2025 23:04:38
OLAPSYS 08-OCT-2025 23:04:44 20-OCT-2025 15:13:12
OLAP_DBA 08-OCT-2025 23:04:45
OLAP_USER 08-OCT-2025 23:04:45
MDSYS 08-OCT-2025 23:08:39 20-OCT-2025 15:13:12
MDDATA 08-OCT-2025 23:08:39 20-OCT-2025 15:13:12
RDFCTX_ADMIN 08-OCT-2025 23:11:11
LBACSYS 08-OCT-2025 23:12:07 20-OCT-2025 15:13:12

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
LBAC_DBA 08-OCT-2025 23:12:07
DVF 08-OCT-2025 23:12:46 20-OCT-2025 15:13:12
DV_MONITOR 08-OCT-2025 23:12:52
DV_ADMIN 08-OCT-2025 23:12:52
DV_OWNER 08-OCT-2025 23:12:52
DV_ACCTMGR 08-OCT-2025 23:12:52
DV_PATCH_ADMIN 08-OCT-2025 23:12:52
PDBADMIN 20-OCT-2025 15:13:10 20-OCT-2025 15:13:10 18-APR-2026 15:13:10
DV_STREAMS_ADMIN 08-OCT-2025 23:12:52
DV_GOLDENGATE_ADMIN 08-OCT-2025 23:12:52
DV_XSTREAM_ADMIN 08-OCT-2025 23:12:52

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
DV_GOLDENGATE_REDO_ACCESS 08-OCT-2025 23:12:52
DV_AUDIT_CLEANUP 08-OCT-2025 23:12:52
DV_DATAPUMP_NETWORK_LINK 08-OCT-2025 23:12:52
DV_POLICY_OWNER 08-OCT-2025 23:12:52
HR 20-OCT-2025 15:41:48 20-OCT-2025 15:41:48 20-OCT-2025 15:54:42
APEX_240200 20-OCT-2025 15:14:39 20-OCT-2025 15:14:39
FLOWS_FILES 20-OCT-2025 15:14:39 20-OCT-2025 15:14:39
APEX_PUBLIC_USER 20-OCT-2025 15:14:39 20-OCT-2025 15:39:31
APEX_PUBLIC_ROUTER 20-OCT-2025 15:14:40
APEX_ADMINISTRATOR_READ_ROLE 20-OCT-2025 15:14:56
APEX_ADMINISTRATOR_ROLE 20-OCT-2025 15:14:56

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
APEX_GRANTS_FOR_NEW_USERS_ROLE 20-OCT-2025 15:14:56
ORDS_PUBLIC_USER 20-OCT-2025 15:40:05 20-OCT-2025 15:40:05 01-JUN-2026 07:45:12
ORDS_METADATA 20-OCT-2025 15:40:06
ORDS_ADMINISTRATOR_ROLE 20-OCT-2025 15:40:46
ORDS_RUNTIME_ROLE 20-OCT-2025 15:40:46
_NEXT_USER 08-OCT-2025 22:00:58
SH 20-OCT-2025 15:41:50 20-OCT-2025 15:41:50 20-OCT-2025 15:54:40
CO 20-OCT-2025 15:41:52 20-OCT-2025 15:41:52 20-OCT-2025 15:54:42
HRREST 20-OCT-2025 15:48:33 20-OCT-2025 15:48:33 20-OCT-2025 15:48:39
AV 20-OCT-2025 15:48:35 20-OCT-2025 15:48:35 20-OCT-2025 15:48:35
AOS_GGADMIN 05-JUN-2026 14:22:00 05-JUN-2026 14:22:00

NAME CTIME PTIME EXPTIME LTIME SPARE6
------------------------------ -------------------- -------------------- -------------------- -------------------- --------------------
AOS_DATA 05-JUN-2026 14:43:36 05-JUN-2026 14:43:36 05-JUN-2026 14:43:40 05-JUN-2026 14:43:37
AOS_CODE 05-JUN-2026 14:43:36 05-JUN-2026 14:43:36 05-JUN-2026 14:44:54
AOS_BUILD_DATA 05-JUN-2026 14:43:36
AOS_BUILD_CODE 05-JUN-2026 14:43:36
AOS_CONNECT 05-JUN-2026 14:43:36
AOS_RUNTIME_ADMIN 05-JUN-2026 14:43:40
AOS_RUNTIME_VIEWER 05-JUN-2026 14:43:40
AOS_ADMIN 05-JUN-2026 14:43:40 05-JUN-2026 14:43:40 05-JUN-2026 14:45:30
AOS_VIEWER 05-JUN-2026 14:43:40 05-JUN-2026 14:43:40

181 rows selected.

SQL>

We can also check data files:

SQL> col dt for a20
SQL> col file_name for a50
SQL> set lines 220
SQL> select to_char(creation_time,'DD-MON-YYYY HH24:MI:SS') dt,file_name from v$datafile,dba_data_files where file#=file_id;

DT FILE_NAME
-------------------- --------------------------------------------------
20-OCT-2025 15:13:06 /opt/oracle/oradata/FREE/FREEPDB1/sysaux01.dbf
20-OCT-2025 15:13:54 /opt/oracle/oradata/FREE/FREEPDB1/users01.dbf
20-OCT-2025 15:13:06 /opt/oracle/oradata/FREE/FREEPDB1/system01.dbf
20-OCT-2025 15:13:07 /opt/oracle/oradata/FREE/FREEPDB1/undotbs01.dbf

SQL>

And the filenames:

SQL> col dt for 9999999999
SQL> col file_name for a50
SQL> set lines 220
SQL> select checkpoint_change# dt,file_name from v$datafile_header,dba_data_files where file#=file_id;

DT FILE_NAME
----------- --------------------------------------------------
4736767 /opt/oracle/oradata/FREE/FREEPDB1/sysaux01.dbf
4736767 /opt/oracle/oradata/FREE/FREEPDB1/users01.dbf
4736767 /opt/oracle/oradata/FREE/FREEPDB1/system01.dbf
4736767 /opt/oracle/oradata/FREE/FREEPDB1/undotbs01.dbf

SQL>

In summary, we cannot get a timestamp from the database for a row in a table that does not itself have a timestamp column unless we also have audit or we can use redo logs. We can use SCN but only at the block level and we could also do ID analysis to see what rows are missing but not what is in those rows. We could use off line data block analysis but unless a row was held in the ITL (Interested Transaction List) in the data block we would not get much futher.

SCNs are useful and can create an outline / boundary for instance if the last record of the 3rd block has an SCN and we can estimate a timestamp for the closed block we can estimate the time the block has existed and whether it falls into the range of the attack

#oracleace #sym_42 #oracle #forensics #database #security #scn #hacking #databreach

DV_SECANALYST Analyse Database Vault Views

I have been involved with Oracle Database Vault recently with a deployment and I was asked how can someone view the database vault dictionary views when DV is installed.

So, if we start with no DV enabled in our database and we will focus on the view DBA_DV_FACTOR. With no Database Vault we can connect as SYSDBA and see the view:

SQL> select name from dba_dv_factor;

NAME
--------------------------------------------------------------------------------
Database_Hostname
Database_IP
Database_Instance
Client_IP
Authentication_Method
Identification_Type
Database_Domain
Database_Name
Lang
Language
Network_Protocol

NAME
--------------------------------------------------------------------------------
Proxy_User
Proxy_Enterprise_Identity
Session_User
Domain
Machine
Enterprise_Identity
DV$_Module
DV$_Client_Identifier
DV$_Dblink_Info

20 rows selected.

SQL>

SYSDBA can see the contents of this view. Who owns the view:

SQL> select owner from dba_views where view_name='DBA_DV_FACTOR';

OWNER
--------------------------------------------------------------------------------
DVSYS

SQL>

What permissions are granted on this view:

SQL> select grantee,privilege from dba_tab_privs where table_name='DBA_DV_FACTOR';

GRANTEE
--------------------------------------------------------------------------------------------------------------------------------
PRIVILEGE
----------------------------------------
DV_SECANALYST
SELECT


1 row selected.

SQL>

Check who has been granted DV_SECANALYST:

SQL> select grantee,admin_option from dba_role_privs where granted_role='DV_SECANALYST';

GRANTEE
--------------------------------------------------------------------------------------------------------------------------------
ADM
---
DV_ADMIN
NO

DV_OWNER
YES


2 rows selected.

SQL>

Only DV_OWNER and DV_ADMIN roles have been granted access to this DV_SECANALYST role and DV_OWNER can grant it on. How did SYS access the DBA_DV_FACTOR view:

SQL> create user test_dv identified by test_dv;

User created.

SQL> grant create session, select any table to test_dv;

Grant succeeded.

SQL> connect test_dv/test_dv@//192.168.56.33:1539/xepdb1
Connected.
SQL> select name from dba_dv_factor;

NAME
--------------------------------------------------------------------------------------------------------------------------------
Database_Hostname
Database_IP
Database_Instance
Client_IP
Authentication_Method
Identification_Type
Database_Domain
Database_Name
Lang
Language
Network_Protocol
Proxy_User
Proxy_Enterprise_Identity
Session_User
Domain
Machine
Enterprise_Identity
DV$_Module
DV$_Client_Identifier
DV$_Dblink_Info

20 rows selected.

SQL>

Clearly the access to this view is through SELECT ANY TABLE. I will now enable DV in the CDB and PDB as as there are a lot of steps I will resume after the enable completes:

...
SQL> select * from dba_dv_status;

NAME STATUS
------------------- --------------
DV_CONFIGURE_STATUS TRUE
DV_ENABLE_STATUS TRUE
DV_APP_PROTECTION NOT CONFIGURED

SQL>
SQL> select * from dba_ols_status;

NAME STATU
-------------------- -----
DESCRIPTION
------------------------------------------------------------------------------------------------------------------------
OLS_CONFIGURE_STATUS TRUE
Determines if OLS is configured

OLS_DIRECTORY_STATUS FALSE
Determines if OID is enabled with OLS

OLS_ENABLE_STATUS TRUE
Determines if OLS is enabled


SQL>
...

Database Vault is installed. Let us try and access the DBA_DV_FACTOR view as SYSDBA:

SQL> connect sys/oracle1@//192.168.56.33:1539/xepdb1 as sysdba
Connected.
SQL> select name from dba_dv_factor;
select name from dba_dv_factor
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

No luck, what if i grant DV_SECANALYST to SYSDBA:

SQL> grant dv_secanalyst to sys;
grant dv_secanalyst to sys
*
ERROR at line 1:
ORA-47410: Realm violation for GRANT on DV_SECANALYST


SQL> connect c##dvo/oracle1@//192.168.56.33:1539/xepdb1
Connected.
SQL> grant dv_secanalyst to sys;

Grant succeeded.

SQL> connect sys/oracle1@//192.168.56.33:1539/xepdb1 as sysdba
Connected.
SQL> select name from dba_dv_factor;
select name from dba_dv_factor
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

So, SYS cannot grant DV roles, so we must use DVO to do this BUT when we connect again as SYS it still cannot access the DBA_DV_FACTOR view as it is blocked by DV even when it has the correct role.

What if we grant DV_SECANALYST to our test user:

SQL> connect test_dv/test_dv@//192.168.56.33:1539/xepdb1
Connected.
SQL> select name from dba_dv_factor;
select name from dba_dv_factor
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL> connect c##dvo/oracle1@//192.168.56.33:1539/xepdb1
Connected.
SQL> grant dv_secanalyst to test_dv;

Grant succeeded.

SQL> connect test_dv/test_dv@//192.168.56.33:1539/xepdb1
Connected.
SQL> select name from dba_dv_factor;

NAME
------------------------------------------------------------------------------------------------------------------------
Database_Hostname
Database_IP
Database_Instance
Client_IP
Authentication_Method
Identification_Type
Database_Domain
Database_Name
Lang
Language
Network_Protocol
Proxy_User
Proxy_Enterprise_Identity
Session_User
Domain
Machine
Enterprise_Identity
DV$_Module
DV$_Client_Identifier
DV$_Dblink_Info

20 rows selected.

SQL>

It works. So, in a vanilla DV install a user with SELECT ANY TABLE cannot see DBA_DV_FACTOR but if granted the DV_SECANALYST role it can.

Maintenance is more complex with DV enabled as we need DV_SECANALYST to see the DV views and we cannot see them with SYSDBA even when it has this role. We also must use a DVO user to make the grants and a DV_ACCTMGR user to create and maintain users. This is separation of duties.

#oracleace #sym_42 #oracle #databasevault #vault #dv #dv_secanalyst

I forgot my Oracle Database Vault owner Password

Let us imagine that I have a 21c database that has Database Vault (DV) enabled and I have forgotten my Database Vault owner password. In my case C##DVO. This user as well as a backup (That I have also forgotten the password for) is needed to disable or enable database vault. Yes, the database can still be used and can be stopped and re-started with SYDBA but what if there is also an error in the Database Vault then potentially I need to be able to login with a user that has DV_OWNER to be able to disable DV and fix the issue and also to be sure that I can fully support this database.

Even though I have the password for a user with the DV_ACCTMGR role this cannot be used to change the DVO password. Only the DVO can change their own password as DV protects itself by not allowing the password of the owner to be changed except by the owner and not even by the ACCTMGR user.

This behaviour is intentional from Oracle to make it very hard to disable DV if you do not have access to a DV OWNER.

So, if we have got in this state - we forgot the DV OWNER password then what can we do?

Oracle states that it cannot be recovered or fixed. It suggests that we "find" the password in external password vaults, pieces of paper, in files lying around or indeed any way you might find a file; alternately Oracle also suggests checking for other users with the DV_OWNER role to instead use them.

Alternately I have read that Oracle may be able to help customers via an SR in this case. I also read (with no proof) that Oracle are able to set events during the boot process so that between MOUNT and OPEN and after the bootstrap and dictionary loaded so that DV can be disabled without the DV ONWER password. I have no idea if this is true or not as I cannot find anything on Google.

Lets explore some of the ideas on the internet. Someone suggested that a STARTUP UPGRADE could bypass DV and allow the DVO password to be changed or DV to be disabled:

root@192.168.56.33's password:
Last login: Tue May 26 07:32:23 2026 from 192.168.56.1
[root@ol21cxe ~]# su - oracle
Last login: Tue May 26 07:32:36 BST 2026 on pts/1
[oracle@ol21cxe ~]$ source /opt/oracle/product/21c/dbhomeXE/bin/oraenv
ORACLE_SID = [oracle] ? XE
The Oracle base has been set to /opt/oracle
[oracle@ol21cxe ~]$ sqlplus / as sysdba

SQL*Plus: Release 21.0.0.0.0 - Production on Wed May 27 13:36:11 2026
Version 21.3.0.0.0

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


Connected to:
Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0

SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup upgrade
ORACLE instance started.

Total System Global Area 1157625952 bytes
Fixed Size 9685088 bytes
Variable Size 905969664 bytes
Database Buffers 234881024 bytes
Redo Buffers 7090176 bytes
Database mounted.
Database opened.
SQL> exec dbms_macadm.disable_dv;
BEGIN dbms_macadm.disable_dv; END;

*
ERROR at line 1:
ORA-06550: line 1, column 7:
PLS-00904: insufficient privilege to access object DVSYS.DBMS_MACADM
ORA-06550: line 1, column 7:
PL/SQL: Statement ignored


SQL> grant execute on dvsys.dbms_macadm to sys;

grant execute on dvsys.dbms_macadm to sys
*
ERROR at line 1:
ORA-47401: Realm violation for GRANT on DVSYS.DBMS_MACADM


SQL> alter user c##dvo identified by oracle1;
alter user c##dvo identified by oracle1
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

So, no, that idea does not work. Another idea on the internet is to shutdown the database and unlink DV in the binaries. Let us try that:

SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> exit
Disconnected from Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
[oracle@ol21cxe ~]$ cd $ORACLE_HOME/rdbms/lib
[oracle@ol21cxe lib]$ make -f ins_rdbms.mk dv_off ioracle
dv_off has been deprecated
chmod 755 /opt/oracle/product/21c/dbhomeXE/bin
/usr/bin/ar: creating /opt/oracle/product/21c/dbhomeXE/lib/libserver.a

- Linking Oracle
rm -f /opt/oracle/product/21c/dbhomeXE/rdbms/lib/oracle
rm -f /opt/oracle/product/21c/dbhomeXE/rdbms/lib/oracle.map; /opt/oracle/product/21c/dbhomeXE/bin/orald -o /opt/oracle/product/21c/dbhomeXE/rdbms/lib/oracle -m64 -z noexecstack -Wl,--disable-new-dtags -L/opt/oracle/product/21c/dbhomeXE/rdbms/lib/ -L/opt/oracle/product/21c/dbhomeXE/lib/ -L/opt/oracle/product/21c/dbhomeXE/lib/stubs/ -Wl,-Map,/opt/oracle/product/21c/dbhomeXE/rdbms/lib/oracle.map -Wl,-E /opt/oracle/product/21c/dbhomeXE/rdbms/lib/opimai.o /opt/oracle/product/21c/dbhomeXE/rdbms/lib/ssoraed.o /opt/oracle/product/21c/dbhomeXE/rdbms/lib/ttcsoi.o -Wl,--whole-archive -lperfsrv -Wl,--no-whole-archive /opt/oracle/product/21c/dbhomeXE/lib/nautab.o /opt/oracle/product/21c/dbhomeXE/lib/naeet.o /opt/oracle/product/21c/dbhomeXE/lib/naect.o /opt/oracle/product/21c/dbhomeXE/lib/naedhs.o /opt/oracle/product/21c/dbhomeXE/rdbms/lib/config.o -ldmext -lserver -loraodm -lofs -lcellst -lnnet -lskgxp -lsnls -lnls -lcore -lsnls -lnls -lcore -lsnls -lnls -lxml -lcore -lunls -lsnls -lnls -lcore -lnls -lclient -lvsnst -lcommon -lgeneric -lknlopt -loraolap -lskjcx -lslax -lpls -lrt -lplp -ldmext -lserver -lclient -lvsnst -lcommon -lgeneric `if [ -f /opt/oracle/product/21c/dbhomeXE/lib/libavserver.a ] ; then echo "-lavserver" ; else echo "-lavstub"; fi` `if [ -f /opt/oracle/product/21c/dbhomeXE/lib/libavclient.a ] ; then echo "-lavclient" ; fi` -lknlopt -lslax -lpls -lrt -lplp -ljavavm -lserver -lwwg `cat /opt/oracle/product/21c/dbhomeXE/lib/ldflags` -lncrypt -lnsgr -ln -lnl -lngsmshd -lnro `cat /opt/oracle/product/21c/dbhomeXE/lib/ldflags` -lncrypt -lnsgr -ln -lnl -lngsmshd -lnnzst -lzt -lztkg -lmm -lsnls -lnls -lcore -lsnls -lnls -lcore -lsnls -lnls -lxml -lcore -lunls -lsnls -lnls -lcore -lnls -lztkg `cat /opt/oracle/product/21c/dbhomeXE/lib/ldflags` -lncrypt -lnsgr -ln -lnl -lngsmshd -lnro `cat /opt/oracle/product/21c/dbhomeXE/lib/ldflags` -lncrypt -lnsgr -ln -lnl -lngsmshd -lnnzst -lzt -lztkg -lsnls -lnls -lcore -lsnls -lnls -lcore -lsnls -lnls -lxml -lcore -lunls -lsnls -lnls -lcore -lnls `if /usr/bin/ar tv /opt/oracle/product/21c/dbhomeXE/rdbms/lib/libknlopt.a | grep "kxmnsd.o" > /dev/null 2>&1 ; then echo " " ; else echo "-lordsdo -lserver"; fi` -L/opt/oracle/product/21c/dbhomeXE/lib/ -lctxc -lctx -lzx -lgx -lctx -lzx -lgx -lclscest -loevm -lclsra -ldbcfg -lhasgen -lnnzst -lzt -lxml -lgeneric -locr -locrb -locrutl -lhasgen -lnnzst -lzt -lxml -lgeneric -lgeneric -lorazip -loraz -llzopro5 -lorabz2 -lorazstd -loralz4 -lipp_z -lipp_bz2 -lippdc -lipps -lippcore -lippcp -lsnls -lnls -lcore -lsnls -lnls -lcore -lsnls -lnls -lxml -lcore -lunls -lsnls -lnls -lcore -lnls -lsnls -lunls -llxled -lsnls -lnls -lcore -lsnls -lnls -lcore -lsnls -lnls -lxml -lcore -lunls -lsnls -lnls -lcore -lnls -lasmclnt -lcommon -lcore -ledtn -laio -lons -lmql1 -lipc1 -lmql1 -lipc1 -lfthread `cat /opt/oracle/product/21c/dbhomeXE/lib/sysliblist` -Wl,-rpath,/opt/oracle/product/21c/dbhomeXE/lib -lm `cat /opt/oracle/product/21c/dbhomeXE/lib/sysliblist` -ldl -lm -L/opt/oracle/product/21c/dbhomeXE/lib `test -x /usr/bin/hugeedit -a -r /usr/lib64/libhugetlbfs.so && test -r /opt/oracle/product/21c/dbhomeXE/rdbms/lib/shugetlbfs.o && echo -Wl,-zcommon-page-size=2097152 -Wl,-zmax-page-size=2097152 -lhugetlbfs`
/usr/bin/ar: /opt/oracle/product/21c/dbhomeXE/rdbms/lib/libknlopt.a: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/lib/crtbegin.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/rdbms/lib/opimai.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/rdbms/lib/ssoraed.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/rdbms/lib/ttcsoi.o: No such file or directory
/usr/bin/ld: cannot find -lperfsrv
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/lib/nautab.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/lib/naeet.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/lib/naect.o: No such file or directory
/usr/bin/ld: cannot find /opt/oracle/product/21c/dbhomeXE/lib/naedhs.o: No such file or directory
/usr/bin/ld: cannot find -ldmext
/usr/bin/ld: cannot find -loraodm
/usr/bin/ld: cannot find -lcellst
/usr/bin/ld: cannot find -lnnet
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lclient
/usr/bin/ld: cannot find -lvsnst
/usr/bin/ld: cannot find -lcommon
/usr/bin/ld: cannot find -lgeneric
/usr/bin/ld: cannot find -lknlopt
/usr/bin/ld: cannot find -loraolap
/usr/bin/ld: cannot find -lslax
/usr/bin/ld: cannot find -lpls
/usr/bin/ld: cannot find -lplp
/usr/bin/ld: cannot find -ldmext
/usr/bin/ld: cannot find -lclient
/usr/bin/ld: cannot find -lvsnst
/usr/bin/ld: cannot find -lcommon
/usr/bin/ld: cannot find -lgeneric
/usr/bin/ld: cannot find -lavstub
/usr/bin/ld: cannot find -lknlopt
/usr/bin/ld: cannot find -lslax
/usr/bin/ld: cannot find -lpls
/usr/bin/ld: cannot find -lplp
/usr/bin/ld: cannot find -ljavavm
/usr/bin/ld: cannot find -lwwg
/usr/bin/ld: cannot find -lnbeq
/usr/bin/ld: cannot find -lntmq
/usr/bin/ld: cannot find -lnhost
/usr/bin/ld: cannot find -lnus
/usr/bin/ld: cannot find -lnldap
/usr/bin/ld: cannot find -lldapclnt
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntcps
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntns
/usr/bin/ld: cannot find -lntwss
/usr/bin/ld: cannot find -lncrypt
/usr/bin/ld: cannot find -lnsgr
/usr/bin/ld: cannot find -ln
/usr/bin/ld: cannot find -lnl
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lnro
/usr/bin/ld: cannot find -lnbeq
/usr/bin/ld: cannot find -lntmq
/usr/bin/ld: cannot find -lnhost
/usr/bin/ld: cannot find -lnus
/usr/bin/ld: cannot find -lnldap
/usr/bin/ld: cannot find -lldapclnt
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntcps
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntns
/usr/bin/ld: cannot find -lntwss
/usr/bin/ld: cannot find -lncrypt
/usr/bin/ld: cannot find -lnsgr
/usr/bin/ld: cannot find -ln
/usr/bin/ld: cannot find -lnl
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lnnzst
/usr/bin/ld: cannot find -lzt
/usr/bin/ld: cannot find -lztkg
/usr/bin/ld: cannot find -lmm
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lztkg
/usr/bin/ld: cannot find -lnbeq
/usr/bin/ld: cannot find -lntmq
/usr/bin/ld: cannot find -lnhost
/usr/bin/ld: cannot find -lnus
/usr/bin/ld: cannot find -lnldap
/usr/bin/ld: cannot find -lldapclnt
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntcps
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntns
/usr/bin/ld: cannot find -lntwss
/usr/bin/ld: cannot find -lncrypt
/usr/bin/ld: cannot find -lnsgr
/usr/bin/ld: cannot find -ln
/usr/bin/ld: cannot find -lnl
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lnro
/usr/bin/ld: cannot find -lnbeq
/usr/bin/ld: cannot find -lntmq
/usr/bin/ld: cannot find -lnhost
/usr/bin/ld: cannot find -lnus
/usr/bin/ld: cannot find -lnldap
/usr/bin/ld: cannot find -lldapclnt
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntcps
/usr/bin/ld: cannot find -lntcp
/usr/bin/ld: cannot find -lntns
/usr/bin/ld: cannot find -lntwss
/usr/bin/ld: cannot find -lncrypt
/usr/bin/ld: cannot find -lnsgr
/usr/bin/ld: cannot find -ln
/usr/bin/ld: cannot find -lnl
/usr/bin/ld: cannot find -lngsmshd
/usr/bin/ld: cannot find -lnnzst
/usr/bin/ld: cannot find -lzt
/usr/bin/ld: cannot find -lztkg
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lordsdo
/usr/bin/ld: cannot find -lctxc
/usr/bin/ld: cannot find -lctx
/usr/bin/ld: cannot find -lzx
/usr/bin/ld: cannot find -lgx
/usr/bin/ld: cannot find -lctx
/usr/bin/ld: cannot find -lzx
/usr/bin/ld: cannot find -lgx
/usr/bin/ld: cannot find -lclscest
/usr/bin/ld: cannot find -lnnzst
/usr/bin/ld: cannot find -lzt
/usr/bin/ld: cannot find -lgeneric
/usr/bin/ld: cannot find -lnnzst
/usr/bin/ld: cannot find -lzt
/usr/bin/ld: cannot find -lgeneric
/usr/bin/ld: cannot find -lgeneric
/usr/bin/ld: cannot find -lorazip
/usr/bin/ld: cannot find -loraz
/usr/bin/ld: cannot find -llzopro5
/usr/bin/ld: cannot find -lorabz2
/usr/bin/ld: cannot find -lorazstd
/usr/bin/ld: cannot find -loralz4
/usr/bin/ld: cannot find -lipp_z
/usr/bin/ld: cannot find -lipp_bz2
/usr/bin/ld: cannot find -lippdc
/usr/bin/ld: cannot find -lipps
/usr/bin/ld: cannot find -lippcore
/usr/bin/ld: cannot find -lippcp
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -llxled
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lsnls
/usr/bin/ld: cannot find -lasmclnt
/usr/bin/ld: cannot find -lcommon
/usr/bin/ld: cannot find -ledtn
/usr/bin/ld: cannot find -lfthread
make: *** [/opt/oracle/product/21c/dbhomeXE/rdbms/lib/oracle] Error 1

That does not work either as the line "dv_off has been deprecated" shows that you cannot unlink the software anymore to disable DV. Another idea suggested by someone was to create a "pfile"; set the parameter enable_dv=false and then open the database. No one else suggested this but let us try for completeness:

SQL> create pfile='/tmp/initxe.ora' from spfile;

File created.

SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL>
SQL> exit
[oracle@ol21cxe lib]$ cat /tmp/initxe.ora
XE.__data_transfer_cache_size=0
XE.__db_cache_size=167772160
XE.__inmemory_ext_roarea=0
XE.__inmemory_ext_rwarea=0
XE.__java_pool_size=184549376
XE.__large_pool_size=16777216
XE.__oracle_base='/opt/oracle'#ORACLE_BASE set from environment
XE.__pga_aggregate_target=385875968
XE.__sga_target=1157627904
XE.__shared_io_pool_size=67108864
XE.__shared_pool_size=671088640
XE.__streams_pool_size=33554432
XE.__unified_pga_pool_size=0
*.audit_file_dest='/opt/oracle/admin/XE/adump'
*.audit_trail='db'
*.compatible='21.0.0'
*.control_files='/opt/oracle/oradata/XE/control01.ctl','/opt/oracle/oradata/XE/control02.ctl'
*.db_block_size=8192
*.db_name='XE'
*.diagnostic_dest='/opt/oracle'
*.dispatchers='(PROTOCOL=TCP) (SERVICE=XEXDB)'
*.enable_pluggable_database=true
*.local_listener='LISTENER_XE'
*.nls_language='ENGLISH'
*.nls_territory='UNITED KINGDOM'
*.open_cursors=300
*.permit_92_wrap_format=TRUE
*.pga_aggregate_target=364m
*.processes=300
*.remote_login_passwordfile='EXCLUSIVE'
*.sga_target=1093m
*.undo_tablespace='UNDOTBS1'
*.enable_dv=false
[oracle@ol21cxe lib]$ sqlplus / as sysdba

SQL> startup pfile='/tmp/initxe.ora';
LRM-00101: unknown parameter name 'enable_dv'
ORA-01078: failure in processing system parameters
SQL>

I knew this would not work as no other sites suggested it. A quick check in the database shows this parameter does not exist and also a detailed check of all parameters including hidden underscore ones and none relate to disabling DV.

Are there any other DV_ONWER users that we may know the password of:

SQL> col grantee for a30
SQL> col granted_role for a30

SQL> select grantee, granted_role, common from dba_role_privs where granted_role in ('DV_OWNER','DV_ACCTMGR');

GRANTEE GRANTED_ROLE COM
------------------------------ ------------------------------ ---
C##ACCO DV_ACCTMGR NO
C##ACCO_BK DV_ACCTMGR NO
C##DVO_BK DV_OWNER NO
C##DVO DV_OWNER NO
C##ACCO_BK DV_ACCTMGR YES
C##DVO_BK DV_OWNER YES

6 rows selected.

SQL>

The backup users have the roles granted COMMON and each user also has the roles granted locally (backup and non backup) so if we knew the password of C##DVO_BK then we could get in with that and disable DV and then change the C##DVO password to a known value.

Can a user with DV_ACCTMGR change the password of a user with DV_OWNER?

SQL> connect c##acco/oracle1@//192.168.56.33:1539/xe
Connected.
SQL> alter user c##dvo identified by oracle1;
alter user c##dvo identified by oracle1
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL> alter user c##dvo_bk identified by oracle1;
alter user c##dvo_bk identified by oracle1
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

Only DV_OWNER can change their own password, they cannot change the password of another user with DV_OWNER. DV_ACCTMGR can change other passwords in the database though but a user with DV_OWNER cannot change the password of a user with DV_ACCTMGR:

SQL> connect c##dvo_bk/oracle1@//192.168.56.33:1539/xe
Connected.
SQL> alter user c##dvo identified by oracle1;
alter user c##dvo identified by oracle1
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

only DVO can change his own password. BTW, other users can still change their own passwords even with password restrictions out of the box OOTB in DV

SQL> alter user c##dvo identified by oracle1;

User altered.

SQL>

Of course I have not really forgotten the C##DVO password I am just pretending I have for testing, hence i can demonstrate re-setting the DV_OWNER password above.

Let us see if we generate the password hash of a know password from another database can we re-apply it to our database when the password is not known:

SQL> connect sys/oracle1@//192.168.56.33:1539/xe as sysdba
Connected.
SQL> set long 1000000
SQL> select dbms_metadata.get_ddl('USER','C##DVO') from dual;

DBMS_METADATA.GET_DDL('USER','C##DVO')
--------------------------------------------------------------------------------

CREATE USER "C##DVO" IDENTIFIED BY VALUES 'S:680448D96099E877FE4A3A5938001775
D8284A41ACA3732A67B45193E287;T:40B680A3A8453A1F0AAC83EAA3FB8D05764C155527B3B8A21
FDB01E9F18F9FE868CF3BBBC4246205F09E66B0B6062CB22402614CC106A83118206BB784C09BFFF
AD97B9689B57687666E0DFA2332CF75'
DEFAULT TABLESPACE "USERS"
TEMPORARY TABLESPACE "TEMP"


SQL>

One method that would work to recover the password providing its short enough or a dictionary password is to crack it:

SQL> sho user
USER is "SYS"
SQL> grant select on sys.user$ to system;

Grant succeeded.

SQL> connect system/oracle1@//192.168.56.33:1539/xe
ERROR:
ORA-28002: the password will expire within 7 days


Connected.
SQL> alter user system identified by oracle1;

User altered.

SQL>

PFCLScan 2026 Password Cracker



Let us try another method. We can attempt to update SYS.USER$ directly with SQL. This is not supported and is only for research and entertainment:

SQL> connect sys/oracle1@//192.168.56.33:1539/xe as sysdba
Connected.
SQL> update sys.user$ set spare4='S:680448D96099E877FE4A3A5938001775
2 D8284A41ACA3732A67B45193E287;T:40B680A3A8453A1F0AAC83EAA3FB8D05764C155527B3B8A21
3 FDB01E9F18F9FE868CF3BBBC4246205F09E66B0B6062CB22402614CC106A83118206BB784C09BFFF
4 AD97B9689B57687666E0DFA2332CF75' where name='C##DVO';
update sys.user$ set spare4='S:680448D96099E877FE4A3A5938001775
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

Did not work, can we use ALTER USER identified by values:

SQL> connect sys/oracle1@//192.168.56.33:1539/xe as sysdba
Connected.
SQL> alter user c##dvo identified by values 'S:680448D96099E877FE4A3A5938001775
2 D8284A41ACA3732A67B45193E287;T:40B680A3A8453A1F0AAC83EAA3FB8D05764C155527B3B8A21
3 FDB01E9F18F9FE868CF3BBBC4246205F09E66B0B6062CB22402614CC106A83118206BB784C09BFFF
4 AD97B9689B57687666E0DFA2332CF75';
alter user c##dvo identified by values 'S:680448D96099E877FE4A3A5938001775
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

What about STARTUP MIGRATE?

SQL> shutdown immediate
Database closed.
Database dismounted.
ORACLE instance shut down.
SQL> startup migrate
ORACLE instance started.

Total System Global Area 1157625952 bytes
Fixed Size 9685088 bytes
Variable Size 905969664 bytes
Database Buffers 234881024 bytes
Redo Buffers 7090176 bytes
Database mounted.
Database opened.
SQL> sho parameter cluster_database

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
cluster_database boolean FALSE
SQL> alter user c##dvo identified by oracle1;
alter user c##dvo identified by oracle1
*
ERROR at line 1:
ORA-01031: insufficient privileges


SQL>

The only viable solution found seems to be to try and crack the password or find the password on paper, file or externally. If the password is simple and a dictionary word then cracking would work. If it is long and complex then this is likely not practical either as cracking would take a lot of resource or not be possible if the key space is too large

So, back to the beginning; someone suggested that Oracle via an SR can interrupt the boot process between STARTUP MOUNT and OPEN and after bootstrap whereby DV can be disabled. I really do not know if this is true or not; it might be otherwise Oracle would be telling customers to rebuild and potentially lose data.

There is one final method that I am not going to demo and it would not be supported. Almost 20 years ago in this blog I talked in a post How to hack SYS password without logging into the database. This post referenced papers by Miladin Modrakovic and Graham Thornton who talked about using BBED to change the SYS password in the datafile

This still makes sense to me as an unsupported solution. If the database is shutdown then DV is not protecting it. If we were able to change the C##DVO password directly in the data file and fix the CRC - checksum then this would get around the lost DVO password. Its a thought experiment of course and would not be supported by Oracle!!

#oracleace #sym_42 #oracle #security #hacking #database #vault #password #lost #cracking

World Password Day and Oracle Security

I am slightly late with this one as the event itself was on the 7th May 2026. The World Password Day 2026 is a day to try and highlight that passwords are weak. An article I saw on line said that 48% of passwords can be cracked in less than 1 minute and another article said that 68% of passwords can be cracked in one day. OK, this has to be by survey and no one tried to crack all passwords in the world to come up with figures of 48% and 68% but its reflective.

The purpose of world password day is a call to get people to stop treating passwords as the perimeter to systems and the to shift identity as the perimeter and reduce reliance on passwords. Use pass phrases that are more than 25 characters and start to adopt MFA and pass keys and also to make behaviour and risk part of the logon process.

A good example in Oracle would be to use a logon trigger or audit trails and reporting analysis to show patterns of why connects, from where and when and with what.

Obviously in Oracle implement password profiles and a verify function. MFA in the same way as twitter or facebook or linkedin or ... where you are sent a text is possible with various features from Oracle that cost extra but from 23.9 RU and 19.28 MFA is supported from the database as a core feature.

You can / could create a sort of MFA in the standard database by using various factors to check if the user not only has the password but conforms to other factors such as program, location, program, ... I know this is not MFA but we could verify a user not only on password. If we create a logon trigger then these factors can be tested.

My experience over many years is that Oracle databases when reviewed often do not have password profiles, verification functions or strong passwords. I have also seen passwords that have not changed in years and not many but some for decades.

Trevor who I have known for many years has set up a password quiz to coincide with world password day and he has already had some people take part and he is looking to report any differences between the UK/EU and USA and also younger vs older people.

Please do the quiz if you have time, its simple and quick but should provide valuable, insights. Trevor will analyse the results and show on his site and I will share some of the analysis here once it is available.

#oracleace #oracle #security #world #password #day #cracking # hacking

Securing Data in Oracle without Cost Options

I did a presentation at the UKOUG conference at the East Side rooms in Birmingham at the end of 2025. The focus of this talk was to highlight the problem of securing data held in an Oracle database without using cost options from Oracle such as VPD, OLS, Database vault and more.

I have nothing against these cost options, in fact I love to work with them when customers ask me to help design and implement VPD or Database vault or others such as masking. BUT, some customers cannot justify paying for cost options or they cannot use them because they have licensed standard edition. Yes, I know VPD is included in Enterprise Edition of the database, it is just an example of the options.

This presentation covers the problem and then uses an example of the UNIFIED_AUDIT_TRAIL to show how Oracle protects this table and also showing that it is not protected using cost options. I then investigate how we might protect a table using standard features of the database. We then quickly discover that if we use one feature such as a trigger then we need to stop anyone disabling or removing or changing the trigger. Similarly if we use a grant then we need to stop anyone from changing that grant.

We cannot replicate 100% what Oracle does with cost options BUT we can get close. What we are trying to implement as an example is a read only table with some extras. The table is READ ONLY but data must be added to it in the first place. So we allow data adding BUT only through one specific interface. We must allow purging of the data so we also allow limited delete but again through a controlled interface. No UPDATE of data is allowed. We use this design scope as a basis to build a sample design and this is shown.

The whole design also should be audited to ensure any attempts to bypass the security model are caught

I implement the design as an example and then add the protections of the protections.

This is layered security. The slides for A Design Pattern To Secure Your Database are available.

#oracleace #sym_42 #oracle #security #lockdown #protection #data #hacking #data #databreach #readonly