“我结合了静态分析与LLM Agent”——LAESA工具的设计与实现

​ 随着LLM的问世,安全代码审计领域又迎来了新的分析和探索。LLM模型如 CodeBERT、GPT-4、Llama 2 等,具备强大的代码语义理解和自动化推理能力,能够用于代码补全、漏洞检测、代码审计、恶意代码分析等任务。所有普通人类程序员能读懂的代码,GPT-4O都能读懂,人类程序员因知识不足无法理解的代码,LLM都能通过训练后完全掌握。

​ 既然都能读懂代码,LLM当然也可以分析代码的缺陷,随便粘贴可以人工审计的代码片段,GPT-4O已经可以分析代码中的脆弱性、找出其中存在的漏洞并提出修改意见。这不就是高于人工审计的表现吗?LLM比人类更快,未来也必将比人类更准确,所以让LLM分析完整项目,去取代静态分析工具,也只是时间问题。

那现在为什么LLM取代不了静态分析软件呢?我认为存在以下几种问题:

1、对复杂上下文的理解能力有限,这会导致多轮推理或深入的代码行为分析时准确性不足。

2、目前分析依赖人工提问,多为一次性任务,缺乏任务规划能力,无法做到对完整源码自动化分析。

3、在长上下文下分析会出现幻觉,或对部分分析不进行回答。

4、安全分析领域知识仍要加强,需要更多高质量的数据集。

……

​ 以上这些问题,其实现在都有现存的解决方案。如:问题2要提高自动化分析能力,我们可以引入LLM Agent框架,通过调用工具,多轮对话的方式实现自动化;问题3我们可以对项目代码进行摘要,提炼出可以分析漏洞但又不失准确性的的代码片段,大大减少上下文Token信息;问题4我们可以训练安全领域的小模型,但现在对于我而言不切实际。至于问题一,多依赖于模型的能力,于我而言目前只能选择能力较强的大模型。

​ 基于以上所述,我开始了LLM结合静态分析的探索,经过多次实验和改进后,我和室友设计出了基于LLM Agent的Java API漏洞自动化挖掘工具——LAESA

LAESA可以做什么呢?

​ 它可以实现对于Java源码中存在的API相关漏洞的全自动化检测POC生成与验证,是从检测漏洞到自动化测试一个闭环过程。意思是只要你在本地搭建好待测项目,我们可以一键启动,完成漏洞的分析报告,生成对应漏洞的POC,并且根据POC回显信息自动化分析测试结果。

我们是怎么做到的呢?

​ 遇到问题,我就会去想解决方法。如上分析所说,LLM目前取代不了静待分析存在4种问题,那么我就会针对他们中的几种进行解决。

​ 我首先通过静态分析的方式,利用现有框架,开发了一个静态分析工具,该工具就用来完成该类漏洞的代码摘要;为实现自动化分析,我们采用了市面上比较流行的LLM Agent框架Autogen(未来会考虑MCP),基于此我们实验自动化;在分析完整项目时,会出现上下文信息不足等问题,我们在Autogen框架中开发了几个工具,帮助我们自动化查询,从而使上下文信息更加精确;在安全领域知识不全的情况下,我们通过优化Prompt的方式,引入REG知识库,使分析结果更加准确。

为什么只做Java API方面?

​ 因为我在做自动化POC的时候,构造请求包的时候突然想到上述idea,POC验证大多数是要URL,不多说了,正好也想把这些结合。

以下是工具完整流程图

![blog](D:ty小论文1blog.png)

工具具体细节

​ 这里我们主要分为三部分完成:一、API漏洞关键信息提取;二、LLM Agent功能设计;三、 自动化 POC 生成与验证

API漏洞关键信息提取

​ 在代码摘要这部分,我要清楚LLM(这里采用GPT-4o)究竟需要多少的代码片段才能够清楚的审计出是否存在某一类漏洞。

​ 经过实验发现,只需要紧跟着注解下面的主函数体,GPT-4o就以及可以解析绝大部分的API危险参数类漏洞问题。我们将代码拆解成:

[请求方法] api路径
方法名
方法主体

​ 那么一个真实项目,经过这样提取后会“瘦身”多少呢?

![image-20250412160713274](D:ty小论文1image-20250412160713274.png)![image-20250412160920547](D:ty小论文1image-20250412160920547.png)

​ 一个140MB的项目会变成60KB,经过计算,大约10600token,对于目前的LLM的上下文长度绰绰有余。

​ 下面是一个真实项目拆解后的一个片段,这同样是个我们已经分配CVE编号的项目。

