Post

PATH hijacking: A way to lead to PrivEsc

PATH hijacking: A way to lead to PrivEsc

Originally published on Medium.

What is the PATH variable ?

As you may already know, the PATH variable contains a directory’s list separated by : (Linux/maxOs) or ; (Windows). You can check its content by typing the following command on a terminal :

1
echo $PATH

When we execute a command like ls, ps or nano, the shell looks for the executable in the order of the directories listed in PATH variable. That’s how the magic trick appears.

What is the PATH hijacking ?

It exploits writable directories in the system’s PATH environment variable. Therefore, it allows to execute malicious binary instead of legitimate one. How is it exploited by an attacker ?

  1. An attacker finds a writable directory that appears before legitimate system directoris in PATH variable.
  2. The attacker create and put a malicious executable, holding the same name as a legitimate binary (e.g. ls, sudo, nano, …) in this writable directory.
  3. When a script (or a user) executes this command, system will launch this malicious binary first, instead of the legitimate one, located farther in the PATH variable.
  4. The malicious binary runs with the permissions of the current user (or higher, if misconfigured).

This technique can be used to gain root privilege. It is not the command itself that is vulnerable, but the environment in which it is executed. If a writable directory is in the PATH, and it is at the top of the list, then any command without an absolute path can be hijacked. This become dangerous if a root script or service uses this misconfigured PATH.

Attack example

I wrote a small script in c++ to retrieve writable directory located in the PATH variable of the current environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bool isOwnedByRoot(const string& dir) {
    struct stat info;
    if (stat(dir.c_str(), &info) != 0) {
        return false;
    }
    return info.st_uid == 0;
}

vector<string> getParsePaths() {
    vector<string> paths;
    const char* pathEnv = getenv("PATH");
    if (!pathEnv) {
        cerr << "Error : PATH not defined." << endl;
        return paths;
    }

    string pathStr(pathEnv);
    istringstream ss(pathStr);
    string token;

    while (getline(ss, token, ':')) {
        paths.push_back(token);
    }
    
    return paths;
}

vector<string> getFilterPaths(const vector<string> paths) {
    vector<string> filterPaths;
    for(auto& path_from_vector : paths) {
        path dir_path = path_from_vector;
        if (exists(dir_path) && is_directory(dir_path)) {
            perms p = status(dir_path).permissions();
            if ((p & perms::owner_write) != perms::none ||
                (p & perms::group_write) != perms::none ||
                (p & perms::others_write) != perms::none) {
                if (!isOwnedByRoot(path_from_vector)) {
                    filterPaths.push_back(path_from_vector);
                }
            }
        }
    }
    return filterPaths;
}

int main() {
    vector<string> pathDirs = getParsePaths();
    vector<string> filterPaths = getFilterPaths(pathDirs);
    cout << "Possible path hijack found (" << filterPaths.size() << "): " << endl;
    for (auto& dir : filterPaths) {
        cout << " - " << dir << endl;
    }
    return 0;
}

By running this script on my linux environment, I manage to retrieve writable directories.

Press enter or click to view image in full size

Figure 2: Retrieve writable directories from PATH variable

As you can see my script provides me with different directories. If we want to leverage the ls command, we can suppose that the legitimate binary is located in the /usr/bin path. We can locate the ls binary by typing the following command :

Figure 3: Locate ls binary

As /home/user/.local/bin and /home/user/.cache/bin are located before the /usr/bin directory in the PATH variable, we can then leverage both of them to lead to our exploit. Let’s use the first one, /home/user/.local/bin.

We can create a malicious binary called ls (as we want to hijack the ls command, we need to set our binary with the same name), containing whatever we want. For this example, I am just going to display a message in the terminal. However, this technique can be used to gain root privilege if the command is executed by a user or a service with root permission.

Press enter or click to view image in full size

Figure 4: Create a malicious binary

Now that we have create or malicious ls binary and that we have provided it with execution permission we can execute the ls command in the terminal.

Press enter or click to view image in full size

Figure 5 : Execute the exploit

As you can see, the attack worked well as the legitimate ls binary is not executed anymore by the system environment.

Mitigations

How to protect from this attack ?

  • Never include writable directories in the PATH, especially at the beginning of the string.
  • Always use absolute paths in system scripts (e.g. /usr/bin/apt instead of apt).
This post is licensed under CC BY 4.0 by the author.