I. What is DoS?

DoS is short for Denial of Service, which means denial of service. The attack that causes DoS is called a DoS attack, and its purpose is to make the computer or network unable to provide normal services. Denial of Service exists on various web services, this web service can be implemented in c, c++, or go, java, php, python, and other languages.

II. Status of Java DoS

In various open source and closed source java system products, we often see announcements about DoS defects, most of which are CPU exhaustion type or business offload type DoS. CPU exhaustion type DoS mainly includes “regular backtracking CPU exhaustion, code repeatedly executes a lot of CPU exhaustion, dead loop code exhaustion CPU “etc. The business offloading DoS is a type of DoS that is strongly coupled with the system business, and is not universal in general. It is briefly described below with a few simple examples.

  • Regular backtracking exhausts CPU
1
Pattern.matches(controllableVariableRegex, controllableVariableText)
  • CPU exhausted by repeated code execution
1
2
3
for(int i = 0; i < controllableVariable; i++) {
//do something, e.g. consume cpu
}
  • Dead loop consumes CPU
1
2
3
while(controllableBoolVariable) {
//do something, e.g. consume cpu
}
  • business uninstall DoS (when any user has access to the uninstall method, the business can be maliciously uninstalled, resulting in denial of service for normal users)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private final Set<String> availables = new HashSet();

public void service(String type, String name) {
if (availables.contains(type)) {
//do service
} else {
//reject service
}
}

public void uninstall(String type) {
availables.remove(type)
}

When I was digging into the XStream deserialization exploit chain, I also found an exploit chain that could launch a “regular backtracking CPU exhaustion, dead loop CPU exhaustion” type DoS attack, and ended up with multiple CVE number tags and signatures for XStream.

Is it possible that these are the only types of DoS that exist in the Java system? I don’t think so. I think there are bound to be a lot of other types of DoS flaws in Java systems that we just haven’t discovered yet. When I was auditing a Java system one day, I suddenly found a defect that was different from these DoS types, and when I audited a large number of other Java systems, it existed universally, and I knew it was a universal defect - Memory DoS.

III. Java Exception Mechanism

In web services implemented in c, c++ and other languages, there may be various types of DoS such as null pointer DoS, CPU exhaustion DoS, etc. Why are there so few types of DoS in Java? This brings us to the exception mechanism in Java.

Java exceptions in the JRE source code implementation, mainly divided into java.lang.Exception and java.lang.Error, they all have a common implementation of java.lang.Throwable.

Exceptions are some errors in a program, but not all errors are exceptions, and errors can sometimes be avoided.

For example, if your code is missing a semicolon, then running out the result is prompted by the error java.lang.Error; if you use System.out.println(11/0), then you are because you used 0 as a divisor and will throw the exception java.lang.ArithmeticException.

Exceptions can occur for a number of reasons, and usually contain the following major categories.

  • The user has entered illegal data.
  • The file to be opened does not exist.
  • The connection was broken during network communication, or the JVM memory overflowed.

Some of these exceptions are caused by user errors, some are caused by program errors, and others are caused by physical errors.

To understand how Java exception handling works, you need to master the following three types of exceptions.

  • Checked exceptions: The most represented checked exceptions are those caused by user errors or problems, which are not foreseen by the programmer. For example, an exception occurs when a non-existent file is to be opened, and these exceptions cannot simply be ignored at compile time.
  • Runtime exceptions: Runtime exceptions are exceptions that may be avoided by the programmer. In contrast to checked exceptions, runtime exceptions can be ignored at compile time.
  • Errors: Errors are not exceptions, but problems that are out of the programmer’s control. Errors are usually ignored in the code. For example, an error occurs when a stack overflow occurs, and they are not checked at compile either.

The description of the Java exception mechanism, described above, makes it clear that when an exception occurs, it can often be mostly caught and handled, but when an error occurs, it means that the program is no longer running properly. That is to say, most of the exceptions we generate in the Java system, there is no way to cause a DoS, only to cause an error that will prevent the program from running properly and lead to a DoS, which is why in Java, the type of DoS is relatively small.

