On a recent project we gained the ability to specify environment variables but not the process that was executed. We were also unable to control the contents of a file on disk, and bruteforcing process identifiers (PIDs) and file descriptors found no interesting results, eliminating remote LD_PRELOAD exploitation. Fortunately, a scripting language interpreter was executed which enabled us to execute arbitrary commands by specifying particular environment variables. This blog post discusses how arbitrary commands can be executed by a range of scripting language interpreters when supplied with malicious environment variables.
A quick read of the
ENVIRONMENT section of the
perlrun(1) man page reveals plenty of environment variables worth investigating.
PERL5OPT environment variable allows specifying command-line options, but is restricted to only accepting the options
This unfortunately means that
-e, which allows supplying perl code to run, is out.
All is not lost though, as demonstrated in the exploit for CVE-2016-1531 by Hacker Fantastic.
The exploit writes a malicious perl module to
/tmp/root.pm and supplies the environment variables
PERL5LIB=/tmp to achieve arbitrary code execution.
However this was an exploit for a local privilege escalation vulnerability and a generic technique should ideally not require access to the file system. Looking at blasty’s exploit for the same CVE, the exploit did not require creating a file and used the environment variables
The same environment variables were also used to solve a CTF challenge in 2013.
One final nicety of a generic technique would be to use a single environment variable instead of two.
@justinsteven found this was possible by leveraging
-M can be used to load a perl module, the
-M option allows adding extra code after the module name.
Proof of Concept
ENVIRONMENT VARIABLES section of the
python(1) man page,
PYTHONSTARTUP initially appears like it may be a piece of a straightforward solution.
It allows specifying a path to a Python script that will be executed prior to displaying the prompt in interactive mode.
The interactive mode requirement didn’t seem like it would be an issue as the
PYTHONINSPECT environment variable can be used to enter interactive mode, the same as specifying
-i on the command line.
However, the documentation for the
-i option explains that
PYTHONSTARTUP will not be used when python is started with a script to execute.
This means that
PYTHONINSPECT cannot be combined and
PYTHONSTARTUP only has an effect when the python REPL is immediately launched.
This ultimately means that
PYTHONSTARTUP is not viable as it has no effect when executing a regular Python script.
Other environment variables which looked promising were
PYTHONPATH. Both of these will let you gain arbitrary code execution but require you to also be able to create directories and files on the filesystem. It may be possible to loosen those requirements through the use of the proc filesystem and/or ZIP files.
The majority of the remaining environment variables are simply checked if they contain a non-empty string, and if so, toggle a generally benign setting. One of the rare exceptions to this is
Making progress with PYTHONWARNINGS
The documentation for
it is equivalent to specifying the -W option. The
-W option is used for warning control to specify which warnings and how often they are printed. The full form of argument is
action:message:category:module:line. While warning control didn’t seem like a promising lead, that quickly changed after checking the implementation.
The above code shows that as long as our specified category contains a dot, we can trigger the import an arbitrary Python module.
The next problem is that the vast majority of modules from Python’s standard library run very little code when imported. They tend to just define classes to be used later, and even when they provide code to run, the code is typically guarded with a check of the
__main__ variable (to detect if the file has been imported or run directly).
An unexpected exception to this is the antigravity module. The Python developers included an easter egg in 2008 which can be triggered by running
import antigravity. This import will immediately open your browser to the xkcd comic that joked that
import antigravity in Python would grant you the ability to fly.
As for how the
antigravity module opens your browser, it uses another module from the standard library called
webbrowser. This module checks your PATH for a large variety of browsers, including mosaic, opera, skipstone, konqueror, chrome, chromium, firefox, links, elinks and lynx. It also accepts an environment variable
BROWSER that lets you specify which process should be executed. It is not possible to supply arguments to the process in the environment variable and the xkcd comic URL is the one hard-coded argument for the command.
The ability to turn this into arbitrary code execution depends on what other executables are available on the system.
Leveraging Perl for Arbitrary Code Execution
One approach is to leverage Perl which is commonly installed on systems and is even available in the standard Python docker image. However, the
perl binary cannot itself be used. This is because the first and only argument is the xkcd comic URL. The comic URL argument will cause an error and the process to exit without the
PERL5OPT environment variable being used.
Fortunately, when Perl is available it also common to have the default Perl scripts available, such as perldoc and perlthanks. These scripts will also error and exit with an invalid argument, but the error in this case happens later than the processing of the
PERL5OPT environment variable. This means you can leverage the Perl environment variable payload detailed earlier in this blog post.
Proof of Concept
A blog post by Michał Bentkowski provided a payload for exploiting Kibana (CVE-2019-7609). A prototype pollution vulnerability was used to set arbitrary environment variables which resulted in arbitrary command execution. Michał’s payload used the
NODE_OPTIONS environment variable and the proc filesystem, specifically
Although Michał’s technique was creative and worked perfectly for their vulnerability, the technique is not always guaranteed to work and has some constraints that would be nice to remove.
The first constraint is that it using
/proc/self/environ, or knowing/bruteforcing the environment variable’s name that will appear first and overwriting it’s value.
Another constraint, as the first environment variable’s value finishes with a single line comment (
//). Therefore, any newline character in other environment variables will likely cause a syntax error and prevent the payload from executing. The use of multi-line comments (
/*) will not fix this issue as they must be closed to be syntactically valid. Therefore, in the rare case that an environment variable contains a newline character, it is required to know/bruteforce the environment variable’s name and overwrite it’s value to a new value that does not contain a newline.
Removing these contraints is an exercise left for the reader.
Proof of Concept
If you run
ltrace -e getenv php /dev/null you will find PHP uses the
PHPRC environment variable.
The environment variable is used when attempting to find and load the configuration file
An exploit by neex for CVE-2019-11043 used a series of PHP settings to achieve arbitrary code execution.
Orange Tsai also has a great blog post on creating their own exploit for the same CVE, which uses a slightly different list of settings.
Using this knowledge, plus the knowledge gained from the previous NodeJS technique, and some help from Brendan Scarvell, a two environment variable solution was found for PHP.
The same constraints exist for this technique as the NodeJS examples.
Proof of Concept
A generic solution for Ruby has not been found yet.
Ruby does accept an environment variable
RUBYOPT to specify command-line options.
The man page states that
RUBYOPT can contain only -d, -E, -I, -K, -r, -T, -U, -v, -w, -W, --debug, --disable-FEATURE and --enable-FEATURE.
The most promising option is
-r which causes Ruby to load the library using require.
However, this is limited to files with an extension of
An example of a somewhat useful
.rb file that has been found is
tools/server.rb from the json gem that is available after installing Ruby on Fedora systems.
When this file is required, a web server is started as shown below:
Sticking with Fedora, another approach is to leverage the fact that
/usr/bin/ruby is actually a Bash script which starts
/usr/bin/ruby-mri. The script calls Bash functions which can be overwritten with environment variables.
Proof of Concept
This post has explored interesting use cases of environment variables which could assist in achieving arbitrary code execution against various scripting language interpreters, without writing files to disk. Hopefully you’ve enjoyed reading and are inspired to find and share improved payloads for these and other scripting languages. If you find a generic technique that works against Ruby, we would be very interested in hearing about how you achieved this.
Thanks for reading, ciao Bella!