<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML ><HEAD ><TITLE >SQL 注入</TITLE ><META NAME="GENERATOR" CONTENT="Modular DocBook HTML Stylesheet Version 1.79"><LINK REL="HOME" TITLE="PHP 手册" HREF="index.html"><LINK REL="UP" TITLE="数据库安全" HREF="security.database.html"><LINK REL="PREVIOUS" TITLE="加密存储模型" HREF="security.database.storage.html"><LINK REL="NEXT" TITLE="错误报告" HREF="security.errors.html"><META HTTP-EQUIV="Content-type" CONTENT="text/html; charset=UTF-8"></HEAD ><BODY CLASS="sect1" BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#840084" ALINK="#0000FF" ><DIV CLASS="NAVHEADER" ><TABLE SUMMARY="Header navigation table" WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" ><TR ><TH COLSPAN="3" ALIGN="center" >PHP 手册</TH ></TR ><TR ><TD WIDTH="10%" ALIGN="left" VALIGN="bottom" ><A HREF="security.database.storage.html" ACCESSKEY="P" >上一页</A ></TD ><TD WIDTH="80%" ALIGN="center" VALIGN="bottom" >章 27. 数据库安全</TD ><TD WIDTH="10%" ALIGN="right" VALIGN="bottom" ><A HREF="security.errors.html" ACCESSKEY="N" >下一页</A ></TD ></TR ></TABLE ><HR ALIGN="LEFT" WIDTH="100%"></DIV ><DIV CLASS="sect1" ><H1 CLASS="sect1" ><A NAME="security.database.sql-injection" >SQL 注入</A ></H1 ><P > 很多 web 开发者没有注意到 SQL 查询是可以被篡改的,因而把 SQL 查询当作可信任的命令。殊不知道,SQL 查询可以绕开访问控制,从而绕过身份验证和权限检查。更有甚者,有可能通过 SQL 查询去运行主机操作系统级的命令。 </P ><P > 直接 SQL 命令注入就是攻击者常用的一种创建或修改已有 SQL 语句的技术,从而达到取得隐藏数据,或覆盖关键的值,甚至执行数据库主机操作系统命令的目的。这是通过应用程序取得用户输入并与静态参数组合成 SQL 查询来实现的。下面将会给出一些真实的例子。 </P ><P > 由于在缺乏对输入的数据进行验证,并且使用了超级用户或其它有权创建新用户的数据库帐号来连接,攻击者可以在数据库中新建一个超级用户。 <TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" CLASS="EXAMPLE" ><TR ><TD ><DIV CLASS="example" ><A NAME="AEN6630" ></A ><P ><B >例 27-2. 一段实现数据分页显示的代码……也可以被用作创建一个超级用户(PostgreSQL系统)。 </B ></P ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br />$offset </font><font color="#007700">= </font><font color="#0000BB">$argv</font><font color="#007700">[</font><font color="#0000BB">0</font><font color="#007700">]; </font><font color="#FF8000">// 注意,没有输入验证!<br /></font><font color="#0000BB">$query </font><font color="#007700">= </font><font color="#DD0000">"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;"</font><font color="#007700">;<br /></font><font color="#0000BB">$result </font><font color="#007700">= </font><font color="#0000BB">pg_query</font><font color="#007700">(</font><font color="#0000BB">$conn</font><font color="#007700">, </font><font color="#0000BB">$query</font><font color="#007700">);<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ></DIV ></TD ></TR ></TABLE > 一般的用户会点击 <CODE CLASS="varname" >$offset</CODE > 已被斌值的“上一页”、“下一页”的链接。原本代码只会认为 <CODE CLASS="varname" >$offset</CODE > 是一个数值。然而,如果有人尝试把以下语句先经过 <A HREF="function.urlencode.html" ><B CLASS="function" >urlencode()</B ></A > 处理,然后加入URL中的话: <DIV CLASS="informalexample" ><P ></P ><A NAME="AEN6636" ></A ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><PRE CLASS="sql" >0; insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd) select 'crack', usesysid, 't','t','crack' from pg_shadow where usename='postgres'; --</PRE ></TD ></TR ></TABLE ><P ></P ></DIV > 那么他就可以创建一个超级用户了。注意那个 <TT CLASS="literal" >0;</TT > 只不过是为了提供一个正确的偏移量以便补充完整原来的查询,使它不要出错而已。 </P ><DIV CLASS="note" ><BLOCKQUOTE CLASS="note" ><P ><B >注意: </B > <TT CLASS="literal" >--</TT > 是 SQL 的注释标记,一般可以使用来它告诉 SQL 解释器忽略后面的语句。 </P ></BLOCKQUOTE ></DIV ><P > 对显示搜索结果的页面下手是一个能得到密码的可行办法。攻击者所要做的只不过是找出哪些提交上去的变量是用于 SQL 语句并且处理不当的。而这类的变量通常都被用于 <TT CLASS="literal" >SELECT</TT > 查询中的条件语句,如 <TT CLASS="literal" >WHERE, ORDER BY, LIMIT</TT > 和 <TT CLASS="literal" >OFFSET</TT >。如果数据库支持 <TT CLASS="literal" >UNION</TT > 构造的话,攻击者还可能会把一个完整的 SQL 查询附加到原来的语句上以便从任意数据表中得到密码。因此,对密码字段加密是很重要的。 <TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" CLASS="EXAMPLE" ><TR ><TD ><DIV CLASS="example" ><A NAME="AEN6647" ></A ><P ><B >例 27-3. 显示文章……以及一些密码(任何数据库系统) </B ></P ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br />$query </font><font color="#007700">= </font><font color="#DD0000">"SELECT id, name, inserted, size FROM products<br /> WHERE size = '$size'<br /> ORDER BY $order LIMIT $limit, $offset;"</font><font color="#007700">;<br /></font><font color="#0000BB">$result </font><font color="#007700">= </font><font color="#0000BB">odbc_exec</font><font color="#007700">(</font><font color="#0000BB">$conn</font><font color="#007700">, </font><font color="#0000BB">$query</font><font color="#007700">);<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ></DIV ></TD ></TR ></TABLE > 可以在原来的查询的基础上添加另一个 <TT CLASS="literal" >SELECT</TT > 查询来获得密码: <DIV CLASS="informalexample" ><P ></P ><A NAME="AEN6651" ></A ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><PRE CLASS="sql" >' union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable; --</PRE ></TD ></TR ></TABLE ><P ></P ></DIV > 假如上述语句(使用 <TT CLASS="literal" >'</TT > 和 <TT CLASS="literal" >--</TT >)被加入到 <CODE CLASS="varname" >$query</CODE > 中的任意一个变量的话,那么就麻烦了。 </P ><P > SQL 中的 UPDATE 也会受到攻击。这种查询也可能像上面的例子那样被插入或附加上另一个完整的请求。但是攻击者更愿意对 <TT CLASS="literal" >SET</TT > 子句下手,这样他们就可以更改数据表中的一些数据。这种情况下必须要知道数据库的结构才能修改查询成功进行。可以通过表单上的变量名对字段进行猜测,或者进行暴力破解。对于存放用户名和密码的字段,命名的方法并不多。 <TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" CLASS="EXAMPLE" ><TR ><TD ><DIV CLASS="example" ><A NAME="AEN6658" ></A ><P ><B >例 27-4. 从重设密码……到获得更多权限(任何数据库系统) </B ></P ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br />$query </font><font color="#007700">= </font><font color="#DD0000">"UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';"</font><font color="#007700">;<br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ></DIV ></TD ></TR ></TABLE > 但是恶意的用户会把 <TT CLASS="literal" >' or uid like'%admin%'; --</TT > 作为变量的值提交给 <CODE CLASS="varname" >$uid</CODE > 来改变 admin 的密码,或者把 <CODE CLASS="varname" >$pwd</CODE > 的值提交为 <TT CLASS="literal" >"hehehe', admin='yes', trusted=100 "</TT >(后面有个空格)去获得更多的权限。这样做的话,查询语句实际上就变成了: <DIV CLASS="informalexample" ><P ></P ><A NAME="AEN6665" ></A ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br /></font><font color="#FF8000">// $uid == ' or uid like'%admin%'; --<br /></font><font color="#0000BB">$query </font><font color="#007700">= </font><font color="#DD0000">"UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%'; --"</font><font color="#007700">;<br /><br /></font><font color="#FF8000">// $pwd == "hehehe', admin='yes', trusted=100 "<br /></font><font color="#0000BB">$query </font><font color="#007700">= </font><font color="#DD0000">"UPDATE usertable SET pwd='hehehe', admin='yes', trusted=100 WHERE<br />...;"</font><font color="#007700">;<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ><P ></P ></DIV > </P ><P > 下面这个可怕的例子将会演示如何在某些数据库上执行系统命令。 <TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" CLASS="EXAMPLE" ><TR ><TD ><DIV CLASS="example" ><A NAME="AEN6668" ></A ><P ><B >例 27-5. 攻击数据库所在主机的操作系统(MSSQL Server)</B ></P ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br />$query </font><font color="#007700">= </font><font color="#DD0000">"SELECT * FROM products WHERE id LIKE '%$prod%'"</font><font color="#007700">;<br /></font><font color="#0000BB">$result </font><font color="#007700">= </font><font color="#0000BB">mssql_query</font><font color="#007700">(</font><font color="#0000BB">$query</font><font color="#007700">);<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ></DIV ></TD ></TR ></TABLE > 如果攻击提交 <TT CLASS="literal" >a%' exec master..xp_cmdshell 'net user test testpass /ADD' --</TT > 作为变量 <CODE CLASS="varname" >$prod</CODE >的值,那么 <CODE CLASS="varname" >$query</CODE > 将会变成 <DIV CLASS="informalexample" ><P ></P ><A NAME="AEN6674" ></A ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br />$query </font><font color="#007700">= </font><font color="#DD0000">"SELECT * FROM products<br /> WHERE id LIKE '%a%'<br /> exec master..xp_cmdshell 'net user test testpass /ADD'--"</font><font color="#007700">;<br /></font><font color="#0000BB">$result </font><font color="#007700">= </font><font color="#0000BB">mssql_query</font><font color="#007700">(</font><font color="#0000BB">$query</font><font color="#007700">);<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ><P ></P ></DIV > MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统添加用户的命令。如果这个程序是以 <TT CLASS="literal" >sa</TT > 运行而 MSSQLSERVER 服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。 </P ><DIV CLASS="note" ><BLOCKQUOTE CLASS="note" ><P ><B >注意: </B > 虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。使用不同的方法,各种数据库都有可能遭殃。 </P ></BLOCKQUOTE ></DIV ><DIV CLASS="sect2" ><H2 CLASS="sect2" ><A NAME="security.database.avoiding" >预防措施</A ></H2 ><P > 也许有人会自我安慰,说攻击者要知道数据库结构的信息才能实施上面的攻击。没错,确实如此。但没人能保证攻击者一定得不到这些信息,一但他们得到了,数据库有泄露的危险。如果你在用开放源代码的软件包来访问数据库,比如论坛程序,攻击者就很容得到到相关的代码。如果这些代码设计不良的话,风险就更大了。 </P ><P > 这些攻击总是建立在发掘安全意识不强的代码上的。所以,永远不要信任外界输入的数据,特别是来自于客户端的,包括选择框、表单隐藏域和 cookie。就如上面的第一个例子那样,就算是正常的查询也有可能造成灾难。 </P ><P ></P ><UL ><LI ><P > 永远不要使用超级用户或所有者帐号去连接数据库。要用权限被严格限制的帐号。 </P ></LI ><LI ><P > 检查输入的数据是否具有所期望的数据格式。PHP 有很多可以用于检查输入的函数,从简单的<A HREF="ref.var.html" >变量函数</A >和<A HREF="ref.ctype.html" >字符类型函数</A >(比如 <A HREF="function.is-numeric.html" ><B CLASS="function" >is_numeric()</B ></A >,<A HREF="function.ctype-digit.html" ><B CLASS="function" >ctype_digit()</B ></A >)到复杂的 <A HREF="ref.pcre.html" >Perl 兼容正则表达式函数</A >都可以完成这个工作。 </P ></LI ><LI ><P > 如果程序等待输入一个数字,可以考虑使用 <A HREF="function.is-numeric.html" ><B CLASS="function" >is_numeric()</B ></A > 来检查,或者直接使用 <A HREF="function.settype.html" ><B CLASS="function" >settype()</B ></A > 来转换它的类型,也可以用 <A HREF="function.sprintf.html" ><B CLASS="function" >sprintf()</B ></A > 把它格式化为数字。 <TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" CLASS="EXAMPLE" ><TR ><TD ><DIV CLASS="example" ><A NAME="AEN6698" ></A ><P ><B >例 27-6. 一个实现分页更安全的方法</B ></P ><TABLE BORDER="0" BGCOLOR="#E0E0E0" CELLPADDING="5" ><TR ><TD ><code><font color="#000000"> <font color="#0000BB"><?php<br /><br />settype</font><font color="#007700">(</font><font color="#0000BB">$offset</font><font color="#007700">, </font><font color="#DD0000">'integer'</font><font color="#007700">);<br /></font><font color="#0000BB">$query </font><font color="#007700">= </font><font color="#DD0000">"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;"</font><font color="#007700">;<br /><br /></font><font color="#FF8000">// 请注意格式字符串中的 %d,如果用 %s 就毫无意义了<br /></font><font color="#0000BB">$query </font><font color="#007700">= </font><font color="#0000BB">sprintf</font><font color="#007700">(</font><font color="#DD0000">"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;"</font><font color="#007700">,<br /> </font><font color="#0000BB">$offset</font><font color="#007700">);<br /><br /></font><font color="#0000BB">?></font> </font> </code></TD ></TR ></TABLE ></DIV ></TD ></TR ></TABLE > </P ></LI ><LI ><P > 使用数据库特定的敏感字符转义函数(比如 <A HREF="function.mysql-escape-string.html" ><B CLASS="function" >mysql_escape_string()</B ></A > 和 <B CLASS="function" >sql_escape_string()</B >)把用户提交上来的非数字数据进行转义。如果数据库没有专门的敏感字符转义功能的话 <A HREF="function.addslashes.html" ><B CLASS="function" >addslashes()</B ></A > 和 <A HREF="function.str-replace.html" ><B CLASS="function" >str_replace()</B ></A > 可以代替完成这个工作。看看<A HREF="security.database.storage.html" >第一个例子</A >,此例显示仅在查询的静态部分加上引号是不够的,查询很容易被攻破。 </P ></LI ><LI ><P > 要不择手段避免显示出任何有关数据库的信心,尤其是数据库结构。参见<A HREF="security.errors.html" >错误报告</A >和<A HREF="ref.errorfunc.html" >错误处理函数</A >。 </P ></LI ><LI ><P > 也可以选择使用数据库的存储过程和预定义指针等特性来抽象数库访问,使用户不能直接访问数据表和视图。但这个办法又有别的影响。 </P ></LI ></UL ><P > 除此之外,在允许的情况下,使用代码或数据库系统保存查询日志也是一个好办法。显然,日志并不能防止任何攻击,但利用它可以跟踪到哪个程序曾经被尝试攻击过。日志本身没用,要查阅其中包含的信息才行。毕竟,更多的信息总比没有要好。 </P ></DIV ></DIV ><DIV CLASS="NAVFOOTER" ><HR ALIGN="LEFT" WIDTH="100%"><TABLE SUMMARY="Footer navigation table" WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0" ><TR ><TD WIDTH="33%" ALIGN="left" VALIGN="top" ><A HREF="security.database.storage.html" ACCESSKEY="P" >上一页</A ></TD ><TD WIDTH="34%" ALIGN="center" VALIGN="top" ><A HREF="index.html" ACCESSKEY="H" >起始页</A ></TD ><TD WIDTH="33%" ALIGN="right" VALIGN="top" ><A HREF="security.errors.html" ACCESSKEY="N" >下一页</A ></TD ></TR ><TR ><TD WIDTH="33%" ALIGN="left" VALIGN="top" >加密存储模型</TD ><TD WIDTH="34%" ALIGN="center" VALIGN="top" ><A HREF="security.database.html" ACCESSKEY="U" >上一级</A ></TD ><TD WIDTH="33%" ALIGN="right" VALIGN="top" >错误报告</TD ></TR ></TABLE ></DIV ></BODY ></HTML >