Looking through the JRE on the implementation of the error java.lang.Error, you can see that there are very, very many, and the main character today is java.lang.OutOfMemoryError, that is, if we can make the program generate java.lang.OutOfMemoryError error, you can achieve DoS. most java programmers should be familiar with it, throwing java.lang.OutOfMemoryError error, usually appear in jvm memory shortage, or memory leak resulting in gc can not be reclaimed.

IV. A widespread Memory DoS

As mentioned in the previous section, we can implement DoS if we can make the program generate java.lang.OutOfMemoryError error, which is called Memory DoS, a DoS attack that runs out of memory and causes the program to throw an error.

So, how do you get a Java system to generate a java.lang. The answer must be “run out of memory”!

I have scanned several java systems, components through a simple code scanning tool, which found a large number of exploitable Memory DoS flaws, yes, this means that I can make these systems generate java.lang. And these systems and components include the more famous ones like Java SE, WebLogic, Spring, Sentinel, Jackson, xstream, etc.

0x01 Java SE

In my scan of Java SE, I found three classes that can cause the system to generate java.lang.OutOfMemoryError errors when deserializing, and perform Memory DoS attacks on the system. I immediately reported it to Oracle, and it was eventually fixed in Java SE 8u301

Let’s take a look at the Java SE 8u301 fix and before the fix, the difference between them compared.

java.time.zone.ZoneRules#readExternal

Before the fix.

1
2
3
4
5
6
7
8
9
static ZoneRules readExternal(DataInput in) throws IOException, ClassNotFoundException {
    int stdSize = in.readInt();
    long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY
                                     : new long[stdSize];
    for (int i = 0; i < stdSize; i++) {
        stdTrans[i] = Ser.readEpochSec(in);
    }
    ...
}

After the repair.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static ZoneRules readExternal(DataInput in) throws IOException, ClassNotFoundException {
    int stdSize = in.readInt();
    if (stdSize > 1024) {
        throw new InvalidObjectException("Too many transitions");
    }
    long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY
                                     : new long[stdSize];
    for (int i = 0; i < stdSize; i++) {
        stdTrans[i] = Ser.readEpochSec(in);
    }
    ...
}

java.awt.datatransfer.MimeType#readExternal

Before the fix.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
    String s = in.readUTF();
    if (s == null || s.length() == 0) { // long mime type
        byte[] ba = new byte[in.readInt()];
        in.readFully(ba);
        s = new String(ba);
    }
    try {
        parse(s);
    } catch(MimeTypeParseException e) {
        throw new IOException(e.toString());
    }
}

After the repair.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
    String s = in.readUTF();
    if (s == null || s.length() == 0) { // long mime type
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int len = in.readInt();
        while (len-- > 0) {
            baos.write(in.readByte());
        }
        s = baos.toString();
    }
    try {
        parse(s);
    } catch(MimeTypeParseException e) {
        throw new IOException(e.toString());
    }
}

com.sun.deploy.security.CredentialInfo#readExternal

Before the fix.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
    try {
        this.userName = (String)var1.readObject();
        this.sessionId = var1.readLong();
        this.domain = (String)var1.readObject();
        this.encryptedPassword = new byte[var1.readInt()];

        for(int var2 = 0; var2 < this.encryptedPassword.length; ++var2) {
            this.encryptedPassword[var2] = var1.readByte();
        }
    } catch (Exception var3) {
        Trace.securityPrintException(var3);
    }

}

After the repair.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
    try {
        this.userName = (String)var1.readObject();
        this.sessionId = var1.readLong();
        this.domain = (String)var1.readObject();
        int var2 = var1.readInt();
        if (var2 > 4096) {
            throw new SecurityException("Invalid password length (" + var2 + "). It should not exceed " + 4096 + " bytes.");
        }

        this.encryptedPassword = new byte[var2];

        for(int var3 = 0; var3 < this.encryptedPassword.length; ++var3) {
            this.encryptedPassword[var3] = var1.readByte();
        }
    } catch (Exception var4) {
        Trace.securityPrintException(var4);
    }

}

