[【通过】] 挖掘PHP禁用函数绕过利用姿势

[复制链接]
SevenLu 发表于 2017-2-26 21:19:24 | 显示全部楼层 |阅读模式

正式成员|主题 |帖子 |积分 16

前言

     记得以前乌云还在的时候,有个哥们在zone里问php如果把dl,exec,system,passthru,popen,proc_open,pcntl_exec,shell_exec 这些函数都禁用了,应该如何执行命令。当时我看他给出的php版本很低,就直接用反序列化uaf直接调用exec的原型强行反弹了个shell。不过最后那哥们找到了一种利用LD_PRELOAD mail函数的方法进行了绕过,见原drops上的文章 《利用环境变量LD_PRELOAD来绕过php disable_function执行系统命令》。这个方法其实在08年就已经有人提出来了:php-bug 。应该还是有人记得这个哥们的,反正我是特别佩服他。其实在php中,类似的问题还是存在很多的,本文将记录一次完整的php禁用函数绕过姿势的挖掘过程。在开头就说明一下,这次这个姿势在实战环境下没什么卵用,我写这个只是为了说明php中类似问题的利用方法。

php mcrypt模块
    有一天上班公交车上看feedly,在LR师傅的博客上看到php协议流文档的翻译,然而那个翻译真的看得我蛋疼,所以我直接又去官网翻了下php://filter文档,发现php的过滤器支持Encryption Filters :filters.encryption ,其中有两个参数比较有意思:

1.png

这两个参数可以指定加密算法和模式的模块目录。但是后面的文档中并没有对这两个参数和需要实现的接口做进一步描述,之后我发现,这个加密过滤器其实是mcrypt这个扩展模块中的接口,mcrypt模块中的mcrypt_module_open函数是一个更通用的方
法:mcrypt_module_open该方法中有两个参数  algorithm_directory mode_directory 可以指定模块加载的目录,按照文档中的说法,如果不指定,则为php.ini中的默认值。看到这里的时候我觉得这个地方只要我编译一个带有加密函数接口的so库,并在该接口中插入恶意的代码,然后通过这个参数指定到这个目录,在调用加密方法的时候,我插入的代码就会被执行了。然后我这么做的时候,发现不管怎样更改directory参数,我指定的so都不会被加载,就算指定不存在的位置,也不会报错,然后我找了很多资料,发现对这个参数的具体使用,so库需要实现哪些接口,命名上的要求,完全没有任何文档说明。接下来是一些反复跳坑的过程,最后说下这个指定目录到底应该如何调用so库,以及他导致的php禁用函数绕过。

环境配置

    首先下载php源码(php版本不限制,我用的是php5.4.34)和依赖

2.png
    接下来编译安装,开始第一次跳坑:
3.png
    接下来使用这段测试代码应该就可以输出密文了:

4.png
5.png
    然后按照我们一开始的思路测试下加载指定目录的so文件,发现并无卵用,这时我花了一天的时间去google关于php mcrypt扩展的文档,对于这个指定dir的参数并没有文档说明,然后又去问了一些php大牛,他们给出的答复似乎并不能解决问题。然后被逼无奈我开始怼源码。

源码分析
    首先定位到 mcrypt_module_open 函数原型,这个函数是在 libmcrypt 中的,我建议如
果食用ctag分析的话,把libmcryptmcrypt的源文件放在一起建立索引。
          function mcrypt_module_open --> libmcrypt-2.5.8/lib/mcrypt_modules.c : 166
    他是调  mcrypt_dlopen --> libmcrypt-2.5.8/lib/mcrypt_modules.c : 128
这个函数中 141-144 行如下:
[PHP] 纯文本查看 复制代码
if (_mcrypt_search_symlist_lib(filename)!=NULL) {
handle->handle = MCRYPT_INTERNAL_HANDLER;
return handle->handle;
}
    filename 就是原来的  algorithm 也就是加密算法名,
         mcryptsearch_symlist_lib --> libmcrypt-2.5.8/lib/mcrypt_modules.c : 51
    他会在全局数组 mps 里搜索这个算法名name,全局数组 mps 在编译时由 makefile 生成到mcrypt_symb.c 中,差不多是这个形式:

[PHP] 纯文本查看 复制代码
const mcrypt_preloaded mps[] = {
{"cbc", NULL},
{"cbc_LTX__init_mcrypt", cbc_LTX__init_mcrypt},
{"cbc_LTX__mcrypt_set_state", cbc......
...
{"rijndael-128", NULL},
{"rijndael_128_LTX__mcrypt_....
....
}
    也就是说这个name在这个数组中出现的话,就会让  mcrypt_dlopen 直接返回MCRYPT_INTERNAL_HANDLER ,MCRYPT_INTERNAL_HANDLER   (void *)-1 ,没啥实际意义,就是个flag,返回这个值会导致调用 mcryptsearch_symlist_sym --> libmcrypt-2.5.8/lib/mcrypt_modules.c : 65,该函数会直接返回 mps 中的算法的地址,所以根本不会我指定的dir位置加载,会直接返回系统libmcrypt.so中的算法进行调用。所以我觉得,算法名必须要不同于库中给出的标准算法名才可以,然后我把算法名和so的名字更改之后,仍然没有成功,他会直接返回找不到加密模块的错误。然后我又看了两个多小时源码,最后没办法,静态分析弄的头都大了,直接上gdb调
6.png
    执行到181行的时候s进到mcrypt_dlopen函数里,141 行是上面我们提到的那个判断,执行到这里的时候我们看下执行结果:
7.png
    那个算法名我已经修改了,删了一个'i',返回是NULL,如果是原来的算法名的话,返回是0xffffffff,所以这个分支会跳过。继续往下执行可以看下paths:
8.png
    按照程序逻辑来说是没有问题的,继续往下执行,发现一个很奇怪的问题,157行的函数直接会被跳过,而159行的  lt_dlopenext(filename); 返回是0,导致返回的句柄是NULL:
9.png
    我当时觉得问题就在这个函数里了,就跟进去调,发现这个函数怎么都s不进去,并且无法在 lt_dlsetsearchpath 和 lt_dlopenext 这两个函数上下断点,报错找不到symbol,这个问题困扰了我好久,之后我就继续读源码,发现libdefs.h中有这样一段代码:
[C++] 纯文本查看 复制代码
#ifdef USE_LTDL
# include <ltdl.h>
#else
# define lt_dlexit() 0
# define lt_dlinit() 0
# define lt_dlclose(x) 0
# define lt_dlsym(x,y) 0
# define lt_dlhandle void*
# define lt_ptr_t void*
# define lt_dlerror() 0
# define lt_dlopenext(x) 0
# define lt_dlsetsearchpath(x) 0
#endif
    如果没有定义  USE_LTDL 这个宏的话,那么这两个函数会直接变为两个返回0的宏定义,我们重新编译下libmcrypt 加上CFLAGS参数,让他把宏也编译到gdb调试信息中去:
10.png
    下断点到155行看一下宏:
11.png
    果然此处是个宏,那么我们只要找到指定USE_LTDL宏的选项就好了,configure.in 中104行:
12.png
    此处定义了USE_LTDL,发现这是个分支,进入条件是:
13.png
    所以只要指定 opt_enable_ltdl的操作在75行:
14.png
    所以,我们要指定  --enable-dynamic-loading 这个选项,这样才能开启USE_LTDL,也就是动态加载。再编译一次:
15.png
    之后我并没有往后看他需要调用的接口,我直接把libmcrypt中的所有  rijndael-256都替换成了  rjndael-256包括文件名和文件内容,要注意一些隐藏文件夹),然后修改 modules/algorithms/rjndael-256.c ,添加头文件:
[C++] 纯文本查看 复制代码
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
    在 mcryptencrypt 函数的定义部分结束后添加一行:
16.png
    将 modules/algorithms/.libs/rjndael-256.so 拷贝到目标文件夹,再次测试,发现还是错误,再跟进去调一下,还是原来的位置下断点,我们发现,lt_dlopenext仍然返回NULL,但是这次我们可以s进去了,简单读一下代码发现他首先会提取后缀名,如果要执行lt_dlopen函数,则需要满足条件:
[PHP] 纯文本查看 复制代码
if (ext && ((strcmp (ext, archive_ext) == 0)
#ifdef LTDL_SHLIB_EXT
|| (strcmp (ext, shlib_ext) == 0)
#endif
))
{
return lt_dlopen (filename);
}
    否则,按照后缀默认是archive_ext进行之后的操作,输出下archive_ext:
17.png
看下LTDL_SHLIB_EXT:
18.png
    未定义,其实这个LTDL_SHLIB_EXT应该就是so,不过需要你手动安装ltdl库才有这定义,但是就算该宏定义了,la文件也是必须的,因为在后面的操作中,需要操作
handle->info.name这个值来调用so文件,而这个值需要从la文件的 dlname='rjndael-
256.so' 中得到,所以在目标文件夹中需要la和so文件,那么我们将其全复制过来:
19.png
    改下源代码:
20.png
    搞定。


结束
    最后得到的应用场景与一开始想的有很大差异,限制很多,变得了没啥用的鸡肋姿势,不过我觉得这个分析的过程还是有一些收获的,所以就随便写点纪录下来。这说明动态加载在php中是风险很高的一个选项,此处可能鸡肋,但是不排除存在其他可以加载la和so的函数接口,就算找不到文档,硬怼源码也是可以搞定的。更多的利用姿势等待大家挖掘,有更屌的姿势环境发邮件与我探讨。








评分

参与人数 1酒票 +5 收起 理由
管理05 + 5 欢迎加入90!

查看全部评分

 楼主 SevenLu 发表于 2017-2-26 21:19:59 | 显示全部楼层

正式成员|主题 |帖子 |积分 16

我上传不了图片和附件。。也不知道为啥,像是编码问题,有乱码。。。。

点评

浏览器问题。  发表于 2017-2-27 12:13
管理05 发表于 2017-2-27 13:01:53 | 显示全部楼层

管理|主题 |帖子 |积分 936

我已经给你编辑了
孤城浪子 发表于 2017-2-28 10:23:45 | 显示全部楼层

正式成员|主题 |帖子 |积分 162

完全看不懂啊,直接搞源码的大牛
小透明 发表于 2017-2-28 13:27:10 | 显示全部楼层

正式成员|主题 |帖子 |积分 153

wooyun的文章我好像看过,不过没有你思考的那么多
Yaseng 发表于 2017-3-2 14:45:48 | 显示全部楼层

90Sec Team|主题 |帖子 |积分 177

不是默认的话就鸡肋了   
ab10012358 发表于 2017-3-2 20:23:00 | 显示全部楼层

正式成员|主题 |帖子 |积分 48

最近也在学php,离审计还有很长的路要走啊
快速回复 返回顶部 返回列表