Oracle Database PL/SQL注入漏洞原理

Catalogue
  1. 1. 1.PL/SQL概述
    1. 1.1. 1.1 PL/SQL 块结构
    2. 1.2. 1.2 PL/SQL 变量和类型
      1. 1.2.1. 1.2.1 变量声明
      2. 1.2.2. 1.2.2 游标
      3. 1.2.3. 1.2.3 游标FOR循环
      4. 1.2.4. 1.2.4 游标变量
      5. 1.2.5. 1.2.5 属性
    3. 1.3. 1.3 PL/SQL 控制语句
      1. 1.3.1. 1.3.1 简单IF结构
      2. 1.3.2. 1.3.2 IF-ELSE结构
      3. 1.3.3. 1.3.3 多重IF
      4. 1.3.4. 1.3.4 CASE
      5. 1.3.5. 1.3.5 简单循环
      6. 1.3.6. 1.3.6 WHILE循环
      7. 1.3.7. 1.3.7 FOR循环
      8. 1.3.8. 1.3.8 GOTO
      9. 1.3.9. 1.3.9 NULL
    4. 1.4. 1.4 PL/SQL 子程序
      1. 1.4.1. 1.4.1 理解PL/SQL过程
      2. 1.4.2. 1.4.2 理解PL/SQL函数
    5. 1.5. 1.5 包-Package
      1. 1.5.1. 1.4.1 PL/SQL包的优点
      2. 1.5.2. 1.4.2 理解包体
      3. 1.5.3. 1.4.3 包特性的例子
  2. 2. 2.PL/SQL 注入漏洞
    1. 2.1. 2.1 Oracle 权限
    2. 2.2. 2.2 PL/SQL 注入分类
    3. 2.3. 2.3 PL/SQL 漏洞利用
    4. 2.4. 2.4 PL/SQL DBMS_SQL包
      1. 2.4.1. 2.4.1 DBMS_SQL执行流程
      2. 2.4.2. 2.4.2 DBMS_SQL特点
      3. 2.4.3. 2.4.3 漏洞成因
    5. 2.5. 2.5 PL/SQL 注入防护建议

[TOC]

by Tahir

1.PL/SQL概述

PL/SQL是一种ORACLE数据库服务器的编程语言

继承了ADA( 阿达·洛芙莱斯)第4代语言的特点

PL/SQL是一个可移植、高效的事务处理语言:

  1. 支持SQL
  2. 支持面向对象编程
  3. 良好的性能
  4. 效率高
  5. 可移植
  6. 与Oracle集成
  7. 高度安全

PL/SQL的编码和运行时系统是一项技术,而不是一个独立的产品。可以把这项技术想象成一个能够编译并运行PL/SQL块和子程序的引擎。这个引擎可以安装在Oracle服务器上或安装在Oracle Forms,Oracle Reports这样的开发工具中。所以,PL/SQL可以在两种环境中存在:

  1. Oracle数据库服务器
  2. Oracle开发工具

这两种环境是独立的。PL/SQL虽被绑定到Oracle服务器上,但在某些工具中是无法使用的。在这两种环境下,PL/SQL引擎都能接受有效的PL/SQL块或子程序。下图是PL/SQL引擎处理匿名块的过程,引擎会处理过程化语句,而把SQL语句发送给Oracle服务器端的SQL语句执行程序(SQL Statement Executor)来处理。

1.1 PL/SQL 块结构

1
2
3
4
5
6
7
8
[DECLARE]
declaration_statements
BEGIN
executable_statements

[EXCEPTION]
exception_handing_statements
END

1.2 PL/SQL 变量和类型

1.2.1 变量声明

语法:variable_name [CONSTANT] type [NOT NULL] [:=value];

  variable_name: 用于定义变量名,变量名的命名要符合标识符命名规范。

  type: 变量需要使用的数据类型,可以使用所有SQL类型或PL/SQL类型。用方括号 [] 括起来的是可选部分。

  CONSTANT: 表示声明为一个常量,常量在定义时需要指定初始值,一旦定义其值,不能再被改变。

  NOT NULL: 用于约束变量的值不能为空。

  :=value: 用于为变量赋初始值。