With these three examples, do you see anything?

Yes, this is a kind of Memory DoS defect that exists by taking advantage of the fact that the capacity parameter is controllable when the array is initialized. When a malicious user controls the capacity parameter and sets the parameter value size to the int maximum value of 2147483647-2 (2147483645 is the maximum limit for array initialization), then, when the array is initialized, the JVM will run out of memory, thus causing the system to generate a java.lang.OutOfMemoryError error and realize Memory DoS.

0x02 WebLogic

In my scan of Weblogic, I found dozens of classes that can cause the system to generate java.lang.OutOfMemoryError errors during deserialization, enabling Memory DoS attacks on the system. The scan took a few minutes, but I spent a lot of time writing the report.

Weblogic&rsquo;s scan results

With these three examples, do you see anything? Weblogic&rsquo;s scan results

Yes, this is a kind of Memory DoS defect that exists by taking advantage of the fact that the capacity parameter is controllable when the array is initialized. When a malicious user controls the capacity parameter and sets the parameter value size to the int maximum value of 2147483647-2 (2147483645 is the maximum limit for array initialization), then, when the array is initialized, the JVM will run out of memory, thus causing the system to generate a java.lang.OutOfMemoryError error and realize Memory DoS.

After reporting it to Oracle, I learned that it was fixed in the 2021.07 security notification at https://www.oracle.com/security-alerts/cpujul2021.html.

The Memory DoS in WebLogic is not very different from the Java SE one, so I won’t list them one by one.

com.tangosol.run.xml.SimpleDocument#readExternal(java.io.ObjectInput)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    int cch = in.readInt();
    char[] ach = new char[cch];
    Utf8Reader reader = new Utf8Reader((InputStream)in);

    int cchBlock;
    for(int of = 0; of < cch; of += cchBlock) {
        cchBlock = reader.read(ach, of, cch - of);
        if (cchBlock < 0) {
            throw new EOFException();
        }
    }

    XmlHelper.loadXml(new String(ach), this, false);
}

However, the preceding examples in WebLogic as well as Java SE, are sinks that attack at the time of array initialization. in fact, there is another, it is the java collection Collection, when a Collection is initialized, often in its internal implementation, will initialize one or more arrays to store data. Then, if at the time of Collection initialization, we can control its capacity parameter, it can make the JVM memory is insufficient, thus causing the system to generate java.lang.OutOfMemoryError error, resulting in Memory DoS.

weblogic.deployment.jms.PooledConnectionFactory#readExternal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void readExternal(ObjectInput in) throws IOException {
    int extVersion = in.readInt();
    if (extVersion != 1) {
        throw new IOException(JMSPoolLogger.logInvalidExternalVersionLoggable(extVersion).getMessage());
    } else {
        this.wrapStyle = in.readInt();
        this.poolName = in.readUTF();
        this.containerAuth = in.readBoolean();
        int numProps = in.readInt();
        this.poolProps = new HashMap(numProps);

        for(int inc = 0; inc < numProps; ++inc) {
            String name = in.readUTF();
            String value = in.readUTF();
            this.poolProps.put(name, value);
        }

        this.poolManager = JMSSessionPoolManager.getSessionPoolManager();
        this.poolManager.incrementReferenceCount(this.poolName);
        JMSPoolDebug.logger.debug("In PooledConnectionFactory.readExternal()[poolManager=" + this.poolManager + "]");
    }
}

As you can see, this line of code initializes the HashMap, and we are able to control the size of its construction parameter values.

1
this.poolProps = new HashMap(numProps);

Now the HashMap construction parameters we can control, meaning that we can freely specify the size of its initialization capacity, if its size exceeds the memory available to the JVM, it will cause the system to generate java.lang.OutOfMemoryError error and achieve Memory DoS.

0x03 Spring

