从AST到100个某知名OA前台注入

2019年2月在写这篇文章 挖掘暗藏ThinkPHP中的反序列利用链 , 寻找PHP反序列化的POP Chain时, 我就在想这种纯粹的体力劳动可不可以更现代化一点, 不仅仅是Ctrl+Shift+F这种机械重复的体力劳动, 当时了解了一些相关的项目/论文, 包括不限于NavexPrvdCobraCodeql. 鉴于Cobra代码开源, 也相对简单, 后来有一阵子某知名OA漏洞爆发, 于是参考了CobraPHP Parser尝试实现一个通过遍历Java AST(抽象语法树)进行漏洞挖掘的工具, 没想到效果出奇的好, 筛选出160个前台注入点, 手工编写了约50个前台注入EXP.

文中涉及的漏洞均为workflowcentertreedata通告的相似漏洞研究, 补丁版本之后均已失效

预备知识

某知名OA介绍

某知名OA是使用Java编写的一个OA套件, 代码相对古老, 其中sql查询语句多是拼接, 且代码中没有过滤, 其过滤是通过统一的Filter实现的, 存在一些绕过的情况.

某知名OA的主体功能是通过JSP实现的, 这里是目前只有PMD支持解析, 但是没有尝试, 从idea的的解析结果来看, 大概是解析不到具体函数逻辑的, 好在JSP可以编译成Java Servlet, 某知名OA使用的Resin Server 也会缓存编译好的Java Servlet, 这里倒是省了不少麻烦.

编译原理基础

了解过编译原理的同学都知道, 一般语言的编译都是通过 词法分析,语法分析, 然后解析成AST(抽象语法树), 这里包含了一个程序源文件的所有结构化信息, 通过遍历AST的方式, 我们可以精确的取出我们需要的信息, 而不是笨拙的使用全局搜索, 正则表达式这种会丢失上下文信息的方式.

一般的编译过程如下图所示

环境准备

首先这里需要搭建某知名OA的环境, 这里以某知名OA 8为例, 可以去百度下载Ecology8.100.0531

默认配置安装完成就OK了

遍历某知名OA的JSP文件路径

先使用Python获取到某知名OA文件夹中的JSP文件路径, 这里可以自己过滤一下

