内容纲要

前言

这题拖延了快半个月了 , 现在才彻底搞明白 . 刚做的时候虽然能拿到 RCE , 但 Bypass Open_basedir 不会 , Bypass disable_functions 也不会 , 总不能照抄大佬们的 Payload 吧 , 那样还有啥意义~

搞懂之后准备记录下过程 , 其中的坑有点多 , 看各个师傅做的如此轻松 , 我真是太菜了 !


环境准备

环境使用的是 glzjin/bytectf_2019_babyblog , 来源为 Github . 咱们 Clone 一下~

然后别急着 docker-compose , 题目环境有点问题 , 需要修改一下~

打开 file/html/register.php 文件 , 看第 10 行

可看出是一个判断验证码是否正确的过程 , 从 POST 方法取到 verify 参数的值 , 然后对其 MD5 校验后再取其前5位 , 最后判断是否与 Sessionverify 参数的值相同 .

那么 $_SESSION['verify'] 的值是在哪里获取的呢 ? 看该文件第 38 行 .

使用 Mersenne Twister 算法返回随机整数 , 然后对其 MD5 校验并取其前 5 位 , 作为 $_SESSION['verify'] 的值

那么 POST 方法传递的参数值在什么呢 ? 可以看 /template/register.html 文件第 14 行

POST 方法传递的实际上是用户输入的 $_SESSION['verify'] 的值 .

那么问题就来了 , 我们从用户那里获取到 $_SESSION['verify'] 的值( 假设用户输入正确 ) , 再对它进行 MD5 校验并截取前 5 位 , 肯定不会和 $_SESSION['verify'] 本身相同啊 .

也就是说 , substr(md5($_SESSION['verify']) , 0 , 5) 肯定不等于 $_SESSION['verify'] , 因此在注册时无论你输入怎样的验证码 , 都不会通过验证 .

总的来说 , 这里需要修改 file/html/register.php 文件第 10 行为如下

这样才能确保当用户输入正确验证码时 , 可以注册成功 . 现在 , 你可以 docker-compose 了~


Babyblog

来看一看题目吧~

  1. login.php

    URL 键入 127.0.0.1:8302 , 即可看到登录页面 .

    可以登录或者注册 , 但现在咱没帐号 , 因此前往注册页面

  2. Register.php

    输入用户名 , 密码 以及 验证码 即可注册成功

    若之前没有修改 register.php , 则现在会一直显示验证码错误

  3. index.php

    主页可以看到最近发布的帖子 , 并且可以修改或者删除已发布的帖子 . 同时 Writing 选项卡可以发布新的帖子 . 其他的选项卡都没啥用 .

  4. edit.php

    编辑帖子的页面 , 其中有个 replace 功能 , 但是需要 VIP 用户才可以使用 .

  5. writing.php

    没啥好说的 , 正常的发布页面 .

仅看已有的页面 , 并没有发现哪里有明显的问题 , 实际上当我通过 BurpSuite 跑了一遍整个站点后 , 依旧没有找到有问题的地方 . 因此 , 必须要从其他地方入手 , 看一下是否存在什么其他线索 .


思路

dirsearch 拿到站点后台源码

一般遇到这种没有头绪的题目 , 拿 dirsearch 爆破一遍后台路径肯定是没错的 .

然后发现了 www 目录的压缩包 , 猜测里面有后台源码 , 下载到本地并且解压 .

然后就拿到了源码 . 准备审计 .


审计源码发现二次注入漏洞

简单的看下拿到个各个 PHP 文件

  1. config.php

    包含 与数据库连接 和 一个全局使用的安全过滤函数 , 这个安全过滤函数看得头疼 , 后面用到再说吧 .

  2. about.php

    判断当前用户的登录状态

  3. delete.php

    用于用户删除自己的帖子 , 包含两条数据库操作语句 , 但每条 SQL 语句的 id 参数都有 intval() 函数做类型强制转换 , 看不到利用点 .

  4. edit.php

    用于用户编辑自己已发布的帖子 , 包含三条数据库操作语句 , 但是 SQL 语句中的 $_POST['title'] , $_POST['$content'] 参数有 addslashes() 函数做敏感字符转义 , $_POST['id'] 参数有 intval() 函数做类型强制转换 , 这些都看不到利用点 .

    $row['title'] 参数是直接从 SELECT 语句返回数据中取出的 , 没有进行任何过滤与转换 , 就直接插入到 UPDATE 语句中了 , 这里可能存在二次注入漏洞 .

  5. index.php

    输出当前用户的所有文章 , 包含一条数据库操作语句 , 但是参数是从 Session 中直接获取 , 而非用户可控 . 看不到利用点 .

  6. login.php

    用于用户登录 , 包含一条数据库操作语句 , 但 $_POST['username'] 参数有 addslashes() 函数做敏感字符转义 , 看不到利用点 .

  7. logout.php

    销毁 Session , 用户等出 , 没有啥好说的 .

  8. register.php

    用于用户注册 , 包含三条数据库操作语句 , 但实际上只与 $_POST['username']$_POST['password'] 两个参数有关 , 但 $_POST['username'] 参数有 addslashes() 函数做敏感字符转义 , $_POST['password'] 参数有 md5() 做加密校验 , 看不到利用点 .

  9. replace.php

    这是 VIP 用户才能使用的功能 . 包含四条数据库操作语句 , 但是所用的参数都不可以利用 . 看似没有利用点 .

    但该页面存在 preg_replace 函数 , 该函数的 /e 任意代码执行的安全问题在测试时经常被使用 . 虽然现在还不知道 PHP 版本 , 但放在这就很可能存在利用点 .

  10. writing.php

    用于用户发表新帖子 , 包含一条数据库操作语句 , 但两个字符串参数都有 addslashes() 函数做敏感字符转义 , 看不到利用点 .

