欢迎来到7N的个人博客!

web

安全开发(一):如何利用AI大模型辅助代码审计


avatar
7ech_N3rd 2025-01-22 237

传统审计

正则匹配

原理就是通过正则匹配来实现,挨个进行正则匹配,比如这条:

 <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讲讲自动化检测越权的逻辑漏洞,
且听下回分解

暂无评论

发表评论