I'm testing one of our PHP web applications for security issues.
The system the code is running on runs with at least PHP7.2.
Now I found something like the following in the code (simplified for this question, but boils down to this):
$file = $_GET['file'];
$path = "/some/directory/" . $file;
$path = str_replace(['../', '..'], '', $path);
echo file_get_contents($path);
Is it possible to modify the file parameter in a way that we can escape /some/directory, so that after the str_replace() the file_get_contents()-call looks something like: file_get_contents(/some/directory/../../etc/passwd)?
Edit:
I can't change the order of code execution. I can only define the value of $_GET['file'] with my request.
Furthermore I know how to make this more secure but for my research I intend to break it.
Basically what needs to be done is somehow tricking out the str_replace() into leaving some ../ behind.
I tried for a few hours now, with various approaches, but - luckily for our application - couldn't get it working.
Do you have any ideas?
You can fiddle around with the code here: https://3v4l.org/3ehYA
Related
This question already has answers here:
making print_r use PHP_EOL
(5 answers)
Closed 6 years ago.
I've been coding in PHP for a long time (15+ years now), and I usually do so on a Windows OS, though most of the time it's for execution on Linux servers. Over the years I've run up against an annoyance that, while not important, has proved to be a bit irritating, and I've gotten to the point where I want to see if I can address it somehow. Here's the problem:
When coding, I often find it useful to output the contents of an array to a text file so that I can view it's contents. For example:
$fileArray = file('path/to/file');
$faString = print_r($fileArray, true);
$save = file_put_contents('fileArray.txt', $faString);
Now when I open the file fileArray.txt in Notepad, the contents of the file are all displayed on a single line, rather than the nice, pretty structure seen if the file were opened in Wordpad. This is because, regardless of OS, PHP's print_r function uses \n for newlines, rather than \r\n. I can certainly perform such replacement myself by simply adding just one line of code to make the necessary replacements, ans therein lies the problem. That one, single line of extra code translates back through my years into literally hundreds of extra steps that should not be necessary. I'm a lazy coder, and this has become unacceptable.
Currently, on my dev machine, I've got a different sort of work-around in place (shown below), but this has it's own set of problems, so I'd like to find a way to "coerce" PHP into putting in the "proper" newline characters without all that extra code. I doubt that this is likely to be possible, but I'll never find out if I never ask, so...
Anyway, my current work-around goes like this. I have, in my PHP include path, a file (print_w.php) which includes the following code:
<?php
function print_w($in, $saveToString = false) {
$out = print_r($in, true);
$out = str_replace("\n", "\r\n", $out);
switch ($saveToString) {
case true: return $out;
default: echo $out;
}
}
?>
I also have auto_prepend_file set to this same file in php.ini, so that it automatically includes it every time PHP executes a script on my dev machine. I then use the function print_w instead of print_r while testing my scripts. This works well, so long as when I upload a script to a remote server I make sure that all references to the function print_w are removed or commented out. If I miss one, I (of course) get a fatal error, which can prove more frustrating than the original problem, but I make it a point to carefully proofread my code prior to uploading, so it's not often an issue.
So after all that rambling, my question is, Is there a way to change the behavior of print_r (or similar PHP functions) to use Windows newlines, rather than Linux newlines on a Windows machine?
Thanks for your time.
Ok, after further research, I've found a better work-around that suite my needs, and eliminates the need to call a custom function instead of print_r. This new work-around goes like this:
I still have to have an included file (I've kept the same name so as not to have to mess with php.ini), and php.ini still has the auto_prepend_file setting in place, but the code in print_w.php is changes a bit:
<?php
rename_function('print_r', 'print_rw');
function print_r($in, $saveToString = false) {
$out = print_rw($in, true);
$out = str_replace("\n", "\r\n", $out);
switch ($saveToString) {
case true: return $out;
default: echo $out;
}
}
?>
This effectively alters the behavior of the print_r function on my local machine, without my having to call custom functions, and having to make sure that all references to that custom function are neutralized. By using PHP's rename_function I was able to effectively rewrite how print_r behaves, making it possible to address my problem.
I need to download a file via PHP ftp_get(), but the foolish provider is using directories and file names contaning whitespace.. The file path I'm dealing with is similar to /product info/more stuff/inventory and stuff.csv
The spaces in the path and in the filename itself is making it difficult to retrieve anything. I already tried the following without success:
$path = "/product\ info/more\ stuff/inventory\ and\ stuff.csv";
$path = "/product%20info/more%20stuff/inventory%20and%20stuff.csv";
$path = '"/product info/more stuff/inventory and stuff.csv"';
Thanks again for taking the time to help me out.
Your third attempt, quoting the complete path, was already the recommended approach. Though it very much depends on the actual server implementation.
FTP per RFC859 is comprised of a terminal session and a data transfer channel. Basically the FTP server provides a mini-shell on the command port. As such, typical shell string escaping rules do apply. URL encoding can be ruled out here.
I'd advise first to use single quotes however. Preferrably use escapeshellarg() to apply them. And try ftp_nb_get() while at it.
$path = "/foo foo/bar bar/baz baz.csv";
ftp_nb_get($con, "save.csv", escapeshellarg($path), 2);
If that doesn't work, further debugging is necessary. While all ftp_* function arguments are left unprocessed, you could as well try to send a ftp_raw request. This won't actually activate the data channel reading, but might return a more concrete error response.
print_r(ftp_raw($con, "RETR '/path to/some file.csv'\r\n"));
And I'm just gonna say it, if you're still getting a file not found error then; it's entirely possible that the file really doesn't exist at the presumed location. In that case manually traverse the directory structure with ftp_nlist and ftp_rawlist with var_dump (in case of extra trailing spaces for subdirs).
Alternatively just use PHPs ftp:// stream wrapper (which also supports PASV mode). Whose implementation is distinct from that of the ext/ftp functions. Here funnily enough, URL encoding is again the correct approach, but quoting still necessary (ftp_fopen_wrapper.c does not quote itself):
= file_get_contents("ftp://user:pw#example.org/'path%20to/file%20and.csv'");
// Inline quotes may likely trip up some FTP server implementations..
A much better alternative though is just using cURL.
// You'll have to use the long-winded PHP curl functions of course.
print curl("ftp://.../file with spaces.csv")->exec();
Last option is just resorting to calling a Unixland client. (If not wget, than a plain ftp client.)
$url = escapeshellarg("ftp://user:pw#ftp.example.org/$path");
$file = `wget $url`;
If you still can't retrieve any files, you'll have to look for an alternative FTP client in PHP for further debugging. Guess who wrote one.
To get a list of files or folders with spaces in the path.
ftp_chdir($conn, $path);
$children = ftp_rawlist($conn,'-a .');
Source
I was running over a sample script and hit on this particular issue.
The script starts off by setting
$docroot=$_SERVER['DOCUMENT_ROOT'];
For writing to a file,
#$fp = fopen("$docroot/../parts/partsorders.txt",'ab');
is used. But no matter what, this fails to write to the file.
After a tinkering with it for a while, I set the command to
#$fp = fopen('$docroot/../parts/partsorders.txt','ab');
used single quotes instead of double quotes and it worked fine!
My question is, isn't the former double quoted format supposed to work instead of the single quotes. What is happening here ?
Here is the stripped down code, guys (Assume that the file exists in the server) :
<?php
$docroot=$_SERVER['DOCUMENT_ROOT'];
$outputstring = "herpderp";
?>
<html>
<head>
<title>Quotes</title>
</head>
<body>
<?php
#$fp=fopen("$docroot/../parts/partsorders.txt","ab");
flock($fp, LOCK_EX);
if(!$fp) {
echo "<p><strong>Failed.</strong></p></body></html>";
exit;
}
fwrite($fp,$outputstring,strlen($outputstring));
flock($fp,LOCK_UN);
fclose($fp);
echo "<p>Order written.</p>";
?>
</body>
</html>
There are differences between single and double quoted strings in PHP. Single quoted strings do not cause the expansion of variable names while double quoted strings do. See here. Why your code works when you use the variable name with single quoted strings doesn't make sense to me. Furthermore, it's a bad idea to use # in front of your fopen commands, it will prevent you from seeing the error output.
The double quote one is the one that should evaluate $docroot for you. What the single quote should do is try to open a file that actually has $docroot as a string in it's path. Does
#$fp = fopen($docroot . '/../parts/partsorders.txt','ab');
yield the same result?
And do you use #to supress errors? In that case it should be before the function I think
$fp = #fopen($docroot . '/../parts/partsorders.txt','ab');
But don't do that when trying to find errors in your application. The thing is that you could very well get an error that it can't find the file and you just don't notice.
UPDATE
Thanks to pinkgothic for his comment, firstly for providing the correct terminology and secondly for pointing out that this answer is wrong.
I have experimented with the theory that shell is trying to expand the environment variable $docroot and found that shell expansion is not possible in fopen().
I tried the following code and got this error (I have a file called test.txt in my home directory)
$fp = fopen( '$HOME/test.txt', 'rb' );
//PHP Warning: fopen($HOME/test.txt): failed to open stream: No such file or directory in php shell code on line 1
So unless the OP has some configuration which allows shell expansion in fopen(), my answer is, as I say, incorrect.
The string $docroot/../parts/partsorders.txt is being sent directly to the operating system shell. As there is no $docroot variable set it is replaced by an empty string, so it is the same as using
#$fp = fopen('/../parts/partsorders.txt','ab');
Which is an absolute path starting from /, the document root.
#run from shell (bash)
~$ echo $docroot/../parts/partsorders.txt
/../parts/partsorders.txt
After my comment, I had a night's sleep and realised fopen() may not actually use/be like realpath() (which expects all segments to be set, even the ones that are irrelevant for the final normalised path).
It doesn't.
Accordingly, the reason your file is opened is actually fairly simple:
'$docroot/../parts/partsorders.txt'
...which is read as "pastorders.txt in the 'parts' folder which is a folder found in the folder above the '$docroot' folder which is in the current working directory" collapses to...
'parts/partsorders.txt'
...because fopen() simply vanishes $docroot/.. away without checking that $docroot exists. (Functions like realpath() do check it, which was throwing me off.)
So, your file is actually in <current working directory>/parts/partsorders.txt. (Since you're opening the file with the a flag, if it didn't exist there before, it was definitely created there.)
Whatever your $_SERVER['DOCUMENT_ROOT'] contains, it seems it's not what you want. Additionally, in some setups, you can't reasonably .. above $_SERVER['DOCUMENT_ROOT'] - permissions actually won't let you.
If that environment variable outright isn't set (if that's even possible; but I think this demonstrates the problem even if it isn't), the path is quite different:
"$docroot/../parts/partsorders.txt"
...becomes:
"/../parts/partsorders.txt"
...which tries to get up the hierarchy past the root point (/), which would of course not work.
I suggest echoing out or logging $_SERVER['DOCUMENT_ROOT'] and taking a look at what it actually contains, and if it's what you expect it to be.
What might be worth looking for is __DIR__ (or in older PHP versions, dirname(__FILE__)), which takes the directory the file. As long as the file knows where it is, you can just read out files relative to its location.
I'm writing a setup/installer script for my application, basically just a nice front end to the configuration file. One of the configuration variables is the executable path for mysql. After the user has typed it in (for example: /path/to/mysql-5.0/bin/mysql or just mysql if it is in their system PATH), I want to verify that it is correct. My initial reaction would be to try running it with "--version" to see what comes back. However, I quickly realised this would lead to me writing this line of code:
shell_exec($somethingAUserHasEntered . " --version");
...which is obviously a Very Bad Thing. Now, this is a setup script which is designed for trusted users only, and ones which probably already have relatively high level access to the system, but still I don't think the above solution is something I want to write.
Is there a better way to verify the executable path? Perhaps one which doesn't expose a massive security hole?
Running arbitrary user commands is like running queries based on user input... Escaping is the key.
First, validate if it is an executable using is_executable().
PHP exposes two functions for this: escapeshellarg() and escapeshellcmd().
escapeshellarg() adds single quotes around a string and quotes/escapes any existing single quotes allowing you to pass a string directly to a shell function and having it be treated as a single safe argument.
escapeshellcmd() escapes any characters in a string that might be used to trick a shell command into executing arbitrary commands.
This should limit the amount of risk.
if(is_executable($somethingAUserHasEntered)) {
shell_exec(escapeshellarg($somethingAUserHasEntered) . " --version");
}
After all, doing rm --version isn't very harmful, and "rm -rf / &&" --version will get you anywhere very fast.
EDIT: Since you mentioned PATH... Here is a quick function to validate if the file is an executable according to PATH rules:
function is_exec($file) {
if(is_executable($file)) return true;
if(realpath($file) == $file) return false; // Absolute Path
$paths = explode(PATH_SEPARATOR, $_ENV['PATH']);
foreach($paths as $path) {
// Make sure it has a trailing slash
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if(is_executable($path . $file)) return true;
}
return false;
}
You could try a simple file_exists call to determine if something exists at that location, along with an is_executable to confirm that it's something you can run.
have you looked at is_dir() or is_link() or is_file() or is_readable()
Hope these help.
system('which '.escapeshellarg($input)) will give you the absolute path to the executable, regardless if it's just the name or an absolute path.
Something like:
/directory/a/b - /directory/ = a/b
Is it possible to do this easily?
Since you're working with paths, platform sensitivity is important; Windows has a different path separator than most other platforms, and to write reusable code you can't snub a platform.
PHP has a few functions to deal with paths. If you're handed a really strange path like ~foo/bar//bitty/../index.php, use realpath to clean that up for you.
$path = realpath("~foo/bar//bitty/../index.php");
/* output: /home/foo/bar/index.php */
Other functions will aid you -- for example, to get the path part of a filename by itself, use dirname:
print dirname($path);
/* output: /home/foo/bar */
Once you have that, split on the separators and do whatever work you want. The real trick is having PHP worry about all the weirdness in paths for you, and then just working with each part separately. Look into pathinfo and basename as well. I think this is what you were asking for, not how to do dumb string replacements.
Don't forget not allowing injection to your application! Working with paths from Web input is dangerous. Never trust user input.
echo str_replace("/directory/","","/directory/a/b");
And to use this on other types of strings, your full string goes in the third parameter, and whatever you're "subtracting" goes as the first parameter.
Using the dirname() funciton and some strings you can cut the original path up and get the pieces.
<?php
// from: http://php.net/manual/en/function.dirname.php
$path = "/dirname/a/b";
$dir = dirname(dirname($path));
echo "dir at front=$dir\n";
$len = strlen($dir);
$dirname = substr ( $path, 0, $len+1 );
echo "dirname=$dirname\n";
$last_2 = substr ( $path, $len+1 );
echo "last_2=$last_2\n";
?>
results in
$ php x.php
dir at front=/dirname
dirname=/dirname/
last_2=a/b