aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/jaz/Zer.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/jaz/Zer.java')
-rw-r--r--src/main/java/jaz/Zer.java361
1 files changed, 361 insertions, 0 deletions
diff --git a/src/main/java/jaz/Zer.java b/src/main/java/jaz/Zer.java
new file mode 100644
index 00000000..b4d49041
--- /dev/null
+++ b/src/main/java/jaz/Zer.java
@@ -0,0 +1,361 @@
+// 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.Jazzer;
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.function.Function;
+
+/**
+ * A honeypot class that reports a finding on initialization.
+ *
+ * Class loading based on externally controlled data could lead to RCE
+ * depending on available classes on the classpath. Even if no applicable
+ * gadget class is available, allowing input to control class loading is a bad
+ * idea and should be prevented. A finding is generated whenever the class
+ * is loaded and initialized, regardless of its further use.
+ * <p>
+ * This class needs to implement {@link Serializable} to be considered in
+ * deserialization scenarios. It also implements common constructors, getter
+ * and setter and common interfaces to increase chances of passing
+ * deserialization checks.
+ * <p>
+ * <b>Note</b>: Jackson provides a nice list of "nasty classes" at
+ * <a
+ * href=https://github.com/FasterXML/jackson-databind/blob/2.14/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/SubTypeValidator.java>SubTypeValidator</a>.
+ * <p>
+ * <b>Note</b>: 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 {@code jaz.Zer}.
+ */
+@SuppressWarnings({"rawtypes", "unused"})
+public class Zer
+ implements Serializable, Cloneable, Comparable<Zer>, Comparator, Closeable, Flushable, Iterable,
+ Iterator, Runnable, Callable, Function, Collection, List {
+ static final long serialVersionUID = 42L;
+
+ // serialized size is 41 bytes
+ private static final byte REFLECTIVE_CALL_SANITIZER_ID = 0;
+ private static final byte DESERIALIZATION_SANITIZER_ID = 1;
+ private static final byte EXPRESSION_LANGUAGE_SANITIZER_ID = 2;
+
+ // A byte representing the relevant sanitizer for a given jaz.Zer instance. It is used to check
+ // whether the corresponding sanitizer is disabled and jaz.Zer will not report a finding in this
+ // case. Each sanitizer which relies on this class must set this byte accordingly. We choose a
+ // single byte to represent the sanitizer in order to keep the serialized version of jaz.Zer
+ // objects small (currently 41 bytes) so that it fits in the 64 byte limit of the words that can
+ // be used with Jazzer's methods that guide the fuzzer towards generating inputs that contain or
+ // are equal to target strings. This limit comes from the corresponding libFuzzer hooks that
+ // Jazzer uses under the hood.
+ private byte sanitizer = REFLECTIVE_CALL_SANITIZER_ID;
+
+ // Common constructors
+ public Zer() {
+ reportFindingIfEnabled();
+ }
+
+ public Zer(String arg1) {
+ reportFindingIfEnabled();
+ }
+
+ public Zer(String arg1, Throwable arg2) {
+ reportFindingIfEnabled();
+ }
+
+ public Zer(byte sanitizer) {
+ this.sanitizer = sanitizer;
+ reportFindingIfEnabled();
+ }
+
+ // A special static method that is called by the expression language injection sanitizer. We
+ // choose a parameterless method to keep the string that the sanitizer guides the fuzzer to
+ // generate within the 64-byte boundary required by the corresponding guiding methods.
+ public static void el() {
+ if (isSanitizerEnabled(EXPRESSION_LANGUAGE_SANITIZER_ID)) {
+ reportFinding();
+ }
+ }
+
+ private void reportFindingIfEnabled() {
+ if (isSanitizerEnabled(sanitizer)) {
+ reportFinding();
+ }
+ }
+
+ private static void reportFinding() {
+ Jazzer.reportFindingFromHook(new FuzzerSecurityIssueHigh("Remote Code Execution\n"
+ + "Unrestricted class/object creation based on externally controlled data may allow\n"
+ + "remote code execution depending on available classes on the classpath."));
+ }
+
+ private static boolean isSanitizerEnabled(byte sanitizerId) {
+ String allDisabledHooks = System.getProperty("jazzer.disabled_hooks");
+ if (allDisabledHooks == null || allDisabledHooks.equals("")) {
+ return true;
+ }
+
+ String sanitizer;
+ switch (sanitizerId) {
+ case DESERIALIZATION_SANITIZER_ID:
+ sanitizer = "com.code_intelligence.jazzer.sanitizers.Deserialization";
+ break;
+ case EXPRESSION_LANGUAGE_SANITIZER_ID:
+ sanitizer = "com.code_intelligence.jazzer.sanitizers.ExpressionLanguageInjection";
+ break;
+ default:
+ sanitizer = "com.code_intelligence.jazzer.sanitizers.ReflectiveCall";
+ }
+ return Arrays.stream(allDisabledHooks.split(",")).noneMatch(sanitizer::equals);
+ }
+
+ // Getter/Setter
+
+ public Object getJaz() {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ public void setJaz(String jaz) {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public int hashCode() {
+ reportFindingIfEnabled();
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ reportFindingIfEnabled();
+ return super.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ reportFindingIfEnabled();
+ return super.toString();
+ }
+
+ // Common interface stubs
+
+ @Override
+ public void close() {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public void flush() {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public int compareTo(Zer o) {
+ reportFindingIfEnabled();
+ return 0;
+ }
+
+ @Override
+ public int compare(Object o1, Object o2) {
+ reportFindingIfEnabled();
+ return 0;
+ }
+
+ @Override
+ public int size() {
+ reportFindingIfEnabled();
+ return 0;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public Object[] toArray() {
+ reportFindingIfEnabled();
+ return new Object[0];
+ }
+
+ @Override
+ public boolean add(Object o) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean addAll(Collection c) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection c) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public Object get(int index) {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public Object set(int index, Object element) {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public void add(int index, Object element) {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public Object remove(int index) {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ reportFindingIfEnabled();
+ return 0;
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ reportFindingIfEnabled();
+ return 0;
+ }
+
+ @Override
+ @SuppressWarnings("ConstantConditions")
+ public ListIterator listIterator() {
+ reportFindingIfEnabled();
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings("ConstantConditions")
+ public ListIterator listIterator(int index) {
+ reportFindingIfEnabled();
+ return null;
+ }
+
+ @Override
+ public List subList(int fromIndex, int toIndex) {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public boolean retainAll(Collection c) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection c) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public boolean containsAll(Collection c) {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public Object[] toArray(Object[] a) {
+ reportFindingIfEnabled();
+ return new Object[0];
+ }
+
+ @Override
+ public Iterator iterator() {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public void run() {
+ reportFindingIfEnabled();
+ }
+
+ @Override
+ public boolean hasNext() {
+ reportFindingIfEnabled();
+ return false;
+ }
+
+ @Override
+ public Object next() {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public Object call() throws Exception {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ public Object apply(Object o) {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ @Override
+ @SuppressWarnings("MethodDoesntCallSuperMethod")
+ public Object clone() {
+ reportFindingIfEnabled();
+ return this;
+ }
+
+ // readObject calls can directly result in RCE, see https://github.com/frohoff/ysoserial for
+ // examples. Since deserialization doesn't call constructors (see
+ // https://docs.oracle.com/javase/7/docs/platform/serialization/spec/input.html#2971), we emit a
+ // finding right in the readObject method.
+ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+ // Need to read in ourselves to initialize the sanitizer field.
+ stream.defaultReadObject();
+ reportFindingIfEnabled();
+ }
+}