原文: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及密码,类似画面如下:

图 1:
LVVS001

在使用者按下登入按钮后,我们将其输入的资讯送往资料库,验证使用者输入的登入资讯是否正确:

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密码是什么,一律可以登入系统。

图 2:
LVVS002

这是为什么呢?很简单,起因于下面这行程式码:

SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM USERS WHERE USER_ID = '" 
                + userName + "' AND PASSWORD = '" + password + "'", conn);

我们使用传统ASP常见的手法,以组装SQL指令的方式,将使用者的输入融入既定的SQL语句中,但却忽略了一件重要的事:使用者可以输入任意的字串,包括了部份的SQL指令!透过输入部份的SQL指令及微调,使用者可以轻易的改变这段SQL指令,甚至是叠加另一串SQL指令,而我们的网页则照单全收,以上的输入,会将整句SQL语句调整成下面这样:

图 3:
LVVS003

透过必然成真的条件式,再加上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资料列,如下图:

图 4:
LVVS004

如果有心人士在URL上键入DROP TABLE或是INSERT的QueryString,将资料任意的删除或插入恶意的连结Script (详见后述的Mass SQL Injection一节),那后果不堪设想。


未启用Custom Error Page的漏洞


你应该已经知道,写ASP.NET应用程式的第一道安全手续就是启用Custom Error Page功能,让骇客们无法透过预设的错误网页来取得不该取得的资讯,若未启用Custom Error Page,那么下图是可能发生在你的网站中的:

图 5:
LVVS005

有了这些资讯,具有耐心的骇客,要透过输入不同的字元来探测整段SQL语句就不困难了,防堵的最佳办法就是启用Custom Error Page设定:

Web.config

...............略<customErrors mode="On" defaultRedirect="DefaultError.htm"></customErrors>............略 

一旦启用后,错误发生时会导向DefaultError.html,结果变成下面这样:

图 6:
LVVS006

检测你的网页有无SQL Injection的可能性

OK,那有没有办法可以检测现在的网页是否受SQL Injection威胁呢?如果你是网站管理者,而非设计师,那么你只有依赖现在常见的网页漏洞检测工具,对网页进行黑箱测试,不过提醒你,目前的网页漏洞测试工具大多是针对PHP、ASP所设计的,能测出来的漏洞相当有限,有时即使是安全的网页,也会因为未实作过滤法(后述),而导致误判。


如果你是程式设计师,事情就简单的多了,只要检视一下程式码,看看动态组装SQL语句的部份是否有SQL Injection即可,图007是一个确认SQL Injection是否存在于你的程式中的公式。

图 7:
LVVS007

只要你的程式中,有SQL字串加上使用者输入值的情况,那么该网页存在SQL Injection危机的可能性就高达99.9%。


前辈的叮咛:防止SQL Injection的方法


在数千个网站的入侵事件发生后,许多资安专家提出了各种防范SQL Injection的方法,其中不外乎图008的四种。

图 8:
LVVS008

过滤法可以阻止特定字如【--】、【 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攻击时的结果:

图 9:
LVVS009

不过,这种过滤法还不完善,因为资深的骇客仍然可以透过将网页存成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");