Privilege escalations

This post is part of the Source Code Review series.


Privilege escalations lie in the mist of process permissions.

The idea of a privilege escalation is to gain more rights than you have by getting a privileged process to execute some code on your behalf. This can be done legitimately as with sudo on GNU/Linux systems. It can also be done by finding a password in a log file. But those aren't worth an article. Let's talk about when it's done unknowingly of the privileged process by exploiting some flashy bug.

We will concentrate on one type of privilege escalation that involves no memory corruption (although it is interesting to note that what is done with such corruption is very similar but with memory addresses instead of files).

../image/rainbowdash_and_scootaloo_flying.png

Missing verification

The easiest case is when there is no verification at all:

origin      = "/tmp/mytmp/my-cd-list"
destination = "/tmp/mytmp/my-cd-list.bkp"

with open(origin, "rb") as fi:
    with open(destination, "wb") as fo:
        fo.write(fi.read())

Let's assume that this code is ran as a high-privileged process as a scheduled job and that the files always exist. We will also suppose for simplification that the rights over /tmp/mytmp allow any user to delete or create any file in that directory, but the two files can't be read or modified by anybody but the privileged process.

Any user could remove the files and replace them by symbolic links to other files. Doing so the attacker would get at the same time an arbitrary read of any file by copying them elsewhere, or an arbitrary write which he could use to overwrite the content of sudo for example.

Keywords: open write read chmod rename chdir

Race condition

It is not always as clear as in the previous snippet. It can happen as a race condition:

import subprocess

with open("/tmp/mytmp/tmp.sh", "w") as f:
    f.write("echo 'Harmless script'")

subprocess.Popen(["sh", "/tmp/mytmp/tmp.sh"])

When programming, we tend to see our code as one continuous flow of execution. That's not the case: the OS can stop our program at any time to execute someone else's code. Here one could swap the file by another between the moment when the file is written and the call to Popen.

Failed exception

Another case is the bogus exception:

import subprocess

try:
    with open("/tmp/mytmp/tmp.sh", "w") as f:
        f.write("echo 'Harmless script'")

except: # TODO: handle error
    pass

subprocess.Popen(["sh", "/tmp/mytmp/tmp.sh"])

This makes the problem way easier for an attacker: just create a file /tmp/mytmp/tmp.sh that has no write rights. The call to open will raise an exception, the write will never be executed and the program will gracefully continue its execution running your code.

Any time an error is ignored it is important to understand what is really ignored and what it could mean to an attacker.

Keywords: catch(Exception) except:  "return true;"

Time Of Check To Time Of Use

TOCTTOUs are a special case of race condition. They happen when a program first checks a property and then uses a resource on the assumption of the first property.

import os

origin      = "/tmp/cd-list/my-cd-list"
destination = "/tmp/cd-list/my-cd-list.bkp"

if os.path.islink(origin) or os.path.islink(destination):
    exit(1)

with open(origin, "rb") as fi:
    with open(destination, "wb") as fo:
        fo.write(fi.read())

The code looks fine at first: by checking that the argument is a real file and not some link we avoid the risk of the user maliciously putting a symbolic link to change the actual file used. However, with a closer look it is vulnerable to a TOCTTOU.

As the check happens before the file is opened it is possible for an attacker to swap the file for a malicious link between the call to islink and the call to open.

Of course the time frame is narrow but privilege escalations have a huge advantage over remote exploits: the attacker already has a foot on the system. He can try as long as he wants before finally getting into the time frame as he only needs to win the race once.

../image/rainbowdash-running.png

The solution would be to:

  1. open both files

  2. check that none is a link

  3. read/write the files

Keywords: isfile islink exists stat symlink

Unsafe paths

When executing a file, if a path isn't explicitly given, it is inferred from a list of directories containing common executables: the PATH.

When calling ls which is used? What program is really being used? What if another executable with the same name was created in another directory of the PATH?

While this doesn't happen much with common PATHs because creating files in the first PATH directories generally needs many privileges, it is quite common to have similar flaws with programs that rely on deduction for paths or use custom PATH variables.

This can also touch libraries of course, it doesn't have to be an executable. In python importing a file is done by executing it. On windows LoadLibrary loads a DLL, but if only the name is given then it starts its search in the executable directory (which may very well be the web download directory).

There is also a special case for Windows unquoted paths. Let's imagine a batch script with the following content:

@echo off
C:\Super Calc\calc++.exe

Straight and forward, this executes the program with a full path to avoid any ambiguity... or is there? Let's decompose the reasoning of Windows to find what to execute:

  1. There is a space after Super, it must be the name of the program

  2. Does C:\Super.exe exist? Nope.

  3. Then it must be a folder. Does "Super Calc" exist?

  4. Yes, execute "C:\Super Calc\calc++.exe".

This means that if an attacker has the right to create a file in C: he can create the file "C:\Super.exe" and this file will be executed instead of "C:\Super Calc\calc++.exe" by the script. If the script isn't run with the same privileges as the attacker it is a privilege escalation.

This unquoted path vulnerability is a great example of what can go wrong when relying on smart systems.

Keywords: path import loadlibrary filename dll so

Conclusion

This covers the most common issues leading to privilege escalations. We talked a great deal about files because that's a common point of failure but note that it could very well be any other kind of resource.

Privilege escalations based on memory corruption generally happen the same way: some privileged thread or process executes a function pointer that the attacker swaps with his own using a bug such as a buffer overflow or a use-after-free.

Here are some good examples of privilege escalations on real software: