[<prev] [next>] [day] [month] [year] [list]
Message-ID: <20050901055602.5DCF.PHP@ter.dk>
Date: Thu Sep 1 11:35:04 2005
From: php at ter.dk (Peter Brodersen)
Subject: PHP glob() filename disclosure vulnerability
under safe_mode and open_basedir restriction
Hi,
This post has been posted to bugtraq (BID 12701) earlier without
disclosure of content.
Anyhow:
---
SUMMARY:
PHP native function glob() discloses pathnames in error messages on
system out of open_basedir- and safe_mode-limits. All known stable
versions of PHP are vulnerable (PHP4, PHP5).
Furthermore, safe_mode and open_basedir checks are only performed on
first file in glob() result instead of all files matched.
Information has been posted to PHP security contact and php.internals,
without any response (PHP-bug #28932 - dismissed as "Bogus" without
addressing the exact issue).
The error messages can be utilized by a user in a shared host
environment to retrieve complete directory lists of the system fairly
easy. Link to code example follows.
Example: http://basedir.ter.dk/globeater.php
Example with debug: http://basedir.ter.dk/globeater.php?debug=1
Source code: http://basedir.ter.dk/globeater.phps
For safe_mode and open_basedir, glob() only performs a single UID and
path check on first file matched. One might fetch a file list specifying
a file at first with same UID as the script inside the open_basedir path ,
e.g.: print_r(glob("{.,/tmp}/*",GLOB_BRACE));
Example: http://basedir.ter.dk/globall.php
---
BASICS:
The web scripting language PHP offers several solutions to limit the
usual problems in a shared host environment.
At a global level, methods such as safe_mode- and
open_basedir-restrictions prevent users from accessing directories/files
with other UID than the current script executed (safe_mode-restriction),
and accessing directories/files not under the specified location
(open_basedir-restriction).
The manual states at http://www.php.net/manual/en/features.safe-mode.php
"The PHP safe mode is an attempt to solve the shared-server security
problem. It is architecturally incorrect to try to solve this problem at
the PHP level, but since the alternatives at the web server and OS
levels aren't very realistic, many people, especially ISP's, use safe
mode for now." safe_mode isn't safe as such, but native functions should
be implemented following safe_mode policies.
At a per-host-level, individual session.save_path for each virtual host
prevents session sharing across different virtual host. Without this
setting, a user can read and manipulate data in a session.
When a function access a file or directory out of it limits (i.e.
restricted by safe_mode and/or open_basedir), a warning is thrown:
$ php -d safe_mode=On -r 'readfile("/etc/passwd");'
Warning: readfile(): SAFE MODE Restriction in effect. The script whose
uid is 1000 is not allowed to access /etc/passwd owned by uid 0 in
Command line code on line 1
$ php -d open_basedir=/home/someuser -r 'readfile("/etc/passwd");'
Warning: readfile(): open_basedir restriction in effect.
File(/etc/passwd) is not within the allowed path(s): (/home/someuser) in
Command line code on line 1
---
ISSUE:
The glob()-function is used to find pathnames matching a pattern. The
issue here is that the pattern is expanded to find a file and then
safe_mode/open_basedir-checks are performed. While this might be the
only way of performing this check (as glob might match directories with
different owners, e.g. glob("/home/*/public_html") ), the warning still
discloses the matched, previously unknown file name:
$ php -d safe_mode=On -r 'glob("/tmp/faketmp/phptest_sess_*");'
Warning: glob(): SAFE MODE Restriction in effect. The script whose uid
is 1000 is not allowed to access
/tmp/faketmp/phptest_sess_03735f0f339412345678901234567890 owned by uid
0 in Command line code on line 1
Retrieving the error message using output buffering/capture, one can
utilize these error messages and craft a fairly simple script that find
every filename (available for the user php is executed as - usually the
Apache user) in few attempts per file as opposed to brute force guessing
filenames.
---
METHOD:
Exploiting error messages:
- Match first file using glob("/target/directory/*")
- Retrieve warning about first file matched (eg. apache)
- Check matced file plus ?* (eg. apache?*)
- Walk all way backwards using square brackets
(eg. apache => apach[f-z], apac[i-z], apa[d-z], ap[b-z], a[q-z], [b-z])
Exploiding first-file-check-only:
- Craft a glob pattern that matches a "good" file (with same UID as
script and inside open_basedir path) at first and the target directory
afterwards
- E.g. glob("{.,/tmp}/*",GLOB_BRACE)
---
IMPACT:
Users at a webhosting company are able to retrieve file names out of
their bounds. Only the filenames are accessible, not the actual content.
This might still be a concern, as the native method for PHP storing
sessions are using files with the token name as part of the filename,
revealing session names, enabling users to hijack existing session
(though not being able to directly manipulating it - unless
session.save_path are the same for all hosts).
Furthermore, this might be a tool for gathering information about the
system, searching for other possible exploits.
---
WORKAROUND:
At a global level glob() could be disabled using the
disable_function-directive in php.ini configuration file, e.g.:
disable_functions glob
This might not be a practical solution as many php scripts use that
function for normal operation.
At at local level a user might want to create his own session mechanism
where filenames does not directly disclose session tokens.
session_set_save_handler() could be used but is cumbersome on a large
scale.
Patching ext/standard/dir.c might prevent glob disclosing file names
unknown to users in advance.
---
OTHER ISSUE:
glob() raises no warning if used on another owner's directory and there
is no match. Even if glob is fixed to prevent disclosure of file name,
this might still be used to finding a file name (test if /dir/[a-z]*
provides warning or not, test if /dir/[a-n]* provides warning or not,
etc. - file names might be found using about 8 glob() requests per
letter - still better than brute force). This is quite the opposite of
the method mentioned above, as we are walking forward to find the
filename, not backwards.
---
NOTES:
As mentioned, safe_mode isn't meant to be magically safe. This is not my
belief either, but as it is a centralized method that several web hosts
depend on carrying users with access to no shells or other languages
than PHP, it should provide security in its own world. Of course, PHP
isn't able to prevent users with access to other languages or shells to
retrieve entire file lists, but users in pure PHP environments shouldn't
be able to (under safe_mode/open_basedir-restriction).
Information about a webhost's session.save_path might be retrieved by
tricking an error message - such as adding ?PHPSESSID=_ (or any other
invalid session name) to the URL and looking at last line of output such
as:
Unknown(): Failed to write session data (files). Please verify that the
current setting of session.save_path is correct (/path/to/session/data)
in Unknown on line 0
Other workarounds, solutions or concept ideas (based on general php
behaviour and not at php-code-level) to solve issues with glob, shared
sessions and session hijacking when session filename is known:
* Most important: No disclosure of file name on
safe_mode/open_basedir-error when using glob(). This could prevent file
structure harvesting.
* UID check for each and every glob()-entry, not just the first one. It
requires more effort but it might be a small price to pay.
* glob() of non-existent folders should be restricted as well -
glob("/home/nonexistent/") provides no error, but glob("/home/existent/")
does.
* Session file names could be hashed values. In that case the filename
itself can't be used for session hijacking.
* Append the UID to the session file in safe_mode (just as the UID is
appended to the realm in HTTP authentication) - or maybe (hash of)
SERVER_NAME where available.
* An option to prevent running of PHP scripts with the same UID as the
Apache user (as it is easy to create a script with the UID of the Apache
user, bypassing some safe_mode-limitations).
Personal opinion: I can't see why setups only using PHP (and e.g. FTP
restricted to homedir as ftp-root) absolutely have to be insecure based
on arguments that has nothing to do with PHP-only-access. I don't
believe that security comes in a box, but any issue to be solved on a
global/centralized level is better than asking sysadms and developers of
performing custom, individual workarounds.
---
RELATED:
Example of reading and manipulating data across virtual hosts with same
session.save_path:
http://stock.ter.dk/session.php
More info at PHP bug #28242
---
--
- Peter Brodersen
Powered by blists - more mailing lists