最近在 Stack Overflow 无意中发现一个挺有意思的问题:
是否可以令永假式
a == 1 && a == 2 && a == 3的值为true?
C++
当时看到题干,条件反射就想到了可以通过 C++ 实现,因为 C++ 是可以重写运算符的,其实现代码如下:
#include <iostream>
using namespace std;
class NumOP {
    private:
        int num;
    public:
        NumOP(int num) {
            this->num = num;
        }
        bool operator==(const int& num) {
            return this->num <= num;
        }
};
int main() {
    NumOP a(1);
    if (a == 1 && a == 2 && a == 3) {
        cout << "impossable!" << endl;
    } else {
        cout << "It's right." << endl;
    }
    return 0;
}
因为闲得慌,又萌生出了一个念头:其他语言是否也都可以实现这个表达式呢?
其实仔细分分析一下题干,要使得表达式成真,可以从两个思路切入:
- 要么 
==的判定逻辑被篡改 - 要么 
a的值要在判断过程中自动变化,此时a不可能是基础数据类型(可能是对象、是函数、或是引用) 
Python
因为 Python 和 C++ 同样支持运算符重写,于是类似地可以得到 Python 的实现代码:
class NumOP :
    def __init__(self, num) :
        self.num = num
    def __eq__(self, num) :
        return self.num <= num
def main() :
    a = NumOP(1);
    if (a == 1 and a == 2 and a == 3) :
        print("impossable!")
    else :
        print("It's right.")
if __name__ == '__main__' :
    main()
Ruby
而对于 ruby 则可以利用它的一个语法糖简单实现:调用函数函数时,其参数列表可以不写括号。
那么只需要定义一个无入参的函数 a ,根据条件动态控制函数 a 的返回值即可,其实现代码如下:
def a
    $i ||= 0    # $i 是全局变量
    $i += 1
end
if (a == 1 && a == 2 && a == 3)
    puts "impossable!"
else
    puts "It's right."
end
JavaScript
对于 JavaScript ,可以利用运算符 == 的松散相等特性:当 == 两边操作数的类型不相同时, JS 引擎会尝试把其中一个操作数类型转换成另一个操作数类型。
在这题里面,若左侧操作数 a 是对象,右侧是数字,则会隐式调用对象 a 的 valueOf 方法将其转换成数字;若转换失败则调用 toString 方法后再将其转换成数字。
显然,只需要控制 valueOf 逻辑使其满足每次 == 的判定即可,其实现代码如下:
注:此方法对于严格相等运算符
===不起作用。
var a = {
    i: 1, 
    valueOf: function() {
        return this.i++;
    }
}
if (a == 1 && a == 2 && a == 3) {
    console.log("impossable!")
} else {
    console.log("It's right.")
}
Java
这么多语言中,最麻烦的就是 Java 了。主要是 Java 不允许重写运算符,只能利用 a 做文章。
但 Java 要求 == 两边类型一致,而右侧的 1/2/3 是 int 基础类型,因此 a 会受到 Java 的 编译语法 约束,只可能是 int 基础类型或其包装类 Integer。而结合本题来看,a 只可能是 Integer 对象。
根据 Java 的语言特性,Integer == int 在比对之前,会自动拆包使得两边的类型一致,事实上会变成 Integer.intValue() == int。
理论上本应只需要重写 Integer.intValue() 即可。
而事实上 Integer 声明了 final,不允许被继承,直接导致无法重写 Integer.intValue()。
public final class Integer extends Number implements Comparable<Integer> {
    ......
    public int intValue() {
        return value;
    }
    ......
}
换言之无法直接实现。
但是若条件变更如下,则有可能实现:
a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3
该条件比对的是 Integer == Integer,由于两侧操作数均是对象,实际比对的是对象地址的引用,只需要想办法篡改两个引用的对象(使其相同)即可达到目的。
此时可以利用 Java【静态缓存】的特性 —— Integer 为了优化空间和效率,对于特定范围的常量值会放入常量池:
- 当 
Integer类 第一次 被载入内存时,会通过内部类IntegerCache把[-128, 127]范围内的整数包装成Integer对象并缓存到Integer cache[]数组。 - 以后再用 
Integer初始化变量时,若其赋值范围在[-128, 127]之间,则直接返回cache数组中对应的引用,不再重新开辟内存。 
详细可见 Integer 的源码:
public final class Integer extends Number implements Comparable<Integer> {
    ......
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];
        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }
        private IntegerCache() {}
    }
    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    ......
}
回到这题判断条件中的 1/2/3,因为是通过计算在 IntegerCache 数组索引,从而获取其包装类对象:
(Integer) 1 => Integer.valueOf(1) => IntegerCache.cache[129](Integer) 2 => Integer.valueOf(2) => IntegerCache.cache[130](Integer) 3 => Integer.valueOf(3) => IntegerCache.cache[131]
那么只需要篡改 IntegerCache 数组,使得:
IntegerCache.cache[130] = IntegerCache.cache[129]IntegerCache.cache[131] = IntegerCache.cache[129]
就可以令 1/2/3 取得的包装类是同一个对象(此时的 1/2/3 纯粹就是索引值)。
篡改方法可以用例 Java 的反射机制:
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
public class Java {
    public static void main(String[] args) throws Exception {
        // 利用反射机制获取 Integer cache[] 数组
        Class clazz = Integer.class.getDeclaredClasses()[0];
        Field field = clazz.getDeclaredField("cache");
        field.setAccessible(true);
        Integer[] cache = (Integer[]) field.get(clazz);
        cache[130] = cache[129];    // 令 (Integer) 2 = (Integer) 1
        cache[131] = cache[129];    // 令 (Integer) 3 = (Integer) 1
        field.setAccessible(false);
        Integer a = Integer.valueOf(1);
        if(a == (Integer) 1 && a == (Integer) 2 && a == (Integer) 3) {
            System.out.println("impossable!");
        } else {
            System.out.println("It's right.");
        }
    }
}
但是这种做法不够优雅,毕竟改了题目。
那有没有不改题目的实现方式呢?
是有的。
虽然 Integer 声明了 final,不允许被继承,导致无法重写 Integer.intValue()。
但是可以利用 AOP 切到 Integer.intValue() 方法进行篡改。
在 Stack Overflow 就有人给出了类似的解题思路(理论上是可行的,但我并没有去验证,有兴趣的同学可以试试):
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
@PrepareForTest(Integer.class)
@RunWith(PowerMockRunner.class)
public class TestJava {
    /**
     * 利用 AOP 把 Integer.intValue() 替换为 AtomicInteger.getAndIncrement()
     */
    @Before
    public void before() {
        AtomicInteger ai = new AtomicInteger(1);                // 自增整数
        replace(method(Integer.class, "intValue")).with(
                (proxy, method, args) -> ai.getAndIncrement()   // lambda
        );
    }
    @Test
    public void test() {
        Integer a = 1;
        if(a == 1 && a == 2 && a == 3) {
            System.out.println("impossable!");
        } else {
            System.out.println("It's right.");
        }
    }
}
结语
通过前面的解题过程可以发现,弱类型语言 相较于 强类型语言 会更容易实现底层逻辑篡改,主要是因为对语法特性的校验会更宽松。
C++ 虽然和 Java 一样属于强类型语言,但是因为没有限制运算符重写而被钻了空子
在渗透测试中,或者可以利用类似的手段,绕过一些条件语句达到目的。