[
    {
  "http_method": "POST",
  "api_path": "/manage-api/v1/upload/file",
  "method_name": "upload",
  "method_body": "/**\r\n * 图片上传\r\n */\r\n@RequestMapping(value = \"/upload/file\", method = RequestMethod.POST)\r\n@ApiOperation(value = \"单图上传\", notes = \"file Name \\\"file\\\"\")\r\npublic Result upload(HttpServletRequest httpServletRequest, @RequestParam(\"file\") MultipartFile file, @TokenToAdminUser AdminUserToken adminUser) throws URISyntaxException {\r\n    logger.info(\"adminUser:{}\", adminUser.toString());\r\n    String fileName = file.getOriginalFilename();\r\n    String suffixName = fileName.substring(fileName.lastIndexOf(\".\"));\r\n    //生成文件名称通用方法\r\n    SimpleDateFormat sdf = new SimpleDateFormat(\"yyyyMMdd_HHmmss\");\r\n    Random r = new Random();\r\n    StringBuilder tempName = new StringBuilder();\r\n    tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName);\r\n    String newFileName = tempName.toString();\r\n    File fileDirectory = new File(Constants.FILE_UPLOAD_DIC);\r\n    //创建文件\r\n    File destFile = new File(Constants.FILE_UPLOAD_DIC + newFileName);\r\n    try {\r\n        if (!fileDirectory.exists()) {\r\n            if (!fileDirectory.mkdir()) {\r\n                throw new IOException(\"文件夹创建失败,路径为:\" + fileDirectory);\r\n            }\r\n        }\r\n        file.transferTo(destFile);\r\n        Result resultSuccess = ResultGenerator.genSuccessResult();\r\n        resultSuccess.setData(NewBeeMallUtils.getHost(new URI(httpServletRequest.getRequestURL() + \"\")) + \"/upload/\" + newFileName);\r\n        return resultSuccess;\r\n    } catch (IOException e) {\r\n        e.printStackTrace();\r\n        return ResultGenerator.genFailResult(\"文件上传失败\");\r\n    }\r\n}"
},
{
  "http_method": "POST",
  "api_path": "/manage-api/v1/upload/files",
  "method_name": "uploadV2",
  "method_body": "/**\r\n * 图片上传\r\n */\r\n@RequestMapping(value = \"/upload/files\", method = RequestMethod.POST)\r\n@ApiOperation(value = \"多图上传\", notes = \"wangEditor图片上传\")\r\npublic Result uploadV2(HttpServletRequest httpServletRequest, @TokenToAdminUser AdminUserToken adminUser) throws URISyntaxException {\r\n    logger.info(\"adminUser:{}\", adminUser.toString());\r\n    List<MultipartFile> multipartFiles = new ArrayList<>(8);\r\n    if (standardServletMultipartResolver.isMultipart(httpServletRequest)) {\r\n        MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) httpServletRequest;\r\n        Iterator<String> iter = multiRequest.getFileNames();\r\n        int total = 0;\r\n        while (iter.hasNext()) {\r\n            if (total > 5) {\r\n                return ResultGenerator.genFailResult(\"最多上传5张图片\");\r\n            }\r\n            total += 1;\r\n            MultipartFile file = multiRequest.getFile(iter.next());\r\n            multipartFiles.add(file);\r\n        }\r\n    }\r\n    if (CollectionUtils.isEmpty(multipartFiles)) {\r\n        return ResultGenerator.genFailResult(\"参数异常\");\r\n    }\r\n    if (multipartFiles != null && multipartFiles.size() > 5) {\r\n        return ResultGenerator.genFailResult(\"最多上传5张图片\");\r\n    }\r\n    List<String> fileNames = new ArrayList(multipartFiles.size());\r\n    for (int i = 0; i < multipartFiles.size(); i++) {\r\n        String fileName = multipartFiles.get(i).getOriginalFilename();\r\n        String suffixName = fileName.substring(fileName.lastIndexOf(\".\"));\r\n        //生成文件名称通用方法\r\n        SimpleDateFormat sdf = new SimpleDateFormat(\"yyyyMMdd_HHmmss\");\r\n        Random r = new Random();\r\n        StringBuilder tempName = new StringBuilder();\r\n        tempName.append(sdf.format(new Date())).append(r.nextInt(100)).append(suffixName);\r\n        String newFileName = tempName.toString();\r\n        File fileDirectory = new File(Constants.FILE_UPLOAD_DIC);\r\n        //创建文件\r\n        File destFile = new File(Constants.FILE_UPLOAD_DIC + newFileName);\r\n        try {\r\n            if (!fileDirectory.exists()) {\r\n                if (!fileDirectory.mkdir()) {\r\n                    throw new IOException(\"文件夹创建失败,路径为:\" + fileDirectory);\r\n                }\r\n            }\r\n            multipartFiles.get(i).transferTo(destFile);\r\n            fileNames.add(NewBeeMallUtils.getHost(new URI(httpServletRequest.getRequestURL() + \"\")) + \"/upload/\" + newFileName);\r\n        } catch (IOException e) {\r\n            e.printStackTrace();\r\n            return ResultGenerator.genFailResult(\"文件上传失败\");\r\n        }\r\n    }\r\n    Result resultSuccess = ResultGenerator.genSuccessResult();\r\n    resultSuccess.setData(fileNames);\r\n    return resultSuccess;\r\n}"
}
]

