[TOC]
by Tahir
1.PL/SQL概述
PL/SQL是一种ORACLE数据库服务器的编程语言
继承了ADA( 阿达·洛芙莱斯)第4代语言的特点
PL/SQL是一个可移植、高效的事务处理语言:
- 支持SQL
- 支持面向对象编程
- 良好的性能
- 效率高
- 可移植
- 与Oracle集成
- 高度安全
PL/SQL的编码和运行时系统是一项技术,而不是一个独立的产品。可以把这项技术想象成一个能够编译并运行PL/SQL块和子程序的引擎。这个引擎可以安装在Oracle服务器上或安装在Oracle Forms,Oracle Reports这样的开发工具中。所以,PL/SQL可以在两种环境中存在:
- Oracle数据库服务器
- Oracle开发工具
这两种环境是独立的。PL/SQL虽被绑定到Oracle服务器上,但在某些工具中是无法使用的。在这两种环境下,PL/SQL引擎都能接受有效的PL/SQL块或子程序。下图是PL/SQL引擎处理匿名块的过程,引擎会处理过程化语句,而把SQL语句发送给Oracle服务器端的SQL语句执行程序(SQL Statement Executor)来处理。
1.1 PL/SQL 块结构
1 | [DECLARE] |
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 | DECLARE |
注: :=和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 | DECLARE |
在为PL/SQL变量赋值时,需要注意变量的类型。下面列出了常用的变量类型的赋值方式。
1 | DECLARE |
1.2.2 游标
Oracle使用工作区(work area)来执行SQL语句,并保存处理信息。PL/SQL可以让我们使用游标来为工作区命名,并访问存储的信息。游标的类型有两种:隐式和显式。PL/SQL会为所有的SQL数据操作声明一个隐式的游标,包括只返回一条记录的查询操作。对于返回多条记录的查询,我们可以显式地声明一个游标来处理每一条记录。如下例:
1 | DECLARE |
由多行查询返回的行集合称为结果集(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 | DECLARE |
为了使用每一个查询到的行中的每一个字段,我们可以使用点标志(dot notation),它的作用就像一个域选择器。
1.2.4 游标变量
游标变量的使用方式和游标类似,但更加灵活,因为它不绑定于某个特定的查询,所以可用于打开任何返回类型相兼容的查询语句。游标变量是真正的PL/SQL变量,我们可以为它赋值,把它传递给子程序。如下例,我们把游标变量作为存储过程open_cv的一个参数传进去,程序执行时,可以根据choice值的不同,灵活地打开不同的查询内容。
1 | PROCEDURE open_cv(generic_cv IN OUT genericcurtyp, choice NUMBER) IS |
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 | DECLARE |
当我们执行语句
1 | FETCH c1 INTO emp_rec; |
表emp中ename字段的值就会赋给emp_rec的ename域,sal字段值赋给sal域,依此类推。
1.3 PL/SQL 控制语句
1.3.1 简单IF结构
1 | IF <布尔表达式> THEN |
1.3.2 IF-ELSE结构
1 | IF <布尔表达式> THEN |
1.3.3 多重IF
1 | IF <布尔表达式1> THEN |
注意:ELSIF不能写成ELSEIF
1.3.4 CASE
语法一:
1 | CASE 条件表达式 |
语法二:
1 | CASE |
1.3.5 简单循环
语法:
1 | LOOP |
1.3.6 WHILE循环
语法:
1 | WHILE <布尔表达式> LOOP |
1.3.7 FOR循环
语法:
1 | [<<标签>>] |
说明:
使用关键字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 | PROCEDURE debit_account(acct_id INTEGER, amount REAL) IS |
在被调用时,这个过程接受一个银行账号和借贷金额。它使用账号从accts表中查询账目结算信息。然后用借款金额计算新的账目结算。如果计算后的余额比零小,异常就会被抛出;否则,该账号相关信息就会被更新。
1.4.1 理解PL/SQL过程
过程是一个能执行某个特定操作的子程序。我们可以用下面的语法来编写过程:
1 | [CREATE [OR REPLACE]] |
parameter的含义如下:
1 | parameter_name [IN | OUT [NOCOPY] | IN OUT [NOCOPY]] datatype |
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 | DECLARE |
过程有两个部分,过程说明和过程体。说明部分由关键字PROCEDURE开头,以过程名或参数列表结尾。参数声明是可选的。没有参数的过程是不用使用圆括号的。
过程体由关键字IS(或AS)开头,并以END结尾,END后面可以跟上一个可选的过程名。过程体有三个部分:声明、执行和可选的异常处理。
声明部分包括本地声明,它处于IS和BEGIN之间。在匿名PL/SQL块使用的关键字DECLARE在这里不再需要。执行部分包括许多语句,它们被放到BEGIN和EXCEPTION(或END)之间,并且至少要有一条语句出现在过程的执行部分。NULL语句可以满足这个需求。异常处理部分包含异常处理程序,它被放在关键字EXCEPTION和END之间。
在下面的过程raise_salary中,我们会根据给定的金额来为雇员加薪:
1 | PROCEDURE raise_salary(emp_id INTEGER, amount REAL) IS |
在调用时,过程接受雇员编号和薪资调整金额,然后用雇员编号从emp表找出指定雇员的当前工资。如果雇员编号无法找到或是当前工资为空,异常就会被抛出,否则工资就会被更新。
过程可以作为一个PL/SQL语句来调用。例如,我们可以像下面这样调用raise_salary:
1 | raise_salary(emp_id, amount); |
1.4.2 理解PL/SQL函数
函数是一个能够计算结果值的子程序,函数除了有一个RETURN子句之外,其它结构跟过程类似。我们可以用下面的语法来编写(本地)函数:
1 | [CREATE [OR REPLACE ] ] |
函数的语法结构与过程类似,这里就不再重复。但有几个不同点还是需要注意的。
PARALLEL_ENABLE选项能声明一个在并发DML操作的从属会话(slave session)中被安全调用的存储函数。主(logon)会话的状态不会被从属会话所共享。每个从属会话都有它自己的状态,这是在会话开始时初始化的。函数的结果不应依赖于会话(静态)变量的状态。否则结果就可能随着会话而发生变化。
提示DETERMINISTIC能帮助优化程序避免冗余的函数调用。如果存储函数的调用跟前一次调用时所使用的参数相同,优化程序就直接选出前一次的计算结果值。函数结果不应该依赖于会话变量或模式对象的状态。否则结果会随着调用而发生变化。只有DETERMINISTIC函数才允许被函数索引或是参数query_rewrite_enabled为TRUE的实体化视图调用。
我们不能对参数或是函数返回值的类型添加约束,但可以像前面的过程那样使用间接的约束方法。
1.5 包-Package
PL/SQL可以让我们把逻辑相关的类型、变量、游标和子程序放在一个包内,这样更加清楚易理解。包通常有两部分组成:包说明部分和包体部分。包说明部分是应用程序的接口,它声明了类型、常量、变量、异常、游标和可以使用的子程序。包体用于实现包说明部分声明的子程序和游标。
下面的例子是把两个雇用相关的过程进行打包:
1 | CREATE PACKAGE emp_actions AS -- package specification |
只有在包说明中声明的内容对应用程序是可见的,而包体中的内容是不可见,无法访问的。包被编译后会存放到数据库中,当我们首次调用包中的子程序时,整个包就会被加载到内存当中,所以,后续的调用不再涉及到磁盘的读取问题。因此,包可以提高效率改善性能。
1.4.1 PL/SQL包的优点
包提供了几个优点:模块化、方便应用程序设计、信息隐藏、附加功能和良好的性能。
- 模块化
包能让我们把逻辑相关的类型、常量、变量、异常和子程序等放到一个命名的PL/SQL模块中。每一个包都容易理解,包与包之间接口简单、清晰。这将有助于程序开发。
- 轻松的程序设计
设计应用程序时,我们首先要确定的是包说明中的接口信息。我们可以在没有包体的条件下编写并编译说明部分。然后引用该包的存储子程序也会被编译。在完成整个应用程序之前,我们是不需要完全实现包体部分的。
- 信息隐藏
有了包,我们就可以指定哪些类型、常量、变量、异常和子程序等是公有(可见和可访问)或私有(隐藏和不可访问)。例如,如果一个包里包含了四个子程序,其中三个是公有的一个是私有的。包就会隐藏私有子程序的实现,这样的话,如果实现内容发生改变,受到影响的只有包本身(不是我们的应用程序)。同样,对用户隐藏实现细节也能保证包的完整性。
- 附加功能
打包公有变量和游标在一个会话期会一直存在。所以,它们可以被当前环境下的所有子程序共享。并且它们允许我们跨事务来维护数据而不用把它保存在数据库中。
- 良好的性能
在我们首次调用打包子程序时,整个包就会被加载到内存中。所以,以后调用包中的相关子程序时,就不需要再次读取磁盘了。包能阻塞级联依赖,这样就能避免不必要的编译。例如,如果我们改变打包函数的实现,Oracle不需要重新编译调用子程序,因为它们并不依赖于包体。
1.4.2 理解包体
包体是对包说明的实现。也就是说包体中包含了包说明中声明的每一个游标和子程序的实现。一定要记住,包体内实现的内容只有在包说明中声明之后才能在包外引用。为了匹配包说明和包体,PL/SQL做了一个token-by-token的子程序头比较。所以,除了空白内容,头部内容必须完全一致。否则,PL/SQL就会抛出异常,如下例所示:
1 | CREATE PACKAGE emp_actions AS |
1.4.3 包特性的例子
下面是一个名为emp_actions的包。包说明声明了类型、游标、异常和子程序:
- 类型EmpRecTyp和DeptRecTyp
- 游标desc_salary
- 异常invalid_salary
- 函数hire_employee和raise_salary
- 过程fire_empire和raise_salary
在编写包之后,我们就可以开发引用它声明的类型,调用它的子程序、游标和异常的应用程序。创建包时,它就会被存放在Oracle数据库中供广泛地调用。
1 | CREATE PACKAGE emp_actions AS |
2.PL/SQL 注入漏洞
2.1 Oracle 权限
在创建用户之前,可以登录系统管理员权限去创建一个用户(因为系统管理员有这个权限),下面是三种用户分类:
1 | sys; //系统管理员,拥有最高权限 |
在登陆的时候,可以选择下面三种登录方式:
1 | sqlplus / as sysdba; //登陆sys帐户 |
可以参考Oracle安全标签机制(OLS-BLP模型)
- 定义者权限(默认情况下)
- 调用者权限(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包一般分为如下几步:
open cursor:打开cursor
parse cursor:解析你要执行的SQL语句,生成任务计划
bind variable:如果要执行的SQL语句中包含变量,在此就需要绑定变量
execute:执行SQL语句
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()