接下来看反射的第一句Class.forName("java.lang.Runtime")
LDC "java.lang.Runtime" INVOKESTATIC java/lang/Class.forName (Ljava/lang/String;)Ljava/lang/Class; ASTORE 4
由于调用STATIC方法不需要this然后返回值保存在局部变量表第5位
这里我给反射三步的LDC分别给上自己的flag做跟踪
注意到LDC命令执行完后保存至栈顶
@Override public void visitLdcInsn(Object cst) { if(cst.equals("java.lang.Runtime")){ super.visitLdcInsn(cst); operandStack.get(0).add("ldc-runtime"); return; } if(cst.equals("getRuntime")){ super.visitLdcInsn(cst); operandStack.get(0).add("ldc-get-runtime"); return; } if(cst.equals("exec")){ super.visitLdcInsn(cst); operandStack.get(0).add("ldc-exec"); return; } super.visitLdcInsn(cst); }
下一句rt.getMethod("getRuntime")稍微复杂
ALOAD 4 LDC "getRuntime" ICONST_0 ANEWARRAY java/lang/Class INVOKEVIRTUAL java/lang/Class.getMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; ASTORE 5
中间主要是多了一步ANEWARRAY操作
这个染成黄色的过程在代码中如下
@Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if(opcode==Opcodes.INVOKEVIRTUAL){ boolean getMethod = name.equals("getMethod") && owner.equals("java/lang/Class") && desc.equals("(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"); if(getMethod){ if(operandStack.get(1).contains("ldc-get-runtime")){ super.visitMethodInsn(opcode, owner, name, desc, itf); logger.info("-> get getRuntime method"); operandStack.get(0).add("method-get-runtime"); return; } } }
下一步是rt.getMethod("exec", String.class)和上面几乎一致,不过数组里添加了元素
ALOAD 4 LDC "exec" ICONST_1 ANEWARRAY java/lang/Class DUP ICONST_0 LDC Ljava/lang/String;.class AASTORE INVOKEVIRTUAL java/lang/Class.getMethod (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method; ASTORE 6
这一步几乎重复,就不再画图了,可以看出最后保存到局部变量表第7位
其中陌生的命令有DUP和AASTORE两个,暂不分析,我们在method.invoke中细说
代码中的处理类似
@Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if(opcode==Opcodes.INVOKEVIRTUAL){ boolean getMethod = name.equals("getMethod") && owner.equals("java/lang/Class") && desc.equals("(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"); if(getMethod){ if(operandStack.get(1).contains("ldc-exec")){ super.visitMethodInsn(opcode, owner, name, desc, itf); logger.info("-> get exec method"); operandStack.get(0).add("method-exec"); return; } } }
接下来该最关键的一行了:ex.invoke(gr.invoke(null), cmd)
ALOAD 6 ALOAD 5 ACONST_NULL ICONST_0 ANEWARRAY java/lang/Object INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; ICONST_1 ANEWARRAY java/lang/Object DUP ICONST_0 ALOAD 3 AASTORE INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
第一步的INVOKEVIRTUAL只是得到了Runtime对象
第二步的INVOKEVIRTUAL才是exec(obj,cmd)执行命令的代码
所以我们重点从第二步分析
ICONST_1 ANEWARRAY java/lang/Object DUP ICONST_0 ALOAD 3 AASTORE INVOKEVIRTUAL java/lang/reflect/Method.invoke (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
在AASTORE之前的过程如下(防止干扰栈中存在的其他元素没有画出)
- 之所以要DUP正是因为AASTORE需要消耗一个数组引用
- 这里的ICONST_1代表初始化数组长度为1
AASTORE和INVOKE的过程如下(之前在栈中没有画出的元素都补充到)
注意其中的细节
- 消耗一个数组做操作实际上另一个数组引用对象也改变了,换句话说加入了cmd参数
所以我们需要手动处理下AASTORE情况以便于让参数传递下去
@Override public void visitInsn(int opcode) { if(opcode==Opcodes.AASTORE){ if(operandStack.get(0).contains("get-param")){ logger.info("store request param into array"); super.visitInsn(opcode); // AASTORE模拟操作之后栈顶是数组引用 operandStack.get(0).clear(); // 由于数组中包含了可控变量所以设置flag operandStack.get(0).add("get-param"); return; } } super.visitInsn(opcode); }
至于最后一步的判断就很简单了
@Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if(opcode==Opcodes.INVOKEVIRTUAL){ boolean invoke = name.equals("invoke") && owner.equals("java/lang/reflect/Method") && desc.equals("(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); if(invoke){ // AASTORE中设置的参数 if(operandStack.get(0).contains("get-param")){ // 如果栈中第3个元素是exec的Method if(operandStack.get(2).contains("method-exec")){ // 认为造成了RCE logger.info("find reflection webshell!"); super.visitMethodInsn(opcode, owner, name, desc, itf); return; } super.visitMethodInsn(opcode, owner, name, desc, itf); logger.info("-> method exec invoked"); } } } super.visitMethodInsn(opcode, owner, name, desc, itf); }
其实栈中第2个元素也可以判断下,我简化了一些不必要的操作
总结
代码在:https://github.com/EmYiQing/JSPKiller
后续考虑加入其他的一些检测,师傅们可以试试Bypass手段哈哈