环境
- 信呼OA的2.6.5版本
- 在Linux服务器上搭建,可以使用宝塔等面板搭建
- 默认账号密码:admin:123456
入口文件分析
像是这种大一些的OA其实分析起来并不是特别快,让我们一步一步来吧
index.php分析
index.php 内容不多啊,就这么些

入口就是一个 get 方法
$_uurl = $rock->get('rewriteurl');那这个方法,看起来像是获取请求参数的,跟进去看下

发现确实如此,在传递 name 这个参数的同时,还会默认设置 dev 参数为空字符串,lx 参数为0
再去看他的逻辑就是如果存在 GET 请求 name 参数,那么就把 val 的值更改为 name,如果为空的情况下,val 就还是默认的 dev 空字符串,最后将参数传递到另一个 jmuncode 函数中去。
那么这个函数里面的内容其实就是根据传入的参数 lx 进行对应的解密/码,然后对传递的内容进行一系列过滤,包括 %20 等非法字符以及 xss 的过滤,最后调用 register 方法

其实这个方法也是存在过滤的,最后才将这个数据返回,简单来说这个 get 方法就是除了根据 lx 进行解码以外就是过滤操作

然后接下来我们回到 index.php 发现下面就是对这个 m、d、a 进行判断的操作了

我们不看第一个 if,因为之前看了一下,这个 if 判断的 uurl 参数是由上面的 rewriteurl 参数得到的,但是 OA 基本不触发这个参数,反而这个 m、d、a 更多,所以就重点分析这三个参数
可以看到这三个参数是通过 gettoken 方法获取到的,继续进去分析,发现没法正常跟进去,可以通过搜索大法,结合 lm 名称和传入的参数数量来判断

找到对应的方法之后,分析他的逻辑:其实就是对这个传入的参数进行一次数组的遍历,数组是存储着默认值的,有默认值就设置为默认值,没有就通过 GET 方法获取

最后的话还传递了 ajaxbool 和 mode 以及判断是否安装,如果没安装就去安装,安装了就直接包含对应的 View 文件
if(!$config['install'] && $mode != 'install')$rock->location('?m=install');
include_once('include/View.php');View 文件分析
接下来就来分析这个 View 文件,发现他前面都是在做一些对 index.php 的补充,对于 m、d、a、ajaxbool 等参数再次确认是否传递,没有传递就设置为默认值

接下来看到他做了一个根据字符串格式包含文件的一个操作,这个操作的意思就是,根据位置进行匹配,0 对应 ROOT_PATH,1 对应 p 参数

简单来说就是导入了 webmain 目录下的 webmainAction.php 文件

根据这个规则,可以进一步进行理解下面的字符串模板导入了
接下来就是判断文件是否存在,存在就包含,不存在就包含 m 当中没有 | 字符串的,并且包含进来,并且进行了实例化,将 a 开头以 Action 结尾的给到了 actname,其中看 ajaxbool 的值如果为 true 那么给 Ajax 结尾的如果是 false 那么就给 Action 结尾的,最后判断对应的类当中是否存在 actname 方法,存在的话就进行了调用

最后就是一些渲染的东西了,无关紧要,到这边我们入口文件其实就是已经分析的大差不差了,可以尝试访问随便访问一个
http://192.168.48.144:7000/index.php?m=cog&d=system&a=getinfo&ajaxbool=true

