|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2015-03-09 22:56 UTC] ak4t0sh at free dot fr
Description:
------------
Hi,
During a code review i found a strange behaviour in most of php function that deal with path handling.
Indeed I found a code that required a file this way : `require __DIR__."../../include/myfile.php";` building a path like like `/path/to../../include/myfile.php` instead of the right path `/path/to/../include/myfile.php`
But strangely the file was correctly loaded by require...
As you can see when you execute test script in the "normal case" the file is loaded even if file_exists() return false...
The others parts of the test script are here as a POC to demonstrate that such behaviour could lead to a LFI exploitation. To test them remove the exit at line 22.
Indeed as the file `/path/to/../include/myfile.php` is loaded without any error or warning when calling `/path/to../../include/myfile.php` the developer could miss the coding error.
But if an attacker could create a symlink `/path/to.` which target an especially crafted file tree that could allow him to read / write / execute others (private) files. (LFI)
In fact this is not a very easy LFI to exploit but in my case the file was a template file so as an attacker a possible exploitation would be to replace the loaded stream by my own (malicious) code without editing the original source code.
The problem seems to be located to the "../../" part only.
Indeed the following paths logically failed :
/path/to../../include../myfile.php
/path/to/../include../myfile.php
After some researches i suppose that the problem is in "zend_resolve_path" function but as i'm not familiar with php internal code i prefer let the experts look and decide if it's a bug or a feature ;)
This has been tested on default installation of PHP 5.6.5, 5.4.36 and PHP 5.3.3 on Debian Jessie, Wheezy and Squeeze
Arnaud Trouvé (ak4t0sh)
Test script:
---------------
wget http://www.arnaudtrouve.fr/php-dotted-path-issue/dotted-path-issue-test.sh
chmod +x dotted-path-issue-test.sh
wget http://www.arnaudtrouve.fr/php-dotted-path-issue/poc.txt
mv poc.txt poc.php
./dotted-path-issue-test.sh
Expected result:
----------------
cf test script comments
Actual result:
--------------
cf test script comments
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 01:00:01 2025 UTC |
ok thanks for the explanation i was wrong there is no problem with dot character handling i thought there would a check from the entire path validity before its resolution. However i think there is a real problem in path resolving consistency between file_exists/stream_resolve_include_path/realpath and fopen/file_get_contents/file_put_contents/require/include...(others) Indeed the fact is in the test case 2 (aka WTF : single dotted path in test file) file_exists/stream_resolve_include_path/realpath are all returning false but we can still open file using fopen, etc... Here a smaller script to test it : ======================================================================== <?php mkdir(__DIR__."/foo"); file_put_contents(__DIR__."/foo/bar.php", "content"); var_dump(file_exists(__DIR__."/foo/bar.php"), , realpath(__DIR__."/foo/bar.php"), stream_resolve_include_path(__DIR__."/foo/bar.php"), file_get_contents(__DIR__."/foo/bar.php")); var_dump(file_exists(__DIR__."/inexistant_dir/../foo/bar.php"), realpath(__DIR__."/inexistant_dir/../foo/bar.php"), stream_resolve_include_path(__DIR__."/inexistant_dir/../foo/bar.php"), file_get_contents(__DIR__."/inexistant_dir/../foo/bar.php")); ?> ======================================================================== So on one hand i have file_exists (and others functions) which return me the the file "inexistant_dir/../foo/bar.php" does not exists and on the other hand i can still open it using file_get_contents... So if i follow your reasoning about ".." that remove the previous component of path we can not really rely on file_exists because in this script it should have remove "inexistant_dir" from path and so return true. Same for realpath and stream_resolve_include_path which should have return __DIR__."/foo/bar.php" instead of false. So theorically i can write a code like this (i know it's stupid - it's for demo) ======================================================================== <?php mkdir(__DIR__."/foo"); file_put_contents(__DIR__."/foo/bar.php", "content"); if (file_exists(__DIR__."/inexistant_dir/../foo/bar.php") == false) echo file_get_contents(__DIR__."/inexistant_dir/../foo/bar.php"); #does not exists ? i don't care open it anyway ?> ======================================================================== As there is no problem with dot character i renamed this issue. Is there an explanation to such inconsistency ? If not as i cannot do it i let you reopen the issue and remove the security flag because i think this should be fixed or documented at least.