1
2
3
4
5
6
7
8
DECLARE
  v_empname VARCHAR2(20); --定义员工名称变量
  v_deptname VARCHAR2(20); --定义部门名称变量
  v_hiredate DATE NOT NULL := SYSDATE; --定义入职日期变量
  v_empno INT NOT NULL DEFAULT 1111; --变量员工编码变量
BEGIN
  NULL; --不执行任何代码
END;

​ 注: :=和DEFAULT是可以互换使用的,都用来为变量赋初始值。一旦出现了NOT NULL关键字,后面必须具有赋初始值的语句。

类型 子类 说 明 范 围 ORACLE限制
CHAR CharacterStringRowidNchar 定长字符串 民族语言字符集 0à32767可选,确省=1 2000
VARCHAR2 Varchar, StringNVARCHAR2 可变字符串民族语言字符集 0à327674000 4000
BINARY_INTEGER 带符号整数,为整数计算优化性能
NUMBER(p,s) Dec Double precisionIntegerIntNumericRealSmall int 小数, NUMBER 的子类型高精度实数整数, NUMBER 的子类型整数, NUMBER 的子类型与NUMBER等价与NUMBER等价整数, 比 integer 小
LONG 变长字符串 0->2147483647 32,767字节
DATE 日期型 公元前4712年1月1日至公元后4712年12月31日
BOOLEAN 布尔型 TRUE, FALSE,NULL 不使用
ROWID 存放数据库行号
UROWID 通用行标识符,字符类型

 如果变量在声明时没有指定初始值,默认情况下,变量被初始化为NULL值。如果未给变量赋值,就直接使用变量,将会产生意想不到的结果。

  根据变量的不同类型,可以为变量直接赋常量值,也可以使用表达式来计算变量的值。

  下面的代码根据薪资和加薪比例来计算员工的结果薪资值:

1
2
3
4
5
6
7
8
9
DECLARE
  v_salary NUMBER(7,2);
  v_rate NUMBER(7,2) :=0.12;
  v_base_salary NUMBER(7,2) :=1200;

BEGIN
 v_salary := v_base_salary*(1+v_rate); --使用表达式为变量赋值
  DBMS_OUTPUT.put_line('员工的薪资值为:'||v_salary);
END;

在为PL/SQL变量赋值时,需要注意变量的类型。下面列出了常用的变量类型的赋值方式。

1
2
3
4
5
6
7
8
9
10
11
DECLARE
  v_string VARCHAR2(200);
  v_hire_date DATE;
  v_bool BOOLEAN; --PL/SQL布尔类型
BEGIN
  v_bool:=True; --为布尔类型赋值
  v_hire_date:=to_date('2019-04-28','yyyy-mm-dd'); --使用函数为日期赋值
  v_hire_date:=SYSDATE; --使用日期函数赋值
  v_hire_date:=date'2019-04-28'; --直接赋静态日期值
  v_string:='this is a string';
END;

1.2.2 游标

Oracle使用工作区(work area)来执行SQL语句,并保存处理信息。PL/SQL可以让我们使用游标来为工作区命名,并访问存储的信息。游标的类型有两种:隐式和显式。PL/SQL会为所有的SQL数据操作声明一个隐式的游标,包括只返回一条记录的查询操作。对于返回多条记录的查询,我们可以显式地声明一个游标来处理每一条记录。如下例:

1
2
3
4
5
DECLARE
CURSOR c1 IS
SELECT empno, ename, job
FROM emp
WHERE deptno = 20;

由多行查询返回的行集合称为结果集(result set)。它的大小就是满足我们查询条件的行的个数。如下图所示,显式游标”指向”当前行的记录,这可以让我们的程序每次处理一条记录。

