x01 rand 缺陷导致密钥泄露目标: 0dac0a717c3cf340e:82/index.php 随便写点东西,抓包,发现 html 源码里有个?x_show_source:
于是访问 0dac0a717c3cf340e/index.php?x_show_source ,找到源码。
分析一下,发现这里每个新的 session 会生成两个随机字符串,SECRET_KEY 和CSRF_TOKEN。其中 CSRF_TOKEN 是防御 CSRF 的 token,会直接显示在表单中;而SECRET_KEY 是类似密钥的东西,在后面需要利用这个密钥给数据签名。 但密钥是不知道的,这就是本题第一个难点,如何得知密钥。我们看到随机字符串生成函数rand_str:
可见,这里用的是 rand 函数生成的随机数。在 linux 下,PHP 的 rand 函数是调用 glibc 库中的 rand 函数,其实现是有缺陷的。可见这篇文章: /2016/02/11/cracking-php-rand/ 其提到一个公式:
也就是说,rand 生成的第 i 个随机数,等于 i-3 个随机数加 i-31 个随机数的和。 所以,我们只要生成大于 32 个随机数,就可以陆续推测出后面的随机数是多少了。我们看到代码:
当一个新请求来到时,index.php 会先生成 6 个随机数组成的字符串作为 SECRET_KEY,再生成 16 个随机数组成的字符串 CSRF_TOKEN,而且 CSRF_TOKEN 是已知的。那么一次请求最多生成 22 个随机数,是不到 31 的,所以并不能使用上面的公式。 我们知道 HTTP1.1 协议支持 Keep-Alive,也就是说一个 TCP 连接支持收发多个 HTTP 数据包,只要 TCP 连接不断那么这个随机数生成就是连续的。所以我只需要发送两个带有 Keep-Alive的数据包即可拿到一共 44 个随机数。
这 44 个随机数大概是这样的:
然后我们再次发送不带 session 的数据包,则再次生成『6 未知+16 已知』,这时『6 未知』就可以推测了。根据公式,a[45] = a[14] + a[42],而 a[14]和 a[42]正好是已知的;根据公式,a[50] = a[19] + a[47],而 a[14]和 a[42]也是已知的。
所以,我们是可以推算出 a[45]~a[50]这 6 个随机数的,进而推算出此时的 SECRET_KEY。
当然,实际操作时会有一定误差,一般是推算出来的值比真实值小 1。那么,我们一共推算 6个随机数,可能的情况就是:
做一个笛卡尔乘积,一共得到如下一些情况:
依次试一遍就好了。
0x02 PHP 鸡肋任意代码执行依次测试上述推测出的 SECRET_KEY,当页面返回值不再提示 Permission deny!!时,说明预测准确。此时我们拿到了 SECRET_KEY,即可计算 hmac,实际上计算 hmac 是为了控制$act,$act 是后面 PHP 执行的函数:
$act(),这里等于说存在一个『任意代码执行』漏洞。但这个漏洞比较鸡肋,虽然可以执行任意函数,但因为没有传入参数,所以导致执行诸如 assert、system 之类的函数是没用的,会报错:
那么,我们只能利用 php 里一些不含参数的函数。php 里有几个 get 开头的函数,其效果还是蛮强的:
主要有以下一些:
其中,第 1~4 个方法十分致命。一般一个网站加密密钥、数据库配置信息多半存在常量或全局变量中,通过第 2、3 个方法即可全部获取,而通过第 1、4 个方法可以大致获取网站结构,了解函数状况。 这里,我们通过调用 get_defined_functions,即可获得一个包含所有已经定义的函数的数组。不过,我们需要设置 HTTP 头:
因为我们要获取的是数组,数组直接输出是会被强制转换成字符串的。所以我将 X-REQUESTED-WITH 设置为 XMLHttpRequest,即可让输出结果转换成 json,这样数组就被保留了:
输出所有函数,我发现用户函数中有几个函数在源码中没看到:
分别执行一下,发现 fd_show_source 是读取源码:
0x03 提权+任意文件读取漏洞整理一下这个源码,发现主要逻辑在 fg_safebox 函数中,观察一下:
先调用了_fd_init()。然后检查用户 session[role]是否是 admin 或 user,并检查用户是否有权限执行某函数。 先看看_fd_init:
实际上是从 cookie 中取出信息并用 json_decode 解码后作为 session,我们的目标是控制$_SESSION['userinfo']['role']。有三个地方注意一下就好了:
1、cookie 中取出的信息先进行签名认证,但因为密钥 SECRET_KEY 已经拿到了,所以不成问题 2、admin 和 user 这两个字符串不能出现在 json 中,我们可以利用 unicode 编码,比如{role: \u0075ser} 3、role 的值不能为 admin 主要是第三个问题,role 的值不能是 admin,那么执行不了 read 方法:
而 read 方法很明显是有任意文件读取漏洞的,所以现在做的是提权。 我们执行 fd_config()函数,可以得到权限分配的数组:
可以看到,admin 对应的方法有 read,而 user 对应的方法有 view、alist、random,在flag.php 的 97 行对权限进行检查:
当$action 在$config['role']['admin']数组中时,如果你的 role 又不是 admin,则提示权限错误。
其实这里又涉及到 php 的大小写敏感问题,php 语言的方法名、类名、函数名是大小写不敏感的,也就是说平时执行 phpinfo()可以读取 php 信息,执行 PhPInfO()效果也是一样的。
所以,我只需要传入的$action 为 READ 等包含大写字母即可绕过 in_array 的限制,而最后仍然可以执行 read 方法。
执行 read 方法后即可读取任意文件,按常规渗透方式读取一些常见文件
在/etc/apache2/httpd.conf 的最后几行发现 flag:
0x04 编写脚本 这个题其实难度并不大,但复杂,十分复杂,几乎不可能通过手工拿到 flag,必须要写脚本。 首先,我要先写一个获取 SECRET_KEY 的脚本,就是我在 0x01 中说到的,利用 rand 函数缺陷预测 SECRET_KEY,并通过笛卡尔乘积生成可能的情况,一一测试,最终找到正确的SECRET_KEY。 给出我的脚本:
有几点要注意的: 1、CSRF_TOKEN 每次使用完就会销毁,所以每次发送 POST 请求之前都需要获取一个CSRF_TOKEN 2、为了保证 Keep-Alive,使用 requests 库的 session 类来维持会话 3、为了生成 44 个随机数,需要发送两次数据包,发送数据包前需要更换 sessionid,否则第二次不会再生成新的随机数。我的做法是发送前自己生成随机字符串作为sessionid 4、笛卡尔积可以用 python 的 itertools.product 方法 5、最终获取准确的 secret_key 后,要输出这个 secret_key,同时还要输出当前sessionid,后续操作均需要带着这个 sessionid 这个脚本有一定的失败率,具体为什么不细讲了,多试几次肯定 Ok 就是了:
将刚才获取的 secret 和 sessionid 填入脚本,执行即可读取../../etc/passwd 文件。我们可以在 sys.argv[1]传入想执行的函数,比如
当然,最终我们要执行的是 fg_safebox,在 post 包中设置 method=reaD,filename 是想读的文件,cookie 中配置好 role=user 的 json 字符串,执行即可:
本文为网络安全技术研究记录,文中技术研究环境为本地搭建或经过目标主体授权测试研究,内容已去除关键敏感信息和代码,以防止被恶意利用。文章内提及的漏洞均已修复,在挖掘、提交相关漏洞的过程中,应严格遵守相关法律法规。