// 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.
*
* 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.
*
* Note: Jackson provides a nice list of "nasty classes" at
* SubTypeValidator.
*
* 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 {@code jaz.Zer}.
*/
@SuppressWarnings({"rawtypes", "unused"})
public class Zer
implements Serializable, Cloneable, Comparable, 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();
}
}