多行查询处理有些像文件处理。例如,一个COBOL程序打开一个文件,处理记录,然后关闭文件。同样,一个PL/SQL程序打开一个游标,处理查询出来的行,然后关闭游标。就像文件指针能标记打开文件中的当前位置一样,游标能标记出结构集的当前位置。

我们可以使用OPEN,FETCH和CLOSE语句来控制游标,OPEN用于打开游标并使游标指向结果集的第一行,FETCH会检索当前行的信息并把游标指移向下一行,当最后一行也被处理完后,CLOSE就会关闭游标。

1.2.3 游标FOR循环

在大多需要使用显式游标的情况下,我们都可以用一个简单的游标FOR循环来代替OPEN,FETCH和CLOSE语句。首先,游标FOR循环会隐式地声明一个代表当前行的循环索引(loop index)变量。下一步,它会打开游标,反复从结果集中取得数据并放到循环索引的各个域(field)中。当所有行都被处理过以后,它就会关闭游标。下面的例子中,游标FOR循环隐式地声明了一个emp_rec记录:

1
2
3
4
5
6
7
8
9
10
11
DECLARE
CURSOR c1 IS
SELECT ename, sal, hiredate, deptno
FROM emp;
...
BEGIN
FOR emp_rec IN c1 LOOP
...
salary_total := salary_total + emp_rec.sal;
END LOOP;
END;

为了使用每一个查询到的行中的每一个字段,我们可以使用点标志(dot notation),它的作用就像一个域选择器。

1.2.4 游标变量

游标变量的使用方式和游标类似,但更加灵活,因为它不绑定于某个特定的查询,所以可用于打开任何返回类型相兼容的查询语句。游标变量是真正的PL/SQL变量,我们可以为它赋值,把它传递给子程序。如下例,我们把游标变量作为存储过程open_cv的一个参数传进去,程序执行时,可以根据choice值的不同,灵活地打开不同的查询内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PROCEDURE open_cv(generic_cv IN OUT genericcurtyp, choice NUMBER) IS
BEGIN
IF choice = 1 THEN
OPEN generic_cv FOR
SELECT *
FROM emp;
ELSIF choice = 2 THEN
OPEN generic_cv FOR
SELECT *
FROM dept;
ELSIF choice = 3 THEN
OPEN generic_cv FOR
SELECT *
FROM salgrade;
END IF;
...
END;

1.2.5 属性

PL/SQL的变量和游标都有着让我们能够直接引用它们各自的数据类型或结构的属性。数据库字段和表也有类似的属性。”%”是一个属性的指示符。

  • %TYPE

%TYPE可以提供一个变量或数据库字段的数据类型,这在声明存放数据库值的变量时是非常有用的。假设我们要声明一个存放表books中的字段my_title的字段的变量时,就可以这样使用%TYPE属性:

1
my_title books.title%TYPE;

这样声明my_title有两个优点,第一,我们不必知道title具体类型;第二,如果我们改变了数据库中对该字段的数据类型定义的话,my_title的数据类型会在运行时做出相应的改变,并不需要手动地进行维护。

  • %ROWTYPE

在PL/SQL中,记录用于将逻辑相关数据组织起来。一个记录是由许多相关域的组合。%ROWTYPE属性返回一个记录类型,其数据类型和数据表的数据结构相一致。这样的记录类型可以完全保存从数据表中查询(SELECT)或从游标/游标变量取出(FETCH)的行记录。

行中的字段和记录中的域对应的名称和数据类型都完全一致。下面的例子中,我们声明一个dept_rec的记录。它的域名称和数据类型与表dept中的字段名称和数据类型就完全一样。

1
DECLARE  dept_rec  dept%ROWTYPE;  -- declare record variable

我们可以使用”.”来引用记录中的域。

1
my_deptno := dept_rec.deptno;

