Security context
Medium· 5.8GHSA-gpxv-776p-7gc7 CVE-2020-2100CWE-406Published May 24, 2022

Jenkins vulnerable to UDP amplification reflection attack

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

0 → fixed in 2.204.22.205 → fixed in 2.219

Details

Jenkins 2.218 and earlier, LTS 2.204.1 and earlier supports two network discovery services (UDP multicast/broadcast and DNS multicast) by default. The UDP multicast/broadcast service can be used in an amplification reflection attack, as very few bytes sent to the respective endpoint result in much larger responses: A single byte request to this service would respond with more than 100 bytes of Jenkins metadata which could be used in a DDoS attack on a Jenkins controller. Within the same network, spoofed UDP packets could also be sent to make two Jenkins controllers go into an infinite loop of replies to one another, thus causing a denial of service. Jenkins 2.219, LTS 2.204.2 now disables both UDP multicast/broadcast and DNS multicast by default. Administrators that need these features can re-enable them again by setting the system property `hudson.DNSMultiCast.disabled` to `false` (for DNS multicast) or the system property `hudson.udp` to `33848`, or another port (for UDP broadcast/multicast). These are the same system properties that controlled whether these features were enabled in the past, so any instances explicitly enabling these features by setting these system properties will continue to have them enabled.

The fix

[SECURITY-1641]

Wadeck Follonier· Jan 14, 2020, 11:06 AM+892cd28a6d934
core/src/main/java/hudson/DNSMultiCast.java+1 1
@@ -88,5 +88,5 @@ public void close() {
private static final Logger LOGGER = Logger.getLogger(DNSMultiCast.class.getName());
- public static boolean disabled = SystemProperties.getBoolean(DNSMultiCast.class.getName()+".disabled");
+ public static boolean disabled = SystemProperties.getBoolean(DNSMultiCast.class.getName()+".disabled", true);
}
core/src/main/java/hudson/UDPBroadcastThread.java+2 1
@@ -129,7 +129,8 @@ public void shutdown() {
interrupt();
}
- public static final int PORT = SystemProperties.getInteger("hudson.udp",33848);
+ // The previous default port was 33848, before the "disabled by default" change
+ public static final int PORT = SystemProperties.getInteger("hudson.udp", -1);
private static final Logger LOGGER = Logger.getLogger(UDPBroadcastThread.class.getName());
test/src/test/java/hudson/UDPBroadcastThreadSEC1641Test.java+29 0
@@ -0,0 +1,29 @@
+package hudson;
+
+import jenkins.model.Jenkins;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.JenkinsRule;
+
+import java.lang.reflect.Field;
+
+import static org.junit.Assert.assertNull;
+
+public class UDPBroadcastThreadSEC1641Test {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Test
+ public void ensureThereIsNoThreadRunningByDefault() throws Exception {
+ UDPBroadcastThread thread = getPrivateThread(j.jenkins);
+ assertNull(thread);
+ }
+
+ private static UDPBroadcastThread getPrivateThread(Jenkins jenkins) throws Exception {
+ Field threadField = Jenkins.class.getDeclaredField("udpBroadcastThread");
+ threadField.setAccessible(true);
+
+ return (UDPBroadcastThread) threadField.get(jenkins);
+ }
+}
test/src/test/java/hudson/UDPBroadcastThreadTest.java+57 0
@@ -1,11 +1,17 @@
package hudson;
+import jenkins.model.Jenkins;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.BeforeClass;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.ParserConfigurationException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
@@ -17,18 +23,40 @@
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
/**
* @author Kohsuke Kawaguchi
*/
public class UDPBroadcastThreadTest {
@Rule public JenkinsRule j = new JenkinsRule();
+
+ @BeforeClass
+ public static void forceActivateUDPMulticast() throws Exception {
+ // required to be done before JenkinsRule starts the Jenkins instance
+ // as the usage of this port is in the constructor
+ updatePort(33848);
+ }
+
+ private static void updatePort(int newValue) throws Exception {
+ Field portField = UDPBroadcastThread.class.getField("PORT");
+
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(portField, portField.getModifiers() & ~Modifier.FINAL);
+ portField.setInt(null, newValue);
+ }
+
/**
* Old unicast based clients should still be able to receive some reply,
* as we haven't changed the port.
*/
@Test public void legacy() throws Exception {
+ updatePort(33848);
DatagramSocket s = new DatagramSocket();
sendQueryTo(s, InetAddress.getLocalHost());
s.setSoTimeout(15000); // to prevent test hang
@@ -60,18 +88,38 @@ public class UDPBroadcastThreadTest {
// we should at least get two replies since we run two broadcasts
try {
+ // from first (Jenkins one) (order does not matter)
+ receiveAndVerify(s);
+ // from second
receiveAndVerify(s);
+ // from third
receiveAndVerify(s);
} catch (SocketTimeoutException x) {
Assume.assumeFalse(UDPBroadcastThread.udpHandlingProblem);
throw x;
}
+
+ // to fail fast
+ s.setSoTimeout(2000);
+ try {
+ receiveAndVerify(s);
+ fail("There should be only 3 listeners");
+ } catch (SocketTimeoutException x) {
+ // expected to throw
+ }
} finally {
third.interrupt();
second.interrupt();
}
}
+ @Test
+ public void ensureTheThreadIsRunningWithSysProp() throws Exception {
+ UDPBroadcastThread thread = getPrivateThread(j.jenkins);
+ assertNotNull(thread);
+ assertTrue(thread.isAlive());
+ }
+
private void sendQueryTo(DatagramSocket s, InetAddress dest) throws IOException {
DatagramPacket p = new DatagramPacket(new byte[1024],1024);
p.setAddress(dest);
@@ -86,11 +134,20 @@ private void receiveAndVerify(DatagramSocket s) throws IOException, SAXException
DatagramPacket p = new DatagramPacket(new byte[1024],1024);
s.receive(p);
String xml = new String(p.getData(), 0, p.getLength(), "UTF-8");
+ //example: <hudson><version>2.164.4-SNAPSHOT</version><url>http://localhost:23146/jenkins/</url><server-id>be6757793486931ff50c259b66c77704</server-id><slave-port>23149</slave-port></hudson>
System.out.println(xml);
+ Assert.assertThat(xml, Matchers.containsString("<hudson>"));
// make sure at least this XML parses
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setNamespaceAware(true);
spf.newSAXParser().parse(new InputSource(new StringReader(xml)),new DefaultHandler());
}
+
+ private static UDPBroadcastThread getPrivateThread(Jenkins jenkins) throws Exception {
+ Field threadField = Jenkins.class.getDeclaredField("udpBroadcastThread");
+ threadField.setAccessible(true);
+
+ return (UDPBroadcastThread) threadField.get(jenkins);
+ }
}

References