本篇主要分享一些关于近期所看到或碰到的一些问题的个人开发。
关于fastjson触发get方法
(1)某些只存在getter不存在setter的可以触发,如fastjson<=1.2.24时候的TemplatesImpl链,可能部分文章会说需要参数类型或者方法返回值类型符合一些条件才可以,最开始自己调试源码时候也进入了误区,其实在那部分代码判断上下文的某位还是会反射调用,故不成立(具体代码判断参考fastjson源码)。
(2)利用@ref触发。
关于fastjson @ref
在一些利用中会使用$ref引用对象触发Getter(部分文章说Fastjson>=1.2.36才有@ref特性,具体未测)。
在parseObject解析序列化fastjson字符串的时候扫描到"会调用scanString进行字符串扫描,当扫描完毕字符串的时候会与@type以及@ref进行对比,因为这两个需要进行特殊处理。
针对于$ref的处理就是会添加一个resolveTask,在当前JSONParse#parse方法执行完毕后的返回上一层后将调用com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask方法来进行resolveTask的处理,在处理这些task的时候会获取出$ref键对应的值,然后对其进行解析,判断是否以$开头,$开头代表引用前面出现过的对象,首先直接尝试使用$后面这一串从上下文对象数组(contextArray)中直接获取对象,可以解决$对象名这种情况,当时会出现两种获取不到的情况:(1)$对象名.属性字段名;(2)不存在的对象名,当获取不到的时候就会进行解析,这里创建了一个JSONPath对象解析(JSONPath.compile(ref),内部存在缓存机制),在接着isRef方法中会调用com.alibaba.fastjson.JSONPath#init方法,在这个方法中完成了$ref后面表达式的解析
if (ref.startsWith("$")) {
refValue = this.getObject(ref);
if (refValue == null) {
try {
JSONPath jsonpath = JSONPath.compile(ref);
if (jsonpath.isRef()) {
refValue = jsonpath.eval(value);
}
} catch (JSONPathException var11) {
}
}
}
在com.alibaba.fastjson.JSONPath#init方法中调用会创建以$ref后面表达式作为参数创建一个JSONPathParser对象,然后调用它的explain方法对$ref之后的表达式进行解析,在解析时候会根据情况会创建PropertySegment并且记录对其属性的深度等级(level),这个深度等级用于后递归获取属性使用。
protected void init() {
if (this.segments == null) {
if ("*".equals(this.path)) {
this.segments = new Segment[]{JSONPath.WildCardSegment.instance};
} else {
JSONPathParser parser = new JSONPathParser(this.path);
this.segments = parser.explain();
this.hasRefSegment = parser.hasRefSegment;
}
}
}
com.alibaba.fastjson.JSONPath.JSONPathParser#explain方法逻辑
public Segment[] explain() {
if (this.path != null && this.path.length() != 0) {
Segment[] segments = new Segment[8];
while(true) {
Segment segment;
PropertySegment propertySegment;
do {
// 读取属性名
segment = this.readSegement();
if (segment == null) {
if (this.level == segments.length) {
return segments;
}
// 创建装属性的数组并且拷贝
Segment[] result = new Segment[this.level];
System.arraycopy(segments, 0, result, 0, this.level);
return result;
}
if (!(segment instanceof PropertySegment)) {
break;
}
propertySegment = (PropertySegment)segment;
} while(!propertySegment.deep && propertySegment.propertyName.equals("*"));
if (this.level == segments.length) {
Segment[] t = new Segment[this.level * 3 / 2];
System.arraycopy(segments, 0, t, 0, this.level);
segments = t;
}
// 注意这里:level记录读取属性深度
segments[this.level++] = segment;
}
} else {
throw new IllegalArgumentException();
}
}
解析完毕后我们来到重点部分,那就计数获取部分了,也就是这个方法调用:
refValue = jsonpath.eval(value);
该方法的代码如下
public Object eval(Object rootObject) {
if (rootObject == null) {
return null;
} else {
this.init();
Object currentObject = rootObject;
// 从最上层逐层解析
for(int i = 0; i < this.segments.length; ++i) {
Segment segment = this.segments[i];
// 注意这里:并没有创建新的对象,对象延用,也就是通过这种方式实现了属性深度的效果,类似ConstantTransformer,不过多了部分逻辑类似:currentObject=ConstantTransformer(currentObject)
currentObject = segment.eval(this, rootObject, currentObject);
}
// 最终返回解析得到的引用对象
return currentObject;
}
}
以上仅个人之前对fastjson系列分析时候记录,由于当时使用的sublime记录的存文本,故无图,自行调试。
关于fastjson高版本(1.2.68)利用链的问题
当时做体系化学习的时候发现利用网上的写文件链(四哥博客上的链子)玩不了,同Java8版本,当时测试的是u181,不知道是不是这个原因,个人感觉系统平台的原因更大,说了些为什么高版本用不了吧。
在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int, int[])中会遍历扫描json字符串还原字段
如果没有字段则会继续扫描json字符,处理特殊情况,即@type、$ref这种情况,之所以这么做的目的是处理接口实现这种情况,因为接口没有字段,只有方法,所以会接着扫描@type,这个@type是实现了这个接口的类,然后进行反序列化还原且以这个接口作为期望类,也就是最终还原的对象类型,说白了就类似某类实现了某个接口我们以这个接口类型对象接收这个实例一样(class a implements inter;inter b=new a();)。
在该方法内部会采用ASM技术扫描字节码文件获取方法参数名列表
注意这里的TypeCollecter,这便是fastjson中的相关信息收集器。
这里accept扫描过程中会调用readMethod方法
在readMethod方法中通过visitMethod扫描方法,这个visitMethod方法就是com.alibaba.fastjson.asm.TypeCollector#visitMethod方法
在com.alibaba.fastjson.asm.TypeCollector#visitMethod方法中会获取LocalVariableTable符号信息,这在我们的Java8(本地测试环境:Windows10,Java8u181)下并不存在,只有code也就是Java字节码反汇编结果,其表现和汇编极其类似。下图是Java8u181的情况
下图为Java11的情况
其实本质就是符号问题,有点bin里面符号问题的味道(逆向无符号).....
关于Java动态编译
最近弄毕设的时候碰到的问题吧,关于动态编译其实网上文章挺多的,其本质实际上就是代码层面模拟javac编译.java为.class的过程,获取JCompiler并创建编译任务触发。
public class TestDynamicCompilation {
public static void main(String[] args) {
//获取Javac编译器对象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获取文件管理器:负责管理类文件的输入输出
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
//获取要被编译的Java源文件
File file = new File("/project/test/TestHello.java");
//通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
//生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
//执行编译任务
task.call();
}
}
关于Java AST
个人项目碰到的问题,也是前段时间线下面试的问题,算是做个复盘学习吧,对于安全领域其实更多关注的是javac这层前端编译(按照网上说法Java编译有好几层,javac被叫为前端编译,具体可以看看美团技术团队有篇关于Java编译器的文章),javac做的事情就是:词法分析(拆分token)=>语法分析=>语义分析=>注解处理=>数据流分析=>中间代码生成(class),具体流程自行查阅javac编译流程,重点在语法分析阶段完成后会产生抽象语法树(AST),也就是我们关注的东西,在IDEA中有个名为JavaParser AST Inspector的插件可以实现AST解析,通过对其逆向分析发现其内部使用的JavaParser这个库进行获取,根据其使用过程的一些数据对象等不难看出其实际上就是在模拟javac编译的部分操作。
关于AST的一些看法,可以采用对表达式语句(ExpressionStatement)类型判断并进行对应的处理然后配合递归达到反webshell混淆的目的,之前实习期间有看到过一些恶心的webshell,当时的思路是利用ASM针对于一些特定方法做处理进行还原。
总结
最近离职回校弄毕设有点小忙没注意体系化的学习,有点零散,感叹一句:还是学校好啊,物美价廉.......
原文始发于微信公众号(安全之道):技术杂谈
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论