zl程序教程

您现在的位置是:首页 >  后端

当前栏目

PHP代码审计之taocms

PHP代码 审计
2023-06-13 09:15:18 时间

原文首发在:先知社区

https://xz.aliyun.com/t/12068

环境搭建:

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

路由分析:

该系统有两个路由,一是前台功能点路由,二后台功能点路由,但两个路由代码类似只不过后台路由添 加了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中。

通过分析源码,我们知道输入的单引号会替换为两个双引号,如果我们输入 \' 这样在替换为两个双引号的时候第一个双引号前会有一个反斜杠,那么我们就可以闭合前面的双引号,我们的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

发现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)延时,看到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级,没有用最高级的,所以没有对输入做到完全过滤的方法

这个位置也有一个注入点

7.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']);//

8.任意文件删除

根据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即是

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

9.SQL Injection

根据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数组,打印该数组可以发现注入的语句已经成功执行并返回了结果

REF

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