Introduction
In a recent hacking project, we got the ability to specify environment variables, but not a running process. We also could not control the contents of the file on disk, and brute-force of process identifiers (PIDs) and file descriptors did not give interesting results, excluding remote LD_PRELOAD exploits . Fortunately, a scripting language interpreter was executed, which allowed us to execute arbitrary commands by setting certain environment variables. This blog discusses how arbitrary commands can be executed by a number of scripting language interpreters under malicious environment variables.
Perl
A cursory reading of the
ENVIRONMENT
man page section perlrun(1)
reveals many environment variables worth exploring. The environment variable PERL5OPT
allows you to set command line options, but is limited to only accepting options CDIMTUWdmtw
. Unfortunately, this means a lack -e
, which makes it possible to load perl code to run.
All is not lost, however, as shown in the exploit for CVE-2016-1531 from Hacker Fantastic . The exploit writes a malicious perl module to a file
/tmp/root.pm
and provides environment variables PERL5OPT=-Mroot
and PERL5LIB=/ tmp
for executing arbitrary code. However, this was an exploit for a local privilege escalation vulnerability, and the general method should ideally not require access to the file system. Looking atexploit by blasty for the CVE same, he did not require the creation of a file, use environment variables PERL5OPT=-d
and PERL5DB=system("sh");exit;
. The same variables were used to solve the CTF problem in 2013.
The final subtlety of the generic method is to use one environment variable instead of two. @justinsteven found that this is possible with
PERL5OPT=-M
. While perl module to download, you can use either -m
or -M
, but an option -M
allows you to add extra code after the module name.
Proof of concept
Example 0: Executing arbitrary code with an environment variable versus perl executing an empty script (/ dev / null)
$ docker run --env 'PERL5OPT=-Mbase;print(`id`)' perl:5.30.2 perl /dev/null
uid=0(root) gid=0(root) groups=0(root)
Python
Judging by the section
ENVIRONMENT VARIABLES
in mana on python(1)
, it PYTHONSTARTUP
initially looks like a simple solution. It allows you to specify the path to a Python script that will be executed before the prompt is displayed interactively. The requirement for interactive mode did not seem to be a problem, as an environment variable PYTHONINSPECT
can be used to enter interactive mode, just like -i
on the command line. However, the documentation for the option -i
explains what PYTHONSTARTUP
will not be used when python is started with a script to execute. This means that PYTHONSTARTUP
both PYTHONINSPECT
cannot be combined, and PYTHONSTARTUP
only has an effect when the Python REPL is immediately started. This ultimately means thatPYTHONSTARTUP
not viable because it has no effect when a regular Python script is executed.
Environment variables
PYTHONHOME
and looked promising PYTHONPATH
. Both allow arbitrary code execution, but require you to be able to create directories and files on the filesystem as well. It may be possible to relax these requirements by using a virtual / proc file system and / or ZIP files.
Most of the rest of the environment variables are simply checked for a non-empty string, and if so, include a generally benign setting. One of the rare exceptions is
PYTHONWARNINGS
.
Working with PYTHONWARNINGS
The documentation for
PYTHONWARNINGS
says that this is equivalent to specifying a parameter -W
. This parameter is -W
used for alert management to specify alerts and how often to display them. The full form of the argument is action:message:category:module:line
. While monitoring alerts did not seem like a promising clue, this changed quickly after testing the implementation.
Example 1: Python-3.8.2 / Lib / warnings.py
[...]
def _getcategory(category):
if not category:
return Warning
if '.' not in category:
import builtins as m
klass = category
else:
module, _, klass = category.rpartition('.')
try:
m = __import__(module, None, None, [klass])
except ImportError:
raise _OptionError("invalid module name: %r" % (module,)) from None
[...]
This code shows that as long as our specified category contains a dot, we can start importing an arbitrary Python module.
The next problem is that the vast majority of modules from the Python standard library execute very little code when imported. They usually just define the classes to be used later, and even when they provide code to run, the code is usually protected by checking the __main__ variable (to determine if the file was imported or run directly).
An unexpected exception to this rule is the antigravity module . The Python developers in 2008 included an easter egg that can be invoked by running
import antigravity
... This import will immediately open an xkcd comic in your browser jokingly that the antigravity import in Python makes it possible to fly.
As for how the module
antigravity
opens your browser, it uses another module from the standard library called webbrowser
. This module checks your PATH for a wide variety of browsers including mosaic, opera, skipstone, konqueror, chrome, chromium, firefox, links, elinks, and lynx. It also accepts an environment variable BROWSER
indicating which process to execute. No arguments can be provided to a process in an environment variable, and the comic's xkcd url is the only 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.
Using Perl to Execute Arbitrary Code
One approach is to use Perl, which is usually installed on the system and is even available in the standard Python Docker image. However, you cannot use the binary
perl
by itself, because the first and only argument is the comic's xkcd url. This argument will throw an error and the process will terminate without using an environment variable PERL5OPT
.
Example 2: PERL5OPT has no effect when a URL is passed to perl
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perl https://xkcd.com/353/
Can't open perl script "https://xkcd.com/353/": No such file or directory
Fortunately, when Perl is available, default Perl scripts such as perldoc and perlthanks are often available as well. These scripts will also fail with an invalid argument, but the error in this case occurs later than the processing of the environment variable PERL5OPT. This means you can use the Perl environment variable payload detailed earlier in this blog.
Example 3: PERL5OPT works as expected with perldoc and perlthanks
$ docker run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perldoc https://xkcd.com/353/
uid=0(root) gid=0(root) groups=0(root)
$ run -e 'PERL5OPT=-Mbase;print(`id`);exit' perl:5.30.2 perlthanks https://xkcd.com/353/
uid=0(root) gid=0(root) groups=0(root)
Proof of concept
Example 4: Executing Arbitrary Code Using Multiple Environment Variables with Python 2 and Python 3
$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:2.7.18 python /dev/null
uid=0(root) gid=0(root) groups=0(root)
Invalid -W option ignored: unknown warning category: 'antigravity.x'
$ docker run -e 'PYTHONWARNINGS=all:0:antigravity.x:0:0' -e 'BROWSER=perlthanks' -e 'PERL5OPT=-Mbase;print(`id`);exit;' python:3.8.2 python /dev/null
uid=0(root) gid=0(root) groups=0(root)
Invalid -W option ignored: unknown warning category: 'antigravity.x'
NodeJS
Michal Bentkowski posted the payload for the Kibana exploit (CVE-2019-7609) on his blog . A prototype pollution vulnerability was used to set arbitrary environment variables that resulted in arbitrary command execution. The payload from Michal used the environment variable
NODE_OPTIONS
and the proc filesystem in particular /proc/self/environ
.
While Michal's technique is creative and works great in his case, it is not always guaranteed to work and has some limitations that would be nice to address.
The first limitation is that it uses
/proc/self/environ
only if the content can be made syntactically valid by JavaScript. To do this, you must be able to create an environment variable and make it appear first in the contents of the file, /proc/self/environ
or know / cheat the name of the environment variable that appears first and overwrite its value.
Another limitation is that the value of the first environment variable ends with a one-line comment (//). Therefore, any newline character in other environment variables is likely to cause a syntax error and prevent the payload from executing. Using multi-line comments (/ *) will not fix the problem, as they must be closed to be syntactically correct. Therefore, in the rare cases when an environment variable contains a newline character, it is necessary to know / unset the name of the environment variable and overwrite its value with a new value that does not contain a newline.
We will leave the elimination of these limitations as an exercise for the reader.
Proof of concept
Example 5. Executing arbitrary code with environment variables against Michal Bentkowski's NodeJS
$ docker run -e 'NODE_VERSION=console.log(require("child_process").execSync("id").toString());//' -e 'NODE_OPTIONS=--require /proc/self/environ' node:14.2.0 node /dev/null
uid=0(root) gid=0(root) groups=0(root)
PHP
If you run it
ltrace -e getenv php /dev/null
, you will find that PHP is using an environment variable PHPRC
. The environment variable is used when trying to find and load a configuration file php.ini
. The neex exploit for CVE-2019-11043 uses a number of PHP parameters to force arbitrary code execution. In Orange Tsai also has an excellent post about creating your own exploit for the CVE, which uses a slightly different list of settings. Using this knowledge, as well as knowledge gained from previous NodeJS techniques, and some help from Brendan Scarwell , a PHP solution was found with two environment variables.
This method has the same limitations as the NodeJS examples.
Proof of concept
Example 6: Executing Arbitrary Code with Environment Variables Against PHP
$ docker run -e $'HOSTNAME=1;\nauto_prepend_file=/proc/self/environ\n;<?php die(`id`); ?>' -e 'PHPRC=/proc/self/environ' php:7.3 php /dev/null
HOSTNAME=1;
auto_prepend_file=/proc/self/environ
;uid=0(root) gid=0(root) groups=0(root)
Ruby
No universal solution for Ruby has been found yet. Ruby does accept an environment variable
RUBYOPT
to specify command line options. The man page says RUBYOPT can only contain -d, -E, -I, -K, -r, -T, -U, -v, -w, -W, --debug, --disable-FEATURE --enable-FEATURE
. The most promising option is -r
forcing Ruby to load the library using require. However, this is limited to files with the extension .rb
or .so
.
An example of a relatively useful file I found
.rb
is tools/server.rb
from the json gem, which is available after installing Ruby on Fedora systems. When this file is required, the web server is started as shown below:
Example 7: Using the RUBYOPT environment variable to start the ruby ββprocess and start the web server
$ docker run -it --env 'RUBYOPT=-r/usr/share/gems/gems/json-2.3.0/tools/server.rb' fedora:33 /bin/bash -c 'dnf install -y ruby 1>/dev/null; ruby /dev/null'
Surf to:
http://27dfc3850fbe:6666
[2020-06-17 05:43:47] INFO WEBrick 1.6.0
[2020-06-17 05:43:47] INFO ruby 2.7.1 (2020-03-31) [x86_64-linux]
[2020-06-17 05:43:47] INFO WEBrick::HTTPServer#start: pid=28 port=6666
Another approach in Fedora is to take advantage of the fact that
/usr/bin/ruby
there is actually a Bash script that launches /usr/bin/ruby-mri
. The script calls Bash functions that can be overwritten by environment variables.
Proof of concept
Example 8: Using an Exported Bash Function to Execute an Arbitrary Command
$ docker run --env 'BASH_FUNC_declare%%=() { id; exit; }' fedora:33 /bin/bash -c 'dnf install ruby -y 1>/dev/null; ruby /dev/null'
uid=0(root) gid=0(root) groups=0(root)
Conclusion
This post looked at some interesting use cases for environment variables that could help you achieve arbitrary code execution through various scripting language interpreters without writing files to disk. I hope you enjoyed reading and were interested in finding and sharing improved payloads for these and other scripting languages. If you find a generic technique that works against Ruby, it will be very interesting to hear about it.
See also: " Dotfile Madness "