传统审计
正则匹配
原理就是通过正则匹配来实现,挨个进行正则匹配,比如这条:
<rule name="读取文件函数中存在变量,可能存在任意文件读取漏洞">
<regmatch>
<regexp>
(file_get_contents|fopen|readfile|fgets|fread|parse_ini_file|highlight_file|fgetss|show_source)\s{0,5}\(.{0,40}\$\w{1,15}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}
</regexp>
</regmatch>
</rule>
就是匹配高危函数后是否带有$
的字符,也就是变量俩判断有无潜在的漏洞,上古时代的神器Seay就有很多这样的规则:
<root>
<phpid vultype="File Inclusion">
<function>
<rule name="文件包含函数中存在变量,可能存在文件包含漏洞">
<regmatch>
<regexp>(include|require)(_once){0,1}(\s{1,5}|\s{0,5}\().{0,60}\$(?!.*(this->))\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="读取文件函数中存在变量,可能存在任意文件读取漏洞">
<regmatch>
<regexp>(file_get_contents|fopen|readfile|fgets|fread|parse_ini_file|highlight_file|fgetss|show_source)\s{0,5}\(.{0,40}\$\w{1,15}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="EXEC">
<function>
<rule name="preg_replace的/e模式,且有可控变量,可能存在代码执行漏洞">
<regmatch>
<regexp>preg_replace\(\s{0,5}.*/[is]{0,2}e[is]{0,2}["']\s{0,5},(.*\$.*,|.*,.*\$)</regexp>
</regmatch>
</rule>
<rule name="call_user_func函数参数包含变量,可能存在代码执行漏洞">
<regmatch>
<regexp>call_user_func(_array){0,1}\(\s{0,5}\$\w{1,15}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="命令执行函数中存在变量,可能存在任意命令执行漏洞">
<regmatch>
<regexp>(system|passthru|pcntl_exec|shell_exec|escapeshellcmd|exec)\s{0,10}\(.{0,40}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="可能存在代码执行漏洞,或者此处是后门">
<regmatch>
<regexp>\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}\s{0,5}\(\s{0,5}\$_(POST|GET|REQUEST|SERVER)\[.{1,20}\]</regexp>
</regmatch>
</rule>
<rule name="``反引号中包含变量,变量可控会导致命令执行漏洞">
<regmatch>
<regexp>`\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}`</regexp>
</regmatch>
</rule>
<rule name="array_map参数包含变量,变量可控可能会导致代码执行漏洞">
<regmatch>
<regexp>array_map\s{0,4}\(\s{0,4}.{0,20}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}\s{0,4}.{0,20},</regexp>
</regmatch>
</rule>
<rule name="eval或者assertc函数中存在变量,可能存在代码执行漏洞">
<regmatch>
<regexp>(eval|assert)\s{0,10}\(.{0,60}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="Information Disclosure">
<function>
<rule name="phpinfo()函数,可能存在敏感信息泄露漏洞">
<regmatch>
<regexp>phpinfo\s{0,5}\(\s{0,5}\)</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="Parameter Injection">
<function>
<rule name="parse_str函数中存在变量,可能存在变量覆盖漏洞">
<regmatch>
<regexp>(mb_){0,1}parse_str\s{0,10}\(.{0,40}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="双$符号可能存在变量覆盖漏洞">
<regmatch>
<regexp>\${{0,1}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}\s{0,4}=\s{0,4}.{0,20}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="extract函数中存在变量,可能存在变量覆盖漏洞">
<regmatch>
<regexp>(extract)\s{0,5}\(.{0,30}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}\s{0,5},{0,1}\s{0,5}(EXTR_OVERWRITE){0,1}\s{0,5}\)</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="Other">
<function>
<rule name="获取IP地址方式可伪造,HTTP_REFERER可伪造,常见引发SQL注入等漏洞">
<regmatch>
<regexp>["'](HTTP_CLIENT_IP|HTTP_X_FORWARDED_FOR|HTTP_REFERER)["']</regexp>
</regmatch>
</rule>
<rule name="文件操作函数中存在变量,可能存在任意文件读取/删除/修改/写入等漏洞">
<regmatch>
<regexp>(unlink|copy|fwrite|file_put_contents|bzopen)\s{0,10}\(.{0,40}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="urldecode绕过GPC,stripslashes会取消GPC转义字符">
<regmatch>
<regexp>^(?!.*\baddslashes).{0,40}\b((raw){0,1}urldecode|stripslashes)\s{0,5}\(.{0,60}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="header函数或者js location有可控参数,存在任意跳转或http头污染漏洞">
<regmatch>
<regexp>(header\s{0,5}\(.{0,30}|window.location.href\s{0,5}=\s{0,5})\$_(POST|GET|REQUEST|SERVER)</regexp>
</regmatch>
</rule>
<rule name="存在文件上传,注意上传类型是否可控">
<regmatch>
<regexp>move_uploaded_file\s{0,5}\(</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="SQLi">
<function>
<rule name="SQL语句select中条件变量无单引号保护,可能存在SQL注入漏洞">
<regmatch>
<regexp>select\s{1,4}.{1,60}from.{1,50}\bwhere\s{1,3}.{1,50}=["\s\.]{0,10}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="SQL语句delete中条件变量无单引号保护,可能存在SQL注入漏洞">
<regmatch>
<regexp>delete\s{1,4}from.{1,20}\bwhere\s{1,3}.{1,30}=["\s\.]{0,10}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="SQL语句insert中插入变量无单引号保护,可能存在SQL注入漏洞">
<regmatch>
<regexp>insert\s{1,5}into\s{1,5}.{1,60}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
<rule name="SQL语句delete中条件变量无单引号保护,可能存在SQL注入漏洞">
<regmatch>
<regexp>update\s{1,4}.{1,30}\s{1,3}set\s{1,5}.{1,60}\$\w{1,20}((\[["']|\[)\${0,1}[\w\[\]"']{0,30}){0,1}</regexp>
</regmatch>
</rule>
</function>
</phpid>
<phpid vultype="XSS">
<function>
<rule name="echo等输出中存在可控变量,可能存在XSS漏洞">
<regmatch>
<regexp>(echo|print|print_r)\s{0,5}\({0,1}.{0,60}\$_(POST|GET|REQUEST|SERVER)</regexp>
</regmatch>
</rule>
</function>
</phpid>
</root>
然后去匹配:
# coding=utf-8
'''
by:Segador
Improved by: 7ech_N3rd
'''
import re
import os
import optparse
import sys
import chardet
import json
from lxml.html import etree
class phpid(object):
def __init__(self, dir):
self._function = ''
self._fpanttern = ''
self._line = ''
self._dir = dir
self._filename = ''
self._vultype = ''
self.choice = '1'
self.results = [] # 用于存储匹配结果
def _run(self):
try:
self.handlePath(self._dir)
# print("danger information Finished!")
# 输出结果为 JSON 格式
print(json.dumps(self.results, indent=4, ensure_ascii=True))
except Exception as e:
print(f"Error: {e}")
raise
def report_id(self, vul,matches):
message = {
"vulnerability": vul,
"function": self._function,
"file": self._filename,
"matches": matches
}
self.results.append(message)
def report_line(self, line_content):
# 将匹配的行号和内容添加到最后一个 result 中
if self.results:
self.results[-1]["matches"].append({
"line": self._line,
"content": line_content.strip()
})
def handlePath(self, path):
dirs = os.listdir(path)
for d in dirs:
subpath = os.path.join(path, d)
if os.path.isfile(subpath):
if os.path.splitext(subpath)[1] in ['.php','.phtml']:
self._filename = subpath
file = "regexp"
self.handleFile(subpath, file)
else:
self.handlePath(subpath)
def handleFile(self, fileName, file):
with open(fileName, 'rb') as f: # 以二进制模式打开
raw_data = f.read()
result = chardet.detect(raw_data) # 检测编码
encoding = result['encoding']
# 使用检测到的编码读取文件
with open(fileName, 'r', encoding=encoding, errors='ignore') as f:
self._line = 0
content = f.read()
content = self.remove_comment(content)
self.check_regexp(content, file)
def function_search_line(self):
with open(self._filename, 'r', encoding='utf-8', errors='ignore') as fl:
self._line = 0
while True:
line = fl.readline()
if not line:
break
self._line += 1
# 调试输出:查看每一行是否包含函数名
# print(f"Checking line {self._line}: {line.strip()}")
if self._function in line:
# print(f'find danger information on line: {line.strip()}')
self.report_line(line.strip())
def regexp_search(self, rule_dom, content):
regmatch_doms = list(rule_dom[0].xpath("regmatch"))
exp_patterns_list = []
# 构建所有的正则表达式列表
for regmatch_dom in regmatch_doms:
regexp_doms = regmatch_dom.xpath("regexp")
exp_patterns = [re.compile(regexp_dom.text) for regexp_dom in regexp_doms]
exp_patterns_list.append(exp_patterns)
matches = [] # 用于存储匹配的行内容
# 逐行检查文件内容
lines = content.splitlines()
for line_num, line in enumerate(lines, 1): # 枚举每一行,line_num 从 1 开始
match_results = [all(exp_pattern.search(line) for exp_pattern in exp_patterns) for exp_patterns in exp_patterns_list]
# 如果当前行匹配所有正则表达式
if all(match_results):
matches.append({
"content": line.strip(),
"vul_type": self._vultype,
})
# 如果有匹配的行,报告漏洞
if matches:
# print(f"find danger information on line: {matches}")
self.report_id(self._vultype, matches)
self.function_search_line() # 调用其他相关处理逻辑
return True
def check_regexp(self, content, file):
if not content:
return
xml_file = "regexp.xml"
self._xmlstr_dom = etree.parse(xml_file)
phpid_doms = self._xmlstr_dom.xpath("phpid")
for phpid_dom in phpid_doms:
self._vultype = phpid_dom.get("vultype")
function_doms = phpid_dom.xpath("function")
for function_dom in function_doms:
self._function = function_dom.xpath("rule")[0].get("name")
self.regexp_search(function_dom, content)
return True
def remove_comment(self, content):
# TODO: remove comments from content
return content
if __name__ == '__main__':
parser = optparse.OptionParser('usage: python %prog [options](eg: python %prog -d /user/php/demo)')
parser.add_option('-d', '--dir', dest='dir', type='string', help='source code file dir')
(options, args) = parser.parse_args()
if options.dir is None or options.dir == "":
parser.print_help()
sys.exit()
dir = options.dir
phpididentify = phpid(dir)
phpididentify._run()
这个就是借鉴大佬Segador的思路,挨个去用正则做遍历匹配。但是这个思路有个严重的问题:那就是误报率非常高,虽然所有的高危函数都已经匹配到了,但是这个其中的变量不一定是用户可控制的。比如
$const=file_get_content("../config.php");
eval($const);
这里就是常量,用户不可控,不是漏洞。还有一种情况
function execu($a){
eval($a);
}
这+个就识别不了同样高危函数的execu
了
AST分析
推荐大家可以去看一下这篇文章:从0开始聊聊自动化静态代码审计工具-腾讯云开发者社区-腾讯云
比如经典昆仑镜项目的:LoRexxar/Kunlun-M: KunLun-M是一个完全开源的静态白盒扫描工具,支持PHP、JavaScript的语义扫描,基础安全、组件安全扫描,Chrome Ext\Solidity的基础扫描。
具体原理就是通过数据流来分析,从输入到污点函数是否存在路径,如果污点函数的传参中有被输入影响的位置,那么就说明这是个漏洞。但是这同样也有个问题,例如在sqli注入中:
<?php
function getUserData($conn) {
$user_input = $_GET['username'];
$query = "SELECT * FROM users WHERE username = '$user_input'";
mysqli_query($conn, $query);
}
$a=addslashes($_GET[1]);
getUserData($conn)
这种添加了魔术方法addslshes
其实是无解的,故不存在sql注入,但是在ast看来用户传参$_GET
确实影响了污点函数mysqli_query
的执行传参,于是会判断为有漏洞。
那还有无更好的办法?
引入AI
都2024年了不会还有人没用过大模型吧?不会吧?同样的,作为通用智能,大模型也可以被用于直接代码审计,但关键是做好提示词工程。单纯的大模型审计在审上万行代码的效果并不好(不是所有的代码文件中都有漏洞,大多数还是正常业务),外加上API调用的费用,多少还是有点不划算。所以优先的做法是先前使用传统的方式如AST,正则把潜在的漏洞点位都覆盖到位,再去上ai,会更好。但是实际效果中,ai确实只能够消除一部分的误报率,由于提示词和上下文等限制,最终还是不能100%做到人工的效果。
具体的设计流程图
回到我们上面的代码,我这里选择的是使用正则表达式。
def run_phpid(directory):
"""运行 phpid.py,并返回输出"""
command = ['python', 'phpid.py', '-d', directory]
# logger.debug(f"执行命令: {' '.join(command)}")
try:
process = subprocess.run(command, capture_output=True, text=True, check=True)
logger.debug(f"phpid.py 执行成功,输出长度: {len(process.stdout)}")
return process.stdout
except subprocess.CalledProcessError as e:
logger.error(f"Error running phpid.py: {e.stderr}")
sys.exit(1)
先把上面的代码保存为phpid.py
,然后把输出的结果使用json解析,然后再通过进行进一步的分析。
提示词工程
首先我们得明确一下我们优化提示词的目标,用最少的token
最精确的定位漏洞。
在真实的代码中往往有很多冗余,第一步我们为了精简提示词采用了正则处理掉多余的注释和空格
def remove_redundancies(php_code):
"""
移除PHP代码中的注释和多余的空白,以减少token数量。
Args:
php_code (str): 原始PHP代码字符串。
Returns:
str: 精简后的PHP代码字符串。
"""
php_code = re.sub(r'//.*|#.*', '', php_code)
php_code = re.sub(r'/\*[\s\S]*?\*/', '', php_code)
php_code = re.sub(r'\s+', ' ', php_code)
php_code = re.sub(r'\s*([\{\};(),=<>]+)\s*', r'\1', php_code)
return php_code.strip()
然后我们得充分利用上一步正则匹配的结果,也就是phpid.py定位的具体漏洞位置:
def regexp_search(self, rule_dom, content):
regmatch_doms = list(rule_dom[0].xpath("regmatch"))
exp_patterns_list = []
# 构建所有的正则表达式列表
for regmatch_dom in regmatch_doms:
regexp_doms = regmatch_dom.xpath("regexp")
exp_patterns = [re.compile(regexp_dom.text) for regexp_dom in regexp_doms]
exp_patterns_list.append(exp_patterns)
matches = [] # 用于存储匹配的行内容
# 逐行检查文件内容
lines = content.splitlines()
for line_num, line in enumerate(lines, 1): # 枚举每一行,line_num 从 1 开始
match_results = [all(exp_pattern.search(line) for exp_pattern in exp_patterns) for exp_patterns in exp_patterns_list]
# 如果当前行匹配所有正则表达式
if all(match_results):
matches.append({
"content": line.strip(),
"vul_type": self._vultype,
})
# 如果有匹配的行,报告漏洞
if matches:
# print(f"find danger information on line: {matches}")
self.report_id(self._vultype, matches)
self.function_search_line() # 调用其他相关处理逻辑
return True
我们再CodePro.py这里接受了对应的传参并且将其嵌入到提示词里面:
def analyze_file(file_path, file_content,matches,plugins=None):
...
matches_content=""
for match in matches:
matches_content+=f"潜在的{match['vul_type']}漏洞,具体位置:{match['content']}"
...
在真实的代码审计中我们会发现有很多的依赖项,有很多漏洞的污点函数其实是在这些依赖里面的,有啥好的办法去解决呢?
最简单的,大力出奇迹,直接解析php中的include
,require
,之类的函数,直接将其中引用的代码文件直接存到提示词里面。但是这又将面临新的问题:
- 引用的代码文件中也会引用其他更加底层的代码文件,如何在控制提示词成本的前提下保证质量?
- 在依赖中会有大量的冗余代码,例如和漏洞毫无关联的函数
面对这种ai提示词特有的问题,我们也可以换种思路。用提示词插件解决
既然大量的代码占地方,那么我们为啥不直接先用AI终结出有用的函数,和对应依赖高危的污点函数功能成人类听得懂的语言呢?
这是o1-mini总结的TP5框架的特性:
我认为还挺全面的,也可以自己总结对应web框架的特性
在对应的python代码层面我们自然要将其嵌入提示词,并且启用json格式方便输出解析:
def analyze_file(file_path, file_content,matches,plugins=None):
route = extract_route(file_content) or os.path.basename(file_path)
plugin_content=""
if plugins!=None:
for plugin in plugins:
if get_file_content("./plugin/{}.txt".format(plugin),encoding='utf-8')==None:
print("[-]Error:Failed to load plugins:"+str(plugin)+"\nplz check ./plugins folder")
sys.exit(1)
plugin_content+=get_file_content("./plugin/{}.txt".format(plugin),encoding='utf-8')
matches_content=""
for match in matches:
matches_content+=f"潜在的{match['vul_type']}漏洞,具体位置:{match['content']}"
if clean_php_code:
file_content = remove_redundancies(file_content)
prompt = f"""
{plugin_content}
作为一个安全专家,请分析以下PHP代码是否存在安全隐患:
文件路径:{file_path}
路由信息:{route}
--------------文件内容开始-------------------
{file_content}
--------------文件内容结束-------------------
{matches_content}
如果存在安全隐患,请提供以下信息:
1. 漏洞类型
2. 漏洞描述
3.攻击者可能的利用方式
4. 修复建议
如果不存在安全隐患,请回复"该文件不存在安全隐患"。
请使用如下 JSON 格式输出你的回复:
例如:
{{
"is_vulnerable": 1,
"reason": "明确存在漏洞,原因。以及攻击者可以通过发送...来进行攻击",
"FixSuggestion":"..."
}}
{{
"is_vulnerable": 0.7,
"reason": "很可能存在漏洞,原因:...。",
"FixSuggestion":"..."
}}
{{
"is_vulnerable": 0.4,
"reason": "无法明确存在漏洞,原因:...。",
"FixSuggestion":"..."
}}
{{
"is_vulnerable": 0.1,
"reason": "不存在漏洞,原因:...。",
"FixSuggestion":"..."
}}
注意:
- 请确保你的回复符合 JSON 格式。
- is_vulnerable 为0-1的整数类型,表示是否存在漏洞的可能性。
- reason FixSuggestion 为字符串类型
"""
return ask_gpt(prompt,api_key,api_url)
最后再加上对应的输出报告逻辑就可以了
项目地址:Cscript-Null/CodeAi-Pro: Se9ad0r大佬写的CodeAi的工具的二开版本,加入了插件功能
但是AI在辅助漏洞挖掘的过程中应该还不止于静态分析:
同样的思路还可以用于交互式应用安全测试,下篇我就会用AI讲讲自动化检测越权的逻辑漏洞,
且听下回分解