# Upload-labs靶场实战 # webshell webshell管理工具 Upload-labs靶场实战 > **方法不唯一** > > WebShell 的内容部分都是: > > ```php > > ``` > > `phpinfo()`用来方便判断 WebShell 是否被执行 > > 靶场的 php 版本为 5.2.17 ## 第一关(前端验证) ### 相关源码 ```javascript function checkFile() { var file = document.getElementsByName('upload_file')[0].value; if (file == null || file == "") { alert("请选择要上传的文件!"); return false; } //定义允许上传的文件类型 var allow_ext = ".jpg|.png|.gif"; //提取上传文件的类型 var ext_name = file.substring(file.lastIndexOf(".")); //判断上传文件类型是否允许上传 if (allow_ext.indexOf(ext_name + "|") == -1) { var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name; alert(errMsg); return false; } } ``` ### 绕过方法 绕过前端验证: 方法一:删除文件提交时触发的检测函数 方法二:先将木马改成`.jpg`,开启BurpSuite抓包,上传文件,将抓到的请求包里的文件后缀名改成`.php` 访问 WebShell 地址,`phpinfo()`代码被执行,说明 WebShell 被成功解析: ![](https://pic.imgdb.cn/item/65614da9c458853aef3ed506.jpg) ​ ## 第二关( MIME 验证) ### 相关源码 ```php ``` ### 绕过方法 MIME 绕过:开启抓包,上传文件,将`Content-Type: application/octet-stream`改成`Content-Type:image/jpeg ` 常见的 Content-Type : - `image/jpeg` :jpg 图片格式 - `image/png` :png 图片格式 - `image/gif` :gif 图片格式 - `text/plain ` :纯文本格式 - `text/xml` :XML 格式 - `text/html` :HTML 格式 访问 WebShell 地址,`phpinfo()`代码被执行,说明 WebShell 被成功解析: ![](https://pic.imgdb.cn/item/6562976fc458853aef246a15.jpg) ​ ## 第三关(等价拓展名) ### 相关源码 ```php ``` ### 绕过方法 黑名单只过滤了`.asp`、`.aspx`、`.php`、`.jsp`,将文件的拓展名**改成等价拓展名**再上传 前提条件: Apache服务器配置文件`httpd.conf`中存在:`AddType application/x-httpd-php .php .phtml .phps .php5 .php3 .php2` 此配置的意思:服务器会将这些扩展名的文件视当作 php 解析 常见的等价拓展名: - asp => asa、cer、cdx - aspx => ashx、asmx、ascx - php => php2、php3、php4、php5、phps、phtml - jsp => jspx、jspf ![](https://pic.imgdb.cn/item/6563672bc458853aefe07e1c.jpg) ​ ## 第四关(`.htaccess`) ### 相关源码 ```php $is_upload = false; $msg = null; // 检查是否提交了表单 if (isset($_POST['submit'])) { // 检查 UPLOAD_PATH 目录是否存在 if (file_exists(UPLOAD_PATH)) { // 不允许上传的文件扩展名数组 $deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht", ".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx", ".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp", ".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx", ".aShx",".aSmx",".cEr",".sWf",".swf",".ini"); // 获取上传文件的原始名称并删除末尾的点 $file_name = trim($_FILES['upload_file']['name']); $file_name = deldot($file_name); // 获取文件扩展名并转换为小写 $file_ext = strrchr($file_name, '.'); $file_ext = strtolower($file_ext); // 去除字符串::$DATA $file_ext = str_ireplace('::$DATA', '', $file_ext); // 去除首尾空格 $file_ext = trim($file_ext); // 检查文件扩展名是否在不允许的列表中 if (!in_array($file_ext, $deny_ext)) { // 获取临时文件路径 $temp_file = $_FILES['upload_file']['tmp_name']; // 设置上传后的文件路径 $img_path = UPLOAD_PATH.'/'.$file_name; // 尝试移动上传的文件到目标路径 if (move_uploaded_file($temp_file, $img_path)) { $is_upload = true; } else { $msg = '上传出错!'; } } else { $msg = '此文件不允许上传!'; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; } } ``` ### 绕过方法 上传一个 `.htaccess` 解析文件,利用其配置,将某个白名单文件的类型解析成 php 文件类型 例如将 WebShell 改成 jpg 格式上传 创建一个` .htaccess `文件,内容如下: ```htaccess SetHandler application/x-httpd-php ``` 这个` .htaccess `文件会让 Apache 服务器将 shell.jpg 当作 php 执行 将 .htaccess 文件上传到服务器,访问 shell.jpg : ![](https://pic.imgdb.cn/item/65636aedc458853aefefd011.jpg) ​ ## 第五关(`.user.ini`) ### 相关源码 ```php ``` ### 绕过方法 大小写,转换,空格,还有点号,正常的php类文件上传不了了,并且拒绝上传 .htaccess 文件 发现没有被限制的后缀名有 `.php7` 以及 `.ini` 上传一个 `.user.ini` 配置文件,利用其配置,使每个 php 文件执行之前自动包含 WebShell 前提条件: - php 版本 > 5.3.0 - 文件上传的目录中需要有 php 文件 先上传个 jpg 后缀的 WebShell ,再上传个`.user.ini`。 `.user.ini`的内容: ```ini auto_prepend_file=shell.jpg #将 shell.jpg 包含在所有 php 文件的头部 ``` 或者: ```ini auto_append_file=shell.jpg #将 shell.jpg 包含在所有 php 文件的尾部 ``` 这意味着在每个 php 文件执行之前都会自动包含名为 shell.jpg 的文件 > `php.ini` 是 php 的系统配置文件,是全局配置文件,对整个 web 服务都起作用 > > `.user.ini` 是 php 的系统配置文件,是局部配置文件,只对该文件所处目录及其子目录下的文件起作用 > 如果换成 php 5.3.29 版本后上传不了文件,就先换回 5.2.17 上传 shell.jpg 和 `.user.ini` ,再换到 5.3.29 访问 upload 目录下原本就存在的 `readme.php`: ![](https://pic.imgdb.cn/item/65637478c458853aef13570b.jpg) ​ ## 第六关(大小写绕过) ### 相关源码 ```php ``` ### 绕过方法 缺少大小写转换,将 `shell.php` 改成 `shell.PHP` 。 > Windows 系统下,对于文件名中的大小写不敏感。而这次的检测代码对大小写是敏感的,从而绕过 > > Linux 系统下,对于文件名中的大小写敏感。 ![](https://pic.imgdb.cn/item/656375fdc458853aef18460b.jpg) ​ ## 第七关(空格绕过) ### 相关源码 ```php ``` ### 绕过方法 对上传的文件名未做去空格的操作。 上传 `shell.php` ,BurpSuite 抓包,在文件拓展名最后加上空格。 > Windows 系统下,对于文件名中空格会被作为空处理,但程序中的检测代码却不能自动删除 空格。从而绕过黑名单。 ![](https://pic.imgdb.cn/item/6563775cc458853aef1ca464.jpg) ![](https://pic.imgdb.cn/item/65637789c458853aef1d3519.jpg) ​ ## 第八关(`.`号绕过) ### 相关源码 ```php ``` ### 绕过方法 Windows 系统下,文件后缀名最后一个点会被自动去除,但是这关的代码没有处理文件后缀名最后一个点,所以可以重命名为`shell.php.`来绕过检测 ![](https://pic.imgdb.cn/item/65637671c458853aef19b994.jpg) ![](https://pic.imgdb.cn/item/656376aec458853aef1a83ee.jpg) ​ ## 第九关(`::$DATA`绕过) ### 相关源码 ```php ``` ### 绕过方法 对上传的文件后缀名未做去除`::$DATA` 处理 Windows 系统下,如果上传的文件名为 `shell.php::$DATA` 会在服务器上生成一个 `shell.php `的文件,其中内容和所上传文件内容相同,并被解析 ![](https://pic.imgdb.cn/item/65637813c458853aef1edb77.jpg) ![](https://pic.imgdb.cn/item/65637831c458853aef1f3abf.jpg) ​ ## 第十关(拼接绕过) ### 相关源码 ```php ``` ### 绕过方法 因为代码是先删一个`(点)`再删一个`(空格)`然后再拿文件后缀名去黑名单里比较,所以可以将 `shell.php` 重命名为 `shell.php. .` 这样在上传后等到和黑名单比较时文件名为 `shell.php.`,绕过黑名单,上传到目录后自动去除最后的`.` ![](https://pic.imgdb.cn/item/65637939c458853aef224cd4.jpg) ![](https://pic.imgdb.cn/item/6563794cc458853aef2287da.jpg) ​ ## 第十一关(双写绕过) ### 相关源码 ```php ``` ### 绕过方法 代码利用 `str_ireplace()`将文件名中符合黑名单的字符串替换成空。 将 `shell.php` 重命名为 `shell.pphphp` ![](https://pic.imgdb.cn/item/65637a48c458853aef256619.jpg) ![](https://pic.imgdb.cn/item/6562976fc458853aef246a15.jpg) ​ ## 第十二关( GET 型截断绕过) ### 相关源码 ```php ``` ### 绕过方法 代码使用白名单限制上传文件类型,但上传文件的存放路径可控 利用方法:抓包,设置上传路径为 `upload/shell.php%00` ,上传文件为 ` shell.jpg` ,保存后为 `/upload/shell.php%00shell.jpg`,但服务端读取到`%00 `时会自动结束,将文件内容保存至 `shell.php` 中。 前提: - php 版本要小于 5.3.4 。 - 文件路径可控。 - 在 `php.ini` 中的 `magic_quotes_gpc` 需要为Off状态。(关闭魔术引号) > `%00`为 Unicode 形式的截断符,用于 GET 型控制路径。 `0x00` 为 16 进制的截断符,用于 POST 型控制路径 ![](https://pic.imgdb.cn/item/65637e1cc458853aef30d02f.jpg) ![](https://pic.imgdb.cn/item/65637e48c458853aef314b72.jpg) ​ ## 第十三关( POST 型截断绕过) ### 相关源码 ```php ``` ### 绕过方法 与上一关类似,不同的是通过POST请求来控制路径。 POST请求添加截断字符的方法: 1. 先在路径 `upload/shell.php` 后面打个 `+` => `upload/shell.php+` 2. 将包的内容转成16进制(选择 Hex ) 3. 找到 `+` 的位置( `+` 的16进制是 `2b` ) 4. 将 `2b` 改成 `00`(替换成截断字符) 其他的做法跟上一关基本一致。 ![](https://pic.imgdb.cn/item/65637f1ac458853aef335a6d.jpg) ![](https://pic.imgdb.cn/item/65637f2ac458853aef338624.jpg) ![](https://pic.imgdb.cn/item/65637e48c458853aef314b72.jpg) ​ ## 第十四关(图片马过`unpack()`) ### 相关源码 ```php ``` ### 绕过方法 代码通过读文件的前 2 个字节,检测上传文件二进制的头信息,判断文件类型。 利用图片马绕过检测。 图片马制作: 在 cmd 里执行 `copy 1.jpg/b+shell.php/a shell.jpg` - 1.jpg 为任意图片 - shell.php 为我们要插入的木马代码 - shell.jpg 为我们要创建的图片马 要利用图片马必须要用到文件包含漏洞 ![](https://pic.imgdb.cn/item/65638228c458853aef3afb87.jpg) ​ ## 第十五关(图片马过`getimagesize()`) ### 相关源码 ```php ``` ### 绕过方法 代码通过 `getimagesize()` 获取上传文件信息。 利用图片马绕过检测 > `getimagesize()` 用于获取图像大小及相关信息,成功则返回一个数组 ​ ## 第十六关(图片马过`exif_imagetype()`) ### 相关源码 ```php ``` ### 绕过方法 代码利用 php 内置函数`exif_imagetype()`读取一个图像的第一个字节并检查其后缀名(需要开启 `php_exif` 模块) 利用图片马绕过检测 ​ ## 第十七关(二次渲染绕过) ### 相关源码 ```php ``` ### 绕过方法 代码对上传图片判断`后缀名`、`content-type`,以及利用`imagecreatefromgif`判断是否为`gif`图片,最后再做了一次`二次渲染` 解决方法:将木马放在不会被二次渲染清除的地方,这个位置需要去不断尝试。推荐用别人做好的图片马: https://wwe.lanzoui.com/iFSwwn53jaf ![](https://pic.imgdb.cn/item/65638463c458853aef401a74.jpg) ​ ## 第十八关(条件竞争) ### 相关源码 ```php ``` ### 绕过方法 代码先将上传的 文件保存在服务器的临时路径里,再通过检查来判断是否转移到正式路径 从保存到临时路径到未通过检测、删除文件之间有个短暂的时间差,在此之前的文件是存在服务器上的,所以可以不断上传一个可以在生成 webshell 的文件,同时不断去访问这个文件,总有几率在文件未被删除时访问到文件,激活代码,产生一个 webshell 文件内容为: ```php '); ?> ``` 重复发包的方法: 将抓到的包转到 Intruder 模块: 线程随便设置一下: 最后点击**"Start attack"**就可以不断发包了。 重复访问url的脚本: ```python import requests import time import concurrent.futures def visit_website(url, request_number): try: with requests.Session() as session: response = session.get(url) if response.status_code == 200: print(f"Request {request_number} - Status code: {response.status_code}") else: print(f"Request {request_number} - Error: {response.status_code}") except requests.exceptions.RequestException as e: print(f"Request {request_number} - An error occurred:", str(e)) # 用户输入要访问的网站地址、请求数量和延迟时间 url = input("请输入要访问的网站地址(包括协议):") num_requests = int(input("请输入请求数量:")) delay = float(input("请输入每次请求的延迟时间(以秒为单位):")) # 创建线程池 with concurrent.futures.ThreadPoolExecutor() as executor: # 提交任务到线程池 future_to_url = {executor.submit(visit_website, url, i+1): i+1 for i in range(num_requests)} # 获取任务结果 for future in concurrent.futures.as_completed(future_to_url): try: future.result() except Exception as e: print("An error occurred:", str(e)) # 控制请求间隔 time.sleep(delay) ``` ​ ## 第十九关( Apache 解析漏洞) ### 相关源码 ```php // index.php $is_upload = false; // 初始化上传标志为false $msg = null; // 初始化上传状态消息为null if (isset($_POST['submit'])) { require_once("./myupload.php"); // 引入上传类文件 $imgFileName = time(); // 生成基于时间的文件名 $u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'], $imgFileName); // 创建MyUpload类的实例 $status_code = $u->upload(UPLOAD_PATH); // 调用上传方法 // 根据上传状态码处理不同情况 switch ($status_code) { case 1: $is_upload = true; $img_path = $u->cls_upload_dir . $u->cls_file_rename_to; break; case 2: $msg = '文件已上传但未重命名。'; break; case -1: $msg = '此文件无法上传到服务器的临时文件存储目录。'; break; case -2: $msg = '上传失败,上传目录不可写。'; break; case -3: $msg = '上传失败,无法上传此类型的文件。'; break; case -4: $msg = '上传失败,上传的文件太大。'; break; case -5: $msg = '上传失败,服务器上已存在同名文件。'; break; case -6: $msg = '文件无法上传,无法复制到目标目录。'; break; default: $msg = '未知错误!'; break; } } // myupload.php // MyUpload类定义了一个文件上传类 class MyUpload { // ... var $cls_arr_ext_accepted = array( ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt", ".html", ".xml", ".tiff", ".jpeg", ".png" ); /** upload() ** ** 上传文件的方法。 ** 这是类外部唯一调用的方法。 ** @para 要上传到的目录的名称 ** @returns void **/ function upload($dir) { $ret = $this->isUploadedFile(); if ($ret != 1) { return $this->resultUpload($ret); } $ret = $this->setDir($dir); if ($ret != 1) { return $this->resultUpload($ret); } $ret = $this->checkExtension(); if ($ret != 1) { return $this->resultUpload($ret); } $ret = $this->checkSize(); if ($ret != 1) { return $this->resultUpload($ret); } // 如果设置文件存在标志为1 if ($this->cls_file_exists == 1) { $ret = $this->checkFileExists(); if ($ret != 1) { return $this->resultUpload($ret); } } // 如果能运行到这里,就准备将文件移动到目标位置 $ret = $this->move(); if ($ret != 1) { return $this->resultUpload($ret); } // 检查是否需要重命名文件 if ($this->cls_rename_file == 1) { $ret = $this->renameFile(); if ($ret != 1) { return $this->resultUpload($ret); } } // 如果运行到这里,一切都按计划进行 :) return $this->resultUpload("成功"); } // ... } ``` > 这关的源码有个错误,打开 19 关的 myupload.php 在此处将 `$dir` 修改为 `$dir.'/'` > > ![](https://pic.imgdb.cn/item/6564506ac458853aefe5c718.jpg) ### 绕过方法 - 图片马 - 条件竞争 - Apache 解析漏洞 这题的预期解法应该是利用 Apache 解析漏洞 **Apache解析漏洞**:如果重复发包上传文件`shell.php.7z`,有几率导致某次服务器无法对上传过来的文件成功重命名,就导致`webshell.php.7z`未被重命名被传到服务器里。Apache 解析漏洞会将`shell.php.*`都当作`shell.php`执行 - 漏洞版本:使用 module 模式与 php 结合的所有版本 apache 存在未知扩展名解析漏洞,使用 fastcig 模式与 php 结合的所有版本 apache 不存在此漏洞 - 漏洞原理:Apache 在遇到多个后缀的文件时,尝试从后往前解析,直到解析到可解析的为止 - test.php.xxx 按照 php 解析 - test.php.xxx.jpg 按照 php 解析 具体步骤: 将上传 shell.php.7z 的数据包发到 Intruder 模块: ![](https://pic.imgdb.cn/item/65642a94c458853aef6f30c8.jpg) 如图设置,最后点击 Start attack : ![](https://pic.imgdb.cn/item/65642b09c458853aef70916c.jpg) 可以看到上传目录出现了来不及被重命名的 shell.php.7z : ![](https://pic.imgdb.cn/item/65642b7dc458853aef71f77b.jpg) ![](https://pic.imgdb.cn/item/65642badc458853aef7291a1.jpg) ​ ## 第二十关(`/.`绕`move_uploaded_file()`) ### 相关源码 ```php $is_upload = false; $msg = null; if (isset($_POST['submit'])) { // 检查是否有表单提交 if (file_exists(UPLOAD_PATH)) { // 检查上传目录是否存在 $deny_ext = array("php", "php5", "php4", "php3", "php2", "html", "htm", "phtml", "pht", "jsp", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml", "asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer", "swf", "htaccess"); $file_name = $_POST['save_name']; // 从表单获取保存的文件名 $file_ext = pathinfo($file_name, PATHINFO_EXTENSION); // 获取文件名的扩展名 if (!in_array($file_ext, $deny_ext)) { // 检查文件扩展名是否被禁止 $temp_file = $_FILES['upload_file']['tmp_name']; // 获取上传的临时文件路径 $img_path = UPLOAD_PATH . '/' . $file_name; // 设置上传后的文件路径 if (move_uploaded_file($temp_file, $img_path)) { // 尝试移动上传的文件到目标路径 $is_upload = true; // 设置上传标志为true } else { $msg = '上传出错!'; // 设置上传错误消息 } } else { $msg = '禁止保存为该类型文件!'; // 设置禁止保存该类型文件的消息 } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!'; // 设置上传目录不存在的消息 } } ``` ### 绕过方法 代码通过`move_uploaded_file()`将上传的文件移动到新位置 `move_uploaded_file()`的特性:移动的时候会忽略掉文件末尾的 `/.` 而`php/.`可以绕过黑名单 所以可以上传后门,命名为`upload-19.php/.` ![](https://pic.imgdb.cn/item/65638683c458853aef44ca17.jpg) ![](https://pic.imgdb.cn/item/65638694c458853aef44ecb6.jpg) ​ ## 第二十一关 ### 相关源码 ```php $is_upload = false; $msg = null; if (!empty($_FILES['upload_file'])) { // 检查是否有上传文件 // 检查MIME类型 $allow_type = array('image/jpeg', 'image/png', 'image/gif'); if (!in_array($_FILES['upload_file']['type'], $allow_type)) { // 检查文件的MIME类型是否在允许的列表中 $msg = "禁止上传该类型文件!"; // 设置MIME类型不允许的消息 } else { // 检查文件名 $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name']; // 获取文件名,如果未提供,则使用上传文件的原始名称 if (!is_array($file)) { $file = explode('.', strtolower($file)); } // 将文件名转换为小写并分割为数组 $ext = end($file); // 获取文件的后缀 $allow_suffix = array('jpg', 'png', 'gif'); // 设置允许的文件后缀列表 if (!in_array($ext, $allow_suffix)) { // 检查文件的后缀是否在允许的列表中 $msg = "禁止上传该后缀文件!"; // 设置后缀不允许的消息 } else { $file_name = reset($file) . '.' . $file[count($file) - 1]; // 构建上传后的文件名 $temp_file = $_FILES['upload_file']['tmp_name']; // 获取上传的临时文件路径 $img_path = UPLOAD_PATH . '/' . $file_name; // 设置上传后的文件路径 if (move_uploaded_file($temp_file, $img_path)) { // 尝试移动上传的文件到目标路径 $msg = "文件上传成功!"; // 设置上传成功消息 $is_upload = true; // 设置上传标志为true } else { $msg = "文件上传失败!"; // 设置上传失败消息 } } } } else { $msg = "请选择要上传的文件!"; // 设置未选择文件的消息 } ``` ### 绕过方法 验证过程: 1. 验证上传路径是否存在 2. 验证`['upload_file']`的 content-type 是否合法(可以抓包修改) 3. 判断 POST 参数是否为空定义`$file`变量(关键:构造数组绕过下一步的判断) 4. 判断 file 不是数组则使用`explode('.', strtolower($file))`对 file 进行切割,将file变为一个数组 5. 数组第一位和`$file[count($file) - 1]`进行拼接,产生保存文件名 file_name 6. 上传文件 绕过: 将要保存的文件名变成一个空间为 3 的数组,第 2 个值 ([1]) 留空 这样子就会把`upload-20.php/`与` . `与拼接 最后就是`upload-20.php/.` `move_uploaded_file()`移动文件的时候会忽略`/.` ![](https://pic.imgdb.cn/item/64cb733f1ddac507ccd84457.jpg) ​ ​ # 文件上传漏洞防御 ## 执行权限 文件上传后存储目录不给执行权限 ## 解码还原 把上传的文件数据编码后存储,固定方式解析 ## 分站存储 将文件保存在另外一台服务器上 ## OSS对象 将文件保存在OSS上