從DVWA靶場學程式碼審計
阿新 • • 發佈:2021-07-08
DVWA是較為經典的一個傳統漏洞的靶場
內建了low,medium,hight,impossible四個安全級別供安全人員去研究相關漏洞。今天就來對impossible這個級別進行程式碼審計,從中學習一下傳統漏洞的程式碼層面的防禦措施。
#檔案上傳
#原始碼 <?php if( isset( $_POST[ 'Upload' ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // Where are we going to be writing to? $target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/'; //$target_file = basename( $uploaded_name, '.' . $uploaded_ext ) . '-'; $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;uniqid() $temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) ); $temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' .$uploaded_ext; // Is it an image? if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) && ( $uploaded_size < 100000 ) && ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) && getimagesize( $uploaded_tmp ) ) { // Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD) if( $uploaded_type == 'image/jpeg' ) { $img = imagecreatefromjpeg( $uploaded_tmp ); imagejpeg( $img, $temp_file, 100); }// else { $img = imagecreatefrompng( $uploaded_tmp ); imagepng( $img, $temp_file, 9); } imagedestroy( $img ); // Can we move the file to the web root from the temp folder? if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { // Yes! $html .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>"; } else { // No $html .= '<pre>Your image was not uploaded.</pre>'; } // Delete any temp files if( file_exists( $temp_file ) ) unlink( $temp_file ); } else { // Invalid file $html .= '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } } // Generate Anti-CSRF token generateSessionToken(); ?>
首先是進行csrf-token檢查,如果校驗不通過,返回到index.php,相關程式碼:
function checkToken( $user_token, $session_token, $returnURL ) {
if( $user_token !== $session_token || !isset( $session_token ) ) {
dvwaMessagePush( 'CSRF token is incorrect' );
dvwaRedirect( $returnURL );
}
}
然後就是對檔案資訊的一個獲取
// File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];//檔名 $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);//檔案字尾 $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];//檔案大小 $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];//檔案型別 $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];//檔案臨時名
接著就是定義上傳路徑和臨時路徑以及對檔名進行加密
$target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';//寫入路徑 $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;//先呼叫uniqid()生成一個微秒級的當前時間的值,然後與檔名拼接,最後整體經過md5加密,再和字尾拼接構成完整的一個檔名 $temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );//定義了臨時檔案寫入路徑為預設值或系統預設目錄,windows為c:\windows\temp,linux為/tmp $temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;//定義了加密後的臨時檔案寫入路徑
接下來就是重新生成圖片,導致圖片馬直接掛掉.
if( $uploaded_type == 'image/jpeg' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_file, 100);
}//
else {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_file, 9);
}
imagedestroy( $img );
然後如果我們上傳的圖片都通過了校驗,那麼此時圖片的路徑會變為web根目錄下的/hackable/uploads/然後回顯路徑.關鍵程式碼:
if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
// Yes!
$html .= "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
$html .= '<pre>Your image was not uploaded.</pre>';
}
程式碼層防禦思路:
後臺白名單+二次驗證(例如圖片重新生成,過濾掉其他可疑資料)
非必要情況下不要回顯檔案路徑到客戶端
#SQL注入:
#原始碼
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到通過prepare()對sql語句進行了預編譯,以及使用了bindParam()來對引數進行繫結,大大降低了sql注入的風險
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
並且限制了查詢結果
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
程式碼層防禦思路:
預處理SQL語句
引數化查詢
不將不必要的資訊返回給客戶端
#檔案包含
#原始碼
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
只允許包含固定檔案像include.php或者file1.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" )
程式碼層防禦思路:
包含的檔案不能由使用者可控
白名單校驗
#RCE(遠端程式碼/命令執行)
#<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );
// Split the IP into 4 octects
$octet = explode( ".", $target );
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
$html .= "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
$html .= '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}
對輸入的引數用explode()打散為ip格式的陣列並進行嚴格校驗
$octet = explode( ".", $target );
// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
程式碼層防禦措施:
非必要情況下不提供命令執行介面
對於輸入的命令進行嚴格過濾(特殊字元& | ;等)
#CSRF
#原始碼
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Sanitise current password input
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );
// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();
// Do both new passwords match and does the current password match the user?
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
// Update database with new password
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();
// Feedback for the user
$html .= "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
$html .= "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
其實關鍵的操作就是檢查csrf-token,以及驗證新密碼與使用者是否匹配
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );
checkToken()程式碼:
function checkToken( $user_token, $session_token, $returnURL ) { # Validate the given (CSRF) token
if( $user_token !== $session_token || !isset( $session_token ) ) {
dvwaMessagePush( 'CSRF token is incorrect' );
dvwaRedirect( $returnURL );
}
}
程式碼層防禦思路:
頁面嵌入token
token與後端使用者session中的token繫結
GET和POST請求都得進行驗證
#XSS-reflect
#原始碼
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );
// Feedback for end user
$html .= "<pre>Hello ${name}</pre>";
}
// Generate Anti-CSRF token
generateSessionToken();
?>
通過htmlspecialchars()函式將預定義的字元轉化為HTML實體,防止xss
$name = htmlspecialchars( $_GET[ 'name' ] );
#XSS-Stored
#原始碼
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );
// Sanitize name input
$name = stripslashes( $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$name = htmlspecialchars( $name );
// Update database
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
對輸入輸出都進行過濾,編碼等操作
$message = stripslashes( $message );
$message = htmlspecialchars( $message );
$name = stripslashes( $name );
$name = htmlspecialchars( $name );
#XSS-dom-based
#原始碼
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}
document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>
$decodeURI = "decodeURI";
if ($vulnerabilityFile == 'impossible.php') {
$decodeURI = "";
}
可以看到我們輸入的引數經過url編碼後無法被decodeURI解碼,也就無法產生dom型xss了
程式碼層防禦思路:
對輸入輸出進行編碼
過濾特殊字元(< > /等)
#WeakSessionId
#原始碼
<?php
$html = "";
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>
使用隨機數+時間戳+固定字串(“Impossible”)進行 sha1 運算,作為 session Id 降低了被爆破的可能