看完了所有页面 , 总结一下 , 现在我们有两个可能的利用点 , 一个是 edit.php 处的二次注入 , 一个是 replace.php 处的 preg_replace /e 任意代码执行 .

但想要利用 preg_replace , 当前用户就必须是 VIP 用户 , 但是在 register.php 中可以明确看到注册的用户默认都不是 VIP

因此我们必须拿到已存在的 VIP 用户或者将自己提升为 VIP 用户 , 这就要用到 SQL 注入了 , 也就是 edit.php 页面上的二次注入漏洞 .

这里从数据库取出 $row['title'] , 没有做任何过滤 , 就直接插入到 UPDATE 语句中 . 这样会出现什么问题呢 ? 看下面的 demo

经过 addslashes() 函数的敏感字符转义 , 存入数据库的 title 值为 'a , 取出数据库的 title 值也为 'a , 这看似没啥问题 . 但是如果我们直接将 'a 再次放入到 SQL 语句中时 , " ' " 是不会被转义的 , 这很可能引发 SQL 注入 .


注出数据库相关信息

现在思路有了

  1. writing.php 页面注入包含 Payload 的恶意 title

  2. edit.php 页面利用 SELECT 语句读取恶意的 title 值 , 然后直接将它插入到 UPDATE 语句中 .

  3. 因为取出的 $row['title'] 是放在 WHERE 语句中做条件判断的 , 因此我们可以利用如下性质 .

    该性质可以用于布尔注入 , 当我们指定的条件为 " 1 " 时 , 可以 UPDATE 成功 , 若条件为 " 0 " , 则不能 UPDATE 成功

  4. 请求 edit.php 页面 , 查看之前是否 UPDATE 成功

    若页面中的 title 值为恶意的 title 值 , 即 UPDATE 语句没有成功执行 , 则我们指定的条件是错误的 , 若页面中 title 值变为了 UPDATE 语句中的 title 值 , 则代表 UPDATE 语句成功执行 , 我们指定的条件是正确的 .

有了思路就可以构造脚本了 , 但是之前 config.php 中将 andor 都过滤了 , 我们如何拼接 Payload 到 WHERE 语句中呢 ?

可以利用 Mysql 中的异或运算符 , 拼接 WHERE 语句的条件 .

Payload 中仅需要有一个条件判断就可以了 , 比如 ASCII(SUBSTR((...),1,1)) > x 这样的经典格式 . 完整的 Payload 如下所示

Payload : 1'^(ascii(substr((select(( ... ),1,1))>1)^'1

构造一个 Python 脚本 , 包含上述的整个流程 .( 我写的脚本很乱 , 在先知社区上看到 W&M 的 WriteUp 中构造的脚本 , 思路非常清晰 . 这里就是修改自他们的脚本 )

然后我们需要写个循环 , 不断调用 http_get() 函数来判断何时 Payload 为 " 1 " . 这里使用二分法 , 以便快速找到边界值 .

然后是一些基本信息 , 比如 username , cookie 之类的~

接着需要调用二分法函数 half() , 并且指定最终的 Payload 的值 . 比如我们想要拿到 当前数据库的版本信息 , 就可以构造如下函数 .

最后在主函数中调用 get_version() 函数就可以了

执行该脚本 , 即可拿到当前数据库版本信息

