最近,我在面对一个相当具有挑战性的情况后,开始思考这个问题,这个问题将在本文中进行剖析。事实上,大多数数据库管理员(DBA)都知道Oracle数据库引擎提供了几种调用外部程序的机制。然而,由于需要广泛的知识技能,我们很少有机会深入了解它们。
几天前,一位IT架构师要求我们查看一些在数据库服务器上由数据库进程执行的SHELL脚本和C程序。事情始于一些“奇怪”的UNIX文件权限,例如:
[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
这很有趣,不是吗?脚本设置了SETUID和SETGID,可能是为了获取更高权限?但这是为了什么目的呢?
从Oracle(可插拔)数据库启动外部作业的方法有哪些?
在深入技术细节和逆向工程之前,让我们快速总结一下Oracle RDBMS提供的所有选项。
DBMS_SCHEDULER作业
根据DBMS_SCHEDULER子程序CREATE_JOB,Oracle支持几种用于调用外部程序的类型:
| 类型 | 描述 |
|---|---|
| STORED_PROCEDURE | 调用外部C/C++子程序或Java存储过程 |
| EXECUTABLE | 调用可以从操作系统命令行执行的任何内容 |
| EXTERNAL_SCRIPT | 调用使用运行作业的计算机的命令外壳的外部脚本 |
请参考DBMS_SCHEDULER包的详细信息。
Java存储过程
事实上,Oracle RDBMS可以使用Java存储过程在数据库服务器上启动一个专用服务器进程,并调用一个SHELL脚本。
以下Java程序源代码是从两个不同的代码库合并而来的:
- 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[1] = "-c";
finalCommand[2] = command;
final Process pr = Runtime.getRuntime().exec(finalCommand);
BufferedReader br_in = null;
try {
br_in = new BufferedReader(new InputStreamReader(pr.getInputStream()));
output = output + "STDOUT: " + "\n";
while ((buff = br_in.readLine()) != null) {
output = output + buff + "\n";
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
br_in.close();
} catch (IOException ioe) {
output = output + "Error printing process output." + "\n";
} finally {
try {
br_in.close();
} catch (Exception ex) {
}
}
BufferedReader br_err = null;
try {
br_err = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
output = output + "\n" + "STDERR: " + "\n";
while ((buff = br_err.readLine()) != null) {
output = output + buff + "\n";
try {
Thread.sleep(100);
} catch (Exception e) {
}
}
br_err.close();
return output;
} catch (IOException ioe) {
output = output + "Error printing execution errors." + "\n";
} finally {
try {
br_err.close();
} catch (Exception ex) {
}
}
} catch (Exception ex) {
output = output + ex.getLocalizedMessage() + "\n";
}
return output;
}
};
/
基本上,该程序执行“/bin/bash -c
为了与Java程序接口,需要一个简单的PL/SQL函数:
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';
/
最后,我们需要为所需的数据库模式授予Oracle OJVM的访问权限:
安全港声明:请确保不要盲目应用相同的java.io.FilePermission,因为它基本上允许在数据库服务器上运行任何内容!
外部C/C++程序(即“extproc”)
这可能是最不为人知的选项之一,但它与Oracle JVM一样古老,两者都于1999年随Oracle 8i引入。
以下源代码主要是在LLM的支持下生成的,但稍作调整以模仿Java程序的行为:
cat <<EOF_ > /opt/oracle_scripts/c_extproc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 包含string.h头文件以使用strcpy和strlen
#define MAX_BUFFER 1024
// 函数:执行Linux命令并返回输出
char* execute_command(const char *command) {
FILE *fp;
char *output = (char*)malloc(MAX_BUFFER * sizeof(char)); // 为输出分配内存
if (output == NULL) {
perror("malloc失败");
return NULL;
}
fp = popen(command, "r"); // 打开到命令的管道
if (fp == NULL) {
perror("popen失败");
free(output);
return NULL;
}
// 缓冲区,用于累积命令输出
char buffer[MAX_BUFFER];
size_t output_size = 0;
// 逐行读取输出并将其累积到“output”中
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
size_t len = strlen(buffer);
// 如有必要,重新分配内存
if (output_size + len >= MAX_BUFFER) {
output = realloc(output, output_size + len + 1); // 为新内容添加空间
if (output == NULL) {
perror("realloc失败");
fclose(fp);
return NULL;
}
}
// 将行追加到输出中
strcpy(output + output_size, buffer);
output_size += len;
}
fclose(fp);
return output;
}
EOF_
使用GNU编译器编译源代码,并最终解决库依赖关系(组合链接):
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
在我们的案例中,程序不包含任何“调用函数——即主程序”,因此它只是一个库。
与Java程序一样,我们需要将外部库与PL/SQL函数绑定:
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);
/
等等,对于Java,我们需要允许Oracle JVM访问文件系统以执行操作系统命令。那么对于外部C/C++程序,需要做些什么吗?
是的,通常ISV建议在Oracle监听器中配置EXTPROC_DLLS参数。然而,在我们的Oracle Cloud LAB中,我们使用了一个不安全的捷径,即通过编辑以下Oracle Home配置文件,将数据库服务器文件系统上的任何库列入白名单:
[oracle@svl-ora-t ~]$ grep '^SET EXTPROC_DLLS=ANY' ${ORACLE_HOME}/hs/admin/extproc.ora
SET EXTPROC_DLLS=ANY
安全港声明:请确保不要盲目遵循这篇文章中的内容,以确保您的安全!
从PL/SQL中的HOST命令
什么?它存在吗?工作中的LLM训练!当然,不存在。
DBMS_SCHEDULER
Oracle调度器在处理外部程序时支持几种作业类型:
- EXECUTABLE 和 EXTERNAL_SCRIPT
为类型为 EXTERNAL_SCRIPT 的作业创建凭据(即现有的本地操作系统用户凭据):
BEGIN
DBMS_CREDENTIAL.CREATE_CREDENTIAL(
credential_name => 'test_creds',
username => 'test',
password => 'test');
END;
/
创建后,如果需要,可以通过初始化参数 PDB_OS_CREDENTIAL 在可插拔数据库级别应用这些凭据。
创建调度器作业:
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需要凭据!
-- 请参考DBMS_SCHEDULER包文档了解详细信息
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;
/
注意:在Oracle 19c Release Update 23数据库中,至少在我的测试中,如果未应用凭据(通过DBMS_CREDENTIALS创建),EXECUTABLE类型的调度器作业不会捕获STDOUT)
STORED_PROCEDURE 和 PLSQL_BLOCK(Java和外部C程序)
Oracle调度器不支持函数。因此,我们创建了一个简单的日志表
DROP TABLE debug;
CREATE TABLE debug
(EXECUTED_DATE TIMESTAMP default SYSTIMESTAMP,
use_case VARCHAR2(20),
output VARCHAR2(1024));
然后,基于Java和C程序的函数创建了存储过程:
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;
/
创建调度器作业
-- 作为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;
/
-- 作为匿名PL/SQL块执行
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;
/
测试用例
那么,哪个古老的Unix命令可以打印有效的用户ID呢?whoami。
执行方法:
执行方法 数据库连接类型 whoami 输出
SELECT run_shell_cmd_with_java('/usr/bin/whoami') FROM DUAL; 通过SQL*NET连接到PDB grid
SELECT run_shell_cmd_with_java('/usr/bin/whoami') FROM DUAL; 通过BEQUEATH协议连接到PDB oracle
SELECT run_shell_cmd_with_c('/usr/bin/whoami') FROM DUAL; 通过SQL*NET连接到PDB grid
SELECT run_shell_cmd_with_c('/usr/bin/whoami') FROM DUAL; 通过BEQUEATH协议连接到PDB oracle
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_AS_EXECUTABLE'); 手动执行数据库作业 nobody
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_AS_EXECUTABLE'); 计划执行数据库作业 nobody
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_AS_EXTERNAL_SCRIPT'); 手动执行数据库作业 test
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_AS_EXTERNAL_SCRIPT'); 计划执行数据库作业 test
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_C_STOREDPROC'); 手动执行数据库作业 grid
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_C_STOREDPROC'); 计划执行数据库作业 oracle
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_JAVA_STOREDPROC'); 手动执行数据库作业 grid
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_JAVA_STOREDPROC'); 计划执行数据库作业 oracle
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_C_PLSQLBLOCK'); 手动执行数据库作业 grid
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_C_PLSQLBLOCK'); 计划执行数据库作业 oracle
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_JAVA_PLSQLBLOCK'); 手动执行数据库作业 grid
exec DBMS_SCHEDULER.RUN_JOB('EXEC_SHELL_CMD_USING_JAVA_PLSQLBLOCK');
注释:
使用Oracle Grid Infrastructure时,所有单客户端访问名称(SCAN)监听器都属于操作系统用户grid。
BEQUEATH协议是一种仅基于传统OSDBA、OSOPER等的本地操作系统认证方法,基于操作系统的用户组映射。
Oracle调度器作业协调进程(CJQn)和工作进程(Jnn)在数据库启动时启动。
额外背景信息
Oracle Cloud数据库环境的特殊性
在我们的实验室中,我们不得不根据Oracle支持文档调整Oracle Home,因为在Oracle Cloud Infrastructure(OCI)上,外部作业功能默认情况下无法正常工作,而根据文档,这被认为是一个“特性”:
DBMS_SCHEDULER作业(外部作业)失败,错误为ORA-27300、ORA-27301、ORA-27302:在sjsec 6d处失败 [OCI DBCS](文档编号2647889.1)
DBMS_SCHEDULER 和 Linux PAM(可插拔认证模块)内核模块
Oracle调度器会在后台切换用户,就像运行了一个su命令一样。如果由于安全原因无法切换用户,则作业将无法执行。
[oracle@svl-ora-t ~]$ su - test
Password:
su: Permission denied
原文地址:https://www.dbi-services.com/blog/what-is-the-most-secure-way-to-call-an-external-job-from-an-oracle-pluggable-database/
原文作者: Jérôme Witt