​ 这个代码片段交给GPT-4o后,会非常准确的判断出存在文件上传漏洞以及成因和修改建议。

![image-20250412154809062](D:ty小论文1image-20250412154809062.png)

这些摘要信息就够了吗?

​ 当然不是!在目前项目的开发过程中,诸如:文件上传、访问、跳转、传参这类漏洞,开发者通常会将它们的判断逻辑、过滤逻辑等代码放在紧跟着的函数体中。当然判断逻辑也可能存在于Controller层、Service层和Filter层中,在SQL注入、存储型XSS漏洞则会将判断逻辑放在其他位置,如引入了Mybatis的项目会将SQL判断放在XML mapper文件中。当然许多规范的开发项目会将特定处理逻辑放在别的地方,存在许多的函数调用,那么只根据上述摘要显然是不够的。

​ 所以,如果想让LLM更准确的分析多种漏洞类型,我们参考了人类审计的方式,让它一定要具备缺什么查什么的能力。基于此,我们采用静态分析框架,又提取了第二份摘要,所有方法信息。

![image-20250523081404765](D:tyOWASP反序列化image-20250523081404765.png)

​ 这份文件很大,但我们不需要将它交给LLM的输入,只需要放在本地,供后面Agent自动查询使用。LLM对于判断不清楚的漏洞,比如“无法分析,不清楚MyMapper.addArgumentResolvers的具体实现”,Agent会自动调用我们写好的工具帮他在本地第二份摘要中查询该方法,并将完整方法体再次输入给LLM,当然,一次的查询可能解决不了问题,如查询完后,仍然“无法分析,不清楚user.add函数的具体实现”,我们则允许它多轮调用工具查询,直到它完全理解为止。

​ 针对XML SQL问题,我们同样在本地提取了第三份摘要,包含所有的XML SQL语句

![image-20250523081630365](D:tyOWASP反序列化image-20250523081630365.png)

​ 同上,当LLM不清楚具体sql逻辑时,可以根据XML id查询,返回完整SQL语句,这样就允许LLM精准分析,完美构造存在漏洞的payload语句。

​ 总结起来,我们一共提取了三份信息:所有api相关信息,所有方法体,所有xml sql语句,这里面只有第一份所有api相关信息作为LLM的初始输入,其他都作为后续的查询使用。

LLM Agent功能设计

​ 过测试发现,仅依赖提取的所有api相关信息api_results.txt文件内容,GPT-4o仅能准确识别约40%的API漏洞。因此,仍需通过设计特定Prompt并结合Agent调用工具的方式,补全更多上下文信息,从而提升漏洞判断的准确性。

​ 当前大语言模型LLM在是否存在漏洞判断中存在的问题主要有:生成结果存在幻觉、Web安全漏洞领域知识不够全。

解决幻觉问题:

我们需要让LLM明确分析和输出问题的界限。

我们明确七类漏洞api漏洞类型:越权访问、XSS、文件上传、SSRF、SQL注入、远程代码执行、目录遍历。针对上述API信息可能判断出其他漏洞类型,我们也不需要LLM进行判断和输出。

为提高分析结果的可控性和一致性,我们规定LLM在根据初始代码摘要片段分析中,仅使用以下三种结果分类:

明确(100%):判断漏洞存在,给出具体依据;

不存在(0%):判断漏洞不存在,说明防护措施;

无法分析:摘要代码段缺少关键信息,需补充上下文。

对于判断结果为“明确”的类型,输出判断依据(如参数未经过滤直接传递),同时将其上下文内容保存,以供后续POC生成使用。

对于判断结果为“不存在”的类型,输出判断依据(如已对参数进行过滤),同时将上下文保存,不参与后续判断。

对于判断结果为“无法分析”的类型,需要通过Agent调用后续提到的工具进行上下文信息补全,需经过多轮判断,终止条件为分析结果“明确”或“不存在”。

输出结果示例:

![image-20250412162316430](D:ty小论文1image-20250412162316430.png)

解决Web安全漏洞领域知识欠缺问题

