PHP代码审计之taocms
环境搭建:
代码审计
首先我们来分析该系统的路由信息,以及如何进行参数的构造。
路由分析:
该系统有两个路由,一是前台功能点路由,二后台功能点路由,但两个路由代码类似只不过后台路由添
加了session校验,我们先来看看前台路由是怎么构造的。
前台路由放在api.php文件中,在代码3、4两行包含了数据库配置文件以及通用的配置文件。
在common.php中22行代码处中调用 __autoload() 魔术方法来加载 Model 文件夹下的功能代码,方便
后续路由的调用。在代码30行去除 get_magic_quotes_gpc() 方法对特殊字符加载的反斜杠,这可能是
为了代码有更好的兼容性吧。
代码5、6两行传入两个参数 ctrl 、 action ,第7行代码其实就是将 action 传过来的参数首字母转换为
大写,因为类名首字母都是大写的,第8行判断该类是否为 Api 或 Comment 如果不是的话就结束代码的
执行了,在这里就是防止通过路由调用后台的功能点代码实现未授权的功能点调用。最后的代码部分就
是将 $m 也就是 action 传过来的类名实例化,然后调用 ctrl 传过来的参数,也就是类中的方法实现功
能的调用。
后台路由代码 admin.php 文件与前台路由代码基本类似,只是在上面添加了session校验,检测是否为
登录状态。
漏洞审计
1.任意文件读取/下载
通过上面的路由信息我们知道功能点文件存放在Model文件夹下,我们去翻找Model文件夹发现
file.php 文件也就是File这个类下存在一个 download() 方法,我们把一些用不到的类先折叠起来方便
查看。
在这个类中的第85行代码处,我们一目了然的看到了 file_get_contents() 函数,看到这个函数想要
利用,我们会下意思的想到两个点:第一该函数的参数是否可控;第二该函数是没有回显的,如果想要
利用是需要使用 echo 等函数配合。显然这里有 echo ,那么第二点已经满足,那么我们只需要查看这里
file_get_contents() 中参数是否可控就可以了,通过观察发现其中的 realpath 参数在代码第10行
的 __construct() 中进行过赋值的,而在其中拼接的path参数也是在$_REQUEST中获取的,那么这里
的第一点我们也满足了,只要传入path我们就可以实现任意文件读取了。
漏洞复现:
由于在上面我们已经分析过路由的构造,所以我们可以不用特意去找功能点就能构造出利用路由。
在路由中 action 传入的是我们要实例化的类名 file , ctrl 则对应我们需要调用的方法 download ,
在路由实例化 file 类时调用构造方法 __construct() ,然后我们传入的 path 也会被带到方法中从而
读取到了ceshi.txt中的内容。
2.任意文件上传
首先我们去创建一个.php后缀的文件
通过这里我们发现 executeupload() 方法中调用了Upload类下的 upload() 方法,这里的上传主要调
用了upload()方法,我们主要去看下他是如何进行过滤的。
在该方法的最上面定义了 $upext 变量,这里包含了可以上传的后缀名,也就是白名单,大致看了这些
后缀没有可利用的。然后下面通过 $_FILES 接收上传文件,通过 pathinfo() 获取上传文件名。关键在于代码106行通过 [extension] 获取后缀名,然后到代码107行进行正则匹配如果上传的文件名
不在 $upext 白名单中,则返回下面的提示信息。这里的 pathinfo() 函数是没有办法绕过的,所以这里
绕过检测进行上传还是比较难的。
这里上传是走不通的,但是在上传的右边有一个创建文件的功能点,我们发现这里竟然没有限制可以上
传任意文件,
在 create() 方法中,首先接收文件名 name ,然后通过 isdir 来判断创建的是目录还是文件,然后分别
做不同的操作进行创建。这段代码简洁明了,可以一目了然的看出并没有对后缀进行校验。
然后我们在看看他是如何进行文件写入的,其实下面的功能点就可以直接写入文件内容(这后台确实很离
谱.....)。
其实这里写入内容的代码也在 File 这个类中,在 save() 方法中只是对该文件是否具有写入权限进行判
断,就直接将内容写入到文件中。
漏洞复现:
3.mysql日志文件getshell
在Sql类下的 excute() 方法,依旧的简洁明了。通过14行传入 $sqltext 参数也就是我们的SQL语句,
在15行实例化 Dbclass 类调用其中 query() 方法直接执行SQL语句。最后18行将我们SQL语句结果进行
打印输出。
漏洞复现:
MySQL日志文件getshell
Mysql 5.6.34版本以后无法通过into outfile、into dumpfile进行文件写入
我们通过日志文件写shell即可
set global general_log = on;
set global general_log_file = '网站绝对路径';
这里的网站绝对路径,在这个系统中是可以轻松拿到的。
4.通过修改配置文件getshell
在后台有这么一个 网站设置 功能点,大致一看这里的内容和config.php文件中内容是一致的,猜测该功能
点可以直接修改config.php文件内容。这里我们应该想到需要需要满足的两个条件,第一config.php是
否是真的可以写入内容,第二写入的config.php文件是否可以访问。
首先我们去查看代码该功能点代码,这里调用 upload() 方法,第53行直接判断config.php是否可写,
然后通过POST接收参数,但是这里参数值会被57行代码处的 safeword() 方法进行过滤,跟进该方法看
看是如何对输入内容进行过滤的。
在 safeword() 方法中需要传入两个参数,一个是需要过滤的字符串,另一个参数则决定走哪个case。
上面没有给出第二个参数则直接走默认level,也就是154行下面的代码,在155行判断了数据库类型是否
为 Sqlite 是的话执行Sqlite的过滤代码,如果不是则走158行的else,,调用 Base::_addslashs() 方
法,跟进该方法。
该方法很简单,将传入的字符通过 addslashes() 函数将特殊字符添加反斜杠,这里没办法绕过。
所以我们只能走上面的if条件,这里只要数据库为 Sqlite ,下面的单引号会被替换为两个单引号(当时以
为将单引号替换为双引号了),而这个替换方式是可以被绕过的。
然后我们返回 upload() 方法的第60行,直接将过滤后的内容通过 file_put_contents() 写入到
config.php中。
上面通过分析源码,我们知道输入的单引号会替换为两个双引号,如果我们输入 \' 这样在替换为两个双
引号的时候第一个双引号前会有一个反斜杠,那么我们就可以闭合前面的双引号,我们的PHP代码就能
逃逸出来。我们的payload可以构造为:
\');@eval($_REQUEST[1]);/*
5.缓存文件getshell
我们在搜索危险函数的时候发现一处很有可能getshell的地方,我们先看这里的$arrayData是否可控。
这里代码37行的 $o 也就是对应代码49行的 $cat 数组中的内容是从数据库中 cms_category 表中获取
的。那么如果这里表中的内容是我们可以控制的那么就能写入任意内容。我们去看下该表中的内容从数据内容可以看出这里的功能点其实就是 管理栏目 中的内容,这里就可以添加内容。这里通过 columsdata() 接收参数,然后通过 add_one() 进行数据插入这里还是通过 safeword() 进行数据的过滤的,但是这里的 safeword() 方法在后续并没有起到过滤的
效果。
梳理一下这里我们的模板文件的内容可以控制了,但是我们 file_put_contents 的模板文件名为
cat_array.inc 并不能解析,这里需要找到一处可以包含该缓存文件的地方。通过搜索该文件可以发现有好几处包含了该模板文件,所以我们这里就可以通过写入缓存文件getshell
了。
cat_array.inc 文件内容如下
然后我们访问刚才包含该文件的路由
6.sql注入
本次主要还是想学习一下sql注入的一些姿势点,造成sql注入的代码原理,所以主要是审计的注入。望大师傅们给点建议。
熟悉cms
直接先进入admin.php和index.php
发现index.php有点难以理解。但是大致可以通过函数名和语义分析出是根据一些变量或者一些路径来渲染,加载模板文件,随后回显到前端。再看admin.php,发现存在action参数和ctrl参数。
发现有两个方法,class_exists和method_exists,这两个函数是判断是否存在类和方法的,接下if内的语句判断,指导action是类名,ctrl是函数名,有点像路由
题目搜索发现处理数据库请求的类为cms方法为lists
直接搜索跟进
找到执行sql语句的函数,跟进发现有个存放sql方法的文件阅读代码,发现有过滤函数对变量进行了一些过滤处理。
阅读代码,发现有过滤函数对变量进行了一些过滤处理。
发现对输入做了处理,不过只有为6的时候才会这样处理,对敏感的字符进行处理,如替换,进制转换等等。
注入点存在处
最后找到几个未过滤的函数方法:delist、getquery、updatelist、get_one、getlist
那就值针对这几个方法看
审计开始
先看第一个函数delist,看到有三个文件有这三个函数,先看第一个
delist
Article类
看到传入的参数有表名、参数id,以及where参数,用于筛选匹配数据在后台管理系统中没看到该模块的调用,然后看CMS类的时候发现CMS继承了Article类,所以看CMS类就好了
Category
这里可以看到仍然没有对id进行过滤,直接使用sleep(5)延时,看到burpsuite返回的时间是5s,符合执行语句,这次只有一次数据库操作,所以返回时间没啥变化
CMS
跟Article类是一样的语句所以其实是通杀
测试bool盲注,对语句进行拼接,看参数知道是id
payload:27) or 1=1#发现回显没啥特征进行,采用延时确认,发现成功延时12s,我们的语句写的是4s,说明经历了三次注入,后续再研究
这里payload使用--+是失败的,还是需要用#号,不是很明白
直接上sqlmap,注入出当前user,使用的一些参数
-v 3 --level 5 --risk 3 --random-agent --current-user --technique T --dbms mysql -p id
继续跟进代码,看看为什么产生了三次延时,看看createtag方法
可以看到传参值也是id,而id在加入sql语句前也没有进行安全处理,只针对tags参数进行过滤,但是我们这个删除执行很明显没有传递tag参数,所以走的是下面的else语句,成功拼接到语句中,根据之前阅读的方法知道,delist、getquery、updatelist、get_one、getlist这几个函数中没有对输入值进行过滤,执行我们的payload。
这里有两条语句都拼接了所以一共延时了3次
getquery
可以看到该方法没有合适的调用,都是在一些模板文件中用于加载数据,所以直接放弃
updatelist
category
看到这里的调用,发现是经过这个add_one处理过的,不是传参的那个status
add_one处理POST传输的数据,对数据了过滤转义,然后返回值,所以不存在注入
而在update函数中,没有进行数据数据过滤处理,有可能存在注入
payload:1) or sleep(4)#getlist
category
根据参数知道id对应的参数为$where参数,对应的同样没有过滤,直接打入payload
延时4s
Admin
全局搜索getlis,在admin.php中找到edit方法存在getlist的调用,并且能够可控参数
那么直接抓包修改id值,注意不能用or,我用or这个payload打的时候没触发sleep()函数,因为or是代表或的意思,而这里id=2,2是存在的,所以就不执行sleep函数,就像命令中的“||”符号。所以用and直接一起执行。
payload:2 and sleep(5)%23
执行成功,直接延时5s
Category
可以看到也是继承的Article,注入位置也是相同的
payload:1 or sleep(5)%23
Cms
筛选到cms类中的updateurl方法存在该函数调用,分析前后发现$addsql参数是由$id参数组合而成的,那么也很明显的存在注入,id没有经过处理。
延时成功
lists方法也存在注入点,继续发现getlist语句的参数由$addsql控制,而该参数能够拼接,发现name参数被用安全方法过滤了危险字符,所以主要看cat和status参数。,在save方法。
这个方法我一直想知道在哪个地方调用,我是用ctrl调用也不行,然后发现他是通过传参调用的,在save方法找到该方法的调用。
读源码的时候发现这个tags参数进行了safeword过滤,但是等级只有3级,没有用最高级的,所以没有对输入做到完全过滤的方法,tags还是能进行注入
根据语句:$tagdata=$this->db->getlist(TB."relations","name='".$tag."'","id,counts",1);可以知道是字符型注入,需要闭合单引号。
payload:test'+or+sleep(2)%23
这个位置也有一个注入点
6.install处getshell
判断是否有POST传入db_name,如果有的话就会赋值给$db_name参数,如果没有就会赋值默认的值,跟进
可以看到这里先调用file_get_contents读取了配置文件当中的内容,接着调用了str_replace将默认值替换成了POST中传入的参数值,这里其实三个参数都能够写入shell文件,这里对db_name进行写入shell;除此之外这里还会在data文件夹下生成install.lock锁文件,作为是否安装过的判断条件
很容易就得到了poc,只需要进行闭合符号,再插入shell就可以了
db_name=|127.0.0.1:3306|root|123456|taocms|');assert($_REQUEST['cmd']);//
7.任意文件删除
先给出poc
?action=file&ctrl=del&path=filepath
先会调用Base类中的catauth方法对$action参数进行判断,之后会判断是否存在相应的类,如果存在的话就实例化该类并赋值给$model,并且会判断$ctrl方法是否存在于$action类中,存在的话就会调用类中无参方法
include/Model/Base.php#119,通过调试发现$_SESSION[TB.'admin_level']=admin,所以返回值为true恒成立,所以上面的代码逻辑会接着往下走
根据poc,继续来跟代码
传入的$action=file,定位到类文件include/Model/File.php
根据File类的构造方法,以及前面传入的参数,$id是可控的,但是没有赋值默认为0,$table即是$action=file,接着这里会对指定文件的真实路径进行拼接,这里的SYS_ROOT就是整个项目的绝对磁盘路径,这里是D:/phpStudy/WWW/
往下看到调用的del方法
这里会对指定绝对路径要删除的文件的全选进行判断,并且如果是文件夹的话会遍历文件夹并判断文件夹是否为空,之后就会直接进行删除的操作,加上目录穿越就可以进行任意文件删除了。
8.SQL Injection
poc
?name=-1%"+union+select+group_concat(table_name)+from+information_schema.tables+where+table_schema%3ddatabase()%23&cat=0&status=&action=cms&ctrl=lists&submit=%E6%9F%A5%E8%AF%A2
根据poc来进行分析
include\Model\Cms.php#112
name,cat,status三个参数都由GET传入,都可控,直接来看调用的DB类中的getlist方法
include/Db/Mysql.php#60
调用的方法除了前三个参数是由前面调用时传入的参数覆盖的,其他两个参数为默认值,调试输出了最后的sql查询语句
select count(*) from cms_cms where 1=1 and name like "%-1%" union select group_concat(table_name) from information_schema.tables where table_schema=database()#%" ORDER BY id DESC limit 20
这里sql执行完之后会调用Base类中的magic2word方法,对结果是否为数组进行判断,如果是数组就会存入新的数组并且返回赋值给$datas数组,打印该数组可以发现注入的语句已经成功执行并返回了结果
之后就可以修改语句进行爆字段值等操作了,由于这里本身是不会有返回结果的,所以需要进行盲注脚本碰撞或者用sqlmap才能够进行利用,还有别处sql注入就不一一分析了。
备注:上述漏洞均已在最新版修复,在github相应的issue下cms作者已作出回复。
总结
通过这次的cms审计学习,发现一些通用的模块方法非常需要注意漏洞的产生,如果这些模块方法中有漏洞点,那么很多的方法、功能模块都会出现漏洞,扩大危害
- 点赞
- 收藏
- 关注作者
评论(0)