假设我们声明了一个用于检索雇员的名字、工资、雇用日期和职称的游标,我们就可以使用%ROWTYPE来声明一个类型相同的记录,如下例:

1
2
3
4
5
6
7
DECLARE
CURSOR c1 IS
SELECT ename, sal, hiredate, job
FROM emp;

emp_rec c1%ROWTYPE; -- declare record variable that represents
-- a row fetched from the emp table

当我们执行语句

1
FETCH c1 INTO emp_rec;

表emp中ename字段的值就会赋给emp_rec的ename域,sal字段值赋给sal域,依此类推。

1.3 PL/SQL 控制语句

1.3.1 简单IF结构

1
2
3
IF <布尔表达式> THEN
满足条件时执行的语句
END IF;

1.3.2 IF-ELSE结构

1
2
3
4
5
IF <布尔表达式> THEN
满足条件时执行的语句
ELSE
不满足条件时执行的语句
END IF;

1.3.3 多重IF

1
2
3
4
5
6
7
8
9
IF <布尔表达式1> THEN
满足条件1时执行的语句
ELSIF <布尔表达式2> THEN
满足条件2时执行的语句
ELSIF <布尔表达式3> THEN
满足条件3时执行的语句
ELSE
满足条件1、2、3均不满足时执行的语句
END IF;

注意:ELSIF不能写成ELSEIF

1.3.4 CASE

语法一:

1
2
3
4
5
6
7
8
9
10
CASE 条件表达式
WHEN 条件表达式结果1 THEN
语句1
WHEN 条件表达式结果2 THEN
语句2
......
WHEN 条件表达式结果n THEN
语句n
[ELSE 条件表达式结果]
END CASE;

语法二:

1
2
3
4
5
6
7
8
9
10
CASE 
WHEN 条件表达式1 THEN
语句1
WHEN 条件表达式2 THEN
语句2
......
WHEN 条件表达式n THEN
语句n
[ELSE 语句]
END CASE;

1.3.5 简单循环

语法:

1
2
3
4
LOOP
循环体语句;
[EXIT WHEN <条件语句>]
END LOOP;

1.3.6 WHILE循环

语法:

1
2
3
WHILE <布尔表达式> LOOP
循环体语句;
END LOOP;

1.3.7 FOR循环

语法:

1
2
3
4
[<<标签>>]
FOR 循环计数器 IN [ REVERSE ] 下限 .. 上限 LOOP
循环体语句;
END LOOP [<<标签>>];

说明:

使用关键字REVERSE,循环变量自动减1。跟在IN REVERSE后面的数字应是从小到大的顺序,而且必须是整数,不能是变量或表达式。

1.3.8 GOTO

GOTO语句用于跳转到指定<<标号>>去执行语句,是无条件跳转到指定的标号去的意思。

注意:标号是用<< >>括起来的标识符。

语法:

1
GOTO label;

GOTO语句缺点是会增加程序的复杂性,降低可读性,所以Oracle建议不要使用。

1.3.9 NULL

空语句,执行没有任何实际效果,可以使某些语句变得有意义,提高程序的可读性,保证其他语句结构的完整性和正确性,通常用于占位置。

1.4 PL/SQL 子程序

子程序就是能够接受参数并被其他程序所调用的命名PL/SQL块。PL/SQL子程序有两种类型,过程和函数。

一般地,过程用于执行一个操作,而函数用于计算一个结果值。

与未命名或匿名PL/SQL块一样,子程序也有声明部分,执行部分和一个可选的异常处理部分。声明部分包含类型、游标、常量、变量、异常和嵌套子程序的声明。这些内容都是本地的,在程序退出时会自动销毁。执行部分包含赋值语句、流程控制语句和Oracle的数据操作语句。异常处理部分包含异常处理程序。思考下面用于记入借方银行账户的debit_account过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PROCEDURE debit_account(acct_id INTEGER, amount REAL) IS
old_balance REAL;
new_balance REAL;
overdrawn EXCEPTION;
BEGIN
SELECT bal
INTO old_balance
FROM accts
WHERE acct_no = acct_id;

