Environment Variable Tricks

Interesting environment variables to load into scripting language interpreters



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 ENVIRONMENTman page section perlrun(1)reveals many environment variables worth exploring. The environment variable PERL5OPTallows 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.pmand provides environment variables PERL5OPT=-Mrootand PERL5LIB=/ tmpfor 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=-dand 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 -mor -M, but an option -Mallows 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 VARIABLESin mana on python(1), it PYTHONSTARTUPinitially 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 PYTHONINSPECTcan be used to enter interactive mode, just like -ion the command line. However, the documentation for the option -iexplains what PYTHONSTARTUPwill not be used when python is started with a script to execute. This means that PYTHONSTARTUPboth PYTHONINSPECTcannot be combined, and PYTHONSTARTUPonly has an effect when the Python REPL is immediately started. This ultimately means thatPYTHONSTARTUPnot viable because it has no effect when a regular Python script is executed.



Environment variables PYTHONHOMEand 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 PYTHONWARNINGSsays that this is equivalent to specifying a parameter -W. This parameter is -Wused 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 runningimport 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 antigravityopens 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 BROWSERindicating 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 perlby 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_OPTIONSand 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/environonly 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/environor 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 RUBYOPTto 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 -rforcing Ruby to load the library using require. However, this is limited to files with the extension .rbor .so.



An example of a relatively useful file I found .rbis tools/server.rbfrom 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/rubythere 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 "



All Articles