原文:http://www.microsoft.com/taiwan/msdn/columns/huang_jhong_cheng/LVSS.htm
作者: 黄忠成
一连串的Mass SQL Injection攻击,让我们回忆起数年前的SQL Injection攻击,多年后的今天,我们仍深陷于同样的危机中,本文详述SQL Injection的历史、肇因、解决及侦测方法,更为读者们引介全新、更加安全的防堵SQL Injection策略。
什么是SQL Injection?
SQL Injection,中译为SQL注入,更为人知的名称是【资料隐码攻击】,意指开发人员于撰写网页应用程式之际,贪图一时方便或是依循前人的惯性写法而开启的一道门。在数年前,一次大型的隐码攻击行动,唤起了所有网站拥有者及设计人员的防骇之心,让我们认知到,网站是一个曝露在所有人面前的公共园地,其安全性不容忽视!在那次的攻击行动中,有数千个网站遭到同一种手法入侵,泄露的资料及因入侵所损失的金额难以估计,而起源竟只是程式设计师的惯性及疏于防范,而我们都曾经是其中一份子。
那具体上,什么是SQL Injection呢?其实说穿了很简单,就是透过网页上的输入区域(INPUT如文字输入框,或是URL中的查询字串),将特定的SQL语句透过网页送往资料库执行。以一个登入网页为例,在设计登入网页时,我们会放两个TextBox控件,分别让使用者填入使用者ID及密码,类似画面如下:
在使用者按下登入按钮后,我们将其输入的资讯送往资料库,验证使用者输入的登入资讯是否正确:
using System; using System.Configuration; using System.Data; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Data.SqlClient; public partial class _Default : System.Web.UI.Page
{ protected void Page_Load(object sender, EventArgs e)
{ }
protected void Button1_Click(object sender, EventArgs e)
{ if (ValidateUser(TextBox1.Text, TextBox2.Text)) Label1.Text = "欢迎你"; else Label1.Text = "登入失败"; }
private bool ValidateUser(string userName, string password)
{ SqlConnection conn = new SqlConnection( "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True"); using (conn) { SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USER_ID = '"
+ userName + "' AND PASSWORD = '" + password + "'", conn);
conn.Open();
return ((int)cmd.ExecuteScalar() > 0);
}
}
}
当你写下这些程式码时,已经开启了SQL Injection的大门了,只要使用者于登入时,填入下图的资讯,那么不管ID密码是什么,一律可以登入系统。
这是为什么呢?很简单,起因于下面这行程式码:
SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USER_ID = '"
+ userName + "' AND PASSWORD = '" + password + "'", conn);
我们使用传统ASP常见的手法,以组装SQL指令的方式,将使用者的输入融入既定的SQL语句中,但却忽略了一件重要的事:使用者可以输入任意的字串,包括了部份的SQL指令!透过输入部份的SQL指令及微调,使用者可以轻易的改变这段SQL指令,甚至是叠加另一串SQL指令,而我们的网页则照单全收,以上的输入,会将整句SQL语句调整成下面这样:
透过必然成真的条件式,再加上SQL的注解,我们的网站就这样曝露在网路上,今天我加的是OR,若是狠一点的加上DROP TABLE等破坏性指令,网站就此拜拜。
这种攻击不仅仅出现在上例这种POST状况,另一种GET状态也常常受到同样的攻击,例如下面的程式码即开启了SQL Injection的大门。
using System; using System.Collections; using System.Configuration; using System.Data; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Data.SqlClient; public partial class QueryStringInjection : System.Web.UI.Page
{ protected void Page_Load(object sender, EventArgs e)
{ if (!IsPostBack) { SqlConnection conn = new SqlConnection("Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True");
using (conn) { SqlCommand cmd = new SqlCommand( "SELECT * FROM Customers WHERE CustomerID = '"+Request.QueryString["ID"]+"'", conn);
conn.Open();
DetailsView1.DataSource =
cmd.ExecuteReader(CommandBehavior.CloseConnection);
DetailsView1.DataBind();
}
}
}
}
试着在URL上键入:
http://localhost:43236/FirstInjection/QueryStringInjection.aspx?ID=VINET' OR 1=1 --
注:http://localhost:43236 是你的 Web Development Server 自动产生的Port,你必须视情况修改。
结果你会看到CustomerID="VINET"以外的ALFKI资料列,如下图:
如果有心人士在URL上键入DROP TABLE或是INSERT的QueryString,将资料任意的删除或插入恶意的连结Script (详见后述的Mass SQL Injection一节),那后果不堪设想。
未启用Custom Error Page的漏洞
你应该已经知道,写ASP.NET应用程式的第一道安全手续就是启用Custom Error Page功能,让骇客们无法透过预设的错误网页来取得不该取得的资讯,若未启用Custom Error Page,那么下图是可能发生在你的网站中的:
有了这些资讯,具有耐心的骇客,要透过输入不同的字元来探测整段SQL语句就不困难了,防堵的最佳办法就是启用Custom Error Page设定:
Web.config
...............略<customErrors mode="On" defaultRedirect="DefaultError.htm"></customErrors>............略
一旦启用后,错误发生时会导向DefaultError.html,结果变成下面这样:
检测你的网页有无SQL Injection的可能性
OK,那有没有办法可以检测现在的网页是否受SQL Injection威胁呢?如果你是网站管理者,而非设计师,那么你只有依赖现在常见的网页漏洞检测工具,对网页进行黑箱测试,不过提醒你,目前的网页漏洞测试工具大多是针对PHP、ASP所设计的,能测出来的漏洞相当有限,有时即使是安全的网页,也会因为未实作过滤法(后述),而导致误判。
如果你是程式设计师,事情就简单的多了,只要检视一下程式码,看看动态组装SQL语句的部份是否有SQL Injection即可,图007是一个确认SQL Injection是否存在于你的程式中的公式。
只要你的程式中,有SQL字串加上使用者输入值的情况,那么该网页存在SQL Injection危机的可能性就高达99.9%。
前辈的叮咛:防止SQL Injection的方法
在数千个网站的入侵事件发生后,许多资安专家提出了各种防范SQL Injection的方法,其中不外乎图008的四种。
过滤法可以阻止特定字如【--】、【 OR 】、【'】的输入,能有效防堵必然成真条件式及错误讯息显示时的漏洞,不过魔高一丈,此法最后仍然遭受破解,透过SQL的转码函式,骇客可以将部份SQL语句做出编码来逃避侦测,最后突破这道防线。但由于转码后的字串相当长,所以只要设计师细心些,搭配MaxLength的设定,还是可以让过滤法奏效,但过滤法其实很脆弱,所以一定要搭配其它的手法方能行之。
下面是一个使用过滤法的例子,利用引入外部JavaScript档案及Form的onSubmit事件,在送出资料前先检测拥有ci Attribute标示的text tag,此法可运行于IE及
FireFox 上:
Injectiondetect.js
function validateInjection() { var i = 0; for(i = 0; i < document.forms[0].elements.length;i++) { if(document.forms[0].elements[i].type == 'text' &&
document.forms[0].elements[i].getAttribute("ci") != null)
{ var elem = document.forms[0].elements[i]; if(elem.value != null &&
(elem.value.indexOf('\'') != -1 || elem.value.indexOf('--') != -1 || elem.value.indexOf(' OR ') != -1)) { alert('possible injection detected.') return false;
}
}
}
return true;
}
.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DefaultWithFilter.aspx.cs" Inherits="DefaultWithFilter" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
<script language='javascript' type="text/javascript" src='injectiondetect.js'>1:2:</script>
</head>
<body>
<form id="form1" onsubmit="return validateInjection()" runat="server">
<div>
<table border="1">
<tr>
<td>使用者編號</td>
<td><asp:TextBox ID="TextBox1" ci="true" MaxLength="12"
runat="server"></asp:TextBox></td>
</tr>
<tr>
<td>密碼</td>
<td><asp:TextBox ID="TextBox2" ci="true" MaxLength="12"
runat="server"></asp:TextBox></td>
</tr>
<tr>
<td colspan=2>
<asp:Button ID="Button1" runat="server" Text="登入" onclick="Button1_Click" />
</td>
</tr>
</table>
<asp:Label ID="Label1" runat="server" Text=""></asp:Label>
</div>
</form>
</body>
</html>
下图是尝试于此网页进行SQL Injection攻击时的结果:
不过,这种过滤法还不完善,因为资深的骇客仍然可以透过将网页存成HTML,移除JavaScript认证并假造ViewState来对网站进行SQL Injection攻击!所以,完善的过滤法应该是Client端与Server都有,Server端如下所示:
.aspx.cs
using System; using System.Collections; using System.Configuration; using System.Data; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Data.SqlClient; public partial class DefaultWithFilter : System.Web.UI.Page
{ protected void Page_Load(object sender, EventArgs e)
{ }
private bool DetectInjection(string input)
{ if (input.IndexOf("'") != -1 ||
input.IndexOf("--") != -1 || input.IndexOf(" OR ") != -1) return true;
return false;
}
protected void Button1_Click(object sender, EventArgs e)
{ if (TextBox1.Text.Length > 12 || TextBox2.Text.Length > 12 ||
DetectInjection(TextBox1.Text) ||
DetectInjection(TextBox2.Text))
{ ClientScript.RegisterStartupScript(typeof(Page), "Alert_Msg",
"alert('possible injection detected.')", true);
return; }
if (ValidateUser(TextBox1.Text, TextBox2.Text)) Label1.Text = "歡迎你"; else Label1.Text = "登入失敗"; }
private bool ValidateUser(string userName, string password)
{ SqlConnection conn = new SqlConnection( "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True"); using (conn) { SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USERS.USER_ID = '"
+ userName + "' AND USERS.PASSWORD = '" + password + "'", conn);
conn.Open();
return ((int)cmd.ExecuteScalar() > 0);
}
}
}
或许你会觉得实作起来挺麻烦的,但这是过滤法所需付出的代价!
除了过滤法外,使用低权限的帐号连结资料库也是安全常识之一,藉由降低连线帐号的权限,可以让DROP TABLE等破坏力超强的手法碰壁,不过这种手法不应该成为唯一防堵SQL Injection的方式,因为你不可能连INSERT都不给执行,而INSERT是骇客入侵网页的常见手法。
使用Parameter是目前已知,一劳永逸逃离SQL Injection的手法,将前述的程式调整成下面这样,即可让其完全逃离SQL Injection。
using System; using System.Collections; using System.Configuration; using System.Data; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Data.SqlClient; public partial class Default2 : System.Web.UI.Page
{ protected void Page_Load(object sender, EventArgs e)
{ }
protected void Button1_Click(object sender, EventArgs e)
{ if (ValidateUser(TextBox1.Text, TextBox2.Text)) Label1.Text = "歡迎你"; else Label1.Text = "登入失敗"; }
private bool ValidateUser(string userName, string password)
{ SqlConnection conn = new SqlConnection( "Data Source=JEFFRAY;Initial Catalog=Northwind;Integrated Security=True"); 