In Spring, I did a simple audit of the source code of its mvc framework and found that in an HttpMessageConverter, there is an array initialization capacity parameter controllable, which will be able to cause the system to generate a java.lang.OutOfMemoryError error when a simple construction is performed on http to achieve Memory DoS.

After I reported this to Spring officials, they decided that it was a security issue, but that it was up to other systems to limit it, so it was not fixed.

I also offered a fix to the Spring developers, but they didn’t end up adopting it, so the security issue still exists, but fortunately, you don’t need to worry about it, because this vulnerability requires certain prerequisites to exploit.

org.springframework.http.converter.ByteArrayHttpMessageConverter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public byte[] readInternal(Class<? extends byte[]> clazz, HttpInputMessage inputMessage) throws IOException {
	long contentLength = inputMessage.getHeaders().getContentLength();
	ByteArrayOutputStream bos =
			new ByteArrayOutputStream(contentLength >= 0 ? (int) contentLength : StreamUtils.BUFFER_SIZE);
	StreamUtils.copy(inputMessage.getBody(), bos);
	return bos.toByteArray();
}

public ByteArrayOutputStream(int size) {
    if (size < 0) {
        throw new IllegalArgumentException("Negative initial size: "
                                           + size);
    }
    buf = new byte[size];
}

As you can see, when we initiate the http request, we are able to use Content-Length this http header to control the initialization capacity of the byte array, if we pass in the size of the Content-Length is the maximum of the Long type, it will cause the system to generate java.lang. OutOfMemoryError error, to achieve Memory DoS attack.

However, to exploit this vulnerability, the prerequisite is that the developer needs to write some implementation of Controller, which requires Controller to receive a parameter of type byte[], because only then Spring is using org.springframework.http. converter.ByteArrayHttpMessageConverter to handle it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * @author JavaIsland
 */
@RestController
public class TestController {

    @PostMapping(value = "/JavaIsland")
    public String test(@RequestBody byte[] bytes) {
        return "JavaIsland";
    }
}

0x04 Sentinel

github: https://github.com/alibaba/Sentinel

Java SE, WebLogic, Spring, but all are nothing more than attacks on array initialization parameters, so are there any other Memory DoS that can cause the system to generate java.lang.OutOfMemoryError errors?

The answer is “yes”. When I audited Alibaba Sentinel, I found that its control platform, which can also be called a registry (sentinel-dashboard), has an http endpoint that can be accessed without authentication, and with a little use, it can cause the system to generate java. OutOfMemoryError error can be caused by the system. If you are familiar with Sentinel, you know that it is an open source flow-limiting fusion component. In the official implementation, clients that access Sentinel will register their services with the registry, probably to reduce the difficulty of using and accessing Sentinel.

This http endpoint is /registry/machine

com.alibaba.csp.sentinel.dashboard.controller.MachineRegistryController#receiveHeartBeat

 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
@ResponseBody
@RequestMapping("/machine")
public Result<?> receiveHeartBeat(String app,
                                  @RequestParam(value = "app_type", required = false, defaultValue = "0")
                                      Integer appType, Long version, String v, String hostname, String ip,
                                  Integer port) {
    if (StringUtil.isBlank(app) || app.length() > 256) {
        return Result.ofFail(-1, "invalid appName");
    }
    if (StringUtil.isBlank(ip) || ip.length() > 128) {
        return Result.ofFail(-1, "invalid ip: " + ip);
    }
    if (port == null || port < -1) {
        return Result.ofFail(-1, "invalid port");
    }
    if (hostname != null && hostname.length() > 256) {
        return Result.ofFail(-1, "hostname too long");
    }
    if (port == -1) {
        logger.warn("Receive heartbeat from " + ip + " but port not set yet");
        return Result.ofFail(-1, "your port not set yet");
    }
    String sentinelVersion = StringUtil.isBlank(v) ? "unknown" : v;

    version = version == null ? System.currentTimeMillis() : version;
    try {
        MachineInfo machineInfo = new MachineInfo();
        machineInfo.setApp(app);
        machineInfo.setAppType(appType);
        machineInfo.setHostname(hostname);
        machineInfo.setIp(ip);
        machineInfo.setPort(port);
        machineInfo.setHeartbeatVersion(version);
        machineInfo.setLastHeartbeat(System.currentTimeMillis());
        machineInfo.setVersion(sentinelVersion);
        appManagement.addMachine(machineInfo);
        return Result.ofSuccessMsg("success");
    } catch (Exception e) {
        logger.error("Receive heartbeat error", e);
        return Result.ofFail(-1, e.getMessage());
    }
}