new_balance := old_balance - amount;

IF new_balance < 0 THEN
RAISE overdrawn;
ELSE
UPDATE accts
SET bal = new_balance
WHERE acct_no = acct_id;
END IF;
EXCEPTION
WHEN overdrawn THEN
...
END debit_account;

在被调用时,这个过程接受一个银行账号和借贷金额。它使用账号从accts表中查询账目结算信息。然后用借款金额计算新的账目结算。如果计算后的余额比零小,异常就会被抛出;否则,该账号相关信息就会被更新。

1.4.1 理解PL/SQL过程

过程是一个能执行某个特定操作的子程序。我们可以用下面的语法来编写过程:

1
2
3
4
5
6
7
8
9
10
[CREATE [OR REPLACE]] 
PROCEDURE procedure_name[(parameter[, parameter]...)]
[AUTHID {DEFINER | CURRENT_USER}] {IS | AS}
[PRAGMA AUTONOMOUS_TRANSACTION;]
[local declarations]
BEGIN
executable statements
[EXCEPTION
exception handlers]
END [name];

parameter的含义如下:

1
2
parameter_name [IN | OUT [NOCOPY] | IN OUT [NOCOPY]] datatype 
[{:= | DEFAULT} expression]

CREATE子句能让我们创建保存在数据库中的独立过程。我们可以从SQLPlus中或是在使用动态SQL的程序中执行CREATE PROCEDURE语句。

AUTHID子句决定了存储过程是按所有者权限(默认)调用还是按当前用户权限执行,也能决定在没有限定修饰词的情况下,对所引用的对象是按所有者模式进行解析还是按当前用户模式进行解析。我们可以指定CURRENT_USER来覆盖掉程序的默认行为。

编译指示AUTONOMOUS_TRANSACTION会告诉PL/SQL编译器把过程标记为自治(独立)。自治事务能让我们把主事务挂起,执行SQL操作,提交或回滚自治事务,然后再恢复主事务。

我们不能对参数的数据类型进行约束,如下例中对acct_id的声明就是不合法的,因为它对CHAR类型进行了长度限制:

1
PROCEDURE reconcile (acct_id CHAR(5)) IS ... -- illegal

但是,我们可以使用下面的方法间接的对字符的长度进行限制:

1
2
3
DECLARE  
SUBTYPE Char5 IS CHAR(5);
PROCEDURE reconcile (acct_id Char5) IS ...

过程有两个部分,过程说明和过程体。说明部分由关键字PROCEDURE开头,以过程名或参数列表结尾。参数声明是可选的。没有参数的过程是不用使用圆括号的。

过程体由关键字IS(或AS)开头,并以END结尾,END后面可以跟上一个可选的过程名。过程体有三个部分:声明、执行和可选的异常处理。

声明部分包括本地声明,它处于IS和BEGIN之间。在匿名PL/SQL块使用的关键字DECLARE在这里不再需要。执行部分包括许多语句,它们被放到BEGIN和EXCEPTION(或END)之间,并且至少要有一条语句出现在过程的执行部分。NULL语句可以满足这个需求。异常处理部分包含异常处理程序,它被放在关键字EXCEPTION和END之间。

在下面的过程raise_salary中,我们会根据给定的金额来为雇员加薪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PROCEDURE raise_salary(emp_id INTEGER, amount REAL) IS  
current_salary REAL;
salary_missing EXCEPTION;
BEGIN
SELECT sal
INTO current_salary
FROM emp
WHERE empno = emp_id;

IF current_salary IS NULL THEN
RAISE salary_missing;
ELSE
UPDATE emp
SET sal = sal + amount
WHERE empno = emp_id;
END IF;

EXCEPTION
WHEN NO_DATA_FOUND THEN
INSERT INTO emp_audit
VALUES (emp_id, *'No such number'*);
WHEN salary_missing THEN
INSERT INTO emp_audit
VALUES (emp_id, *'Salary is null'*);
END raise_salary;

