Damn Vulnerable Web App (DVWA) 是一個基於 PHP 和 MySQL 實作的 web 應用。如同它的名字一樣,這是一個充滿漏洞的應用,因為這個應用的主要目標,就是幫助 web 程式設計師了解安全性的問題。
若要安裝 DVWA,請到 http://www.dvwa.co.uk/,下載後解壓縮,並放到網頁伺服器可讀取的目錄下。接著,我們要將 config 資料夾下的 config.inc.php.dist 複製為 config.inc.php,並修改其中的資料庫帳密;裡面預設使用 root,你也可以(或者必須,依系統而定)新增一個專門的使用者帳號來測試。上述設定完成後,請用瀏覽器連線到 setup.php 進行安裝前的設定的確認,若有出現紅色提示,代表你的伺服器缺少某些東西,或者設定比較安全,從而可能無法測試某些安全問題(以下範例將假設 allow_url_fopen 和 allow_url_include 為 off)。若設定上沒有問題,就可以按下"Create / Reset Database",讓程式自動幫你設定好測試用的資料庫。
接著要連到 login 登入。DVWA 的文件(docs/DVWA_v1.3.pdf)中提到,預設帳密是 admin/password,當然因為資料庫在自己手上,所以也可以直接用 phpmyadmin 連進去看密碼。DVWA 將密碼用 md5 加密儲存,不過因為範例的密碼是很常見的密碼,因此也可以將密文放到網路上搜尋或者自己嘗試字典法暴力破解,例如 gordonb 的密碼是 abc123。登入後,在左下方的"DVWA Security",可以讓你調整安全層級,這裡分成 low/medium/high 三種安全層級狀況讓你測試,並有 impossible 層級,來示範安全的作法(若你玩過較舊的版本,則舊版本的 high 就是現在的 impossible)。
首先我們來看"Brute Force",原始碼的路徑位於 vulnerabilities/brute/source。這裡的範例是輸入帳號密碼,若正確的話就會顯示歡迎訊息,否則顯示錯誤。在 low 層級當中,寫法是這樣的(稍微經過了一點簡化,移除跟破解較無關的部分):
$user = $_GET['username']; $pass = $_GET['password']; $pass = md5($pass); $query = "SELECT * FROM `users` WHERE user='$user' AND password='$pass';"; $result = mysqli_query( $qry ) or die( '<pre>' . mysqli_error() . '</pre>' ); if( $result && mysql_num_rows( $result ) == 1 ) { // ... }
很明顯的,雖然密碼是加密過才送進去比對,但是 $user 可以被 SQL injection,各位可以用帳號為「admin' -- 」(請注意尾巴有空白),搭配隨意密碼來測試。而 medium 和 high 層級,雖然依序加上了 mysqli_real_escape_string 和 stripslashes,以及在登入錯誤時會暫停數秒等機制,但仍然有辦法透過暴力嘗試來登入。
Impossible 層級則是在資料庫中新增了 last_login 和 failed_login 欄位,來分別代表最後嘗試登入的時間和失敗次數,以藉此判斷是否應該鎖定帳號禁止登入,概念如下:
$total_failed_login = 3; $lockout_time = 15; $account_locked = false; Check whether the user has been locked out: select failed_login, last_login from table if( has_result && num of failed_login > $total_failed_login){ Calculate when the user would be allowed to login again if(not allowed){ $account_locked = true; } } Check whether username matches the password if( matched && $account_locked == false ){ Reset `failed_login` of table } else { Random sleep Increase `failed_login` of table } Update `last_login` of table
接著是"Command Injection",原始碼在 vulnerabilities/exec/source。這裡的範例是輸入 IP,網頁會幫你顯示 ping 指令的結果。Low 層級當中對輸入沒有做任何過濾,因此你可以使用例如「127.0.0.1 && dir D:\」之類的指令,來對該段程式碼進行設計外的操作;medium 層級當中,將用以區隔指令的"&&"和";"(for Linux)過濾掉,但仍可以用「127.0.0.1 | dir D:\」來繞過;high 層級則是把指令中常見的幾個標點符號過濾掉,但很不幸的在過濾 pipe 時多了一個空白(如下),因此仍能繞過。
// Set blacklist $substitutions = array( '&' => '', ';' => '', '| ' => '', '-' => '', '$' => '', '(' => '', ')' => '', '`' => '', '||' => '', ); // Remove any of the charactars in the array (blacklist). $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
Impossible 層級則僅允許用"."分隔的四個數字。
CSRF 的全名是 Cross Site Request Forgery,中文名稱為跨站請求偽造。DVWA 使用的範例是變更密碼,參數使用 GET 傳遞,原始碼在 vulnerabilities/csrf/source。Low 層級沒有驗證包括舊密碼在內的任何內容,因此只要提供 http://localhost/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change# 這樣的一個連結,就可以任意地進行修改。Medium 和 high 層級則因為加入 HTTP_REFERER 甚至於隨機數值(CSRF token)來驗證,因此需要用修改 HTTP header 等技巧,甚至套用其他漏洞,才能從站外頁面攻擊(因為 php7 移除 HTTP_REFERER,因此可能無法測試)。Impossible 層級則是加入了舊密碼的驗證,因此除了暴力嘗試以外無法破解。
File Inclusion 顧名思義就是含括檔案,原始碼在 vulnerabilities/fi/source。你可以在網頁上看到 file1.php、file2.php 及 file3.php 被列出,而我們的目的是 file4.php 甚至於其他檔案。Low 層級直接把 $_GET 的內容做為檔名,也就是完全沒有防禦。Medium 層級以如下的方式過濾了"../"、"http://"等字串,但你仍可以用"....//"及其他 http 以外的協定來達到惡意目的。
$file = $_GET[ 'page' ]; // Input validation $file = str_replace( array( "http://", "https://" ), "", $file ); $file = str_replace( array( "../", "..\"" ), "", $file );
High 層級只剩下 file4.php 沒有擋住,Impossible 層級則是使用白名單。
File Upload 的原始碼在 vulnerabilities/upload/source,範例中的功能是上傳圖片檔,上傳後的儲存位置在 hackable/uploads。Low 層級沒有任何防禦,可以任意上傳 php 檔案,如果再搭配前面的 File Inclusion,則即使上傳後的位置不在網頁伺服器可讀取的範圍內,也可以經由 include 而被當作 php 執行。
Medium 層級則是使用 MIME type 來驗證,如下:
// File information $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // Is it an image? if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) { // Do something }
但用 tamper data 或其他修改封包內容的技巧,很容易就可以繞過。例如使用以下的 python 程式碼,其中的 PHPSESSID 為登入的相關資訊,需要自行代換;'Upload' 和 'uploaded' 則為示範頁面的表單欄位名稱:
import requests response = requests.post( 'http://localhost/dvwa/vulnerabilities/upload/', data={'Upload': '12345'}, headers={'Cookie': 'PHPSESSID=fr9tae13skqg4hdvr6rosek17d; security=medium'}, files=[('uploaded', ('aaa.php', open('aaa.php', 'rb'), 'image/jpeg'))] ) print(response.content)
High 層級使用附檔名加上了 getimagesize 來檢驗(如下),但是我們可以把惡意程式碼藏在圖片的 EXIF 欄位當中上傳,如果配合較低等級的 File Inclusion 漏洞,就有辦法把它當作 php 來執行。你可以在上傳一張圖片後,將安全層級切換為 Low (Medium 也可以,但是需要另外繞過),以 http://localhost/dvwa/vulnerabilities/fi/?page=../../hackable/uploads/[剛才上傳的檔名] 這個連結來測試。
// File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); // Is it an image? if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) && ( $uploaded_size < 100000 ) && getimagesize( $uploaded_tmp ) ) { // Do something }
Impossible 則是重新讀取圖片內容後再次存檔。
SQL Injection 的原始碼在 vulnerabilities/sqli/source,其範例是輸入使用者編號後顯示資料。Low 層級沒有做任何防禦,且會把查詢到的所有結果顯示出來。我們可以嘗試以下輸入:
- 「1」:這是正常範例,顯示編號 1 的使用者資料
- 「1' or '1'='1」:會讓 WHERE 的條件變為「user_id = '1' or '1'='1'」, 因此會顯示所有資料。
- 「0' union select 123, password from users -- 」:列出所有密碼(密文),當然在此方法中,資料表和欄位名稱必須是已知。
- 「0' union select 123, user() -- 」:user() 是 MySQL 當中,顯示目前進行連線的使用者的方法。
- 「0' union select TABLE_SCHEMA, TABLE_NAME from information_schema.tables WHERE TABLE_SCHEMA='dvwa' -- 」: information_schema 是 MySQL 中用來存放表格資訊的資料庫, 此語句顧名思義是撈出表格名稱。
- 「0' union select TABLE_NAME, COLUMN_NAME from information_schema.COLUMNS WHERE TABLE_SCHEMA='dvwa' -- 」: 依此類推,此語句會顯示出表格名稱和欄位名稱。
至於 medium 層級,雖然使用了 mysql_real_escape_string 以跳脫單引號等字元,但是由於 SQL 語句卻移除了單引號,所以將前述部分範例進行簡單代換後,例如「1 or 1 = 1」,依然可以使用。High 層級使用「$_SESSION['id']」來取值,但如果有其他漏洞能改掉 SESSION 則也會被攻擊,而 impossible 層級則是限制了輸入的使用者編號必須是數字。
XSS 依據出現的地方不同,分成 DOM、reflected,以及 stored 等三種;由於這幾個部分非常相似,故此處以 reflected 做示範,其範例是將輸入的文字印出,原始碼在 vulnerabilities/xss_r/source,測試此範例時,請留意較新版的 Chrome 預設會擋掉 XSS,因此可能需要用 Firefox 或 IE 來測試。Low 層級沒有任何防禦,因此可以輸入任意的 html 或 JavaScript 片段。Medium 層級用以下方法將「<script>」標籤刪除,但依然可以用「<script >」、「<scri<script>pt>」或者「<ScrIpt>」等方法來繞過。
$name = str_replace('<script>', '', $_GET['name']);
High 層級當中雖然禁止了 script 標籤的各種大小寫,也不允許你在中間插入字串,但仍能用「<img src=1 onerror=alert('xss')>」等方法繞過。Impossible 層級則是使用 htmlspecialchars 來過濾使用者輸入內容。