前言 MVC框架是代码审计必需学习的知识,这里以TpV3。2。3框架为例,进行一次对MVC框架代码的漏洞审计,简单学一下MVC的相关知识,希望对正在学习MVC框架的师傅有所帮助。框架 我们这里首先需要了解一下什么是MVC架构,M:Model(模型),其负责业务数据的处理和与数据库的交互V:View(视图),提供了展示数据的各种方式C:Controller(控制器),负责用户请求的调度和处理业务逻辑 具体如下图 如果想对MVC框架进行进一步了解,可以参考https:www。kancloud。cnmanualthinkphp1698 接下来我们需要了解一下ThinkPHP框架。TP3 如果想了解Tp3常见的操作,可以看一下这篇文章 https:blog。csdn。netspc007spcarticledetails103489711目录文件 Tp3的目录如下所示 wwwWEB部署目录(或者子目录) index。php入口文件 README。mdREADME文件 Application应用目录 Public资源文件目录 ThinkPHP框架目录 通俗的说的话,这里的index。php就是提供一个对外的接口,Public就是存放一些公共资源的地方,ThinkPHP是我们的核心框架,其内容如下: ThinkPHP框架系统目录(可以部署在非web目录下面) Common核心公共函数目录 Conf核心配置目录 Lang核心语言包目录 Library框架类库目录 Think核心Think类库包目录 Behavior行为类库目录 OrgOrg类库包目录 Vendor第三方类库目录 。。。更多类库目录 Mode框架应用模式目录 Tpl系统模板目录 LICENSE。txt框架授权协议文件 logo。png框架LOGO文件 README。txt框架README文件 ThinkPHP。php框架入口文件路由格式 Tp3提供了多种路由格式,这里的话对其进行简单介绍。pathinfo模式 pathinfo模式,是ThinkPHP的默认模式,其规范格式如下:http:网址index。php模块控制器操作方法参数参数值 示例如下http:127。0。0。1:8080index。phpHomeIndexindexid2 它的优点显而易见,简化了URL地址。普通模式 普通模式的规范格式如下http:网址index。php?m模块名称c控制器a方法参数参数值 示例如下http:127。0。0。1:8080index。php?mHomecindexaindexid1 兼容模式 兼容模式的规范格式如下http:网址index。php?s模块名称控制器方法参数参数值 示例如下http:127。0。0。1:8080index。php?sHomeindexindexid33 rewrite模式 这个的话首先需要说一下配置,这个想要使用首先需要Apache开启rewrite拓展,我这里的环境是phpstudy集成环境,具体操作如下。 首先打开phpStudyPHPTutorialApacheconfhttpd。conf文件,搜索rewritemodule将其前面的删去 而后去TP的根目录,写一个。htaccess文件,内容如下IfModulemodrewrite。cRewriteEngineonRewriteCond{REQUESTFILENAME}!dRewriteCond{REQUESTFILENAME}!fRewriteRule(。)index。php?s1〔QSA,PT,L〕IfModule 接下来重启phpstudy 此时就配置好了。(如若是linux环境,可参考此文https:blog。csdn。netzhazhajiarticledetails80493513) rewrite的规范格式如下http:网址模块控制器操作方法参数参数值 其实相比于默认模式就是少了个入口文件,看着更简洁了一些。 示例如下http:127。0。0。1:8080Homeindexindexid2 特殊方法 ThinkPHP将一些经常使用的方法进行了封装,也就是我们这里的特殊方法,其目的在于使程序更加安全。 接下来对几个相对较常用的方法进行简单介绍。 如果想进行仔细了解,可以访问如下链接 https:www。cnblogs。comkenshinobiyp9165662。html https:www。thinkphp。cninfotagI方法 I方法是ThinkPHP用于更加方便和安全的获取系统输入变量,可以用于任何地方,用法格式如下:I(变量类型。变量名修饰符,〔默认值〕,〔过滤方法或正则〕,〔额外数据源〕) 示例如下echoI(get。id);等同于GET〔id〕echoI(get。id,0);如果不存在GET〔id〕则返回0echoI(get。name,);如果不存在GET〔name〕则返回空字符串echoI(get。name,,htmlspecialchars);采用htmlspecialchars方法对GET〔name〕进行过滤,如果不存在则返回空字符串M方法 M方法用于实例化一个基础模型类,M方法的调用格式:M(〔基础模型名:〕模型名,数据表前缀,数据库连接信息) 示例如下UserM(User);等效于UsernewModel(User);C方法 C方法是ThinkPHP用于设置、获取,以及保存配置参数的方法。modelC(dbname,thinkphp);;读取当前的URL模式配置参数userTypeC(USERTYPE);获取USERTYPE参数的值漏洞分析SQL注入环境搭建 首先我们需要做一些配置。 我们需要在本地Mysql中新建一个thinkphp数据库用于测试,然后在其中新建一个users数据表,包括id、username、passwd三个字段 接下来我们需要让Tp与Mysql中的数据库进行对接。 打开ApplicationHomeConfconfig。php,写入以下内容lt;?phpreturnarray(DBTYPEmysql,数据库类型DBHOSTlocalhost,服务器地址DBNAMEthinkphp,数据库名DBUSERroot,用户名DBPWDroot,密码DBPORT3306,端口DBPREFIX,数据库表前缀DBCHARSETutf8,字符集DBDEBUGTRUE,数据库调试模式开启后可以记录SQLahrefhttps:www。bs178。comrizhitargetblankclassinfotextkey日志a3。2。3新增); 此时环境便搭建好了。where注入 打开ApplicationHomeControllerIndexController。class。php,添加内容如下publicfunctionselect(){idI(get。id);userM(users);datauserfind(id);vardump(data);} 此时先在I方法处添加断点 接下来我们写入我们的语句http:127。0。0。1:8080index。phphomeindexselect?id〔where〕1and1updatexml(1,concat(0x7e,user(),0x7e),1) 访问之 然后开始单步调试 一开始都是赋值这种,后面看到这里 可以发现有一个htmlspecialchars函数过滤,不过这个主要是针对XSS的,所以对SQL注入影响不大,接着看,到最后 可以发现这里的value就是我们写入的语句,即1and1updatexml(1,concat(0x7e,user(),0x7e),1) 然后这里的话他过滤的关键词是if(pregmatch((EXPNEQGTEGTLTELTORXORLIKENOTLIKENOTBETWEENNOTBETWEENBETWEENNOTINNOTININ)i,value)){value。;} 显而易见,过滤的很少,报错注入的updatexml和extractvalue,以及联合查询的union也未被过滤,所以这里也可以使用其他语句,例如http:127。0。0。1:8080index。phphomeindexselect?id〔where〕1and1extractvalue(1,concat(0x7e,user(),0x7e)) 亦可使用联合查询http:127。0。0。1:8080index。phphomeindexselect?id〔where〕0unionselectuser(),2,3 接下来调整断点,将断点放在find处,进行单步调试 跟进 可以看到这里是判断数值是否为数字字符串串或字符串,满足的话就走if条件下的函数,不过看明显可以看出我们这里是数组(看旁边的options的值为array(1)也可以发现),不满足条件,所以直接走下面。 走到这个pkthisgetPk(); 跟进 在上面发现pkid 接下来继续往下走 这里检验了pk是否为数组,因不满足条件(此处pkid),所以直接走下面 接下来是添加limit1,然后这个用了parseOptions函数进行处理,跟进此自定义函数 这里的话可以看到有一个过滤的,但我们这里的话简单看一下就会发现,这里条件并不满足,我们这里的where的值是0unionselectuser(),2,3 所以这里的话就直接pass,接下来继续走 这些就是一些查询语句,然后将结果返回,接下来到最后 这里进入parseWhere方法 我们这里的where是字符串,所以走if语句,将where的值赋给whereStr 可以看到这里是直接返回了whereStr,没有用过滤函数什么的处理,因此最终返回的仍是我们传入的 具体SQL内容如下SELECTFROMusersWHERE0unionselectuser(),2,3LIMIT1exp注入 测试这个的话需要简单修改一下我们的select()方法,修改后内容如下publicfunctionselect(){maparray(idGET〔id〕);userM(users);datauserwhere(map)find();vardump(data);} 这里需要说明一下,之所以不用I方法,是因为I方法中存在过滤,即thinkfilter函数,其内容如下if(pregmatch((EXPNEQGTEGTLTELTORXORLIKENOTLIKENOTBETWEENNOTBETWEENBETWEENNOTINNOTININ)i,value)){value。;} 可以看出这里过滤了exp,所以不能直接用I方法来写,接下来我们访问网页,写入如下payloadhttp:127。0。0。1:8080index。phphomeindexselect?id〔0〕expid〔1〕1andupdatexml(1,concat(0x7e,user(),0x7e),1) 同时开启调试,单步跟进 这里的话我们的where是数组,所以条件不满足,直接pass,第二个if同理,然后第三个的话,我们这里并未对optinos〔where〕进行赋值,所以就会走else,把where的值赋给他,接着看 到find方法这里,第一个if语句,我们这里options为数组,所以直接pass,第二个if语句,我们的pk值为id,而非数组,所以也pass,到下面跟之前一样,参数被函数parseOptions包裹,接下来跟进这个函数 这里有一个字段类型验证的,我们可以看到这个是满足条件的,所以他会进入下面这个函数,我们首先这个语句if(isscalar(val)){ 这里的isscalar是检验变量是否为标量,什么是标量,官方文档如下 我们这里的val变量值如下 是Array,因此不会进入这个if语句,即不会进入parseType方法,这里直接pass,接着看下面 第一个是执行查询语句返回结果的,第二个是返回预编译的语句,继续跟进 我们这里用了where,跟进parseWhere方法 可以看到这里是拼接,继续往下看 最终返回的是拼接的结果,接下来走到最后,得到SQL语句 如下SELECTFROMusersWHEREid1andupdatexml(1,concat(0x7e,user(),0x7e),1)LIMIT1bind注入 更改内容如下publicfunctionselect(){UserM(users);user〔id〕I(id);data〔passwd〕I(passwd);valuUserwhere(user)save(data);vardump(valu);} payload如下http:127。0。0。1:8080index。phphomeindexselect?id〔0〕bindid〔1〕0andupdatexml(1,concat(0x7e,user(),0x7e),1)passwd1 接下来开启xdebug,然后访问 这里与之前相同,因为是数组,所以让前三个if语句,直接到if(isset(thisoptions〔where〕)) 这里,因为没有设置这个options〔where〕,所以走下面,将where的值赋给他,然后将值返回,接下来继续看 我们这里的data是一个数组,是有值的,所以if(empty(data)) 这个不满足条件,直接看下面,可以看到这里有facade对data进行了处理,跟进这个函数 可以看到这里先是检验了是否有fields是否为空,然后进入if(!empty(thisoptions〔field〕)){ 因为这里并不存在options〔field〕,所以直接pass,走else那里,即将fields值赋给这里的fields,接下来是foreach语句 因为data中的passwd在fields中,所以走下面,即elseif那里 这里的var为1是标量,所以满足条件,接下来data被parseType函数处理,跟进这个函数 可以发现这里就是对内容进行了intval处理,没什么影响,继续跟进 这里的话是使用了filter函数对内容进行了一次过滤,跟进 接下来到parseOptions函数 接下来这里以为val是数组,使用不会进入parseType方法,出来该方法后,到这里 跟进update方法 发现有parseSet方法,跟进此方法 可以看到这里拼接了:,此时的SQL语句为UPDATEusersSETpasswd:0 接下来进入parseWhereItem方法 这里可以看出当expbind时,whereStr是可控的,而后得到拼接后的语句,此时的SQLUPDATEusersSETpasswd:0WHEREid:0andupdatexml(1,concat(0x7e,user(),0x7e),1) 接下来到execute执行函数这里 重点在于这里if(!empty(thisbind)){thatthis;thisqueryStr(thisqueryStr,arraymap(function(val)use(that){return。thatescapeString(val)。;},thisbind));} 这个strtr函数在这里的话就是起到替换作用,比如我们这里,我们传入的是0(payload中的id〔1〕0),那么这里就会拼接变成:0,而这个strtr函数将其替换为1 此时也就得到了我们最终的SQL语句UPDATEusersSETpasswd1WHEREid1andupdatexml(1,concat(0x7e,user(),0x7e),1)命令执行环境搭建 环境配置,我们首先需要在ApplicationHomeController新建一个文件,用之前SQL注入的亦可,这里就用之前的了,修改文件内容如下lt;?phpnamespaceHomeController;useThinkController;classIndexControllerextendsController{publicfunctionindex(value){thisassign(value);thisdisplay();}} 因为该漏洞利用的assign函数需要模板渲染,所以需要新建一个模板文件,模板文件位置如下ThinkPHPApplicationHomeViewIndexindex。html内容随意 这里还需要说明的是,日志和debug的关系 若开启debug模式日志会到:ApplicationRuntimeLogsHome下 若未开启debug模式日志会到:ApplicationRuntimeLogsCommon下 接下来开始复现一下,首先我们创建log文件ThinkPHPindex。php?mlt;?phpinfo();?接下来去包含log文件(这里log的文件名与年月日相关)http:127。0。0。1:8080ThinkPHPindex。php?mHomecIndexaindexvalue〔filename〕。ApplicationRuntimeLogsCommon230118。log 漏洞分析 这里之所以存在漏洞,其原因是 由于在业务代码中如果对模板赋值方法assign的第一个参数可控,则导致模板路径变量被覆盖为携带攻击代码路径,造成文件包含,代码执行等危害。 接下来我们在函数处打上断点 而后访问http:127。0。0。1:8080ThinkPHPindex。php?mHomecIndexaindexvalue〔filename〕。ApplicationRuntimeLogsCommon230118。log 开始单步调试 首先来到这个assign函数,这里的name是数组,其内容为我们的日志文件,可以看到这个函数里用了另一个assign函数来处理变量,跟进 这里判断name是否为数组,我们的name为数组,所以进入if语句,这里的arraymerge是合并数组的,但这里thistvar 为空,所以这里的话其实就是thistVarname,即将name变量的值赋给了tVar 继续跟进 接下来到display函数 同上个相似,这里是用了另一个同名函数来处理变量,跟进 我们这里模板内容为空,看到这里有fetch函数,跟进 首先判断了模板文件是否存在 而后检验使用的是否是PHP原生模板,系统配置的默认引擎是Think,所以这里走else 这里可以看到将thistVar的值赋给了params,而后进入了listen函数,跟进此函数 发现这里经过一些判断后进入了exec函数,跟进此函数 可以发现这里是调用BehaviorParseTemplateBehavior类中的run方法处理params,我们跟进run方法,寻找哪里对日志文件路径进行了处理,最终找到ThinkPHPLibraryThinkTemplate。class。php文件下的fetch方法 loadTemplate函数是读取文件路径的,而后这里用load函数对其进行了处理,我们跟进此函数 filename是之前获取到的的缓存文件路径,vars是带有变量filename的数组,这里的vars不为空,因此使用extract方法的EXTROVERWRITE默认描述对变量值进行覆盖。 最后include日志文件路径,造成文件包含,最终导致包含文件造成RCE。参考链接 https:www。freebuf。comarticlesweb345544。html https:www。freebuf。comvuls282906。html https:mp。weixin。qq。comsOqfruwHf9CAt2dQQfNJw https:forum。butian。netshare546 fromhttps:www。freebuf。comarticlesweb355571。html