暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

从Oracle(可插拔)数据库调用外部作业的最安全方式是什么?

tinge 2025-03-17
120

最近,我在面对一个相当具有挑战性的情况后,开始思考这个问题,这个问题将在本文中进行剖析。事实上,大多数数据库管理员(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程序源代码是从两个不同的代码库合并而来的:

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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论