#python 遍历文件夹
import os
def get_files(path=r"D:\WEAVER\ecology\"):
    g = os.walk(path)
    result = []
    for path, d, file_list in g:
        for filename in file_list:
            full_path = os.path.join(path, filename)
            result.append([full_path, filename])
    return result

然后通过burp intruder的方式遍历某知名OA的JSP在前台的可访问性, 这里使用Python访问也行

获取到如下列表

Request Payload    Status Error  Timeout    Length Comment
7373   workflow/request/WorkflowViewRequestDetailBodyAction.jsp   200    false  false  73584  
7319   workflow/request/WorkflowManageRequestBody.jsp 200    false  false  71216  
7359   workflow/request/WorkflowSignInput.jsp 200    false  false  69746  
6445   web/workflow/request/WorkflowAddRequestBody.jsp    200    false  false  69080  
7372   workflow/request/WorkflowViewRequestDetailBody.jsp 200    false  false  66718  
7297   workflow/request/WorkflowAddRequestBodyDataCenter.jsp  200    false  false  64160  
7322   workflow/request/WorkflowManageRequestBodyDataCenter.jsp   200    false  false  64098  
7301   workflow/request/WorkflowAddRequestFormBody.jsp    200    false  false  62012  
3499   hrm/report/resource/HrmConstRpDataDefine.jsp   200    false  false  61648  
6923   workflow/request/BillBudgetExpenseDetail.jsp   200    false  false  61272  
7295   workflow/request/WorkflowAddRequestBody.jsp    200    false  false  60130  
7370   workflow/request/WorkflowViewRequestBody.jsp   200    false  false  59860    
.....
2368    formmode/import/ProcessOperation.jsp    200 false   false   218 
7378    workflow/request/WorkflowViewSign.jsp   0   false   false   5   
6419    web/WebBBSDsp.jsp   0   false   false   0   
6421    web/WebDsp.jsp  0   false   false   0   
6422    web/WebJournalDsp.jsp   0   false   false   0   
6426    web/WebListDspSecond.jsp    0   false   false   0   

获取Resin生成的Servlet.java

获取到JSP文件的访问权限列表的同时, 某知名OA的目录D:\WEAVER\ecology\WEB-INF\work\_jsp中也生成了对应的JSP Servlet

然后把_jsp目录复制出来, 某知名OA的准备过程就到这里结束了

参考CobraPHP Parser

Cobra 源码理解

cobra/parser.py

# -*- coding: utf-8 -*-

"""
    parser
    ~~~~~~

    Implements Code Parser

    :author:    BlBana <635373043@qq.com>
    :homepage:  https://github.com/WhaleShark-Team/cobra
    :license:   MIT, see LICENSE for more details.
    :copyright: Copyright (c) 2018 Feei. All rights reserved
"""
from phply.phplex import lexer  # 词法分析
from phply.phpparse import make_parser  # 语法分析
from phply import phpast as php
from .log import logger

with_line = True
scan_results = []  # 结果存放列表初始化
repairs = []  # 用于存放修复函数


def export(items):
    result = []
    if items:
        for item in items:
            if hasattr(item, 'generic'):
                item = item.generic(with_lineno=with_line)
            result.append(item)
    return result


def export_list(params, export_params):
    """
    将params中嵌套的多个列表,导出为一个列表
    :param params:
    :param export_params:
    :return:
    """
    for param in params:
        if isinstance(param, list):
            export_params = export_list(param, export_params)

        else:
            export_params.append(param)

    return export_params


def get_all_params(nodes):  # 用来获取调用函数的参数列表,nodes为参数列表
    """
    获取函数结构的所有参数
    :param nodes:
    :return:
    """
    params = []
    export_params = []  # 定义空列表,用来给export_list中使用
    for node in nodes:
        if isinstance(node.node, php.FunctionCall):  # 函数参数来自另一个函数的返回值
            params = get_all_params(node.node.params)

        else:
            if isinstance(node.node, php.Variable):
                params.append(node.node.name)

            if isinstance(node.node, php.BinaryOp):
                params = get_binaryop_params(node.node)
                params = export_list(params, export_params)

            if isinstance(node.node, php.ArrayOffset):
                param = get_node_name(node.node.node)
                params.append(param)

            if isinstance(node.node, php.Cast):
                param = get_cast_params(node.node.expr)
                params.append(param)

            if isinstance(node.node, php.Silence):
                param = get_silence_params(node.node)
                params.append(param)

    return params


def get_silence_params(node):
    """
    用来提取Silence类型中的参数
    :param node:
    :return:
    """
    param = []
    if isinstance(node.expr, php.Variable):
        param = get_node_name(node.expr)

    if isinstance(node.expr, php.FunctionCall):
        param.append(node.expr)

    if isinstance(node.expr, php.Eval):
        param.append(node.expr)

    if isinstance(node.expr, php.Assignment):
        param.append(node.expr)

    return param


def get_cast_params(node):
    """
    用来提取Cast类型中的参数
    :param node:
    :return:
    """
    param = []
    if isinstance(node, php.Silence):
        param = get_node_name(node.expr)

    return param


def get_binaryop_params(node):  # 当为BinaryOp类型时,分别对left和right进行处理,取出需要的变量
    """
    用来提取Binaryop中的参数
    :param node:
    :return:
    """
    logger.debug('[AST] Binaryop --> {node}'.format(node=node))
    params = []
    buffer_ = []

    if isinstance(node.left, php.Variable) or isinstance(node.right, php.Variable):  # left, right都为变量直接取值
        if isinstance(node.left, php.Variable):
            params.append(node.left.name)

        if isinstance(node.right, php.Variable):
            params.append(node.right.name)

    if not isinstance(node.right, php.Variable) or not isinstance(node.left, php.Variable):  # right不为变量时
        params_right = get_binaryop_deep_params(node.right, params)
        params_left = get_binaryop_deep_params(node.left, params)

        params = params_left + params_right

    params = export_list(params, buffer_)
    return params


def get_binaryop_deep_params(node, params):  # 取出right,left不为变量时,对象结构中的变量
    """
    取出深层的变量名
    :param node: node为上一步中的node.left或者node.right节点
    :param params:
    :return:
    """
    if isinstance(node, php.ArrayOffset):  # node为数组,取出数组变量名
        param = get_node_name(node.node)
        params.append(param)

    if isinstance(node, php.BinaryOp):  # node为BinaryOp,递归取出其中变量
        param = get_binaryop_params(node)
        params.append(param)

    if isinstance(node, php.FunctionCall):  # node为FunctionCall,递归取出其中变量名
        params = get_all_params(node.params)

    return params


def get_expr_name(node):  # expr为'expr'中的值
    """
    获取赋值表达式的表达式部分中的参数名-->返回用来进行回溯
    :param node:
    :return:
    """
    param_lineno = 0
    is_re = False
    if isinstance(node, php.ArrayOffset):  # 当赋值表达式为数组
        param_expr = get_node_name(node.node)  # 返回数组名
        param_lineno = node.node.lineno

    elif isinstance(node, php.Variable):  # 当赋值表达式为变量
        param_expr = node.name  # 返回变量名
        param_lineno = node.lineno

    elif isinstance(node, php.FunctionCall):  # 当赋值表达式为函数
        param_expr = get_all_params(node.params)  # 返回函数参数列表
        param_lineno = node.lineno
        is_re = is_repair(node.name)  # 调用了函数,判断调用的函数是否为修复函数

    elif isinstance(node, php.BinaryOp):  # 当赋值表达式为BinaryOp
        param_expr = get_binaryop_params(node)
        param_lineno = node.lineno

    else:
        param_expr = node

    return param_expr, param_lineno, is_re


def get_node_name(node):  # node为'node'中的元组
    """
    获取Variable类型节点的name
    :param node:
    :return:
    """
    if isinstance(node, php.Variable):
        return node.name  # 返回此节点中的变量名


def is_repair(expr):
    """
    判断赋值表达式是否出现过滤函数,如果已经过滤,停止污点回溯,判定漏洞已修复
    :param expr: 赋值表达式
    :return:
    """
    is_re = False  # 是否修复,默认值是未修复
    for repair in repairs:
        if expr == repair:
            is_re = True
            return is_re
    return is_re


def is_sink_function(param_expr, function_params):
    """
    判断自定义函数的入参-->判断此函数是否是危险函数
    :param param_expr:
    :param function_params:
    :return:
    """
    is_co = -1
    cp = None
    if function_params is not None:
        for function_param in function_params:
            if param_expr == function_param:
                is_co = 2
                cp = function_param
                logger.debug('[AST] is_sink_function --> {function_param}'.format(function_param=cp))
    return is_co, cp


def is_controllable(expr):  # 获取表达式中的变量,看是否在用户可控变量列表中
    """
    判断赋值表达式是否是用户可控的
    :param expr:
    :return:
    """
    controlled_params = [
        '$_GET',
        '$_POST',
        '$_REQUEST',
        '$_COOKIE',
        '$_FILES',
        '$_SERVER',
        '$HTTP_POST_FILES',
        '$HTTP_COOKIE_VARS',
        '$HTTP_REQUEST_VARS',
        '$HTTP_POST_VARS',
        '$HTTP_RAW_POST_DATA',
        '$HTTP_GET_VARS'
    ]
    if expr in controlled_params:
        logger.debug('[AST] is_controllable --> {expr}'.format(expr=expr))
        return 1, expr
    return -1, None


def parameters_back(param, nodes, function_params=None):  # 用来得到回溯过程中的被赋值的变量是否与敏感函数变量相等,param是当前需要跟踪的污点
    """
    递归回溯敏感函数的赋值流程,param为跟踪的污点,当找到param来源时-->分析复制表达式-->获取新污点;否则递归下一个节点
    :param param:
    :param nodes:
    :param function_params:
    :return:
    """
    expr_lineno = 0  # source所在行号
    is_co, cp = is_controllable(param)

    if len(nodes) != 0 and is_co == -1:
        node = nodes[len(nodes) - 1]

        if isinstance(node, php.Assignment):  # 回溯的过程中,对出现赋值情况的节点进行跟踪
            param_node = get_node_name(node.node)  # param_node为被赋值的变量
            param_expr, expr_lineno, is_re = get_expr_name(node.expr)  # param_expr为赋值表达式,param_expr为变量或者列表

            if param == param_node and is_re is True:
                is_co = 0
                cp = None
                return is_co, cp, expr_lineno

            if param == param_node and not isinstance(param_expr, list):  # 找到变量的来源,开始继续分析变量的赋值表达式是否可控
                is_co, cp = is_controllable(param_expr)  # 开始判断变量是否可控

                if is_co != 1:
                    is_co, cp = is_sink_function(param_expr, function_params)

                param = param_expr  # 每次找到一个污点的来源时,开始跟踪新污点,覆盖旧污点

            if param == param_node and isinstance(param_expr, list):
                for expr in param_expr:
                    param = expr
                    is_co, cp = is_controllable(expr)

                    if is_co == 1:
                        return is_co, cp, expr_lineno

                    _is_co, _cp, expr_lineno = parameters_back(param, nodes[:-1], function_params)

                    if _is_co != -1:  # 当参数可控时,值赋给is_co 和 cp,有一个参数可控,则认定这个函数可能可控
                        is_co = _is_co
                        cp = _cp

        if is_co == -1:  # 当is_co为True时找到可控,停止递归
            is_co, cp, expr_lineno = parameters_back(param, nodes[:-1], function_params)  # 找到可控的输入时,停止递归

    elif len(nodes) == 0 and function_params is not None:
        for function_param in function_params:
            if function_param == param:
                is_co = 2
                cp = function_param

    return is_co, cp, expr_lineno


def get_function_params(nodes):
    """
    获取用户自定义函数的所有入参
    :param nodes: 自定义函数的参数部分
    :return: 以列表的形式返回所有的入参
    """
    params = []
    for node in nodes:

        if isinstance(node, php.FormalParameter):
            params.append(node.name)

    return params


def anlysis_function(node, back_node, vul_function, function_params, vul_lineno):
    """
    对用户自定义的函数进行分析-->获取函数入参-->入参用经过赋值流程,进入sink函数-->此自定义函数为危险函数
    :param node:
    :param back_node:
    :param vul_function:
    :param function_params:
    :param vul_lineno:
    :return:
    """
    global scan_results
    try:
        if node.name == vul_function and int(node.lineno) == int(vul_lineno):  # 函数体中存在敏感函数,开始对敏感函数前的代码进行检测
            for param in node.params:
                if isinstance(param.node, php.Variable):
                    analysis_variable_node(param.node, back_node, vul_function, vul_lineno, function_params)

                if isinstance(param.node, php.FunctionCall):
                    analysis_functioncall_node(param.node, back_node, vul_function, vul_lineno, function_params)

                if isinstance(param.node, php.BinaryOp):
                    analysis_binaryop_node(param.node, back_node, vul_function, vul_lineno, function_params)

                if isinstance(param.node, php.ArrayOffset):
                    analysis_arrayoffset_node(param.node, vul_function, vul_lineno)

    except Exception as e:
        logger.debug(e)


# def analysis_functioncall(node, back_node, vul_function, vul_lineno):
#     """
#     调用FunctionCall-->判断调用Function是否敏感-->get params获取所有参数-->开始递归判断
#     :param node:
#     :param back_node:
#     :param vul_function:
#     :param vul_lineno
#     :return:
#     """
#     global scan_results
#     try:
#         if node.name == vul_function and int(node.lineno) == int(vul_lineno):  # 定位到敏感函数
#             for param in node.params:
#                 if isinstance(param.node, php.Variable):
#                     analysis_variable_node(param.node, back_node, vul_function, vul_lineno)
#
#                 if isinstance(param.node, php.FunctionCall):
#                     analysis_functioncall_node(param.node, back_node, vul_function, vul_lineno)
#
#                 if isinstance(param.node, php.BinaryOp):
#                     analysis_binaryop_node(param.node, back_node, vul_function, vul_lineno)
#
#                 if isinstance(param.node, php.ArrayOffset):
#                     analysis_arrayoffset_node(param.node, vul_function, vul_lineno)
#
#     except Exception as e:
#         logger.debug(e)


def analysis_binaryop_node(node, back_node, vul_function, vul_lineno, function_params=None):
    """
    处理BinaryOp类型节点-->取出参数-->回溯判断参数是否可控-->输出结果
    :param node:
    :param back_node:
    :param vul_function:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    logger.debug('[AST] vul_function:{v}'.format(v=vul_function))
    params = get_binaryop_params(node)
    params = export_list(params, export_params=[])

    for param in params:
        is_co, cp, expr_lineno = parameters_back(param, back_node, function_params)
        set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)


def analysis_arrayoffset_node(node, vul_function, vul_lineno):
    """
    处理ArrayOffset类型节点-->取出参数-->回溯判断参数是否可控-->输出结果
    :param node:
    :param vul_function:
    :param vul_lineno:
    :return:
    """
    logger.debug('[AST] vul_function:{v}'.format(v=vul_function))
    param = get_node_name(node.node)
    expr_lineno = node.lineno
    is_co, cp = is_controllable(param)

    set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)


def analysis_functioncall_node(node, back_node, vul_function, vul_lineno, function_params=None):
    """
    处理FunctionCall类型节点-->取出参数-->回溯判断参数是否可控-->输出结果
    :param node:
    :param back_node:
    :param vul_function:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    logger.debug('[AST] vul_function:{v}'.format(v=vul_function))
    params = get_all_params(node.params)
    for param in params:
        is_co, cp, expr_lineno = parameters_back(param, back_node, function_params)
        set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)


def analysis_variable_node(node, back_node, vul_function, vul_lineno, function_params=None):
    """
    处理Variable类型节点-->取出参数-->回溯判断参数是否可控-->输出结果
    :param node:
    :param back_node:
    :param vul_function:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    logger.debug('[AST] vul_function:{v}'.format(v=vul_function))
    params = get_node_name(node)
    is_co, cp, expr_lineno = parameters_back(params, back_node, function_params)
    set_scan_results(is_co, cp, expr_lineno, vul_function, params, vul_lineno)


def analysis_if_else(node, back_node, vul_function, vul_lineno, function_params=None):
    nodes = []
    if isinstance(node.node, php.Block):  # if语句中的sink点以及变量
        analysis(node.node.nodes, vul_function, back_node, vul_lineno, function_params)

    if node.else_ is
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值