I recently asked myself this question after having been confronted with a quite challenging situation, which will be been dissected in this post. Indeed, most of the DBA are aware about several mechanism offered by the Oracle database engine to call external programs.
However, we rarely have the opportunity to get deep understanding of all of them due to the broad skills required.
A couple of days ago, we have been asked a by an IT architect to have a look at some SHELL scripts & C programs executed on a database server by database processes; It all began with “strange” UNIX file permissions such as:
[oracle@svl-oat ~]$ ls -l /opt/oracle_scripts/run_prog.sh
-rwxr-sr-x 1 oracle oinstall 68 Mar 12 19:28 /opt/oracle_scripts/run_prog.sh
Interesting isn’t it ? SETUID & SETGID have been set on the scripts to probably gain elevated privileges ? But, for which purpose?
Which methods do we have to start external jobs from Oracle (pluggable) database?
Before moving into all the beloved technical details and reverse engineering. Let’s quickly summarize all options Oracle RBDMS offers.
DBMS_SCHEDULER jobs
According to the DBMS_SCHEDULER sub-program CREATE_JOB. Oracle supports several types for calling external programs:
Type | Description |
---|---|
STORED_PROCEDURE | Calling external C/C++ subprogram or Java Stored procedure |
EXECUTABLE | Calling anything that can be executed from the command line of the operating system |
EXTERNAL_SCRIPT | Calling an external script that uses the command shell of the computer running the job |
Please refer to the DBMS_SCHEDULER package for details.
Java Stored procedure
Indeed, Oracle RBDMS can spawn a dedicated server process on the database server using Java stored procedure and call a SHELL script.
The following Java program source code has been merged from two different code base:
- https://oracle-base.com/articles/8i/shell-commands-from-plsql
- https://innerlife.io/oracle-db-cmd-java
CREATE OR REPLACE AND COMPILE JAVA SOURCE NAMED "JavaExecHostCmd" AS
import java.io.*;
public
class JavaExecHostCmd {
public
static String executeCommand(String command) {
String buff = null;
String output = "";
try {
String[] finalCommand;
finalCommand = new String[3];
finalCommand[0] = "/bin/bash";
// finalCommand[0] = "";
finalCommand[1] = "-c";
finalCommand[2] = command;
// Execute the command...
final Process pr = Runtime.getRuntime().exec(finalCommand);
// Capture output from STDOUT...
BufferedReader br_in = null;
try {
br_in = new BufferedReader(new InputStreamReader(pr.getInputStream()));
output = output + "STDOUT: " + "\n";
while ((buff = br_in.readLine()) != null) {
// System.out.println("stdout: " + buff);
output = output + buff + "\n";
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
br_in.close();
} catch (IOException ioe) {
// System.out.println("Error printing process output.");
output = output + "Error printing process output." + "\n";
// ioe.printStackTrace();
} finally {
try {
br_in.close();
} catch (Exception ex) {
}
}
// Capture output from STDERR...
BufferedReader br_err = null;
try {
br_err = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
output = output + "\n" + "STDERR: " + "\n";
while ((buff = br_err.readLine()) != null) {
// System.out.println("stderr: " + buff);
output = output + buff + "\n";
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
br_err.close();
return output;
} catch (IOException ioe) {
// System.out.println("Error printing execution errors.");
output = output + "Error printing execution errors." + "\n";
// ioe.printStackTrace();
} finally {
try {
br_err.close();
} catch (Exception ex) {
}
}
} catch (Exception ex) {
output = output + ex.getLocalizedMessage() + "\n";
}
return output;
}
};
/
Basically, the program executes a “/bin/bash -c <Command>” and returns its result.
To interface the Java program a simple PL/SQL function is required:
CREATE OR REPLACE FUNCTION run_shell_cmd_with_java (p_command IN VARCHAR2)
RETURN VARCHAR2
AS LANGUAGE JAVA
NAME 'JavaExecHostCmd.executeCommand (java.lang.String) return String';
/
Finally, we need to grant access to the Oracle OJVM for desired database schema:
DECLARE
l_schema VARCHAR2(30) := 'JEW'; -- Adjust as required.
BEGIN
DBMS_JAVA.grant_permission(l_schema, 'java.io.FilePermission', '<<ALL FILES>>', 'read ,write, execute, delete');
DBMS_JAVA.grant_permission(l_schema, 'SYS:java.lang.RuntimePermission', 'writeFileDescriptor', '');
DBMS_JAVA.grant_permission(l_schema, 'SYS:java.lang.RuntimePermission', 'readFileDescriptor', '');
END;
/
Safe Harbor statement: Please ensure you do not blindly apply the same java.io.FilePermission which basically allows to run anything on the database server 🙂
External C/C++ programs aka “extproc”
That’s probably one of the least known option but as old as the Oracle JVM both introduced in 1999 with Oracle 8i.
The following source code was mostly generated with the support of a LLM but slightly adapted to mimic the behavior as the Java program:
cat <<EOF_> /opt/oracle_scripts/c_extproc.c
// Source: ChatGPT
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // Include the string.h header to use strcpy and strlen
#define MAX_BUFFER 1024
// Function to execute a Linux command and return the output
char* execute_command(const char *command) {
FILE *fp;
char *output = (char*)malloc(MAX_BUFFER * sizeof(char)); // Allocate memory for the output
if (output == NULL) {
perror("malloc failed");
return NULL;
}
fp = popen(command, "r"); // Open a pipe to the command
if (fp == NULL) {
perror("popen failed");
free(output);
return NULL;
}
// Buffer to accumulate the command output
char buffer[MAX_BUFFER];
size_t output_size = 0;
// Read the output line by line and accumulate it in 'output'
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
size_t len = strlen(buffer);
// Reallocate memory if necessary
if (output_size + len >= MAX_BUFFER) {
output = realloc(output, output_size + len + 1); // Add space for the new content
if (output == NULL) {
perror("realloc failed");
fclose(fp);
return NULL;
}
}
// Append the line to the output
strcpy(output + output_size, buffer);
output_size += len;
}
fclose(fp);
return output;
}
EOF_
Compile the source using GNU compiler and finally resolve the library dependencies (combine links):
gcc -fPIC -c /opt/oracle_scripts/c_extproc.c -o /opt/oracle_scripts/c_extproc.o
ld -shared -o /opt/oracle_scripts/c_extproc.so /opt/oracle_scripts/c_extproc.o
In our case, the program does not contain any “call to the function – aka. main progtam”, as such it’s just a library.
In the same way as we interfaced the Java program, we need to bind the external library with a PL/SQL function:
CREATE OR REPLACE LIBRARY shell_c_lib is '/opt/oracle_scripts/c_extproc.so';
/
CREATE OR REPLACE FUNCTION run_shell_cmd_with_c (command IN VARCHAR2)
RETURN VARCHAR2
AS EXTERNAL
NAME "execute_command"
LIBRARY shell_c_lib
LANGUAGE C
PARAMETERS (command string, RETURN string);
/
Hold on a second, for Java we had to allow the Oracle JVM to access the file sytem to execute an Operating System command. Is anything required for external C/C++ programs ?
Yes, usually ISVs recommend to configure EXTPROC_DLLS parameter within the Oracle Listener. However, in our Oracle Cloud LAB we used an unsafe shortcut which consists to whitelist any library present on the database server filesystem by editing following Oracle Home configuration file:
[oracle@svl-ora-t ~]$ grep '^SET EXTPROC_DLLS=ANY' ${ORACLE_HOME}/hs/admin/extproc.ora
SET EXTPROC_DLLS=ANY
Safe Harbor statement: Please ensure you do not blindly follow this post for your own safety 😉
HOST command from PL/SQL
Uh? Does it exists? LLM training on the job! Of course, not 😉
DBMS_SCHEDULER
The Oracle scheduler support several job types when working with external programs:
EXECUTABLE and EXTERNAL_SCRIPT
Create credentials for jobs from type EXTERNAL_SCRIPT (aka. existing local Operating System user credentials):
BEGIN
DBMS_CREDENTIAL.CREATE_CREDENTIAL(
credential_name => 'test_creds',
username => 'test',
password => 'test');
END;
/
Once, created if needed the credentials can be applied at pluggable database level only though init. parameter PDB_OS_CREDENTIAL.
Create the scheduler jobs:
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_AS_EXECUTABLE',
job_type=>'EXECUTABLE',
job_action=>'/opt/oracle_scripts/run_prog.sh',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=5;'
);
-- EXTERNAL_SCRIPT requires credentials !
-- Please Refer to DBMS_SCHEDULER package documentation for details
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_AS_EXTERNAL_SCRIPT',
job_type=>'EXTERNAL_SCRIPT',
job_action=>'/opt/oracle_scripts/run_prog.sh',
credential_name=>'test_creds',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=15;'
);
END;
/
Nota Bene: DBMS_SCHEDULER job_type EXECUTABLE do not capture STDOUT as long as no credentials are applied (created with DBMS_CREDENTIALS)! at least this was the case on my Oracle 19c Release Update 23 database
STORED_PROCEDURE and PLSQL_BLOCK (Java and External C program)
Functions are not supported by the Oracle Scheduler. Thus, we created a simple instrumentation table:
DROP TABLE debug;
CREATE TABLE debug
(EXECUTED_DATE TIMESTAMP default SYSTIMESTAMP,
use_case VARCHAR2(20),
output VARCHAR2(1024)
);
And finally, created on top of the functions for the e Java and C program stored procedures:
CREATE OR REPLACE PROCEDURE runproc_shell_cmd_with_c AS
l_output VARCHAR2(100);
BEGIN
l_output := run_shell_cmd_with_c('/usr/bin/whoami');
INSERT INTO debug(use_case, output) VALUES('c_program', l_output);
END;
/
CREATE OR REPLACE PROCEDURE runproc_shell_cmd_with_java AS
l_output VARCHAR2(100);
BEGIN
l_output := run_shell_cmd_with_java('/usr/bin/whoami');
INSERT INTO debug(use_case, output) VALUES('java_program', l_output);
END;
/
Create the scheduler jobs:
-- Once executed as STORED_PROCEDURE
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_USING_C_STOREDPROC',
job_type=>'STORED_PROCEDURE',
job_action=>'runproc_shell_cmd_with_c',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=5;'
);
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_USING_JAVA_STOREDPROC',
job_type=>'STORED_PROCEDURE',
job_action=>'runproc_shell_cmd_with_java',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=5;'
);
END;
/
-- Once executed as anonymous PL/SQL block
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_USING_C_PLSQLBLOCK',
job_type=>'PLSQL_BLOCK',
job_action=>'runproc_shell_cmd_with_c;',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=5;'
);
DBMS_SCHEDULER.CREATE_JOB(
job_name=>'EXEC_SHELL_CMD_USING_JAVA_PLSQLBLOCK',
job_type=>'PLSQL_BLOCK',
job_action=>'runproc_shell_cmd_with_java;',
enabled=>TRUE,
auto_drop=>FALSE,
repeat_interval => 'freq=secondly;bysecond=15;'
);
END;
/
Test cases
So any guess about old Unix command that prints the effective user id ? whoami?
Execution method | Type of database connection | whoami ? |
---|---|---|
SELECT run_shell_cmd_with_java(’/usr/bin/whoami’) AS stdout FROM DUAL; | Connected to PDB over SQL*NET | grid |
Connected to PDB over BEQUEATH protocol | oracle | |
SELECT run_shell_cmd_with_c(’/usr/bin/whoami’) AS stdout FROM DUAL; | Connected to PDB over SQL*NET | grid |
Connected to PDB over BEQUEATH protocol | oracle | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_AS_EXECUTABLE’); | Database job executed manually | nobody |
Database job scheduled | nobody | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_AS_EXTERNAL_SCRIPT’); | Database job executed manually | test |
Database job scheduled | test | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_USING_C_STOREDPROC’); | Database job executed manually | grid |
Database job scheduled | oracle | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_USING_JAVA_STOREDPROC’); | Database job executed manually | grid |
Database job scheduled | oracle | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_USING_C_PLSQLBLOCK’); | Database job executed manually | grid |
Database job scheduled | oracle | |
exec DBMS_SCHEDULER.RUN_JOB(‘EXEC_SHELL_CMD_USING_JAVA_PLSQLBLOCK’); | Database job executed manually | grid |
Database job scheduled | oracle |
Comments:
- With Oracle Grid Infrastructure the entire Single Client Access Name (SCAN) listeners belong to Operating System User grid
- BEQUEATH Protocol is local Operating System authentication method only based on the traditional OSDBA, OSOPER. etc .. O.S group mapping
- Oracle Scheduler job coordination CJQn processes and workers Jnn processes are spawned when database starts.
Additional background information
Oracle Cloud database home specificity
On our LAB we had to tweak the Oracle Home as documented per Oracle support note, for some reason on OCI the external job feature is not working by default and according to the note it’s a feature 🙂
DBMS_SCHEDULER Job (External Job) Failed With ORA-27300, ORA-27301, ORA-27302: failure occurred at: sjsec 6d [OCI DBCS] (Doc ID 2647889.1) |
DBMS_SCHEDULER job type EXECUTABLE
By default any job is executed under the Unix pseudo user “nobody” which is a low-privileged Unix namespace and as such considered as secure. Oracle allows to tweak the behavior by adapting ${ORACLE_HOME}/rdbms/admin/externaljob.ora:
[oracle@svl-ora-t ~]$ grep '^run_' ${ORACLE_HOME}/rdbms/admin/externaljob.ora
run_user = nobody
run_group = nobody
DBMS_SCHEDULER & Linux PAM (Pluggable Authentication Modules) kernel module
Oracle Scheduler switch the user in background like you would run an “su” command. If this is not possible because of security reasons. the Job wont’ be executed at all
[oracle@svl-ora-t ~]$ su - test
Password:
su: Permission denied
Some hardened system on which we worked did not allow to “escape” from oracle O.S user and as such all Oracle Scheduler jobs failed due to the PAM config preventing switching to another user unless member of wheel O.S group:
# Uncomment the following line to require a user to be in the "wheel" group.
auth required pam_wheel.so use_uid
Oracle Support note which is still valid nowadays despite Oracle Linux 6 reference:
Dbms_scheduler Job Executes Shell Script Failed With Error: ORA-27369: job of type EXECUTABLE failed with exit code: 7 !@#–!@#7#@!–#@! (Doc ID 2312329.1) |
Isolate Oracle JVM runtime execution to least privileged O.S user
Oracle delivers Oracle JVM isolation feature using a DBMS_JAVA sub-program to bind execution of program to a specific Operating System user. This setting requires to be conneted as SYS:
SQL> show user
"SYS"
SQL> alter session set container=APP_001I;
Session altered.
SQL> exec dbms_java.set_runtime_exec_credentials(dbuser=>'JEW', osuser=>'test', ospass =>'test');
PL/SQL procedure successfully completed.
From now on, all sub-subsequent execution will be started under the Operating System user “test”. Unfortunately, all executions fail with following error message
Cannot run program "/bin/bash": cannot execute /u01/app/oracle/product/19.0.0.0/dbhome_1/bin/jssu
Unfortunately, we were not able to fix this issue which has been well described in following post:
https://mvelikikh.blogspot.com/2020/04/secure-use-of-runtimeexec-functionality.html
Unbinding the OJVM runtime environment requires as well SYS:
SQL> alter session set container=APP_001I;
Session altered.
SQL> exec dbms_java.set_runtime_exec_credentials(dbuser=>'JEW', osuser=>null, ospass => null);
PL/SQL procedure successfully completed.
Summary
Well, there is no blueprint to deduct from this post which method suits the best for extermal programs, but at least it provides an overview and reproducible test cases to understand under which circumstances some files may or may not need special file system flags.
Indeed, the dissected use cases for several scripts / C programs were amended with SETUID, GID flag to mitigate some past issues nobody remember leaving the system an undesired state, hackers could potentially take advantage of.