本文发表于跳跳糖,转载请联系对方。
这周三在『代码审计知识星球』中发了一段代码,用户可以控制环境变量,但后面没有太多可控的地方,最后找到了一处执行命令,不过命令用户也不可控。用PHP来演示一下就是下面这7行:
代码语言:javascript复制<?php
foreach($_REQUEST['envs'] as $key => $val) {
putenv("{$key}={$val}");
}
//... 一些其他代码
system('echo hello');
?>
请问这段代码如何利用,是否可以getshell?
0x01 LD_PRELOAD
之后的思考
在有上传点(无需控制文件名)的情况下,这段代码其实比较简单了,可以直接用LD_PRELOAD
搞定。上传一个文件名不限的so文件,如hj.jpg
,可以通过LD_PRELOAD=/var/www/html/uploads/hj.jpg
这样的方法劫持并执行任意代码。
但我这里并没有给上传接口,如何解决这个问题呢?这就是本文研究的课题。
打开PHP的底层源码,看下PHP的system函数实际上在做什么。
代码语言:javascript复制#define VCWD_POPEN(command, type) popen(command, type)
// ...
PHPAPI int php_exec(int type, char *cmd, zval *array, zval *return_value)
{
FILE *fp;
// ...
#ifdef PHP_WIN32
fp = VCWD_POPEN(cmd, "rb");
#else
fp = VCWD_POPEN(cmd, "r");
#endif
if (!fp) {
php_error_docref(NULL, E_WARNING, "Unable to fork [%s]", cmd);
goto err;
}
// ...
可见,PHP的system调用的是系统的popen()
。我们再深入一层,看看popen究竟在做什么。
0x02 寻找系统层源码的方法
在此之前,先分享一下我们如何找到一些Linux中自带工具、库的源码。
理论上因为Linux是开源的,所以所有源码都可以拿到。这里介绍三种方法,我们以“echo”这个命令为例。
方法一、在系统源里查找源码
这种方法是相对比较精确的,比如,我们要复现的目标环境是Ubuntu,那么我们就在Ubuntu的apt源里找相关代码。整体过程如下:
我之前在星球介绍过command-not-found,这个网站可以查询到一个命令在各种操作系统中的包名。比如,echo所在的软件包是coreutils:
然后来到Ubuntu Packages里搜索coreutils,找到它的详情页面,右侧就有源码包的下载地址:
下载其中orig的那个文件即可。
方法二、在GNU.ORG下载源码
方法二和方法一略有不同的是,方法二在获取包名后,去GNU官网上找源码,而不是去具体发行版的源里。
还是以echo为例,获取到echo的包名coreutils后,在GNU网站上就可以找到coreutils的详情页面:https://www.gnu.org/software/coreutils/
其中不但给了这个包的介绍、下载地址,还有它的Git仓库,通过Git能获取到更详细的历史代码,这是这个方法的优点:
直接下载源码包或者拉取git仓库即可。
方法三、直接用过apt命令下载源码
上面两种方法获取的源码都可能和线上环境有一些差异,原因我曾在《谈一谈Linux与suid提权》简单介绍过。Ubuntu、Debian这样的Linux发行版,通常会自行给自己仓库里的程序打补丁,而我们前两个方法中下载的源码包都是没打补丁的原始包,这可能会导致我们研究的东西和线上环境存在差异。
第三种方法是第一种方法的命令行版,它的优点就是可以解决“补丁”的问题。
仍然以Ubuntu为例,使用这个方法前需要先配置好apt源,需要有deb-src类型的源。如果你在国内,可以直接使用清华的Ubuntu源,将其中deb-src开头的注释符去掉即可。
然后,我们直接执行apt source [package_name]
即可在当前目录下获得这个软件的源码,并应用所有的补丁包:
上图中,最后生成了4个文件(目录),他们分别是:
- coreutils的源码,已经打好所有补丁
- orig.tar.xz压缩包,其中包含的是原始代码
- debian.tar.xz压缩包,其中包含的是所有的补丁文件
- dsc文件,里面包含的是这个软件的描述和元信息,dsc是description的缩写
这个方法获取到的源码应该是与Ubuntu软件编译时的源码相同,属于最佳方法。其缺点就是源码版本较新,当你想测试存在漏洞的老版本,就不能使用这个方法了;另外如果你手头没有Linux系统,自然也没法使用这个方法。
回到本文研究的popen,我们知道这个函数是Linux glibc提供的一个函数,那么我就去找了glibc的源码。使用方法一,我们很容易找到了下载地址:http://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.31.orig.tar.xz
下载找到popen的代码,跟进会发现,实际上popen最终执行的是这个spawn_process
函数:
static bool
spawn_process (posix_spawn_file_actions_t *fa, FILE *fp, const char *command,
int do_cloexec, int pipe_fds[2], int parent_end, int child_end,
int child_pipe_fd)
{
//...
if (__posix_spawn (&((_IO_proc_file *) fp)->pid, _PATH_BSHELL, fa, 0,
(char *const[]){ (char*) "sh", (char*) "-c",
(char *) command, NULL }, __environ) != 0)
return false;
//...
return true;
}
从第9行代码可看出,最终执行的是命令sh -c "echo hello"
。
0x03 调试dash,找到可利用的环境变量
那么,现在我们的问题变成了:我可以控制执行**sh -c "echo hello"
**时的环境变量,是否可以getshell?
sh -c "echo hello"
虽然是一条命令,但是实际上它执行了两个二进制文件:
- sh
- echo
其中,sh通常只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash。
由于我们目标是Ubuntu,属于debian系,所以我们来研究下echo和dash两个程序是否可利用。
先挑简单的,上面我说了如何找到echo的源码(即coreutils包的源码)。echo的源码不长,总共就200多行,其中只有一个和环境变量相关的操作:
代码语言:javascript复制 bool allow_options =
(! getenv ("POSIXLY_CORRECT")
|| (! DEFAULT_ECHO_TO_XPG && 1 < argc && STREQ (argv[1], "-n")));
但这是个bool类型的变量,并没有利用价值。
接着关注点来到dash。按照前面的方法下载到dash的源码进行阅读,其main函数中有一段引起了我的注意:
代码语言:javascript复制if ((shinit = lookupvar("ENV")) != NULL && *shinit != '