1 簡介
1.1 普通SQL注入技術概述
目前沒有對SQL注入技術的標准定義,微軟中國技術中心從2個方面進行了描述[1]:
(1) 腳本注入式的攻擊
(2) 惡意用戶輸入用來影響被執行的SQL腳本
根據Chris Anley的定義[2], 當一個攻擊者通過在查詢語句中插入一系列的SQL語句來將數據寫入到應用程序中,這種方法就可以定義成SQL注入。Stephen Kost[3]給出了這種攻擊形式的另一個特征,“從一個數據庫獲得未經授權的訪問和直接檢索”,SQL注入攻擊就其本質而言,它利用的工具是SQL的語 法,針對的是應用程序開發者編程過程中的漏洞,“當攻擊者能夠操作數據,往應用程序中插入一些SQL語句時,SQL注入攻擊就發生了”。實際上,SQL注 入是存在於常見的多連接的應用程序中一種漏洞,攻擊者通過在應用程序中預先定義好的查詢語句結尾加上額外的SQL語句元素,欺騙數據庫服務器執行非授權的 任意查詢。這類應用程序一般是網絡應用程序(Web Application),它允許用戶輸入查詢條件,並將查詢條件嵌入SQL請求語句中,發送到與該應用程序相關聯的數據庫服務器中去執行。通過構造一些 畸形的輸入,攻擊者能夠操作這種請求語句去獲取預先未知的結果。
在風險方面,SQL注入攻擊是位居前列的,與緩沖區溢出等漏洞基本相當。而且如果要實施緩沖區溢出攻擊,攻擊者必須首先能繞過站點的防火牆;而對於SQL 注入攻擊,由於防火牆為了使用戶能訪問網絡應用程序,必須允許從Internet到Web服務器的正向連接,因此一旦網絡應用程序有注入漏洞,攻擊者就可 以直接訪問數據庫進而甚至能夠獲得數據庫所在的服務器的訪問權,因此在某些情況下,SQL注入攻擊的風險要高於所有其他漏洞。
SQL注入攻擊利用的是SQL語法,這使得這種攻擊具有廣泛性。理論上說,對於所有基於SQL語言標准的數據庫軟件包括SQL Server,Oracle,MySQL, DB2,Informix等以及與之連接的網絡應用程序包括Active/Java Server Pages, Cold Fusion Management, PHP或Perl等都是有效的。當然各種軟件有自身的特點,實際的攻擊代碼可能不盡相同。SQL注入攻擊的原理相對簡單,且各類基於數據庫系統的應用程序 被廣泛使用,介紹注入漏洞和利用方法的公開出版物也大量問世,造成近年SQL注入攻擊的數量一直增長,注入攻擊的形式也有被濫用的趨勢。
關於針對MS SQL Server的普通SQL注入技術的詳細介紹,可以參考Chris Anley所撰的“SQL Server應用程序中的高級SQL注入”[2]一文和其后續“更多的高級SQL注入”[4],Cesar Cerrundo所撰的“利用SQL注入操縱Microsoft SQL Server” [5]一文,以及SPI實驗室的Kevin Spett撰寫的白皮書“SQL注入 你的網絡應用程序是否會受攻擊?” [6];而針對Oracle的普通SQL注入技術介紹,可以參考Stephen Kost的“針對Oracle開發人員的SQL注入攻擊簡介”[3]一文。
1.2 SQL注入攻擊的防御手段
由於越來越多的攻擊利用了SQL注入技術,也隨之產生了很多試圖解決注入漏洞的方案。目前被提出的方案有:
(1) 在服務端正式處理之前對提交數據的合法性進行檢查;
(2) 封裝客戶端提交信息;
(3) 替換或刪除敏感字符/字符串;
(4) 屏蔽出錯信息。
方 案(1)被公認是最根本的解決方案,在確認客戶端的輸入合法之前,服務端拒絕進行關鍵性的處理操作,不過這需要開發者能夠以一種安全的方式來構建網絡應用 程序,雖然已有大量針對在網絡應用程序開發中如何安全地訪問數據庫的文檔出版,但仍然有很多開發者缺乏足夠的安全意識,造成開發出的產品中依舊存在注入漏 洞;方案(2)的做法需要RDBMS的支持,目前只有Oracle采用該技術;方案(3)則是一種不完全的解決措施,例如,當客戶端的輸入為 “…ccmdmcmdd…”時,在對敏感字符串“cmd”替換刪除以后,剩下的字符正好是“…cmd…”;方案(4)是目前最常被采用的方法,很多安全文 檔都認為SQL注入攻擊需要通過錯誤信息收集信息,有些甚至聲稱某些特殊的任務若缺乏詳細的錯誤信息則不能完成,這使很多安全專家形成一種觀念,即注入攻 擊在缺乏詳細錯誤的情況下不能實施。
而實際上,屏蔽錯誤信息是在服務端處理完畢之后進行補救,攻擊其實已經發生,只是企圖阻止攻擊者知道攻擊的結果而已。本文所介紹SQL盲注技術就是一些攻擊者使用的新技術,其在錯誤信息被屏蔽的情況下使攻擊者仍能獲得所需的信息,並繼續實施注入攻擊。
1.3 本文的結構組織
為了理解盲注攻擊,我們首先將介紹確定SQL注入漏洞所需的服務器的最小響應;其次,我們將構造一個合乎語法的SQL請求,並可以將之替換成任何有效的 SQL請求;最后,我們將討論在沒有詳細錯誤信息的情況下如何利用UNION SELECT語句。本文所討論的盲注攻擊的條件是我們在攻擊前對網絡應用程序、數據庫類型、表結構等等信息都一無所知,這些信息都需要在注入的過程中通過 探測獲得。
2 確定注入漏洞
要進行SQL注入攻擊,首先當然是確認要攻擊的網絡應用程序存在注入漏洞,因此攻擊者首先必須能確立一些與服務器產生的錯誤相關的提示類型。盡管錯誤信息 本身已被屏蔽,網絡應用程序仍然具有能區分正確請求和錯誤請求的能力,攻擊者只需要學習去識別這些提示,尋找相關錯誤,並確認其是否和SQL相關。
2.1 識別錯誤
一個網絡應用程序主要會產生兩種類型的錯誤,第一種是由Web服務器產生的代碼異常(exception),類似於“500:Internal Server Error”,通常如果SQL注入語句出現語法錯誤,比如出現未閉合的引號,就會使服務器拋出這類異常。如果要屏蔽該類錯誤,一般會采用將默認的錯誤信息 替換成一個事先定制的HTML頁面,但只要觀察到有這種響應出現,就可以確認其實是發生了服務器錯誤。在其他情況下,為了進一步屏蔽該類錯誤,有些服務器 一出現異常,會簡單地跳轉到主頁面或前一個訪問過的頁面, 或者顯示一條簡單的錯誤消息但不提供任何細節。
第二類錯誤是由應用程序代碼產生的,這代表其開發者有較好的編程習慣。這類應用程序考慮到可能會出現一些無效的情況,並分別為之產生了一個特定的錯誤信 息。盡管出現這類錯誤一般會返回一個請求有效的響應(200 OK),但頁面仍然會跳轉到主頁面,或者采用某種隱藏信息的辦法,類似於“Internal Server Error”。
為了區分這兩種錯誤,我們看一個例子:有兩個電子商務的應用程序,A和B,兩個應用程序都使用同一個叫proddetails.asp的頁面,該頁面期待 獲得一個參數,叫ProdID。它獲取該參數后,從數據庫中提取相應的產品詳細信息數據,然后對返回的結果進行一些處理。兩個應用程序都是通過一個產品列 表頁面上的鏈接調用proddetails.asp,因此能保證ProdID一直都是存在且有效的。應用程序A認為這樣就不會出現問題,因此對參數不做額 外的檢查,而如果攻擊者篡改了ProdID,插入了一個在數據表中不存在的id,數據庫就會返回一個空記錄。由於應用程序A沒有料到可能會出現空記錄,當 它試圖去處理該記錄中的數據時,就可能會出現異常,產生一個“500:Internal Server Error”。而應用程序B,會在對記錄進行處理前確認記錄的大小超過0,如果是空記錄,則會出現一個錯誤提示“該產品不存在”,或者開發者為了隱藏該錯 誤,會將頁面重新定位到產品的列表頁面。
因此攻擊者為了進行SQL盲注,會首先嘗試提交一些無效的請求,並觀察應用程序如何處理這些錯誤,以及如果出現SQL錯誤會發生什么情況。
2.2 定位錯誤
對要攻擊的應用程序有了初步的認識后,攻擊者會試圖定位由人為構造的輸入而產生的錯誤信息。這時,攻擊者就會使用標准的SQL注入測試技術,比如添加一些 SQL關鍵字(如OR,AND等)和一些META字符(如;或’等)。每一個參數都被獨立地進行測試,而獲得的響應將被檢驗用來判斷是否產生了錯誤。通過 一個攔截代理服務器(intercepting proxy)或者類似的工具可以方便地識別頁面跳轉和其他一些可預測的隱藏錯誤,而任何一個返回錯誤的參數都有可能存在SQL注入漏洞。而在單獨測試每個 參數過程中,必須保證其他參數都是有效的,因為需要避免除注入以外任何其他可能的原因所導致的錯誤影響了判斷結果。測試的結果一般是一個可疑參數的列表, 列表中的一些參數可能的確可以進行注入利用,另外一些參數則可能是由一些SQL無關的錯誤所造成,因此需要被剔除。攻擊者接下來就需要從這些參數中挑選真 正存在注入漏洞的參數,我們稱之為確定注入點。
2.3 確定注入點
SQL字段可以被划分為三個主要類型:數字、字符串和日期。雖然每個類型都有其特點,但卻與注入的過程無關。每一個從網絡應用程序提交給SQL查詢的參數 都屬於以上三個類型中的一類,其中數字參數被直接提交給服務器,而字符串和日期則需要加上引號才被提交,例如:
SELECT * FROM Products WHERE ProdID = 4
與
SELECT * FROM Products WHERE ProdName = 'Book'
而SQL服務器,並不關心它接受到的是什么類型的參數表達式,只要該表達式是相關的類型即可。而這個特點則使攻擊者能夠很容易地確認一個錯誤是否和SQL相關。如果是數字類型,最簡單的處理辦法是使用基本的算術操作,例如以下請求:
/mysite/proddetails.asp?ProdID=4
測試該參數的一種辦法是插入4’作為參數,另一種是使用3+1作為參數,假設這兩個參數已直接被提交給SQL請求語句,則將形成以下兩個SQL請求語句:
(1) SELECT * FROM Products WHERE ProdID = 4'
(2) SELECT * FROM Products WHERE ProdID = 3 + 1
第一個SQL語法有問題,將一定會產生一個錯誤,而第二個如果被順利地執行,返回和最初的請求(即ProdID等於4)一樣的產品信息,這就提示該參數是存在注入漏洞的。
類 似的技術可以被應用於用一個符合SQL語法的字符串表達式替換該參數,這里有兩個區別:第一,字符串表示式是放在引號中的,因此需要阻斷引號;第二,不同 的SQL服務器連結字符串的語法不同,比如MS SQL Server使用符號+來連結字符串,而Oracle使用符號||來連結。例如以下請求:
/mysite/proddetails.asp?ProdName=Book
要測試該ProdName參數是否有注入漏洞,可以先其替換成一個無效的字符串比如Book’,然后再替換成一個可能生成正確字符串的表達式,比如B’+’ook(對於Oracle,是B’||’ook)。這就會形成以下兩個SQL請求語句:
(1) SELECT * FROM Products WHERE ProdName = 'Book''
(2) SELECT * FROM Products WHERE ProdID = 'B' + 'ook'
則第一個仍然可能產生一個SQL錯誤,而第二個則可能返回和最初的請求一樣的值為Book的產品。
我們注意到,即使應用程序已經過濾了’和+等META字符,我們仍然可以在輸入時過把字符轉換成URL編碼(即字符ASCII碼的16進制)來繞過檢查,例如:
/mysite/proddetails.asp?ProdID=3+1就等於/mysite/proddetails.asp?ProdID=3%2B1
/mysite/proddetails.asp?ProdID=B’+’ook就等於/mysite/proddetails.asp?ProdID=B%27%2B%27ook
類 似的,任何表達式都可以用來替換最初的參數。而特殊的系統函數也可以被用來提交以返回一個數字,一個字符串或一個日期,比如Oracle中sysdate 返回一個日期表達式,而在SQL Server中,getdate()會返回日期表達式。其他的技術同樣可以被用來判斷是否存在SQL注入漏洞。
通過以上介紹可以發現,即使沒有詳細的錯誤信息,對於攻擊者來說,判斷是否存在SQL注入漏洞仍然是一個非常簡單的任務。
3 實施注入攻擊
攻擊者在確定注入點后,就要嘗試進行注入利用,這需要其能確定符合SQL語法的注入請求表達式,判斷出后台數據庫的類型,然后構造出所需的利用代碼。
3.1 確定正確的注入句法
這是SQL盲注攻擊中最難也最有技巧的步驟,如果最初的SQL請求語句很簡單,那么確定正確的注入語法也相對容易,而如果最初的SQL請求語句較復雜,那么要想突破其限制就需要多次的嘗試,但進行這些嘗試所需要的基本技術卻是非常簡單。
確定基本的句法的過程即通過標准的SELECT … WHERE語句,被注入的參數(即注入點)就是WHERE語句的一部分。為了確定正確的注入句法,攻擊者必須能夠在最初的WHERE語句后添加其他數據, 使其能返回非預期的結果。對一些簡單的應用程序,僅僅加上OR 1=1就可以完成,但在大多數情況下如果想構造出成功的利用代碼,這樣做當然是不夠的。經常需要解決的問題是如何配對插入語符號 (parenthesis,比如成對的括號),使之能與前面的已使用的符號,比如左括號匹配。另外常見的問題是一個被篡改的請求語句可能會導致應用程序產 生其他錯誤,這個錯誤往往難於和一個SQL錯誤相區分,比如應用程序一次如果只能處理一個記錄,在請求語句后添加OR 1=1可能使數據庫返回1000條記錄,這時就會產生錯誤。由於WHERE語句本質上是一串通過OR、AND或插入語符號連接起來的值為TRUE或 FALSE的表達式,因此要想確定正確的注入句法,關鍵就在於能否成功地突破插入語符號限制並能順利地結束請求語句,這就需要進行多次組合測試。例如,添 加AND 1=2能將整個表達式的值變為FALSE,而添加OR 1=2則不會對整個表達式的值產生影響(除非操作符有優先級)。
對於 一些注入利用,僅僅改變WHERE語句就足夠了,但對於其他情況,比如UNION SELECT注入或存儲過程(stored procedures)注入,還需要能先順利地結束整個SQL請求語句,然后才能添加其他攻擊者所需要的SQL語句。在這種情況下,攻擊者可以選擇使用 SQL注釋符號來結束語句,該符號是兩個連續的破折號(--),它要求SQL Server忽略其后同一行的所有輸入。例如,一個登錄頁面需要訪問者輸入用戶名和密碼,並將其提交給SQL請求語句:
SELECT Username, UserID, Password FROM Users WHERE Username = ‘user’ AND Password = ‘pass’
通過輸入john’--作為用戶名,將會構造出以下WHERE語句:
WHERE Username = ‘john’ --'AND Password = ‘pass’
這時,該語句不但符合SQL語法,而且還使用戶跳過了密碼認證。但是如果是另外一種WHERE語句:
WHERE (Username = ‘user’ AND Password = ‘pass’)
注意到這里出現了插入語符號,這時再使用john’--作為用戶名,請求語句就會錯誤:
WHERE (Username = ‘john' --' AND Password = ‘pass’)
這是因為有未配對的插入語符號,請求語句就不會被執行。
這個例子顯示出使用注釋符號能夠用來判斷請求語句是否被順利地結束了,如果添加了注釋符號且沒有產生錯誤,這就意味着注釋符號前的語句已經順利地被結束。如果出現了錯誤,這就需要攻擊者進行更多的請求嘗試。
3.2 判斷數據庫類型
攻擊者一旦確定了正確的注入句法后,就會開始利用注入去判斷后台數據庫的類型,這個步驟比確定注入句法要簡單得多。攻擊者一般會使用以下幾種技巧,這些技 巧是基於不同類型數據庫引擎在具體實現上的差異。下面只介紹如何區分Oracle和MS SQL Server:
最簡單的辦法,就是前面提到的利用字符串的連結符號,在注入句法已經確定的情況下,攻擊者可以對WHERE語句自由地添加額外的表達式,那么就可以利用字符串的比較來區分數據庫,例如:
AND 'xxx' = 'x' + 'xx' (或者 AND %27xxx%27+%3D+%27x%27+%2B+%27xx%27)
通過將+替換成||,就可以判斷出是數據庫是Oracle還是MS SQL Server,或者是其他類型。
其 他的辦法是利用分號字符(即;),在SQL中,分號是用來將幾個SQL語句連接在同一行中。在注入時,也可以在注入代碼中使用分號,但Oracle驅動程 序卻不允許這樣使用分號。假設在前面使用注釋符號時沒有出現錯誤,那么在注釋符號前加上分號對MS SQL Server是沒有影響的,但如果是Oracle就會產生錯誤。另外,還可以使用COMMIT語句來確認是否允許在分號后再執行其他語句(例如,注入語句 xxx' ; COMMIT --),如果沒有出現錯誤就可以認為允許多句執行。
最后,表達式還可以被替換成能返回正確值的系統函數,由於不同類型的數據庫使用的系統函數也是不同的,因此也可以通過使用系統函數來確定數據庫類型,比如2.3節提到的MS SQL Server的日期函數getdate()與Oracle的sysdate.
3.3 構造注入利用代碼
當所有相關的信息都已獲得后,攻擊者就可以開始進行注入利用,而且在構造注入利用代碼過程中也不再需要詳細的錯誤信息,構造利用代碼本身可以參考其他描述標准SQL注入攻擊的文檔。
由於對於普通的SQL注入利用,已經有很多其他論文進行了詳細的討論,故本文只會在下一節介紹一種UNION SELECT注入。
4 UNION SELECT注入
盡管通過篡改SELECT…WHERE語句來注入對於很多應用程序非常有效,但在盲注情況下,攻擊者仍然願意使用UNION SELECT語句,這是因為與WHERE語句所進行的操作不同,使用UNION SELECT可以讓攻擊者在沒有錯誤信息的情況下依然能訪問數據庫中所有表。
進行UNION SELECT注入需要預先獲知數據庫的表中的字段個數和類型,而這些信息一般被認為在沒有詳細錯誤信息的提示下是不可能獲得的,但本文下面就將給出解決該問題的方法。
另外需要注意的是,進行UNION SELECT的前提是攻擊者已經確定了正確的注入句法,本文的前面一節已經闡明了這在盲注條件下是可以實現的,而且在使用UNION SELECT語句之前,SQL語句中所有的插入語符號都應該已經完成配對,從而可以自由地使用UNION或者其它指令進行注入。UNION SELECT還要求當前語句和最初的語句查詢的信息必須具有相同的數和相同的數據類型,不然就會出錯。
4.1 統計列數
當錯誤信息沒有被屏蔽時,要獲取列數只需要在進行UNION SELECT注入時每次嘗試使用不同的字段數即可,當錯誤信息由“列數不匹配”變成“列的類型不匹配”時,當前嘗試的列數就是正確的。但在盲注條件下,由 於我們對無法獲悉錯誤信息究竟是哪個,所以該方法也就失去了作用。
新的辦法是利用ORDER BY語句,在SELECT語句最后加上ORDER BY能夠改變返回的記錄集的次序,一般是按一個指定的列名的值進行排序。例如,當通過產品號查詢產品時,一個有效的注入語句如下:
SELECT ProdNum FROM Products WHERE (ProdID=1234) ORDER BY ProdNum --
AND ProdName=’Computer’) AND UserName=’john’
人們往往會忽略的是ORDER BY語句后還可以使用數字指代列名,在上例中如果ProdNum是查詢請求返回的記錄中的第一列,則注入1234) ORDER BY 1--返回的結果是一樣的。由於上例查詢請求只返回一個字段,注入1234) ORDER BY 2 --就會出錯,即返回的記錄無法按指定的第二個字段排序。這樣,ORDER BY就可以被利用來對列數進行統計了。由於每個SELECT語句都至少返回一個字段,故攻擊者可以先在注入句法中添加ORDER BY 1來確定語句是否能被正確執行,有時對字段的排序也可能會產生錯誤,這時添加關鍵字ASC或DESC可以解決該問題。一旦確定ORDER BY句法是有效的,攻擊者就會對排序列號從列1到列100進行遍歷(或者到列1000,直到列號被確定為無效),理論上當出現第一個錯誤時,前一個列號就 是要統計的列數,但在實際情況中,有些字段可能不允許排序,那么在出現第一次錯誤時可以再多嘗試一到兩個數字,以確認列號已遍歷完。
4.2 判斷列的數據類型
在統計完列數后,攻擊者需要再判斷列的數據類型,在盲注情況下判斷類型也是有技巧的,由於UNION SELECT要求前后查詢語句查詢的字段類型相同,故如果字段數有限,可以簡單地利用UNION SELECT語句對字段類型進行暴力窮舉(brute force),但如果字段數較多,判斷就會出現問題。根據前文,字段的類型只有數字、字符串和日期三種可能的類型,一旦字段數有10個,那么就意味着有 310(約60,000)種可能的組合,假設每一秒可以自動進行20次嘗試,窮舉一遍也需要近一個小時,如果字段數更多,那么測試所需時間就會令人難以忍 受。
一種簡單的辦法是利用SQL的關鍵字NULL,與靜態字段的注入需要區分是數字類型還是字符類型不同,NULL可以匹配任何一種數據類型。因此可以注入一 個所有查詢字段都為NULL的UNION SELECT語句,那么就不會出現任何類型不匹配的錯誤。讓我們再舉一個與前面類似的例子:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234 AND ProdName=’ Computer’) AND UserName=’john’
假設攻擊者已經獲得了列數(在該例中為4),那么就可以很簡單地構造一個UNION SELECT語句,其中所有查詢字段都為NULL,還需要構造一個不會產生權限問題的FROM語句。對於MS SQL Server,即使忽略FROM語句也不會出錯,但對於Oracle,則可以使用一個名叫dual的表。最后,還需要一個值一定為FALSE的WHERE 語句(比如WHERE 1=2),這是為了確保查詢不會返回只包含null值的記錄集,以杜絕產生其他可能的錯誤。那么針對MS SQL Server的注入語句如下:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234) UNION SELECT NULL,NULL,NULL,NULL
WHERE 1=2 -- AND ProdName=’ Computer’) AND UserName=’john’
這個NULL注入語句有兩個目的,主要目的是構造一個不會產生任何錯誤的UNION SELECT語句以測試UNION語句是否可以被執行,另一個目的是為了對數據庫類型的判斷進行100%確認(可以通過在FROM語句里添加一個數據庫開 發商預置的表名進行測試)。
如果NULL注入語句被順利執行,那么就可以快速地對每個列的類型進行判斷。在每一輪嘗試中,只對一個字段類型進行測試,由於類型只有三類,所以每個字段 最多被測試三次就會有結果,這樣嘗試的次數最多是列數的三倍,而不是以3為底數以列數為指數的次數。假設ProdNum屬於數字類型,其它三個字段都屬於 字符串類型,那么以下順序的注入語句就可以判斷出正確的類型:
• 1234) UNION SELECT NULL,NULL,NULL,NULL WHERE 1=2 --
無錯 句法正確,使用的是MS SQL Server數據庫
• 1234) UNION SELECT 1,NULL,NULL,NULL WHERE 1=2 --
無錯 第一個字段是數字類型
• 1234) UNION SELECT 1,2,NULL,NULL WHERE 1=2 --
出錯 第二個字段不是數字類型
• 1234) UNION SELECT 1,’2’,NULL,NULL WHERE 1=2 --
無錯 第二個字段是字符串類型
• 1234) UNION SELECT 1,’2’,3,NULL WHERE 1=2 --
出錯 第三個字段不是數字類型
• 1234) UNION SELECT 1,’2’,’3’,NULL WHERE 1=2 --
無錯 第三個字段是字符串類型
• 1234) UNION SELECT 1,’2’,’3’,4 WHERE 1=2 --
出錯 第四個字段不是數字類型
• 1234) UNION SELECT 1,’2’,’3’,’4’ WHERE 1=2 --
無錯 第四個字段是字符串類型
攻擊者現在就已經獲得了每一列的數據類型,盲注還可以被應用於從數據庫的表中獲取數據,比如獲得數據表的列表以及它們各自的列名,還可以從應用程序中獲得數據,而這些技術在其他一些關於SQL注入的論文中已經有討論,故本文不再繼續介紹