在调用时,过程接受雇员编号和薪资调整金额,然后用雇员编号从emp表找出指定雇员的当前工资。如果雇员编号无法找到或是当前工资为空,异常就会被抛出,否则工资就会被更新。

过程可以作为一个PL/SQL语句来调用。例如,我们可以像下面这样调用raise_salary:

1
raise_salary(emp_id, amount);

1.4.2 理解PL/SQL函数

函数是一个能够计算结果值的子程序,函数除了有一个RETURN子句之外,其它结构跟过程类似。我们可以用下面的语法来编写(本地)函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[CREATE [OR REPLACE ] ]  
FUNCTION function_name [ ( parameter [ , parameter ]... ) ] RETURN datatype
[ AUTHID { DEFINER | CURRENT_USER } ]
[ PARALLEL_ENABLE
[ { [CLUSTER parameter BY (column_name [, column_name ]... ) ] |
[ORDER parameter BY (column_name [ , column_name ]... ) ] } ]
[ ( PARTITION parameter BY {
[ {RANGE | HASH } (column_name [, column_name]...)] | ANY }
) ]
]
[DETERMINISTIC] [ PIPELINED [ USING implementation_type ] ]
[ AGGREGATE [UPDATE VALUE] [WITH EXTERNAL CONTEXT]
USING implementation_type ] {IS | AS}
[ PRAGMA AUTONOMOUS_TRANSACTION; ]
[ local declarations ]
BEGIN
executable statements
[ EXCEPTION
exception handlers ]
END [ name ];

函数的语法结构与过程类似,这里就不再重复。但有几个不同点还是需要注意的。

PARALLEL_ENABLE选项能声明一个在并发DML操作的从属会话(slave session)中被安全调用的存储函数。主(logon)会话的状态不会被从属会话所共享。每个从属会话都有它自己的状态,这是在会话开始时初始化的。函数的结果不应依赖于会话(静态)变量的状态。否则结果就可能随着会话而发生变化。

提示DETERMINISTIC能帮助优化程序避免冗余的函数调用。如果存储函数的调用跟前一次调用时所使用的参数相同,优化程序就直接选出前一次的计算结果值。函数结果不应该依赖于会话变量或模式对象的状态。否则结果会随着调用而发生变化。只有DETERMINISTIC函数才允许被函数索引或是参数query_rewrite_enabled为TRUE的实体化视图调用。

我们不能对参数或是函数返回值的类型添加约束,但可以像前面的过程那样使用间接的约束方法。

1.5 包-Package

PL/SQL可以让我们把逻辑相关的类型、变量、游标和子程序放在一个包内,这样更加清楚易理解。包通常有两部分组成:包说明部分和包体部分。包说明部分是应用程序的接口,它声明了类型、常量、变量、异常、游标和可以使用的子程序。包体用于实现包说明部分声明的子程序和游标。

下面的例子是把两个雇用相关的过程进行打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE PACKAGE emp_actions AS  -- package specification
PROCEDURE hire_employee(empno NUMBER, ename CHAR, ...);

PROCEDURE fire_employee(emp_id NUMBER);
END emp_actions;

CREATE PACKAGE BODY emp_actions AS -- package body
PROCEDURE hire_employee(empno NUMBER, ename CHAR, ...) IS
BEGIN
INSERT INTO emp
VALUES (empno, ename, ...);
END hire_employee;

PROCEDURE fire_employee(emp_id NUMBER) IS
BEGIN
DELETE FROM emp
WHERE empno = emp_id;
END fire_employee;
END emp_actions;

只有在包说明中声明的内容对应用程序是可见的,而包体中的内容是不可见,无法访问的。包被编译后会存放到数据库中,当我们首次调用包中的子程序时,整个包就会被加载到内存当中,所以,后续的调用不再涉及到磁盘的读取问题。因此,包可以提高效率改善性能。

