前言
这是我在研究一道题目( ByteCTF 2019 Babyblog ) 时学习到的新姿势 , 之前没有接触过这个 , 因此记录一下作为参考~
当时的题目环境是这样的 , 可以成功拿到 RCE , 但是发现执行不了敏感函数 , 无法上传 WebShell . 通过 RCE 读取 phpinfo() 后发现目标主机开启了 disable_functions
以及 open_basedir
我当时到这里就做不下去了 , 虽然我知道几种绕过 open_basedir
的方法 , 但是我拿 disable_functions
一点办法也没有 , 然后在 Google 上学到了一套搭配 LD_PRELOAD
的组合拳 , 这里记录下来~
利用 LD_PRELOAD
Demo
在网上看到一个 demo , 比较有意思
id
是经常被使用到的指令 , 该命令用于显示用户的ID , 以及所属群组的ID , 这点没啥好说的 .
我们编写一个 test.c 文件 , 内容如下
该文件中定义了三个函数 , 类型为 uid_t( 这是一个linux内核基本数据类型 ) , 它们的返回值都为 0 .
然后将该文件编译为一个共享库文件
-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码( Position-Independent Code ) , 则产生的代码中 , 不再使用绝对地址 , 全部使用相对地址 , 因此代码可以被加载器加载到内存的任意位置并且都可以正确的被执行 . 这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的 .
-shared 告诉 GCC 这是一个共享库文件
再通过 export
设置环境变量
最后再次运行 id
命令 , 发现输出结果改变了
而且不仅是 id
命令 , 其他一些命令( 比如 whoami
)的输出结果也不再相同
原理
LD_PRELOAD
是 Linux 系统中的一个环境变量 , 它可以影响程序运行时的链接顺序 . 比如 , 它允许你定义在程序运行前优先加载的动态链接库 .
那么为什么会出现上面这个出现上面这种情况呢 ? 可以使用 ldd
命令来查看
ldd : 该命令用于打印程序或者库文件所依赖的共享库列表
通过这些依赖关系 , 可以看到 test.so( 我们编写的恶意共享库文件 )在其他的动态链接库加载前被加载 .
然后我们可以通过 readelf
查看 id
命令调用了哪些库函数
readelf : 该工具用来显示一个或者多个elf格式的目标文件的信息 , 可以通过它的选项来控制显示哪些信息 .
我们可以看到 id
命令加载的库函数 , 特别注意下面这三个函数
id
命令原本会调用 GLIBC
库中的这三个函数 , 但是由于我们编写的恶意库文件 test.so
在系统加载 libc.so
之前被加载 . 因此恶意代码中定义的函数覆盖了原本动态链接库中的函数 , 导致我们构造的代码可以被执行 .
也就是说 , 如果程序在运行过程中调用了某个标准的动态链接库的函数 , 那么我们就有机会通过设置 LD_PRELOAD
的值来使得程序优先加载我们自己编写的恶意代码 , 覆盖原有代码 , 从而实现劫持 .
同样 , whoami
命令也会在加载 libc.so
之前加载 test.so
, 该指令将从 test.so 中获取 geteuid 库函数 , 从而导致了程序流被劫持 .
现在 , 上面那个 demo 就非常好理解了~
利用思路
那么究竟如何突破 disable_functions 呢? 结合上面的思路 , 我们设想下面这种情况
-
利用漏洞控制Web启动新的进程
-
新的进程调用某个系统函数 func()
-
被调用的系统函数位于系统共享库 lib.so 中 , 因此系统会加载 lib.so
-
构造恶意共享库文件 evil_lib.so , 该文件中包含与 func() 同名的恶意函数 , 并且通过
LD_PRELOAD
让系统优先加载该共享库 -
根据加载文件的优先关系 , 新的进程会调用 evil_lib.so 中的恶意函数 func()
-
由于恶意函数 func() 是可控的 , 从而达到了突破
disable_functions
并执行恶意代码的目的 .
根据上面的思路 , 我们实际要做的有下面这几步
-
寻找内部启动新进程的 PHP 函数
-
查看该进程调用系统函数明细
-
操作系统环境下劫持系统函数注入代码( 简单的说就是构造恶意共享库文件 )
-
PHP 环境下劫持系统函数注入代码
寻找内部启动新进程的 PHP 函数
虽然可以利用 LD_PRELOAD
劫持系统函数 , 但前提是我们要控制 PHP启动新的进程才行( 新的进程才会加载恶意的共享库文件 )
但是由于设置了 disable_functions
, 常见的 system()
, exec()
, shell_exec()
等 PHP 函数肯定无法利用 , 我们需要其他的方式来启动新的进程 .
在 这里 学到了一种姿势 , php解释器自身就可能调用外部程序 , 启动新的进程
文章中有这么一句话
php 函数 goForward() 实现“前进”的功能,php 函数 goForward() 又由组成 php 解释器的 C 语言模块之一的 move.c 实现,C 模块 move.c 内部又通过调用外部程序 go.bin 实现,那么,我的 php 脚本中调用了函数 goForward(),势必启动外部程序 go.bin .
很容易理解 , 但是我并没有在 PHP 官方文档中找到 goForward()
这个函数( 可能是我太菜了== ) , 那么这里就不去深究了 . 总之 , 我们需要去寻找类似 "goForward()" 的真实存在的 PHP 函数 .
通常使用 strace 工具来跟踪系统中的进程调用
strace常用来跟踪进程执行时的系统调用和所接收的信号 .
在Linux世界 , 进程不能直接访问硬件设备 , 当进程需要访问硬件设备(比如读取磁盘文件 , 接收网络数据等等)时 , 必须由用户态模式切换至内核态模式 , 通过系统调用访问硬件设备 .
strace可以跟踪到一个进程产生的系统调用 , 包括参数 , 返回值 , 执行消耗的时间.
-f : 该参数用于跟踪由fork调用所产生的子进程.
一般而言 , 在 处理图片 , 请求网页 , 发送邮件 这三个场景可能启动新的进程 , 需要重点关注 .
-
处理图片
处理图片通常会调用 PHP 封装的
ImageMagick
库 , 我们新建一张图片并查看其是否启动新的进程第一个
execve
代表启动 php 解释器 , 且没有找到第二个execve
, 说明并没有启动新的进程 , 这明显不是我们想要的 . -
请求网页
请求网页时往往调用
CURL_INIT()
, 我们创建一个文件并查看其是否启动新的进程依旧没有启动新的进程 , 这也不是我们想要的
-
发送邮件
php中发送邮件一般会使用 mail() 函数 , 我们构造文件并查看其是否启动新的进程 .
mail() 内部启动了
sendmail
这个外部进程 , 这是我们需要的结果 ! 我们可以尝试通过劫持 sendmail 中调用的系统函数来执行恶意代码 .
查看该进程调用系统函数明细
现在拿到了由 mail() 函数启动的新进程 sendmail , 下面需要查看该进程中调用了哪些系统函数 , 这些系统函数是否存在被劫持的可能 .
如前面所说的 , 我们可以利用 readelf
工具来查看某程序可能调用的系统 API 明细 .
输出内容非常多 . 其实 , 在程序实际运行时 , 会根据命令行参数选项 , 运行环境的不同作出不同的反应 . 因此程序真正运行时调用的系统API可能只是上面输出内容的一个子集 . 因此一般使用 strace
来跟踪实际运行时的系统API调用 .
可以看到 sendmail 调用了非常多的系统函数 , 这么多的函数我们要劫持哪一个呢? 由于被劫持的系统函数得由我们重新实现一次,函数原型必须一致,为减少复杂性,我们会选择劫持那些无参数且常用的系统函数,比如 getuid()
等 .
操作系统环境下劫持系统函数注入代码
准备工作已经做完 , 下面开始构造恶意的共享库文件 evil.so
-
查看 getuid 的函数原型
上面提到了 , 我们要重新实现被劫持的函数 , 且函数原型必须一致 . 在 man 手册里可以看到指定函数的函数原型( 以
getuid()
为例 ) -
构造出恶意共享库文件
我们可以构造出如下的 Payload ( 这里以将一句话存入
/tmp/evil.txt
文件内为例 )然后将该文件编译为共享库文件
-
将构造好的文件放到特定位置
现在我们就得到了恶意的共享库文件 , 然后将它放入可以读取文件的目录中( 比如 Web 根目录 )就可以了
PHP 环境下劫持系统函数注入代码
现在到了最后一步 , 我们仅需要设置 LD_PRELOAD
环境变量 , 并且触发系统函数劫持就可以了 . 在 PHP 中一般使用 putenv()
函数设置系统环境变量
putenv() : 添加设置到服务器环境变量 . 环境变量仅存活于当前请求期间 , 在请求结束时环境会恢复到初始状态 .
这里要注意PHP安全模式的设置 , 可以参考这里
我们构造如下的 php 脚本
通过 putenv()
添加环境变量 , 然后通过 mail() 函数触发 sendmail 调用系统函数 , 从而完成函数劫持
触发了一堆报错 , 产生该报错的原因很简单 . 在实现被劫持函数时 , 我们设置的返回值为 " 0 " , 因此 getuid()
函数的返回值也就为 " 0 " . 但仅有 root 用户的 uid 才为 " 0 " , 而当前登录用户的 uid 为 " 1000 " , 因此在后续执行过程中凡是调用到 getuid() 函数的地方都会产生错误 .
如果你使用 root 用户执行该文件 , 就不会产生这些报错
当然这都不是重点 , 我们构造的恶意共享库文件已经在第一时间被加载完毕 , 至于后面执行的内容会产生什么错误 , 已经与本次利用无关了 . 查看当前目录下的内容 , 可以看到生成的 evil.txt
evil.txt 中保存了之前构造的语句 , 我们构造的代码已经被成功执行 !
读取根目录下的文件
举个小例子 , 比如我们想要读取根目录下的文件 , 但是服务器设置了 disable_functions
, 我们无法直接读取 . 就可以构造如下的 payload.so
经过上述的步骤 , 我们最终可以在 evil.txt
文件中读取到根目录下的内容
补充说明
下面是一些补充内容
putenv() 搭配 error_log()
如果目标站点 ban 掉了 mail()
函数该怎么办呢 ? 可以关注一下 error_log()
函数
error_log() : 发送错误信息到某个地方
重点关注最后一句话 , 当 message_type 为 " 1 " 时 , 该信息类型使用了 mail() 的同一个内置函数 , 我们构造运行脚本并查看下是哪个内置函数 .
可以看到 , 当 message_type
设置为 " 1 " 时 , error_log()
也启动了 sendmail 进程 , 下面的内容不必多言 , 过程都是相同的 .
恶意共享库文件的完善
上面我仅仅是构造了一个最基本的共享库文件 , 网上很多的恶意共享库文件都是这样构造的 .
先判断当前是否设置了 LD_PRELOAD
这个环境变量 , 若没有设置则正常返回 " 0 " , 若设置了则先撤销该变量 , 然后再执行劫持函数 .
之前就提到过 , 由于劫持了 getuid() 函数 , 因此之后凡是调用到该函数的地方都会报错 . 而我们想要的仅仅是在最开始劫持一次 , 执行完指定的代码就可以了 . 因此在劫持时先撤销 LD_PRELOAD
环境变量 , 就不会对后面的操作再产生影响 .
可以看到现在以普通用户的身份执行测试脚本 , 也不会再产生报错了 .
利用 __attribute__
修饰符
那么是不是一定要找到某个系统调用函数呢? 也不是 .
__attribute__
机制是 GNU C 的一大特色 , __attribute__
可以用于设置函数属性( Function Attribute ) , 变量属性( Variable Attribute )和类型属性( Type Attribute )
函数属性的作用为 : 帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大 .
__attribute__((constructor)) 和__attribute__((destructor)) 是函数属性中两个可选项
__attribute__((constructor)) 会使函数在 main() 函数执行前被执行
__attribute__((destructor)) 会使函数在 main() 函数退出后被执行
函数属性 __attribute__((constructor)) 和 __attribute__((destructor)) 在可执行文件或者库文件里都可以生效
根据上面的内容 , 我们可以通过 __attribute__((constructor))
修饰符构造恶意的构造函数 , 让程序执行时优先执行我们的代码 . 如下所示
这样我们仅需要将要执行的代码放入到 __attribute__((constructor))
修饰的函数中 , 就可以在不劫持系统函数的情况下执行我们的代码了 .
一些其他的绕过 disable_function 方式
其实还有很多姿势 , MeetSec 上有一系列文章对如何绕过 disable_function 讲述的非常全面 , 这里记录一下链接
大佬真是太强了啊!
总结
本章简单的总结了如何利用 LD_PRELOAD + PUTENV()
绕过 disable_functions
, 然后发现还有非常多的绕过姿势 , 学海无涯啊 !
国庆出去玩的太嗨了 , 现在得抓紧时间了 !