​ 当前大模型如:GPT-4o,DeepSeek-R1等参数较大的模型对于网络安全方面知识理解较为丰富,只需要引导其输出“尽可能多的”的安全知识就能满足现阶段的基本要求,但是诸如:Qwen-2.5-7B等小模型,在网络安全领域知识明显欠缺,在判断漏洞类型时会出现明显错误,在长下文下会出现:循环输出、出现幻觉等问题。

​ 基于此,我们需要在promt中加入特定安全领域知识,如:

![image-20250412162345138](D:ty小论文1image-20250412162345138.png)

​ 每类漏洞都可基于此进行知识加强,从而使分析结果更加准确。

Agent工具开发

​ 为了让大语言模型具备“多轮补全上下文信息能力”,我们基于微软开源的AutoGen框架构建了Agent协作系统。AutoGen是一个支持多智能体对话与工具调用的框架,它能够将大模型(如 GPT-4o)与外部工具、高级函数甚至人类用户进行灵活编排,从而实现自动推理与任务执行。

​ 在此框架下,我们进一步开发了以下两个辅助工具,配合 Agent 自动执行上下文补全任务。Agent调用工具一,该工具作用是根据方法名快速检索其完整方法体,并输出给LLM。该工具采用正则匹配方式编写的 Python 脚本,能够在3.1节生成的all_methods.txt文件中定位具体实现并将其上下文信息返回给LLM,用于继续判断漏洞是否存在。

自动化 POC 生成与验证

​ 区别于传统静态分析方法仅输出漏洞检测报告,本研究进一步引入大语言模型能力,实现对已识别漏洞的自动化 POC(Proof of Concept)生成与验证,从而完成漏洞检测从“识别”到“验证”的全链条闭环,提高漏洞确认的准确性与系统自动化程度。流程如下:

![image-20250412162549903](D:ty小论文1image-20250412162549903.png)

​ 针对前文 Prompt设计中支持识别的七类常见 Web 漏洞类型(越权访问、XSS、文件上传、SSRF、SQL 注入、远程代码执行、目录遍历),我们在提示词中为每一类漏洞设计了标准化的POC模板(POC Example),以丰富LLM的知识库。大语言模型在获取到判断为“明确”存在漏洞的API相关信息后,将结合以下关键信息生成攻击向量:

(1)API 请求路径与请求方法(如 GET/POST)

(2)参数名、参数位置及数据类型(如来自 @RequestParam、@PathVariable、@RequestBody)

(3)方法体上下文信息(是否存在参数拼接、危险 API 调用等)

(4)项目运行环境基础信息(如 baseUrl、端口号、数据库类型)

(5)POC模板内容

​ 通过上下文理解与模板迁移能力,LLM 能够自动构造具备攻击性、逻辑完整的POC请求,并输出预期的响应特征(例如:数据库异常、文件回显、内网地址响应等),用于辅助判断漏洞是否实际存在。

SSRF POC模版示例:

import requests

BASE_URL = "http://127.0.0.1:8080/ssrf"
SSRF_PAYLOADS = [
    "http://127.0.0.1:8000",  # 测试本地 HTTP 服务器
]

VULNERABLE_APIS = [
    f"{BASE_URL}/one",
    f"{BASE_URL}/two",
    f"{BASE_URL}/three",
    f"{BASE_URL}/four",
    f"{BASE_URL}/five",
]

# 记录测试结果
with open("ssrf_results.txt", "w", encoding="utf-8") as file:
    for api in VULNERABLE_APIS:
        for payload in SSRF_PAYLOADS:
            try:
                response = requests.get(api, params={"url": payload}, timeout=5)

                # 记录响应内容
                file.write(f"API: {api} | Payload: {payload}\n")
                file.write(f"Response Code: {response.status_code}\n")
                file.write(f"Response Body: {response.text}\n\n")

                # 判断 SSRF 是否成功
                if response.status_code == 200 and "Hello" not in response.text:
                    print(f"[+] SSRF 成功: {api} -> {payload}")
                else:
                    print(f"[-] SSRF 失败: {api} -> {payload}")
            except Exception as e:
                print(f"[!] 请求失败: {api} -> {payload} - {str(e)}")
                file.write(f"[!] 请求失败: {api} -> {payload} - {str(e)}\n\n")

LAESA能力

​ 目前项目已经完成在github私有仓库中,经过实测,在靶场中采用GPT-4O作为基底检测与验证的准确率在90%以上。

更多想法

​ 优化方面,目前只是初步探索,未来会将代码摘要做的更加精炼,Agent设计更加完美,同时终极目标是希望安全分析领域出现自己的LLM或小模型。
目前代码在github的私有仓库中,仍在完成论文中,如果你有任何问题,欢迎联系 232270041@hdu.edu.cn