前言
无论进行什么语言的代码审计,首先第一步都是分析这份代码的路由结构,然后再进一步分析
路由分析
入口点分析
phpcms是⼀个⼀分为⼆的cms,有⼀套类似于应⽤的东⻄,包括phpcms,还有⼀套后台的管控中⼼,叫phpsso_server,在⼤多数情况下,它俩部署在⼀台主机上。
两套代码的路由结构是⼀样的,我们来看看phpcms的。
一般来说就是通过modules中的模块找到对应的controller触发路由。

对于PHP来说,直接访问文件也可以触发该文件代码的流程,但是这样的话我们就得在每个文件的头部都加上一个权限校验,这样非常不便于程序员的开发。

所以程序员会统一一个/几个入口点,只有从这些入口点进来的才能进行正确的访问,直接访问路径的会被退出

在统一了入口点的情况下,程序员只需要对入口点进行权限判断即可,大大节省工作量。
比如说这边都是通过index.php进来的,⽤这种⽅式来保障所有的路由都是从index.php进来,这样权限管控就⽐较好做了,以防有漏做权限控制的⻛险。

主入口包含base.php,然后设置IN_PHPCMS为true,其他的路由中会去判断该变量是否存在,存在才能访问


除了index.php还有其他的入口点,通过代码我们可以看出来他们也包含了base.php


可以看到存在三个入口点:index.php、plugin.php、api.php,但是主要入口基本是从index.php来的
正式路由分析
我们在网站随便点击会出现对应的路由结构

http://192.168.139.156:8000/index.php?m=member&c=index&a=register&siteid=1我们的首要任务就是搞懂这个到底是怎么进行路由映射的。
在base.php中可以看到都是一些基本配置的引入,那么我们可以不需要在这边花太多时间,去index.php走下一步

先看phpcms

发现引入了一个类,看看这个方法

一步一步走下来发现这边包含了一个文件,最后还进行了实例化new

通过动调看看这个路径是什么

进入这个类看看,发现这边又加载了一个param类并调用了其中的三种方法,同样的方式找到路径进去看看

实例化过程中对这些提交的数据包进行过滤


下面那些基本就是一些引入配置信息的内容

看看刚刚调用的三个方法:route_m、route_c、route_a




上面三个函数分别获取了GET请求中的值,现在进行初始化


$filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.$filename.'.php';根据这个构造我们可以大致知道m对应modules,c对应的是controller

直接包含对应的class,不断步入之后进入到这个方法

到这里可以看出a就是对应的controller中的事件
所以现在整个路由结构我们基本明确了
http://192.168.139.156:8000/index.php?m=member&c=index&a=register&siteid=1index.php是入口点,m是对应的module,c是controller,a是controller下的事件action
业务分析
我们现在已经把路由搞清楚了,接下来就是把业务弄明白。phpcms是一个一分为二的cms,每当有一些用户权限、用户信息相关的业务发生的时候,phpcms就会与phpsso_server进行通信。在这个通信的过程中我们看看它做了些什么
先看看phpsso_server的代码

这部分逻辑一模一样

与phpcms不同的点在于sso_server只有两个module

并且都调用了父类的构造函数


看看父类的构造函数有些什么东西
<?php
define('IN_PHPSSO', true);
class phpsso {
public $db, $settings, $applist, $appid, $data;
/**
* 构造函数
*/
public function __construct() {
$this->db = pc_base::load_model('member_model');
pc_base::load_app_func('global');
/*获取系统配置*/
$this->settings = getcache('settings', 'admin');
$this->applist = getcache('applist', 'admin');
if(isset($_GET) && is_array($_GET) && count($_GET) > 0) {
foreach($_GET as $k=>$v) {
if(!in_array($k, array('m','c','a'))) {
$_POST[$k] = $v;
}
}
}
if(isset($_POST['appid'])) {
$this->appid = intval($_POST['appid']);
} else {
exit('0');
}
if(isset($_POST['data'])) {
parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);
if(get_magic_quotes_gpc()) {
$this->data = new_stripslashes($this->data);
}
if(!is_array($this->data)) {
exit('0');
}
} else {
exit('0');
}
if(isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
$this->data['avatardata'] = $GLOBALS['HTTP_RAW_POST_DATA'];
if($this->applist[$this->appid]['authkey'] != $this->data['ps_auth_key']) {
exit('0');
}
}
}
}在这里面我们可以发现一个风险函数parse_str,这个函数存在变量覆盖的风险
if(isset($_POST['data'])) {
parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);
if(get_magic_quotes_gpc()) {
$this->data = new_stripslashes($this->data);
}
if(!is_array($this->data)) {
exit('0');
}
} else {
exit('0');
}但是这里面还有一个sys_auth的函数,根据观察是加解密函数,且没有硬编码,所以说我们暂时不能操控这个加解密算法