1.4.1 PL/SQL包的优点

包提供了几个优点:模块化、方便应用程序设计、信息隐藏、附加功能和良好的性能。

  • 模块化

包能让我们把逻辑相关的类型、常量、变量、异常和子程序等放到一个命名的PL/SQL模块中。每一个包都容易理解,包与包之间接口简单、清晰。这将有助于程序开发。

  • 轻松的程序设计

设计应用程序时,我们首先要确定的是包说明中的接口信息。我们可以在没有包体的条件下编写并编译说明部分。然后引用该包的存储子程序也会被编译。在完成整个应用程序之前,我们是不需要完全实现包体部分的。

  • 信息隐藏

有了包,我们就可以指定哪些类型、常量、变量、异常和子程序等是公有(可见和可访问)或私有(隐藏和不可访问)。例如,如果一个包里包含了四个子程序,其中三个是公有的一个是私有的。包就会隐藏私有子程序的实现,这样的话,如果实现内容发生改变,受到影响的只有包本身(不是我们的应用程序)。同样,对用户隐藏实现细节也能保证包的完整性。

  • 附加功能

打包公有变量和游标在一个会话期会一直存在。所以,它们可以被当前环境下的所有子程序共享。并且它们允许我们跨事务来维护数据而不用把它保存在数据库中。

  • 良好的性能

在我们首次调用打包子程序时,整个包就会被加载到内存中。所以,以后调用包中的相关子程序时,就不需要再次读取磁盘了。包能阻塞级联依赖,这样就能避免不必要的编译。例如,如果我们改变打包函数的实现,Oracle不需要重新编译调用子程序,因为它们并不依赖于包体。

1.4.2 理解包体

包体是对包说明的实现。也就是说包体中包含了包说明中声明的每一个游标和子程序的实现。一定要记住,包体内实现的内容只有在包说明中声明之后才能在包外引用。为了匹配包说明和包体,PL/SQL做了一个token-by-token的子程序头比较。所以,除了空白内容,头部内容必须完全一致。否则,PL/SQL就会抛出异常,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE PACKAGE emp_actions AS
...
PROCEDURE calc_bonus(date_hired emp.hiredate%TYPE, ...);
END emp_actions;

CREATE PACKAGE BODY emp_actions AS
...
PROCEDURE calc_bonus(date_hired DATE, ...) IS
-- parameter declaration raises an exception because 'DATE'*
-- does not match 'emp.hiredate%TYPE' word for word*
BEGIN
...
END;
END emp_actions;

1.4.3 包特性的例子

下面是一个名为emp_actions的包。包说明声明了类型、游标、异常和子程序:

  1. 类型EmpRecTyp和DeptRecTyp
  2. 游标desc_salary
  3. 异常invalid_salary
  4. 函数hire_employee和raise_salary
  5. 过程fire_empire和raise_salary

在编写包之后,我们就可以开发引用它声明的类型,调用它的子程序、游标和异常的应用程序。创建包时,它就会被存放在Oracle数据库中供广泛地调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
CREATE PACKAGE emp_actions AS
/* Declare externally visible types, cursor, exception. */
TYPE emprectyp IS RECORD(
emp_id INT,
salary REAL
);

TYPE deptrectyp IS RECORD(
dept_id INT,
LOCATION VARCHAR2
);

CURSOR desc_salary RETURN emprectyp;

invalid_salary EXCEPTION;

/* Declare externally callable subprograms. */
FUNCTION hire_employee(
ename VARCHAR2,
job VARCHAR2,
mgr REAL,
sal REAL,
comm REAL,
deptno REAL
)
RETURN INT;

PROCEDURE fire_employee(emp_id INT);

PROCEDURE raise_salary(emp_id INT, grade INT, amount REAL);

FUNCTION nth_highest_salary(n INT)
RETURN emprectyp;
END emp_actions;

