diff options
Diffstat (limited to 'sanitizers')
16 files changed, 811 insertions, 0 deletions
diff --git a/sanitizers/BUILD.bazel b/sanitizers/BUILD.bazel new file mode 100644 index 00000000..fa84208e --- /dev/null +++ b/sanitizers/BUILD.bazel @@ -0,0 +1,8 @@ +java_library( + name = "sanitizers", + visibility = ["//visibility:public"], + runtime_deps = [ + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers", + "//sanitizers/src/main/java/jaz", + ], +) diff --git a/sanitizers/sanitizers.bzl b/sanitizers/sanitizers.bzl new file mode 100644 index 00000000..8bdea7a9 --- /dev/null +++ b/sanitizers/sanitizers.bzl @@ -0,0 +1,24 @@ +# Copyright 2021 Code Intelligence GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_sanitizer_package_prefix = "com.code_intelligence.jazzer.sanitizers." + +_sanitizer_class_names = [ + "Deserialization", + "ExpressionLanguageInjection", + "NamingContextLookup", + "ReflectiveCall", +] + +SANITIZER_CLASSES = [_sanitizer_package_prefix + class_name for class_name in _sanitizer_class_names] diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel new file mode 100644 index 00000000..65480653 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "sanitizers", + srcs = [ + "Deserialization.kt", + "ExpressionLanguageInjection.kt", + "NamingContextLookup.kt", + "ReflectiveCall.kt", + "Utils.kt", + ], + visibility = ["//sanitizers:__pkg__"], + deps = [ + "//agent:jazzer_api_compile_only", + "//sanitizers/src/main/java/jaz", + ], +) diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt new file mode 100644 index 00000000..f6401dfd --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Deserialization.kt @@ -0,0 +1,168 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.ObjectStreamConstants +import java.lang.invoke.MethodHandle +import java.util.WeakHashMap + +/** + * Detects unsafe deserialization that leads to attacker-controlled method calls, in particular to [Object.finalize]. + */ +@Suppress("unused_parameter") +object Deserialization { + + private val OBJECT_INPUT_STREAM_HEADER = + ObjectStreamConstants.STREAM_MAGIC.toBytes() + ObjectStreamConstants.STREAM_VERSION.toBytes() + + /** + * Used to memoize the [InputStream] used to construct a given [ObjectInputStream]. + * [ThreadLocal] is required because the map is not synchronized (and likely cheaper than + * synchronization). + * [WeakHashMap] ensures that we don't prevent the GC from cleaning up [ObjectInputStream] from + * previous fuzzing runs. + * + * Note: The [InputStream] values can all be assumed to be markable, i.e., their + * [InputStream.markSupported] returns true. + */ + private var inputStreamForObjectInputStream: ThreadLocal<WeakHashMap<ObjectInputStream, InputStream>> = + ThreadLocal.withInitial { + WeakHashMap<ObjectInputStream, InputStream>() + } + + /** + * A serialized instance of our honeypot class. + */ + private val SERIALIZED_JAZ_ZER_INSTANCE: ByteArray by lazy { + // We can't instantiate jaz.Zer directly, so we instantiate and serialize jaz.Ter and then + // patch the class name. + val baos = ByteArrayOutputStream() + ObjectOutputStream(baos).writeObject(jaz.Ter()) + val serializedJazTerInstance = baos.toByteArray() + val posToPatch = serializedJazTerInstance.indexOf("jaz.Ter".toByteArray()) + serializedJazTerInstance[posToPatch + "jaz.".length] = 'Z'.code.toByte() + serializedJazTerInstance + } + + /** + * Guides the fuzzer towards producing a valid header for an ObjectInputStream. + */ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.ObjectInputStream", + targetMethod = "<init>", + targetMethodDescriptor = "(Ljava/io/InputStream;)V" + ) + @JvmStatic + fun objectInputStreamInitBeforeHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val originalInputStream = args[0] as? InputStream ?: return + val fixedInputStream = if (originalInputStream.markSupported()) + originalInputStream + else + BufferedInputStream(originalInputStream) + args[0] = fixedInputStream + guideMarkableInputStreamTowardsEquality(fixedInputStream, OBJECT_INPUT_STREAM_HEADER, hookId) + } + + /** + * Memoizes the input stream used for creating the [ObjectInputStream] instance. + */ + @MethodHook( + type = HookType.AFTER, + targetClassName = "java.io.ObjectInputStream", + targetMethod = "<init>", + targetMethodDescriptor = "(Ljava/io/InputStream;)V" + ) + @JvmStatic + fun objectInputStreamInitAfterHook( + method: MethodHandle?, + objectInputStream: ObjectInputStream?, + args: Array<Any?>, + hookId: Int, + alwaysNull: Any?, + ) { + val inputStream = args[0] as? InputStream + check(inputStream?.markSupported() == true) { + "ObjectInputStream#<init> AFTER hook reached with null or non-markable input stream" + } + inputStreamForObjectInputStream.get()[objectInputStream] = inputStream + } + + /** + * Guides the fuzzer towards producing a valid serialized instance of our honeypot class. + */ + @MethodHooks( + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.ObjectInputStream", + targetMethod = "readObject" + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.ObjectInputStream", + targetMethod = "readObjectOverride" + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "java.io.ObjectInputStream", + targetMethod = "readUnshared" + ), + ) + @JvmStatic + fun readObjectBeforeHook( + method: MethodHandle?, + objectInputStream: ObjectInputStream?, + args: Array<Any?>, + hookId: Int, + ) { + val inputStream = inputStreamForObjectInputStream.get()[objectInputStream] + if (inputStream?.markSupported() != true) return + guideMarkableInputStreamTowardsEquality(inputStream, SERIALIZED_JAZ_ZER_INSTANCE, hookId) + } + + /** + * Calls [Object.finalize] early if the returned object is [jaz.Zer]. A call to finalize is + * guaranteed to happen at some point, but calling it early means that we can accurately report + * the input that lead to its execution. + */ + @MethodHooks( + MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObject"), + MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readObjectOverride"), + MethodHook(type = HookType.AFTER, targetClassName = "java.io.ObjectInputStream", targetMethod = "readUnshared"), + ) + @JvmStatic + fun readObjectAfterHook( + method: MethodHandle?, + objectInputStream: ObjectInputStream?, + args: Array<Any?>, + hookId: Int, + deserializedObject: Any?, + ) { + if (deserializedObject?.javaClass?.name == HONEYPOT_CLASS_NAME) { + deserializedObject.javaClass.getDeclaredMethod("finalize").run { + isAccessible = true + invoke(deserializedObject) + } + } + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt new file mode 100644 index 00000000..9b1e8ca6 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ExpressionLanguageInjection.kt @@ -0,0 +1,83 @@ + +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.invoke.MethodHandle + +/** + * Detects injectable inputs to an expression language interpreter which may lead to remote code execution. + */ +@Suppress("unused_parameter") +object ExpressionLanguageInjection { + + /** + * Try to call the default constructor of the honeypot class. + */ + private const val EXPRESSION_LANGUAGE_ATTACK = + "\${\"\".getClass().forName(\"$HONEYPOT_CLASS_NAME\").newInstance()}" + + @MethodHooks( + MethodHook( + type = HookType.BEFORE, + targetClassName = "javax.el.ExpressionFactory", + targetMethod = "createValueExpression", + ), + MethodHook( + type = HookType.BEFORE, + targetClassName = "javax.el.ExpressionFactory", + targetMethod = "createMethodExpression", + ), + ) + @JvmStatic + fun hookElExpressionFactory( + method: MethodHandle?, + thisObject: Any?, + arguments: Array<Any>, + hookId: Int + ) { + if (arguments[1] is String) { + val expression = arguments[1] as String + Jazzer.guideTowardsContainment(expression, EXPRESSION_LANGUAGE_ATTACK, hookId) + } + } + + // With default configurations the argument to + // ConstraintValidatorContext.buildConstraintViolationWithTemplate() will be evaluated by an + // Expression Language interpreter which allows arbitrary code execution if the attacker has + // control of the method argument. + // + // References: CVE-2018-16621 + // https://securitylab.github.com/research/bean-validation-RCE/ + @MethodHook( + type = HookType.BEFORE, + targetClassName = "javax.validation.ConstraintValidatorContext", + targetMethod = "buildConstraintViolationWithTemplate" + ) + @JvmStatic + fun hookBuildConstraintViolationWithTemplate( + method: MethodHandle?, + thisObject: Any?, + arguments: Array<Any>, + hookId: Int + ) { + val message = arguments[0] as String + Jazzer.guideTowardsContainment(message, EXPRESSION_LANGUAGE_ATTACK, hookId) + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt new file mode 100644 index 00000000..2d4fb9cf --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/NamingContextLookup.kt @@ -0,0 +1,102 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import com.code_intelligence.jazzer.api.MethodHooks +import java.lang.invoke.MethodHandle +import javax.naming.CommunicationException + +object NamingContextLookup { + + // The particular URL g.co is used here since it is: + // - short, which makes it easier for the fuzzer to incorporate into the input; + // - valid, which means that a `lookup` call on it could actually result in RCE; + // - highly reputable, which makes it very unlikely that it would ever host an actual exploit. + private const val LDAP_MARKER = "ldap://g.co/" + private const val RMI_MARKER = "rmi://g.co/" + + @MethodHooks( + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.Context", + targetMethod = "lookup", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialContext", + targetMethod = "lookup", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialDirContext", + targetMethod = "lookup", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialLdapContext", + targetMethod = "lookup", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.Context", + targetMethod = "lookupLink", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialContext", + targetMethod = "lookupLink", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialDirContext", + targetMethod = "lookupLink", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + MethodHook( + type = HookType.REPLACE, + targetClassName = "javax.naming.InitialLdapContext", + targetMethod = "lookupLink", + targetMethodDescriptor = "(Ljava/lang/String;)Ljava/lang/Object;", + ), + ) + @JvmStatic + fun lookupHook(method: MethodHandle?, thisObject: Any?, args: Array<Any?>, hookId: Int): Any { + val name = args[0] as String + if (name.startsWith(RMI_MARKER) || name.startsWith(LDAP_MARKER)) { + Jazzer.reportFindingFromHook( + FuzzerSecurityIssueCritical( + """Remote JNDI Lookup +JNDI lookups with attacker-controlled remote URLs can, depending on the JDK +version, lead to remote code execution or the exfiltration of information.""" + ) + ) + } + Jazzer.guideTowardsEquality(name, RMI_MARKER, hookId) + Jazzer.guideTowardsEquality(name, LDAP_MARKER, 31 * hookId) + // Pretend that the remote endpoint could not be reached for additional protection against + // accidental execution of remote code during fuzzing. + throw CommunicationException() + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt new file mode 100644 index 00000000..7842d879 --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/ReflectiveCall.kt @@ -0,0 +1,34 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.HookType +import com.code_intelligence.jazzer.api.Jazzer +import com.code_intelligence.jazzer.api.MethodHook +import java.lang.invoke.MethodHandle + +/** + * Detects unsafe reflective calls that lead to attacker-controlled method calls. + */ +@Suppress("unused_parameter") +object ReflectiveCall { + + @MethodHook(type = HookType.BEFORE, targetClassName = "java.lang.Class", targetMethod = "forName") + @JvmStatic + fun classForNameHook(method: MethodHandle?, alwaysNull: Any?, args: Array<Any?>, hookId: Int) { + val className = args[0] as? String ?: return + Jazzer.guideTowardsEquality(className, HONEYPOT_CLASS_NAME, hookId) + } +} diff --git a/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt new file mode 100644 index 00000000..3166773b --- /dev/null +++ b/sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers/Utils.kt @@ -0,0 +1,51 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.code_intelligence.jazzer.sanitizers + +import com.code_intelligence.jazzer.api.Jazzer +import java.io.InputStream + +/** + * jaz.Zer is a honeypot class: All of its methods report a finding when called. + */ +const val HONEYPOT_CLASS_NAME = "jaz.Zer" + +internal fun Short.toBytes(): ByteArray { + return byteArrayOf( + ((toInt() shr 8) and 0xFF).toByte(), + (toInt() and 0xFF).toByte(), + ) +} + +// Runtime is only O(size * needle.size), only use for small arrays. +internal fun ByteArray.indexOf(needle: ByteArray): Int { + outer@ for (i in 0 until size - needle.size + 1) { + for (j in needle.indices) { + if (this[i + j] != needle[j]) { + continue@outer + } + } + return i + } + return -1 +} + +internal fun guideMarkableInputStreamTowardsEquality(stream: InputStream, target: ByteArray, id: Int) { + check(stream.markSupported()) + stream.mark(target.size) + val current = stream.readNBytes(target.size) + stream.reset() + Jazzer.guideTowardsEquality(current, target, id) +} diff --git a/sanitizers/src/main/java/jaz/BUILD.bazel b/sanitizers/src/main/java/jaz/BUILD.bazel new file mode 100644 index 00000000..81275a31 --- /dev/null +++ b/sanitizers/src/main/java/jaz/BUILD.bazel @@ -0,0 +1,12 @@ +java_library( + name = "jaz", + srcs = [ + "Ter.java", + "Zer.java", + ], + visibility = [ + "//sanitizers:__pkg__", + "//sanitizers/src/main/java/com/code_intelligence/jazzer/sanitizers:__pkg__", + ], + deps = ["//agent:jazzer_api_compile_only"], +) diff --git a/sanitizers/src/main/java/jaz/Ter.java b/sanitizers/src/main/java/jaz/Ter.java new file mode 100644 index 00000000..7814396f --- /dev/null +++ b/sanitizers/src/main/java/jaz/Ter.java @@ -0,0 +1,24 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaz; + +/** + * A safe to use companion of {@link jaz.Zer} that is used to produce serializable instances of it + * with only light patching. + */ +@SuppressWarnings("unused") +public class Ter implements java.io.Serializable { + static final long serialVersionUID = 42L; +} diff --git a/sanitizers/src/main/java/jaz/Zer.java b/sanitizers/src/main/java/jaz/Zer.java new file mode 100644 index 00000000..0b27609c --- /dev/null +++ b/sanitizers/src/main/java/jaz/Zer.java @@ -0,0 +1,107 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package jaz; + +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueHigh; +import com.code_intelligence.jazzer.api.FuzzerSecurityIssueMedium; +import com.code_intelligence.jazzer.api.Jazzer; +import java.io.IOException; +import java.io.ObjectInputStream; + +/** + * A honeypot class that reports an appropriate finding on any interaction with one of its methods + * or initializers. + * + * Note: This class must not be referenced in any way by the rest of the code, not even statically. + * When referring to it, always use its hardcoded class name "jaz.Zer". + */ +@SuppressWarnings("unused") +public class Zer implements java.io.Serializable { + static final long serialVersionUID = 42L; + + private static final Throwable staticInitializerCause; + + static { + staticInitializerCause = new FuzzerSecurityIssueMedium("finalize call on arbitrary object"); + } + + public Zer() { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueMedium("default constructor call on arbitrary object")); + } + + public Zer(String arg1) { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueMedium("String constructor call on arbitrary object")); + } + + public Zer(String arg1, Throwable arg2) { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueMedium("(String, Throwable) constructor call on arbitrary object")); + } + + private String jaz; + + public String getJaz() { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("getter call on arbitrary object")); + return jaz; + } + + public void setJaz(String jaz) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("setter call on arbitrary object")); + this.jaz = jaz; + } + + @Override + public int hashCode() { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueMedium("hashCode call on arbitrary object")); + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("equals call on arbitrary object")); + return super.equals(obj); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueMedium("clone call on arbitrary object")); + return super.clone(); + } + + @Override + public String toString() { + Jazzer.reportFindingFromHook( + new FuzzerSecurityIssueMedium("toString call on arbitrary object")); + return super.toString(); + } + + @Override + protected void finalize() throws Throwable { + // finalize is invoked automatically by the GC with an uninformative stack trace. We use the + // stack trace prerecorded in the static initializer. + Jazzer.reportFindingFromHook(staticInitializerCause); + super.finalize(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n" + + " Deserialization of arbitrary classes with custom readObject may allow remote\n" + + " code execution depending on the classpath.")); + in.defaultReadObject(); + } +} diff --git a/sanitizers/src/test/java/com/example/BUILD.bazel b/sanitizers/src/test/java/com/example/BUILD.bazel new file mode 100644 index 00000000..d148545a --- /dev/null +++ b/sanitizers/src/test/java/com/example/BUILD.bazel @@ -0,0 +1,32 @@ +load("//bazel:fuzz_target.bzl", "java_fuzz_target_test") + +java_fuzz_target_test( + name = "ObjectInputStreamDeserialization", + srcs = [ + "ObjectInputStreamDeserialization.java", + ], + target_class = "com.example.ObjectInputStreamDeserialization", +) + +java_fuzz_target_test( + name = "ReflectiveCall", + srcs = [ + "ReflectiveCall.java", + ], + target_class = "com.example.ReflectiveCall", +) + +java_fuzz_target_test( + name = "ExpressionLanguageInjection", + srcs = [ + "ExpressionLanguageInjection.java", + "InsecureEmailValidator.java", + ], + target_class = "com.example.ExpressionLanguageInjection", + deps = [ + "@maven//:javax_el_javax_el_api", + "@maven//:javax_validation_validation_api", + "@maven//:javax_xml_bind_jaxb_api", + "@maven//:org_hibernate_hibernate_validator", + ], +) diff --git a/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java new file mode 100644 index 00000000..e26a9117 --- /dev/null +++ b/sanitizers/src/test/java/com/example/ExpressionLanguageInjection.java @@ -0,0 +1,49 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.*; + +class UserData { + public UserData(String email) { + this.email = email; + } + + @ValidEmailConstraint private String email; +} + +@Constraint(validatedBy = InsecureEmailValidator.class) +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@interface ValidEmailConstraint { + String message() default "Invalid email address"; + Class<?>[] groups() default {}; + Class<? extends Payload>[] payload() default {}; +} + +public class ExpressionLanguageInjection { + final private static Validator validator = + Validation.buildDefaultValidatorFactory().getValidator(); + + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + UserData uncheckedUserData = new UserData(data.consumeRemainingAsString()); + validator.validate(uncheckedUserData); + } +} diff --git a/sanitizers/src/test/java/com/example/InsecureEmailValidator.java b/sanitizers/src/test/java/com/example/InsecureEmailValidator.java new file mode 100644 index 00000000..d61e888d --- /dev/null +++ b/sanitizers/src/test/java/com/example/InsecureEmailValidator.java @@ -0,0 +1,36 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import static java.lang.String.format; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class InsecureEmailValidator implements ConstraintValidator<ValidEmailConstraint, String> { + @Override + public void initialize(ValidEmailConstraint email) {} + + @Override + public boolean isValid(String email, ConstraintValidatorContext cxt) { + if (email == null || !email.matches(".+@.+\\..+")) { + // Insecure: do not call buildConstraintViolationWithTemplate with untrusted data! + cxt.buildConstraintViolationWithTemplate(format("Invalid email address: %s", email)) + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/sanitizers/src/test/java/com/example/ObjectInputStreamDeserialization.java b/sanitizers/src/test/java/com/example/ObjectInputStreamDeserialization.java new file mode 100644 index 00000000..c6285609 --- /dev/null +++ b/sanitizers/src/test/java/com/example/ObjectInputStreamDeserialization.java @@ -0,0 +1,32 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; + +public class ObjectInputStreamDeserialization { + public static void fuzzerTestOneInput(byte[] data) { + try { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + ois.readObject(); + } catch (IOException | ClassNotFoundException ignored) { + // Ignored checked exception. + } catch (NullPointerException | NegativeArraySizeException ignored) { + // Ignored RuntimeExceptions thrown by readObject(). + } + } +} diff --git a/sanitizers/src/test/java/com/example/ReflectiveCall.java b/sanitizers/src/test/java/com/example/ReflectiveCall.java new file mode 100644 index 00000000..7f85e486 --- /dev/null +++ b/sanitizers/src/test/java/com/example/ReflectiveCall.java @@ -0,0 +1,32 @@ +// Copyright 2021 Code Intelligence GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.example; + +import com.code_intelligence.jazzer.api.FuzzedDataProvider; +import java.lang.reflect.InvocationTargetException; + +public class ReflectiveCall { + public static void fuzzerTestOneInput(FuzzedDataProvider data) { + String input = data.consumeRemainingAsAsciiString(); + if (input.startsWith("@")) { + String className = input.substring(1); + try { + Class.forName(className).getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException | ClassNotFoundException ignored) { + } + } + } +} |