能看到是成功访问的,其实这几个参数就是 directory、action、module
api.php分析
还有一个入口文件就是 api.php 啦
其实总体上没啥差别,就是自动加了一个 |api 和 |openapi 到 m 参数后面
define('ENTRANCE', 'api');
include_once('config/config.php');
$_paths = '';
$d = 'task';$m = 'index';$a = 'index';
if(isset($_GET['m'])){
$m = $rock->get('m');
$a = $rock->get('a', $a);
}else{
if(isset($_SERVER['PHP_SELF']))$_paths=$_SERVER['PHP_SELF'];
if($_paths==''&&isset($_SERVER['ORIG_PATH_INFO']))$_paths=$_SERVER['ORIG_PATH_INFO'];
$_patha = explode('api.php', $_paths);
$_paths = '/index/index';
if(isset($_patha[1])){
$_paths = $_patha[1];
}else{
if(isset($_SERVER['PATH_INFO']))$_paths=$_SERVER['PATH_INFO'];
}
}
unset($_GET['d']);
unset($_GET['m']);
unset($_GET['a']);
if($_paths){
$_pa = explode('/', $_paths);
if(isset($_pa[1])&&$_pa[1])$m=$_pa[1];
if(isset($_pa[2])&&$_pa[2])$a=$_pa[2];
}
if(substr($m,0,4)=='open'){
$m = ''.$m.'|openapi';
}else{
$m = ''.$m.'|api';
}
include_once('include/View.php');这个的作用就是让我们在使用 api 模块下或者 openapi 模块下不用手动去输入 m=xxx|api 了,转而直接输入 m=xxx 即可
鉴权分析
这个OA的鉴权分析其实就是在继承方法中实现的


api.php 这个入口点也是差不多的,继承的基类也实现了鉴权的方法

也就是说如果包含就会进行鉴权分析,因为继承自基类,而基类存在这个 init 的初始化模块,至于如何将非魔术方法实现效果和魔术方法差不多的这个就不详细进行分析了(比较菜)
由方法重写引发的SQL注入
首先我们需要知道什么是方法重写,方法重写指的是对基类已经实现的方法,再次自己实现
这个SQL注入漏洞就是因为有自实现方法,重写了基类的鉴权方法导致的SQL注入,那么我们可以快速搜索到对应的基类方法
这就是我们的目标了,对应方法是:subscribeAction



对应的这个初始化方法被重写之后完全不做校验,只有一个地方,需要验证 asynkey,如果没有这个值的话,我们就直接被卡死了,所以要先获取到这个值并且进行两次 md5 加密之后才能进行后续步骤


这里可以看到接受参数id、uid、receid、以及 recename 并且进行了 base64 解码(这里就完美绕过了 get 方法的 sql 过滤),然后传递到了 subscribe 方法当中,跟入方法

注意此时我们只关心这个可控并且绕过 SQL 注入检测的参数 recename

但是在此之前还有一个 if 判断,这个我们需要过一下

发现只要让他不等于数字即可,这个参数我们也是可控的,直接到时候设置随便几个字符串就行

发现进入 insert 方法之后又进到这个 record 方法,继续跟进


在经过不断地套娃之后,发现这个总算是进了查询了,也就是说这里是存在 SQL注入的

拿到关键的asynkey
别忘了我们关键的 asynkey 还没拿到,这个东西在后台可以获取(是不是感觉漏洞突然变得鸡肋了起来)

原始值:7775116270679d4e71f8d63b817aaabb
两次md5之后:9bdc49447dd10ca464d8da9562619e91http://192.168.48.144:7000/api.php?m=asynrun&d=task&a=subscribe&id=1&uid=1&receid=asd&recename=1*&asynkey=9bdc49447dd10ca464d8da9562619e91
那么接下来就是 SQLMAP 时刻,记得 BASE64 编码
sqlmap -u "http://192.168.48.144/index.php?m=asynrun|api&d=task&a=subscribe&asynkey=9bdc49447dd10ca464d8da9562619e91&id=1&uid=1&receid=asd&recename=1*" --tamper base64encode.py
总结
通过这个漏洞的分析可以了解到如下几个 tricks:
- 首先就是继承写法鉴权可以通过排查重写父类方法来查看是否绕过
- 其次就是如果存在大量过滤的话,可以直接找一些编解码的方法函数,来自然绕过这些过滤
- 踩坑点:部署的时候不要用除了80端口以外的其他端口,会导致漏洞复现失败(动调发现他会自己去请求接口,接口的地址是不带端口的)
评论 (0)