所以这边的逻辑就是将解密完毕的数据进行变量覆盖,但是目前加解密函数不可控,所以我们无法直接请求这个路由发送数据进行解密,我们先标记一下后续再看
既然他存有这个东⻄这个api,必然是phpcms⾃⼰能向他发送请求,从这个方法也能看出来

如果我们能在某个位置控制请求(我们与phpcms通信,phpcms在后端完成加密的动作,并且能把我们的参数传递到phpsso上),是否就可以跟phpsso通信了呢?
与phpsso进行通信
寻找交互点
那么哪里会出现这种通信点呢?肯定是和用户信息相关的操作了,我们最开始分析路由的那个注册页面就是一个跟用户信息相关的操作,进去看看有没有出现phpcms和phpsso交互的点

这是加载配置的点,下面这个就可能是交互点了,跟进看看

发现了auth_key


现在我们先继续调试,看看能不能不通过这个实现变量覆盖



出现了sys_auth加解密函数,所以上面的_ps_send就是交互函数


请求地址是phpsso,这就是交互点!!!


既然确定了这个点是交互点,那么我们可以回到phpsso的代码下断点继续分析
通过交互点函数往回查找调用点


发现public_checkemail_ajax函数

public function public_checkemail_ajax() {
$this->_init_phpsso();
$email = isset($_GET['email']) && trim($_GET['email']) ? trim($_GET['email']) : exit(0);
$status = $this->client->ps_checkemail($email);
if($status == -5) { //禁止注册
exit('0');
} elseif($status == -1) { //用户名已存在,但是修改用户的时候需要判断邮箱是否是当前用户的
if(isset($_GET['phpssouid'])) { //修改用户传入phpssouid
$status = $this->client->ps_get_member_info($email, 3);
if($status) {
$status = unserialize($status); //接口返回序列化,进行判断
if (isset($status['uid']) && $status['uid'] == intval($_GET['phpssouid'])) {
exit('1');
} else {
exit('0');
}
} else {
exit('0');
}
} else {
exit('0');
}
} else {
exit('1');
}
}这时候我们的路由就是
http://192.168.139.156:8000/index.php?m=member&c=index&a=public_checkemail_ajax&email=test访问这个地址会触发下面的POST数据包,先通过tcpdump看一下流量
tcpdump -i lo port 8000 -v -nne -Ahttp://192.168.139.156:8000/phpsso_server/index.php?m=phpsso&c=index&a=checkemail
要实现debug还得加上这个Cookie

可以看到传递到checkemail函数,parse_str已经将data中的email变量覆盖且数据已经被解密了

继续往下看看有没有什么操作,发现一个数据库的操作,看着挺像变量绑定那么回事的,好像没漏洞的样子,不管怎么说,这都是一个数据平面的交汇处,进去看看



可以看到这里是拼接写法,故存在SQL注入漏洞,现在我们基本可以去尝试一下单引号了
绕过
可以看到这里加了过滤,还记得在本文开头对提交参数分析发现存在对提交参数进行单引号过滤吗?似乎我们的路子被堵死了?

我知道你很急,但是你先别急,parse_str有一个特性,他会对传入的字符串进行自动的URL编码,这时候我们就可以尝试绕过
GET /index.php?m=member&c=index&a=public_checkemail_ajax&email=test%2527 HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: keep-alive
Cookie: XDEBUG_SESSION=PHPSTORM
Host: 192.168.139.156:8000
Pragma: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36%25转化过去是%所以说我们这边没有添加单引号不会被转义过滤,成功绕过


乌云镜像的原文:https://wy.zone.ci/bug_detail.php?wybug_id=wooyun-2015-0131548
抓住_ps_send这个函数可以找到更多入口点,如下面这个点



这些我就不一一展示了,这是15年的文章了,作为一个初学者我感觉也是学到很多的,仅作为个人学习记录,如有错误请多多指教
评论 (0)