2019年2月在写这篇文章 挖掘暗藏ThinkPHP中的反序列利用链 , 寻找PHP反序列化的
POP Chain
时, 我就在想这种纯粹的体力劳动可不可以更现代化一点, 不仅仅是Ctrl+Shift+F
这种机械重复的体力劳动, 当时了解了一些相关的项目/论文, 包括不限于Navex
,Prvd
,Cobra
,Codeql
. 鉴于Cobra代码开源, 也相对简单, 后来有一阵子某知名OA漏洞爆发, 于是参考了Cobra
的PHP 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的准备过程就到这里结束了
参考Cobra
的PHP Parser
Cobra 源码理解
# -*- 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