不用管数据库的版本信息是否完全输出 , 这不是重点 , 我们仅需要判断脚本是否构造正确就可以了~ 我们可以利用该脚本拿到很多信息

  • 读取当前数据库

    Payload : select database()

  • 读取 babyblog 库中所有的表

    Payload : select(group_concat(table_name)) from information_schema.tables where table_schema = 'babyblog'

    注意 , 这里 SELECT (...) FROM (...)config.php 中被过滤了 , 但我们可以利用 SELECT(...) FROM (...) 绕过限制 .

  • 读取 user 表中的所有字段

    Payload : select(group_concat(column_name)) from information_schema.columns where table_name = 'users' and table_schema = 'babyblog'

  • 读取 babyblog.user 表中的所有信息

    Payload : select(group_concat(id,0x3a,username,0x3a,isvip)) from(babyblog.users)

    password 是一串 MD5 , 就不输出了

    可以看到当前数据库里只有一个用户 , 即为刚才注册的用户 , 并且 isvip 字段值为 0 .


提升当前用户为 VIP

我们拿到了足够的信息 , 而且现在的目标很明确 , 即通过 SQL 注入将当前用户提升为 VIP 用户

虽然 UPDATE 相关语句在 config.php 中都被过滤了 , 无法直接构造 Payload , 但依旧可以利用 堆叠注入 + 预处理语句 + Hex编码 绕过正则限制 .

代码不难 , 构造好的函数如下所示

执行后即可提升 epicccal 用户为 VIP 用户

当前用户已经被提升为 VIP 用户 .


Preg_replace /e 任意代码执行

现在我们是 VIP 用户了 , 就可以进入 replace.php 页面并利用 Preg_replace 函数了

replace.php 函数可以看到 , 只有当开启了 regex 功能后 , 才能利用 preg_replace . 现在可以掏出 BurpSuite

利用该函数的方法就不多说了 , 老问题了 . Payload 如下 .

Payload : find=.*/e%00&replace=phpinfo();&regex=1&id=xxxx

然后跳转页面 , 会发现 phpinfo() 被执行了 .

%00 截断是一个比较老的字符串截断方式了 , 需要满足 1 : PHP 版本小于 5.3.4 , 2 : PHP 没有开启 magic_quotes_gpc()

既然能执行代码 , 那肯定先搞个 WebShell 上去 .

Payload : file_put_contents('/var/www/html/webshell.php','<?php eval($_POST[cmd]);?>');

然后问题就来了 , 当你想要遍历目录时 , 会发现 AntSword 提示 : Path Not Or Not Permission , 当你用终端输入任何命令时会显示 : ret 127 .

这是为啥呢 ? 回到 phpinfo() 界面 , 会发现目标主机同时开启了 open_basedirdisable_functions

因此 , 接下来的任务就是 Bypass Open_basedirBypass Disable_functions !


Glob:// Bypass Open_basedir

因为当前版本为 PHP 5.3+ , 所以可以利用 glob:// 伪协议来 Bypass Open_basedir

这里的内容可以参考 浅谈几种Bypass open_basedir的方法 .

 glob:// 伪协议用于查找匹配的路径文件格式 , 它是 PHP 5.3.0 开始生效的一个用于筛选目录的伪协议 .

写个 demo

仅使用 glob:// 伪协议是无法 Bypass Open_basedir 的 , 必须配合其它函数 .

  1. DirectoryIterator + glob://

    DirectoryIterator 是 PHP5 中增加的一个类 , 为用户提供一个简单的查看目录的接口 .

    利用 DirectoryIterator + glob:// 的组合可以无视 Open_basedir , 直接读取某个目录下的文件 .

  2. opendir() + readdir() + glob://

    opendir() 函数可以打开目录句柄 , readdir() 函数可以从目录句柄中读取条目 .

    利用这两个函数搭配 glob:// 也可以无视 Open_basedir , 直接读取某个目录下的文件 .

利用 glob:// 的方法应该是目前最方便的方法 , 它不用去暴力猜解目录 , 而是直接列举文件 , 非常高效 .

当然也存在其他的 Bypass Open_basedir 的方法 , 读者可以自行学习~


LD_PRELOAD + error_log() Bypass disable_functions

具体的原理我已经写在这篇文章里了 : Bypass disable_functions

直接构造恶意脚本

然后将恶意脚本上传到目标服务器上

接着在 preg_replace 的 RCE 处通过 putenv() 设置环境变量并通过 error_log() 函数触发恶意脚本 .

Payload : eval('putenv("LD_PRELOAD=/var/www/html/payload.so");error_log("",1,"","");');

注意 , 这里 putenv()error_log() 必须写在一个 eval() 函数中才能生效

此时在目标服务器的 Web 根目录下会生成 result.php , 其中包含读取到的 Flag .

成功读取到 Flag !


总结

这道题现在看起来不难了 , 我从中学到了很多东西 , 还有部分细节问题值得再研究一下~


poc.py

文章中使用的 poc.py 完整脚本的链接如下 :

bytectf2019-babyblog-poc.py

最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

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