Loading... # 1、目标 基于白盒测试方法,完成文件上传利用,学习文件上传方法及原理,探索文件上传利用场景。 ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3694535907.png) # 2、级别 Low 后台代码: ```php <?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>"; } } ?> ``` 前端代码: ```html <form enctype="multipart/form-data" action="#" method="POST"> <input type="hidden" name="MAX_FILE_SIZE" value="100000" /> Choose an image to upload:<br /><br /> <input name="uploaded" type="file" /><br /> <br /> <input type="submit" name="Upload" value="Upload" /> </form> ``` 关键信息: * 文件大小限制为 100000 Bytes * 成功上传后会打印文件存放路径 文件上传后被直接移动,没有任何检查,且路径可知,很可能存在文件上传漏洞。 先上传一份正常文件: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/952406619.png) 可见文件上传成功,路径为 `../../hackable/uploads/使用前说明.txt`,结合当前页面 url,可知文件上传的路径为 `http://localhost/dvwa/hackable/uploads/使用前说明.txt`。尝试访问: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/984601464.png) 访问成功,满足文件上传漏洞的下述必要条件,确认存在文件上传漏洞: * 文件上传后路径可知 * 文件上传后可以访问 由于后台没有对文件做任何检查,于是尝试上传风险文件,比如一句话木马: ```php <?php @eval($_POST['caidao']);?> ``` 然而连接时出现了一些问题,分析为 [php7.x 菜刀连接失败分析](http://www.wublub.cn/index.php/archives/944/)。 最终文件上传漏洞利用成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/4061297497.png) # 3、级别 Meidum 关键代码: ```php <?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>'; } } ?> ``` 相较于级别 Low 改动为: * 校验上传文件类型为`image/jpg` 或`image/png` 这里后台只对文件类型做了校验,而这个类型是浏览器是发出请求时指定的,所以可以抓包更改绕过。但是在绕过前,以下先简单学习一下文件类型的相关知识。 ## 3.1、Content-Type 学习 对于 enctype="multipart/form-data" 的 form 表单请求,请求头中: * Content-Type = "multipart/form-data" * 具体的上传内容中,还有一个 Content-Type 字段指明了当前上传文件的类型 以下是上传一张图片时的完整请求数据: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2096343167.png) 以下做一些正常上传文件的测试: 上传一张 jpeg 图片,类型为 `imag/jpeg`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3667190646.png) 上传一张 png 图片,类型为 `jmage/png`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3205165397.png) 上传一份 txt 文档,类型为 `text/plain`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3610245307.png) 上传一份 php 文件,类型为 `application/octet-stream`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3047182315.png) 上传一份 html 文件,类型为 `text/html`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1156471352.png) 上传一份 exe 文件,类型为 `application/x-msdownload`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3665082687.png) 可见浏览器会自动识别上传文件的格式,并赋予不同的 Content-Type。当然浏览器并不能认识所有的格式,有时候也不能正确识别文件格式。以下做一些异常测试。 准备一张 jpg 图片,为了方便测试,所以图片大小只有六百多个字节,可以在请求中看到所有数据。首先是正常的上传,文件名 test.jpg,类型为 `imag/ipeg`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/210899617.png) 保持文件内容不变,将文件名更改为 test.php,类型变更为 `application/octet-stream`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2462934719.png) 保持文件内容不变,将文件名更改为 test.html,类型变更为 `test/html`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1455571386.png) 上述测试说明: * **浏览器只是简单的基于文件后缀而进行 Content-Type 识别**。jpg->php,Content-Type 也相应更改 * **浏览器不保证文件内容的合法性**。jpg->php,Content-Type 相应更改了,然而此时文件不是合法的 php 文件 同样的,我们在 test.jpg 图片内容中追加一句 php 代码,文件名保持 test.jpg: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2305608638.png) 类型也依旧是 `image/jpeg`,和上述结论保持一致。 接下来我们尝试绕过,有如下几种方法。 ## 3.2、抓包更改 Content-Type 上传菜刀后台 webshell.php,在 burpsuite 中拦截请求,将 Content-Type 从 `application/octet-stream` 更改为 `image/jpeg`: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1507098356.png) 后台检测通过,上传成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2937678370.png) 菜刀连接成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1363395610.png) 或者上传一张正常图片,拦截到请求然后直接将文件名和文件内容都更改,是一个意思。 ## 3.3、00截断绕过 首先将 webshell.php 改名为 webshell.php.jpg,然后拦截到请求为: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1609431301.png) 这时候上传是能够成功的,但我们无法利用(除非配合文件包含漏洞),因为我们只能访问 webshell.php.jpg,是一个图片文件,而非 php 文件。 所以我们得想办法让后台在接收到 webshell.php.jpg 时,将其保存为 webshell.php。仔细看一下后台保存文件代码: ```php $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); ``` 这时候 $_FILES['uploaded']['name'] = 'webshell.php.jpg',是从请求中的 filename="webshell.php.jpg" 获取的。如果它等于 'webshell.php' 就好办了。 php 底层是基于 C 编写的,C 对字符串使用了 null-terminated string 方式来存储识别,这种方法的优势是无需额外存储字符串长度,劣势是求字符串长度效率较低,而且无法表达带空字符的字符串。 由此我们可以拦截请求并将文件名使用 00 截断的方式处理。在 burpsuite 中拦截到请求后,先将文件名从 webshell.php.jpg 更改为 webshell.php%00.jpg: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/414018933.png) 然后选中 %00,右键选择 URL-decode: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1440215758.png) 设置页面显示不可打印字符,可以看到此时文件名已经变成为 webshell.php\0.jpg: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/173868754.png) 查看十六进制数据,此时 php 和 .jpg 之间插入了一个字节 0x00 数据: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1282015349.png) 或者也可以直接先随便插入一个字节数据,然后在十六进制视图中更改该字节为 0x00,而不是选择 URL-decode。 此时如果该请求到达后台,php 从请求中解析 "filename=webshell.php\x00.jpg" 数据时,因为 php 识别到 \x00 时就认为到达了字符串尾部,所以提取到的文件名只有 webshell.php。 在后台下断点,可见此时 $_FILES['uploaded']['name'] = "webshell.php",截断成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1120611507.png) 上传成功,的确保存为 webshell.php: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3801404639.png) 后续菜刀连接即可。 # 4、级别 High 关键代码: ```php <?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>'; } } ?> ``` 相较于级别 Medium 改动为: * 校验文件后缀,必须为 jpg、jpeg 或 png * 使用 getimagesize 校验文件是否为合法图片 因为增加了校验后缀的逻辑,所以文件在服务器存放时,必定也是 jpg、jpeg 或者 png 格式了,这样子直接访问文件是无法执行代码的,基本上文件上传漏洞的直接利用就失效了。 getimagesize 用于返回一张图片的大小的数据,在图片非法时返回 false,具体可以查看 [php getimagesize 分析及绕过](http://www.wublub.cn/index.php/archives/931/)。 于是我们构造一张图片,十六进制内容显示为: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1447777488.png) 上传成功,说明 getimagesize 被绕过了。当然,相较于直接构造图片文件头,更简单的方法是直接在一张合法图片后面追加代码,这样就不用费心思绕过 getimagesize 了: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1449458296.png) 此时后台保存的是 png 图片,直接利用是不可行的。我们结合 DVWA 网站的级别 Low 文件包含漏洞,首先在菜刀中添加 shell 如下: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1661630586.png) 此时还不能直接文件管理,因为 DVWA 的文件包含漏洞时在登录状态下才能进行的,所以我们需要在菜刀中登录该网站,好让菜刀拿到会话数据。在 shell 上右键“浏览网站”,登录网站后将级别更改为 Low。然后再进行文件管理,成功连接: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2064431944.png) # 5、级别 Impossible 关键代码: ```php <?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' ]; { $aaa = uniqid(); $bbb = $aaa; } // 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; { $aaa = uniqid(); $bbb = $aaa; } // 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(); ?> ``` 相较于级别 High 改动为: * 使用 Anti-CSRF token * 使用 imagecreatefromjpeg+imagejpeg 等二次渲染生成新图片 * 保存文件时通过哈希生成不可猜测的文件名,但后续又把完整路径打印到页面 上述改动对我们有影响的是第二点,imagecreatefromjpeg 接收一个图片路径,返回一个图像对象,或者在图片非法时返回 false。对我们来说,构造的非法图片或追加了 php 代码的图片很有可能无法通过该函数的检测。具体有两种场景: * 直接构造文件头合法的图片,但内容本身非法,然后添加 php 代码 * 在一张合法的图片后部追加 php 代码 先看看直接构造的假图片,图片内容为: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3632304927.png) imagecreatefrompng 函数报错,上传失败。因为 imagecreatefromjpeg 会读取解析图片数据,而我们的图片本身就不是合法的图片,自然无法通过检测。 ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1064798395.png) ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1876007183.png) 接着在一张合法的图片后部追加 php 代码,如下: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1893544432.png) 上传成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3400115494.png) 然而直接在服务器后台查看保存的图片,我们在图片尾部追加的 php 代码已经被去除了。这是因为图片本身的描述结构中没有到 php 代码的相关描述信息,因此 imagecreatefromjpeg 在解析图片时不会读取 php 代码,因此在新生成的图片中也不会有这部分内容: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1114385312.png) 以上两种攻击手段都无法绕过 imagecreatefromjpeg。但实际上还有一种场景,即: * 在合法的图片内容本身,嵌入/替换 图片本身数据 但如果对图片本身的描述数据没有足够了解,很可能会使图片失效。网上有相关的文章介绍,如 [BookFresh Tricky File Upload Bypass to RCE](https://secgeek.net/bookfresh-vulnerability/),但有待验证。 # 6、总结 从级别 Low 到级别 Impossible,对抗文件上传漏洞的思路为: * 校验上传文件的 Content-Type * 校验上传文件的后缀名 * 校验上传文件的内容 * 通过哈希让后台存放的文件不可猜测,或不暴露上传文件的路径 最后修改:2022 年 04 月 17 日 04 : 53 PM © 允许规范转载