内容纲要

赛后想写一些比赛时的心得 , 但是找到的 Docker 环境都无法完美运行 , 或多或少有一些问题 , 这里就不写解题过程了 , 记录一下自己的想法吧~


easyphp

题目直接给了源码

png

乍一看这是一道代码执行的题目 , 题目从URL获取GET参数" _ " 的值并且对其进行过滤检测 , 当通过所有检测后用户的输入内容会被 eval() 执行 . 本题表面上存在两道防线


  1. 第一道防线

    第一道防线分为三层 , 由三个PHP内置函数组成 , 分别对用户输入进行检测

    以执行 eval() 作为突破这道防线的目标

    png

    首先判断是否从GET方法获取 " _ " 参数的值

    然后通过 strlen()` 函数对GET方法对该值进行长度检测 , 如果字符串长度大于 18 就拦截信息

    接下来通过 preg_match() 正则过滤该值中的敏感字符 , 这个正则表达式非常严谨 , 过滤了绝大部分的可写字符

    最后通过 count_chars() 函数来限制该值中不同字符的个数

     count_chars( string , mode ) : 根据不同模式返回字符串中所用字符的信息 
    
     这里选用的是模式3 , 模式3的参数为字符串 , 返回值为所有使用过的不同的字符
    
     strlen() 对这些字符个数进行限制 , 所使用的字符不能超过12个

    绕过这些检测机制并不算难 . 这个正则过滤限制的比较死 , 因此留给我们的字符并不多 . 我之前刚好研究过 无数字字母组成的WebShell , 与这里的情况类似 , 可以往这个方向去思考

    • 先看一下还保留了哪些字符

      png

      对从0 ~ 255的ASCII编码( 先别管有没有那么多 )通过刚才的正则判断 , 找到未被过滤的字符

      png

      发现仅存在很少的可打印字符 , 大部分都是不可打印的字符 .

      在可打印字符中可以找到 " ^ " 异或符号 , 而构造无字母数字WebShell的方法中就存在通过异或运算来获得不同字符的方式 , 因此这种方法或许是可行的

    • 是否剩下的所有字符都可以被我们使用?

      答案肯定是不行的 , 我们最终构造的 Payload 需要放在 URL 中执行 , 而URL 中是存在一些保留字符或者不安全字符的 .

      保留字符是指在URL中具有特定意义的字符 , 不安全字符是指在URL中没有特定意义 , 但在URL所在的上下文中可能具有特殊意义的字符

      https://blog.csdn.net/wzd2012/article/details/79077248

      这些特殊的字符或多或者都存在特殊含义 , 无法在URL中直接使用 . 就算要使用也需要使用单引号包围 ------ 但是单引号已经被正则表达式过滤掉了 .

      因此 , 我们最终的目标是使用那些不可被打印的字符 , 这样浏览器即使解码后也不会把它们识别成有特殊含义的字符

    • 我们构造的Payload究竟是怎么样的?

      现在已经知道了 Payload 需要由那些不可见字符组成 , 那么 Payload 究竟是怎样的呢?

      题目中有这么一条限制 : GET方法获取参数的值不能超过 18 个 字符 , 如果想要在 Payload 中执行一个函数 , 格式肯定 (xxx^xxx)(); 这样的 . 这还必须是无参函数 .

      但即使是无参函数 , 已经使用的字符( 小括号 , 异或符号 , ... ) 也已经使用了6个字符 , 函数名又需要两两字符异或计算得到 , 也就是函数名最多有 ((18 - 6) / 2 = 6)个字符 , 而6个字符连个 phpinfo 都运行不了 , 这样肯定是不可行的 .

      直接调用函数好像是不可行的 , 我们需要换一种思路 ------ 比如使用全局变量 , 至少" $ " 符号还是可以使用的 , 介于题目中对 Payload 长度有限制 , 最短的全局变量为 : $_GET . " $ "本身可用 , 而 _GET 的形式大概是 : {xxxx^xxxx} 这样的 .

      有了全局变量 , 就可以利用 ${} 中的代码是可以执行的特点 , 把要运行的函数名作为参数来动态执行 . 也就是${x}();这样的形式 , 函数名的值 " x " 可以通过全局方法GET来获取

      因此 , 完整的 Payload 应该为 : Payload : ${xxxx^xxxx}{x}();&x= ... , 转换后就变成了 $_GET[x]();&x= ...

      并且 , " _ " 参数的值为 : ${xxxx^xxxx}(x)(); , 长度为 18 个字符 , 恰好满足题目 strlen() 函数的限制

      根据上面的思路 , 就可以构造 FUZZ 脚本来得到 _GET 这个四个字符

      png

      当两个字符都不可以被打印时 , 就对它们进行异或运算 , 如果运算结果刚好为 "_GET" 四个字符中的某一个 , 就把它们输出出来

      png

      可以看到可用的 Payload是非常多的 , 但是它们不可以任意使用 , 因为还有一个 count_chars() 函数的限制

      该函数限制" _ "参数值最多只能使用12个不同的字符 , 而现在 ${xxxx^xxxx}{x}(); 在不考虑 " x " 的情况下已经使用了 7 个字符了 , 因此 x 最多只能使用 5 个不同的字符

      根据这个思路 , 可以在合理的情况下构造 Payload 了 , 这里以执行 phpinfo() 为例 , 构造 Payload 如下

      Payload : _=${%81%81%81%81^%de%c6%c4%d5}{%81}();&%81=phpinfo

      png

      现在可以通过该 Payload 执行 get_the_flag 函数 , 至此第一道防线已经完全突破!


  1. 第二道防线

    我们在执行 phpinfo() 函数时可以看到 , 目标站点使用的是 PHP 7.2 , 注意这个版本 ! 后面很多地方需要考虑到版本因素 .

    以拿到 WebShell 作为突破这层防线的目标

    第二道防线是以 文件上传 的几个步骤建立起来的 , 来看一看代码上是怎么实现的~

    png

    该道防线同样可以划分为三层 , 每层防御如下所示

     首先对文件后缀进行正则检查 , 如果文件后缀是以 " ph " 开头 , 则不通过检测 . 
    
     然后对文件内容进行检查 , 如果文件内容中出现 " <? " 这个部分 , 则不通过检测 .
    
     最后通过 exif_imagetype() 函数对文件类型进行检查 , 如果文件不是一张图片 , 则不通过检测 .

    这三层刚好在之前做的签到题( checkIn ) 中已经出现过了 , 因此这里思路非常明确 .

    下面来谈一谈我的解题思路吧~

    • 怎么突破这三层防御?

      通过 phpinfo() 可以得到很多信息

      因为站点使用的中间件是 Apache2 , 因此对文件后缀的检查可以通过上传 .htaccess 来绕过 .

      因为站点使用的PHP版本是 PHP 7.2 , 所以 <script language='php'> ... </script> 这种写法已无法使用 . 要想绕过 " <? " 的检测 , 必须对文件内容进行编码再上传 .

      exif_imagetype() 对文件类型的检查可以通过添加图片的文件头( 例如 GIF98a )来绕过 .

      当然上述的方法都是理论上的 , 还要结合具体题目来参考 , 来看几个关键问题 .

    • 怎么上传文件

      首先我拿到的 docker 环境无法进入上传的目录 , 我也懒得改了 , 因此我在本地创建了一个上传环境( Apache2 + PHP 7.38 ) , 也更方便研究 .

      另外我不会构造文件上传的数据包...( 我真是太菜了 ) , 因此我又写了一个上传文件的页面 , 然后通过 BurpSuite 抓包拼接 ==

      因此本地环境最终由下面两个文件组成

      png

      upload_file.php 中的内容就是本文最开始给的那段代码

    • 上传的文件是怎样的?

      结合已知的信息以及目标站点对 " ph " 后缀的限制 , 猜测可以通过上传 .htaccess 后门来解析上传的文件 .

      目标站点使用的PHP版本为 PHP 7+ , 因此 <script> 标签无法使用 . 要想绕过 " <? " 限制 , 只能对文件进行编码后再上传( 可能还有其他方法 , 但我只会这个... ) , 当然最常用方式的就是对上传文件的内容进行 BASE64 编码 .

      另外 , 在解析上传文件时肯定要对文件内容进行 BASE64 解码 , 这里如果像上一题那样直接添加 GIF89a 这个图片文件头 , 肯定会影响 BASE64 的解码结果

      因此这里最理想的方式是 : 找到一种能够同时满足 图片文件 . PHP文件 , .htaccess文件 的文件格式 . 要满足PHP文件和配置文件的格式 ,就需要添加文件的 " 不解析行 " 了( 比如注释行 )

      因此先来看一看 PHP 解析的图片类型 .

      png

      在这么多类型中 , 我选择使用 XBM 这种文件格式 . BaiduBaike 是这么解释 XBM 文件的 .

      X-Bitmap(XBM)是一种古老但通用的图像文件格式 , 它与现在的许多Web浏览器都兼容 . X-Windows图形界面(UNIX和Linux常用的GUI)的C代码库xlib中有一个组件专门描述了它的规范 .

      XBM图形的实质上是使用16进制数组来表示二进制图像的C源代码文件 .

      重点是 XBM 的文件格式 , 它的文件格式大概是下面这样的

      png

      XBM 文件头是通过两行 #define 定义的 , 而这种定义方式刚好在 php文件.htaccess文件 中代表注释~

      因此这里在上传文件时 , 可以在上传的文件前加上 XBM 文件头 . 这样既不影响 exif_imagetype() 对文件格式的检测 , 又不影像对 PHP文件 和 .htaccess文件的解析 .


理论有了 , 下面来构造需要用的 Payload .

  1. 首先拦截一个上传文件的数据包 .

    png

  2. 然后拦截请求 get_the_flag 函数的数据包.

    png

  3. 将两个数据包拼接为需要的文件上传数据包.

    png

    因为要通过POST上传文件 , 因此在拼接数据包时要把 GET 方法改为 POST 方法

    数据包返回了 ^_^1 , 这至少说明文件上传成功了

  4. 上传 .htaccess 文件.

    那么 .htaccess 文件需要哪些内容?

    首先 , 因为无法上传 php 文件 , 必须引入一种其他的文件类型( 这里使用 ppp 格式 ) , 并且让它被 PHP 解释器解析( 添加 AddType 参数 ) .

    另外 , 上传的文件需要被解码后再使用 . 可利用 php伪协议( php://filter/ )对上传的文件内容进行解码

    最后 , 如何使用上传的文件 ? 可以通过 .htaccess 文件字段 auto_prepend_file 将上传的文件添加在目录下每个文件的开头 , 这样仅需要访问当前目录下的文件就可以执行上传文件的内容 .

    因此构造如下数据包

    png

    同时我们得到了上传文件的目录

  • 上传 test.ppp 文件

    上传的 test.ppp 文件需要包含 XBM 文件头BASE64编码后的PHP代码就可以了 , 这里还有一个细节 , 后面再说 .

    构造如下数据包 , 为了便于观察 , 选择运行 phpinfo() 这个函数

    png

    png

    访问 test.ppp , 即可成功的运行 phpinfo() 这个函数 .

    png

  • 连接 WebShell

    既然可以成功执行 phpinfo() , 那就可以上传WebShell并连接它

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

    png

    对 Payload 进行 Base64 编码并修改上传数据包 , 覆盖之前的 test.ppp 文件

    png

    启动 Antsword , 连接上传的 WebShell .

    png

    成功拿到目标主机的 WebShell .

    png

    Shell 已经拿到了 , 现在题目第二道防线已经成功突破

    说一个我在上传文件时踩的坑

    在上传 test.ppp 时 , 我遇到一个问题 ------ test.ppp 没有被解析 ,这里谈一谈我的解决过程~

    png

    报错信息 : Warning: Unknown: stream filter (convert.base64-decode): invalid byte sequence in Unknown on line 0

    我在 Google 上没有找到如何解决这样的问题 , 而且几乎没有人提出这个报错 .

    之后我在修改文件时发现 , 注释( #define )中定义的 width 和 height 的数值似乎对文件解析存在影响 . 在控制height不变的情况下 , 修改 width 的值 . 返回结果如下 .

    • width < 10000
      png

    • width >= 10000 && width < 99999
      png

    • width >= 100000
      png

    同理 , 在测试 height 的参数值后发现 , 上传的 test.ppp 前两行需要满足如下规则 :

    width 值的大小为 [10000,100000)
    height 值的大小为 [1000,9999)

    当 test.ppp 文件的前两行同时满足上面的要求 , test.ppp 就可以被 php解释器解析 .

    现在我还不明白为什么会出现这样的结果 , 如果您知道 , 欢迎留言~


  1. 第三道防线

    第三道防线是一个 open_basedir , 今年有个国外大佬在 Twitter 上放出了一个 POC , 大概是这样的 .

    png

    这个 POC 的原理和 PHP 底层有关 , 我太菜了看不懂底层源码 , 因此这里先记录一下 , 后面再研究

    写个 DEMO 做个测试

    1. 因为设置了 open_basedir , 所以 test.php 仅能查看 /tmp 目录下的文件 , 而无法查看根目录 / 下的文件

      png

    2. 并且 open_basedir 不可以被覆盖设置

      png

    3. 使用大佬的 POC 后可以 Bypass Open_basedir ,查看根目录下的文件

      png

    4. 可以通过 readfile() 查看根目录下文件的内容

      png

原题中 flag 就放在根目录下 , 可以通过这个 POC 读取到~


总结

关于 easyweb 这道题的解题思路就写到这了 , 其中还是有不少内容值得再挖一挖的 , 可以再深入学习一下 !

Please follow and like us:
最后修改日期:2019年11月17日

作者

留言

您好,关于您上传文件遇到的问题我有一个想法,和您分享。
base64编码是将3字节编码为4个字节。所以要想我们的shell(PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOyA/Pg==)在decode之后能够正常执行,就要保证shell前面填充的字符数是4的倍数。例如12341234PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOyA/Pg==。!!!注意!!
这里需要是base64中包含的字符才算,即大小写拉丁字母各26个、数字10个、加号+和斜杠/。

123 4#12
34## #PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOyA/Pg==
在其中加入# 空格 换行等是不影响上面这个例子正常decode的。

回到题中的例子,
#define width 10000
#define height 1000
PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOyA/Pg==
属于base64中的字符有6+5+5+6+6+4=32个,是4的倍数,所以可以正确的base64_decode。在数字后面继续加上4的倍数个数字,也是可以正确decode的。
#define width 100006666666666666666
#define height 1000
PD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOyA/Pg==

希望可以帮助作者解答疑惑。
作者的文章写得很详细,对我帮助很大,感谢作者!

作者
epicccal 

感谢您的留言 , 这个点我明白了~

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。