PHP代码审计实战思路浅析
原创 Wnltc0 雷神众测
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
No.1
前言
每一次总结,都可以看作是对前面所学做一次“大扫除”,就像电脑硬盘,只有将那些缓存文件都清掉,才能腾出更多空间来存新的内容。这次的“大扫除”,以PHPCMS为例,以“通读全文”、“敏感函数回溯”、“定向功能点审计”为切入点,重新梳理一遍平时的审计思路。
望大佬们多多指教!
No.2
通读全文
通读全文!从字面意思来看,可以理解成把所有的代码都读一遍,把所有的代码都“读透”。那么问题来了,现在的cms大多数都是参照mvc模式进行开发,一个大的系统被拆分成视图(View)、模型(Model)、控制器(Controller)三个部分。这种软件架构虽然能使得程序的结构变得更加直观,但也只能说是逻辑上的直观,因为在使得程序结构变得直观的同时,也增加了系统结构和实现的复杂度,所以在审计这类cms时,很难通过目录结构或者直接搜索危险函数来进行快速审计。原因在于,基于mvc模式开发出来的cms不像面向过程化开发的cms那样,一个目录就是一个功能模块,一个php文件就是一个功能点。基于mvc模式开发的cms,会有一个统一的入口,所有的请求都从该入口进入,然后由框架统一进行调度,且程序中的一些可重用的操作,例如数据库操作、缓存、安全过滤等都会被封装成框架的核心类库,需要时再去调框架封装好的方法。如果在没了解程序的结构前,就贸然去跟某段代码,很容易迷失在一个个类库的调用链里。
那么,我们在审计这类cms时,首要目的就是,了解程序的基本结构。前面提到,基于mvc模式开发的cms,会有一个统一的入口,所有的请求都从该入口进入。如果我们想读懂这套源码,这个入口倒是个很好的切入点。在正式上手分析前,先来思考下,当一个请求进入这套程序的入口后,程序内部会对它做哪些操作。例如,这个url会怎么解析?程序内部是怎么将url与控制器关联到一起的?实际分析时会经常看到“Demo::test()”这种调用方式,但Demo类并不在这个文件内,而且文件内也没看到include或require,所以这些在程序内部又是怎么实现的呢?
俗话说,要用魔法来打败魔法。既然这是属于开发的东西,在正式分析前,还得来找开发取一下经。先来看下框架的运行流程。
入口文件,对应着前面说的统一入口,虽然说是统一入口,但实际情况中入口文件可能有多个,前台一个、后台一个、接口一个。入口文件的职责在于,定义一些全局性的常量,加载函数库,加载框架启动类等等。这里的自动加载类,说成类自动加载会更加合适,主要目的是,为了简化类加载的步骤,通常会将类文件的加载操作封装成一个函数或者方法,然后在框架启动前或者在框架启动时,通过spl_autoload_register将类加载函数注册到SPL__autoload函数队列中,当程序内的代码调用某个类时,php会调用类加载函数,类加载函数就会到正确的地方去把类库文件加载进来;当然也可以不使用spl_autoload_register函数来自动调用,而是在需要时手动去调用。再下一步,就是启动框架。框架启动后,可能会去加载一些配置文件,然后去调用路由类来解析请求url,根据解析的结果来调用相应的控制器,最后返回结果。
通常情况下,入口文件的位置在网站的根目录下,一般会命名成index.php、admin.php或者api.php。观察当前项目的目录及文件。
这里的index.php、admin.php、api.php和plugin.php都挺像入口文件的,这里选择先跟进index.php。原因是,一般情况下,前台的入口都叫index.php。
代码很简单,定义了一个常量,然后是加载一个php文件,最后调用pc_base类的create_app方法。入口文件、定义常量,那下面的pc_base很大可能就是框架启动类了,跟进到/phpcms/base.php。
往下翻,能看到很多定义常量,还有加载函数库的操作。
继续往下看,就看到了在入口文件中调用的pc_base::create_app方法
从注释来看,该方法的作用是初始化应用程序,继续跟进load_sys_class方法。
从注释来看,该方法用于加载系统类,继续跟进_load_class方法。
同样,也可以通过注释来了解该方法的作用。
_load_class方法用于加载类文件,对应着前面说的类自动加载,找到类加载函数后,继续分析类文件的加载逻辑。从代码中可得知,类库目录在phpcms框架目录下的$path目录,也就是/phpcms目录下,类文件名为“类名.class.php”,如果$path参数为空,则默认到/phpcms/libs/classes目录下去加载类文件,然后根据$initialize参数来决定是否实例化该类。前面的load_sys_class方法在调用load_class时,传入的$path参数是空的,且$initialize=1,也就是说,/phpcms/libs/classes目录下放的是系统类,且加载系统类库时会自动实例化该类。除了load_sys_class,还有load_app_class和load_model,三者的底层都是调用了_load_class,区别在于传入的$path不同。
除了加载类库的_load_class,还有加载函数库的_load_func,和加载配置文件的_load_config。
通过对类加载方法、函数库加载方法、配置文件加载方法的分析,就能找出程序的基本目录结构。
回到前面用来初始化应用程序的create_app方法。
跟进到application类
回顾前面提到的框架运行流程,从入口文件,到类自动加载,到启动框架,框架启动后,下一步就是路由解析,那param类很大可能就是路由类,继续跟进param类。
在param类的构造方法中会使用封装后的addslashes函数对$_POST、$_GET、$_COOKIE数组进行转义。再往后就是调用route_m、route_c、route_a方法解析路由,获取模块、控制器和方法名。
route_m方法获取m参数的值作为模块名,同理,route_c则是获取c参数的值,route_a则是获取a参数的值。同时还会调用safe_deal方法对传入的值做过滤。
路由解析完后,再下一步就是加载控制器。
继续跟进init方法。
继续跟进load_controller方法。
load_controller方法先到“/phpcms/modules/模块名/”目录下加载控制器文件,然后实例化并返回对象。
init方法通过load_controller方法获取到控制器实例后,通过call_user_func方法来执行具体的动作,最后返回结果,请求结束。
No.3
敏感函数回溯
当输入的数据,被当作代码去执行,或者会改变原本设定好的代码逻辑,就可能产生漏洞。拿SQL注入来说,当外部输入的数据被带到数据中执行,并且能通过构造特殊的内容来修改原有的SQL语句结构,就会产生SQL注入。如果我们想挖掘SQL注入漏洞,就应该把关注点放在数据库查询操作上,而PHP中能执行SQL语句的函数有mysql_query、mysqli_query等,这些函数就可以称为敏感函数。从代码中找到这些敏感函数的调用位置,然后去回溯其参数来源,如果来源可控,就存在漏洞。同理,如果想找代码执行漏洞,可以去找能执行代码的函数,例如assest、eval、call_user_func等;如果想找命令执行,就去找能执行系统命令的函数,例如system、shell_exec、popen等。通常的做法有,使用编辑器进行全文检索敏感函数的函数名,然后人工一个个去回溯参数来源,还可以使用一些代码审计工具来进行扫描分析,然后再手动去验证。为了节省时间,一般会先工具扫描一遍,快速找出那些比较明显的漏洞。
扫描结果里的代码注入看到倒是很吸引人,那就先从它入手。从代码来看,很明显的一个eval代码注入。这段代码存在于/phpsso_server/phpcms/libs/functions/global.func.php,从文件名和文件路径来看,string2array函数位于系统函数库中。从左下角的调用链来看,在/phpsso_server/phpcms/modules/admin/credit.php文件中调用了string2array,/phpsso_server目录下的结构于上面的分析的一样,根据上面的分析结果,可得知,credit.php是一个控制类文件。跟进到credit.php的94行。
在92行的位置,将ps_send函数的返回结果赋值给$res,观察左下角的调用链,发现最后调用了fread函数,同时结合传入给ps_send函数的参数名推测,该函数应该是用来发起http请求的,后续跟进分析,得到的结果也是。那creditlist方法的作用应该发起一个请求,将请求结果转换成数组,然后遍历输出。如果能控制响应结果,就能形成代码执行漏洞,而控制了请求url,就能控制响应的结果。
继续分析,在88行的位置,将 $applist[$appid][‘url’] . $applist[$appid][‘apifilename’] 赋值给$url。
在87行,将getcache函数的返回结果赋值给$applist。$appid由get传入,可控。如果getcache的返回结果可控,就有希望实现代码执行。跟进getcache,根据前面的分析,知道在框架启动前会加载系统的函数库,所以可以到系统函数库中寻找getcache函数。
先是加载配置文件,然后调用cache_factory::get_instance(),配置文件内容如下。
跟进cache_factory::get_instance
跟进get_cache方法
跟进load方法
load方法根据传入的缓存配置信息来决定是加载文件缓存还是memcache,而默认情况是加载文件缓存。回到前面的getcache函数,获取到缓存类的实例后,调用该类的get方法。
默认情况下使用文件缓存,所以该方法应该是位于cache_file类。
从get方法内的代码来看,缓存文件的路径在/caches/caches_模块名/caches_$type/下,默认情况下通过require引入并将结果复制给$data返回。
回到credit.php的creditlist方法。
87行,将getcache的返回结果赋值给$applist,getcache的调用链如下。
$applist的内容如下:
既然$applist来源于缓存文件,那如果能找到写入缓存的点,没准就能控制缓存的内容,配合前面的creditlist方法实现代码执行。找到写入缓存的方法为set,文件路径拼接跟get差不多。
通过回溯set方法的调用链即可找出写入缓存的点。
最后在applications控制器的add方法中找到写入缓存的点。
在applications类的add方法,39行处,将url写入数据库,回溯$url的来源,发现在26行处通过post传入。
确定url可控,接下来开始构造利用链。
漏洞利用过程:
1.访问http://192.168.195.1/phpsso_server/index.php,登录phpsso。
2.访问http://192.168.195.1/phpsso_server/index.php?m=admin&c=applications&a=add。
3.填入应用地址,既url,点提交,即可将url写入applist缓存。
添加成功后跳转回应用管理页面
在编辑按钮的请求url中可以得到刚刚添加的appid。
1.服务器启动一个php的server,并创建一个evil.php,内容如下:
2.访问http://192.168.195.1/phpsso_server/index.php?m=admin&c=credit&a=creditlist&appid=2即可触发代码执行。
No.4
定向功能点审计
定向功能点审计感觉更像是黑盒白盒的结合体,通过去浏览网站的功能,对可能存在问题的功能点的代码进行审计,检查是否存在问题。例如,发表留言、发送站内信这些功能,比较容易产生跨站脚本漏洞,审计时可以优先关注php代码中,数据在输入输出时是否有被html编码,如果没有,那就看下,有没有xss代码过滤,如果两者都没有,那很大可能就存在xss,如果有xss过滤,则可以通过分析过滤了那些特殊字符或关键字,如果能找到没有过滤的关键字,就能绕过过滤。前面分析框架运行流程时,对控制器和模型的存放位置,以及调用方式都有所了解,但mvc,除了m和c,还有个v。视图解析,这个贯穿全局的功能,一般情况下是不太容易出问题,但如果存在二次解析,那出现漏洞的概率就要高上不少。
如果不清楚哪个是模板解析类,可以选取一个功能点作为切入点,一步步定位到模板解析类,例如,首页。
其他代码不是我们的关注点,getcache函数是获取缓存的,seo怎么看也不像视图解析的,直接到系统函数库找template函数。
无关紧要的代码先忽略,优先找跟模板加载有关的代码。
在456行的位置,将template_cache类加载进来。然后到/caches/caches_template/下找模板文件,推测这里存的应该是编译后的模板文件。
如果$compiledtplfile存在则返回$compiledtplfile的路径给template函数进行包含。
如果$compiledtplfile不存在,则调用$template_cache->template_compile方法进行编译生成$compiledtplfile文件,然后再包含,跟进template_compile方法。
从代码中可得知,16-34行的主要作用是到/templates/default/模块/目录下读取模板文件。
然后调用template_parse方法解析模板内容,将解析后的结果写入模板缓存。继续跟进template_parse方法。
解析规则如上图,直接解析成php代码的不在关注范围内,原因是,通常情况下,这些标签内容都是不可控的。模板内容不可控的情况下,一般会把注意力放在,那些将解析后的结果交给其他方法再进行一次处理的的解析规则上。
例如91行,93行。91行将从模板中匹配到交给$this->addquote进行处理,但$this->addquote方法仅仅是将传入的内容再转义一下,没什么特别的地方,继续看93行处的self::pc_tag方法。
从注释来看,pc_tag会对前面解析到的内容再进行一次解析,同时还会在解析后的代码前面加入一段“死亡代码“。
当$op为block时,会生成以下代码。最后将二次解析后的代码写入模板缓存,然后在访问相关页面时去包含。
当上述生成的模板缓存被包含时,会去调用应用类block_tag的pc_tag方法,跟进block_tag->pc_tag方法。
该方法根据传入的参数到数据库中查询,然后将查询结果转换成变量,然后走到string2array,也就是前面那个代码注入的点,但这里存在漏洞的点不是这,原因在于,$data是从模板中提取出来的,而模板的内容我们是不可控的,继续往下走,进到$template_url方法。
如果$template变量不为空,则将$template的内容写入/caches/caches_template/block/$id.php
如果$template的值为空,则根据$id到数据库中取出template字段的内容,并写入/caches/caches_template/block/$id.php文件中,如果能同时控制$id和$template,或者能控制数据库中的内容,就可以构造一个恶意文件,然后利用上面的pc标签解析功能包含该文件,实现代码执行。
回到前面的模板解析步骤,分析具体的解析规则。
通过分析上面的正则可得知,pc标签的格式应该为{pc:xx xxx}。
在self::pc_tag方法中将$data按照116行的正则再进行一次提取,将提取的结果通过循环,写到$datas数组中。所以,pc标签的格式应该为{pc:xx xx=xx}。
当$op等于block时,将前面提取的$datas传入arr_to_html方法,转成array(key=>value)这种格式,然后生成以下代码。所以,如果想进入以下分支,pc标签的格式应该为{pc:block xx=xx}。
继续跟进$block_tag->tag方法。
发现会将$data[‘pos’]带入数据库查询,如果结果为空,就不会进入到包含点。所以,pc标签的格式应该为{pc:block pos=xx},通过搜索pc标签找到相关触发点。
根据前面的分析,可得知,触发点在link模块下,在link模块的index控制器中找到的register方法。
但仅有触发点还不够,前面提到,被包含的文件的内容从数据库中获取,所以还需要找到一个讲恶意代码写入数据库的点,或者找一处调用了block_tag->template_url方法的点,但id和template参数需可控。如果想找写入数据库的点,可以优先到与block_tag类位于同一个模块的block_admin类下找。
经过一番寻找,发现add方法有写入数据库的操作,但并没有发现写入template字段的代码。除了插入数据库,还可以找找更新数据库的点。
发现在block_update方法中执行了update操作,还会将template写入数据库,同时template还是从post传入。146行处虽然有调用template_url方法,但$id并不可控,原因是,在128行将id带入到数据库中查询,如果查询结果为空,则不会进入到下面的操作。而id在数据库中是递增的,未必能控制在[1、2、3]中。
但可以先利用add方法添加一条记录,然后利用block_update将恶意代码写入数据库,最后通过访问相关页面触发pc标签解析,生成并包含恶意文件。
漏洞利用思路:
漏洞触发流程:
漏洞利用过程:
先向数据库中添加一条记录,pos的值须在[1, 2, 3]中,type的值为2,pc_hash登录后自动生成,需替换成当前登录用户的。
URL:http://192.168.0.1/index.php?m=block&c=block_admin&a=add&pos=1&pc_hash=gh43rD
POST:dosubmit=&name=bb&type=2
然后更新数据库记录将恶意代码写入数据库,从上一条请求的放回结果中获取id。
URL:http://192.168.0.1/index.php?m=block&c=block_admin&a=block_update&id=1&pc_hash=gh43rD
POST: dosubmit=&template=\
访问http://192.168.195.1/index.php?m=link&c=index&a=register&siteid=1触发pc标签解析,包含恶意文件。
招聘启事
本站(www.100xue.net)部分图文转自网络,刊登本文仅为传播信息之用,绝不代表赞同其观点或担保其真实性。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系(底部邮箱),我们将及时更正、删除,谢谢