从代码层面分析Jenkins未授权访问CVE-2017-1000353
字数 1318 2025-08-24 16:48:16
Jenkins未授权访问漏洞CVE-2017-1000353深度分析
漏洞概述
CVE-2017-1000353是Jenkins CI工具中的一个远程代码执行漏洞,该漏洞存在于Jenkins利用HTTP协议进行双向通信的过程中,通过反序列化攻击实现远程代码执行。
环境搭建
-
下载Jenkins 2.190.3版本:
wget https://repo.huaweicloud.com/jenkins/redhat-stable/jenkins-2.190.3-1.1.noarch.rpm -
安装Jenkins:
rpm -ivh jenkins-2.190.3-1.1.noarch.rpm -
启动服务:
systemctl start jenkins systemctl status jenkins -
关闭防火墙(测试环境)
漏洞原理分析
1. 双向通道建立机制
漏洞的核心在于Jenkins CLI的双向通信机制,主要代码位于hudson.cli.CLIAction类中:
@Extension
@Symbol("cli")
@Restricted(NoExternalUse.class)
public class CLIAction implements UnprotectedRootAction, StaplerProxy {
private transient final Map<UUID, FullDuplexHttpChannel> duplexChannels = new HashMap<UUID, FullDuplexHttpChannel>();
@Override
public Object getTarget() {
StaplerRequest req = Stapler.getCurrentRequest();
if (req.getRestOfPath().length()==0 && "POST".equals(req.getMethod())) {
throw new CliEndpointResponse();
} else {
return this;
}
}
private class CliEndpointResponse extends HttpResponseException {
@Override
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
try {
UUID uuid = UUID.fromString(req.getHeader("Session"));
rsp.setHeader("Hudson-Duplex", "");
FullDuplexHttpChannel server;
if (req.getHeader("Side").equals("download")) {
duplexChannels.put(uuid, server = new FullDuplexHttpChannel(uuid, !Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) {
@Override
protected void main(Channel channel) throws IOException, InterruptedException {
channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
channel.setProperty(CliEntryPoint.class.getName(), new CliManagerImpl(channel));
}
});
try {
server.download(req, rsp);
} finally {
duplexChannels.remove(uuid);
}
} else {
duplexChannels.get(uuid).upload(req, rsp);
}
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
}
关键点:
- 通过
Session和Side请求头控制通道建立 Side: download时创建新通道并放入duplexChannels映射Side: upload时从duplexChannels获取已有通道
2. 通道数据传输过程
FullDuplexHttpChannel类负责数据传输:
public synchronized void download(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {
// 等待上传通道建立
long end = System.currentTimeMillis() + CONNECTION_TIMEOUT;
while (upload == null && System.currentTimeMillis() < end)
wait(1000);
if (upload == null)
throw new IOException("HTTP full-duplex channel timeout: " + uuid);
try {
channel = new Channel("HTTP full-duplex channel " + uuid,
Computer.threadPoolForRemoting,
Mode.BINARY,
upload, out, null, restricted);
} finally {
completed = true;
notify();
}
}
public synchronized void upload(StaplerRequest req, StaplerResponse rsp) throws InterruptedException, IOException {
rsp.setStatus(HttpServletResponse.SC_OK);
InputStream in = req.getInputStream();
if (DIY_CHUNKING) in = new ChunkedInputStream(in);
upload = in;
notify();
while (!completed)
wait();
}
关键点:
download()等待upload通道建立,超时时间为CONNECTION_TIMEOUT- 创建
Channel对象时传入upload输入流 upload()设置输入流并通知download()线程
3. 命令传输与反序列化
ChannelBuilder.negotiate()方法处理命令传输:
protected CommandTransport negotiate(final InputStream is, final OutputStream os) throws IOException {
Mode[] modes = {Mode.BINARY, Mode.TEXT};
byte[][] preambles = new byte[][]{Mode.BINARY.preamble, Mode.TEXT.preamble, Capability.PREAMBLE};
int[] ptr = new int[3];
Capability cap = new Capability(0);
while (true) {
int ch = is.read();
for (int i = 0; i < preambles.length; i++) {
byte[] preamble = preambles[i];
if (preamble[ptr[i]] == ch) {
if (++ptr[i] == preamble.length) {
switch (i) {
case 0: case 1:
return makeTransport(is, os, mode, cap);
case 2:
cap = Capability.read(is);
}
}
}
}
}
}
关键点:
- 检查输入流的前导码(preamble)
- 前导码格式示例:
<===[JENKINS REMOTING CAPACITY]===>rO0ABXNyABpodWRzb24ucmVtb3RpbmcuQ2FwYWJpbGl0eQAAAAAAAAABAgABSgAEbWFza3hwAAAAAAAAAH4= - 前导码包含base64编码的序列化
Capability对象
makeTransport()方法创建传输对象:
protected CommandTransport makeTransport(InputStream is, OutputStream os, Mode mode, Capability cap) throws IOException {
FlightRecorderInputStream fis = new FlightRecorderInputStream(is);
if (cap.supportsChunking())
return new ChunkedCommandTransport(cap, mode.wrap(fis), mode.wrap(os), os);
else {
ObjectOutputStream oos = new ObjectOutputStream(mode.wrap(os));
oos.flush();
return new ClassicCommandTransport(
new ObjectInputStreamEx(mode.wrap(fis), getBaseLoader(), getClassFilter()),
oos, fis, os, cap);
}
}
4. 命令执行流程
Channel构造函数调用transport.setup()启动ReaderThread线程ReaderThread调用read()方法读取命令Command.readFrom()反序列化输入流生成Command对象- 反序列化过程中执行恶意代码
关键代码:
private final class ReaderThread extends Thread {
public ReaderThread(CommandReceiver receiver) {
super("Channel reader thread: " + channel.getName());
this.receiver = receiver;
}
@Override
public void run() {
try {
while (!channel.isInClosed()) {
Command cmd = null;
try {
cmd = read();
// 正常情况下应调用receiver.handle(cmd)
} catch (Throwable t) {
// 错误处理
}
}
} finally {
// 清理代码
}
}
}
public final Command read() throws IOException, ClassNotFoundException {
try {
Command cmd = Command.readFrom(channel, ois);
return cmd;
} catch (RuntimeException e) {
throw new IOException("Failed to read the response", e);
}
}
漏洞利用关键点
- 前导码要求:攻击数据必须包含特定的前导码才能被正确处理
- 反序列化点:
Command.readFrom()方法会反序列化输入数据 - 执行时机:反序列化过程中就会执行恶意代码,而非正常的
handle()方法调用 - 认证绕过:CLI连接建立时不需要任何权限验证
防御措施
- 升级到修复版本
- 禁用Jenkins CLI(通过管理界面或修改配置文件)
- 实施网络访问控制,限制Jenkins管理端口访问
- 使用反向代理并配置适当的访问控制
总结
CVE-2017-1000353漏洞的核心在于Jenkins CLI的双向通信机制中缺乏对反序列化操作的适当安全控制。攻击者可以通过构造恶意的序列化数据,在无需认证的情况下实现远程代码执行。理解这一漏洞需要深入分析Jenkins的通信协议实现、反序列化机制以及线程处理流程。