2.PL/SQL 注入漏洞

2.1 Oracle 权限

在创建用户之前,可以登录系统管理员权限去创建一个用户(因为系统管理员有这个权限),下面是三种用户分类:

1
2
3
sys;   //系统管理员,拥有最高权限
system;//本地管理员,次高权限
scott; //普通用户,密码默认为tiger,默认未解锁

在登陆的时候,可以选择下面三种登录方式:

1
2
3
sqlplus / as sysdba;  //登陆sys帐户
sqlplus sys as sysdba;//同上
sqlplus scott/tiger; //登陆普通用户scott

可以参考Oracle安全标签机制(OLS-BLP模型)

  1. 定义者权限(默认情况下)

  1. 调用者权限(AUTHID RURRENT_USER)

2.2 PL/SQL 注入分类

用户权限 具体权限 漏洞数 影响广度 解决方案
EXECUTE ANT PROCEDURE 降低用户权限、升级补丁、取消 public权限
CREATE ANY TRIGGER 降低用户权限、升级补丁、取消 public权限
CREATE ANY VIEW 降低用户权限、升级补丁、取消 public权限
CREATE functions 降低用户权限、升级补丁、取消 public权限
CREATE procedure 降低用户权限、升级补丁、取消 public权限
CREATE connection-注入匿名 升级补丁、取消 public权限
CREATE connection-snarf攻击 极少 注意关闭游标
CREATE connection-利用java 极少 删除Java、取消 public权限

2.3 PL/SQL 漏洞利用

定义者权限和调用者权限

1
GRANT DBA TO USER
1
SELECT PASSWORD FROM SYS.USER$ WHERE NAME='SYS';

用调用者权限创建一个函数——————————->用带有public权限的SYS函数调用黑客创建的函数

标准SQL注入提权思路例子:

1.用低权限用户写一个调用者权限的函数get_dba,内容是低权限用户无法执行的提权操作,并设置运行时编译。

2.将get_dba函数放入public,使得高权限用户可以防问;社会工程学诱骗,sys用户执行get_dba函数,成功提取。

2.4 PL/SQL DBMS_SQL包

2.4.1 DBMS_SQL执行流程

通常运用DBMS_SQL包一般分为如下几步:

  1. open cursor:打开cursor

  2. parse cursor:解析你要执行的SQL语句,生成任务计划

  3. bind variable:如果要执行的SQL语句中包含变量,在此就需要绑定变量

  4. execute:执行SQL语句

  5. close cursor:在执行后关闭此cursor.

如果你还需要返回执行SQL的结果集,还需要使用define_column,define_array等方法,具体可以看如下流程图:

2.4.2 DBMS_SQL特点

  • 一定条件下可执行任意语句

    1
    DBMS_SQL_PARSE(C IN INTEGER, STATUMENT IN VARCHAR2, LANGUAGE_FLAG IN INTEGER) IS
  • 包是PUBLIC属性,只要能连接到数据库就能调用

  • 调用者权限

想办法绕过调用者权限

1.高权限用户sys,定义一个名为A的public属性的具有定义者权限的过程,并调用了DBMS_SQL.PARSE接口,此接口存在一个参数能传入一个动态字符串并执行sql。

2.低权限soctt用户执行public属性的A过程,虽然报错但是执行成功。

3.提权成功。

2.4.3 漏洞成因

危险的定义者权限

危险的public执行权限

禁止执行任意SQL语句

加强用户的身份识别

例子:在9i中ctxsys是DBA账户

1
ctxsys.driload.validate_stmt('grant dba to public');

10g后把这个包的创建者ctxsys权限从DBA中移除了

2.5 PL/SQL 注入防护建议

DBMS_SQL.PARSE()最后调用ICD_PARSE,未传入了USERID

DBMS_SQL.PARSE_AS_USER()最后调用ICD_PARSE,传入了USERID

所以建议使用DBMS_SQL.PARSE_AS_USER()