com.alibaba.csp.sentinel.dashboard.discovery.AppManagement#addMachine

1
2
3
4
@Override
public long addMachine(MachineInfo machineInfo) {
    return machineDiscovery.addMachine(machineInfo);
}

com.alibaba.csp.sentinel.dashboard.discovery.SimpleMachineDiscovery#addMachine

1
2
3
4
5
6
7
8
9
private final ConcurrentMap<String, AppInfo> apps = new ConcurrentHashMap<>();

@Override
public long addMachine(MachineInfo machineInfo) {
    AssertUtil.notNull(machineInfo, "machineInfo cannot be null");
    AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), o -> new AppInfo(machineInfo.getApp(), machineInfo.getAppType()));
    appInfo.addMachine(machineInfo);
    return 1;
}

By tracing the above code, we can see that the data submitted by the user has to be stored straight to the memory. Because this http endpoint supports GET, POST requests, so when we use POST method in sending http requests and add very large data in the body, such as app parameters, put a 1MBytes or 10MBytes, or even larger data, then it will cause the server to run out of memory and generate java .lang.OutOfMemoryError error, and implement Memory DoS.

0x05 Points to note

Some readers in the test array initialization Memory DoS, found that although it can make the system to generate java.lang.OutOfMemoryError error, but the system does not crash as a result, to achieve the full Memory DoS, what is the reason?

And look at the following three examples.

  • Complete Memory DoS
1
2
3
4
5
6
private byte[] bytes;

public ? service(int size) {
bytes = new byte[size];
//do something
}
  • Memory DoS for a certain amount of time
1
2
3
4
public ? service(int size) {
byte[] bytes = new byte[size];
//do 5s something
}
  • Short Memory DoS
1
2
3
4
public ? service(int size) {
byte[] bytes = new byte[size];
//do 100ms something
}

After reading these three examples, I believe that most people familiar with the JVM gc mechanism will immediately understand that this is actually the difference between a shared variable referencing an object and a local variable referencing an object.

Because the JVM gc mechanism is based on object references to determine whether the object memory needs to be reclaimed, and here, if we initialize an array object that is a local variable reference and only has a local variable reference, then this means that when the execution of this thread stack is complete, if the reference no longer exists, then the JVM will reclaim this piece of memory when it executes gc, so that alone This way we can only get a short period of time Memory DoS, may be 5 seconds (already relatively high quality), or only 0.1 seconds, there is no way to fully realize Memory DoS, and only let this object life cycle long enough, or reference has always existed, then, in its life cycle, we can occupy a long time JVM heap enough memory, so that the JVM can not recycle This part of the memory, so as to achieve Memory DoS.

There is another most important point, the JVM corresponding array initialization size is limited, the maximum array length is 2147483645, which is equal to 2047.999997138977051MBytes, so if we encounter a controlled shared variable reference object scenario, we can only control the size of an object array, once the JVM maximum available heap memory is much larger than its array Once the JVM maximum available heap memory is much larger than its array, it is also more difficult for implementing Memory DoS.

V. Summary

  • As long as Error can be generated in java, the probability is that it will cause DoS
  • Memory DoS can be achieved by controlling the initialization capacity parameters of Array and Collection.
  • Inserting a lot of garbage data into the collection, you can also achieve Memory DoS

Since this article is actually nothing hardcore, so, not to write too many examples, so as not to stink and long.

VI. Some CVEs about Memory DoS