出品|MS08067实验室(www.ms08067.com)
本文作者:LightHouseZ(Ms08067代码审计班第6期成员
最近刚学完MS08067代码审计班的课程,收获了不少代审的思路和知识,接下来谈谈自己的收获和感受。
ART.01
首先说下当时为什么报名了这个代码审计班,在报名代审班前工作了渗透测试一年多快两年了,都是主要以黑盒渗透为主,做了很长一段时间后总觉得黑盒挖洞老是要去猜测一个地方有没有洞,然后还要不停的测试后才验证出一个漏洞。
还有像一些组件、框架的漏洞,例如:fastjson、log4j2、shiro、spring SPEL等这类漏洞,虽然知道怎么去利用,但终究还是不知道其漏洞在代码层面的原理,别人给个payload出来,哦,就知道稍微改下去利用,不知道为什么payload长这个样。
个人而言不是很喜欢这种感觉,就觉得这个漏洞很虚很表面,所以我就来了抱着满满的期待报名了这个代码审计班。其实在报名前我就已经纠结了好几个月了,一直担心自己的Java水平不好跟不上老师的课程,就自己学了一段时间大概学完JavaSE差不多吧,感觉进度真的好慢,这都还没学JavaEE和JavaWEB呢这可得学到猴年马月呀,更别说后面怎么去审计漏洞了,实在忍不住就报了班。
ART.02
果然!报了代审班没令我失望!短短3个月不到时间代审能力突飞猛进!(要是自学我估计得要个一年半载才能达到如今的效果)
授课老师讲的内容很多、很详细,仅看wiki就能学到不少知识,从基础的搭建环境➡JAVASE ➡JavaWEB ➡JavaEE ➡常见漏洞审计(SQL注入、XSS注入、命令执行等)➡反射机制、反序列化➡组件漏洞审计(fastjson、shiro、log4j2等)➡6个实战项目分析➡编写POC ➡代审岗位面试总结。
这课程真是太充实了,都是我想要学的知识点。从基础知识开始一层一层逐渐深入,课程安排得很合理,不会出现跳课的感觉,课上没看懂可以直接问老师,要是还没看懂课下可以重复看录播,在学习群上问老师,老师都会很有耐心的一一解答。
在学完代审课之后像拨开了迷雾一般,曾经只会利用payload去黑盒渗透而不知道底层怎么触发的漏洞,终究是知道了漏洞其中的底层原理(例如:fastjson底层是如何通过反射机制调用利用链、log4j2如何解析“${}“造成jndi注入)。学会了如何用关键字快速去找到一个漏洞利用点(例如:全局搜索”$“,找到没有使用预编译的SQL语句),学会了如何跟踪参数找到利用点(例如:追踪传进后端的请求参数,查看过滤器是否有过滤XSS并显示在页面上),学会了查看lib包版本或者pom文件去快速审计组件漏洞等。收获还是非常大的,至少独立审计项目是完全没问题了。
1.案例介绍
前段时间上综合项目代码分析课的时候收获了一个新思路,是关于Log4j2漏洞入口点寻找的。以前在学习Log4j2漏洞时候看到很多文章在复现时候用到error()方法去复现,当时也不懂Java代码就只能看到别人用error()方法就以为只有error一个方法能复现。
后面学了代审后才知道,原来一共是有6个打印日志的方法,分别是fatal、error、warn、info、debug、trace。那么既然知道有6个方法,当然也要去测试一下是否能复现了。
log4j2版本
测试6个方法
结果是只有error、fatal两个方法可以成功获取到dns解析记录,另外四个warn、info、debug、trace都不可以。
但是!直到了实战项目时候才发现原来并不是只有error、fatal才能解析成功,其他方法也可以但是需要一定利用条件,通常是日志文件配置不当造成。
先来看看log4j2的日志级别,log4j2支持多种日志级别,通过日志级别我们可以将日志信息进行分类,在合适的地方输出对应的日志。哪些信息需要输出,哪些信息不需要输出,只需在一个日志输出控制文件中稍加修改即可。级别由高到低共分为6个:fatal, error, warn, info, debug, trace。
当系统配置的intLevel >= 调用方法的intLevel的时候,log4j2才会启用日志打印或日志存储。
如:
系统配置debug等级(500) >= 调用方法info等级(400),条件成立启用日志打印。✔️
系统配置debug等级(500) >= 调用方法debug等级(500),条件成立启用日志打印。✔️
系统配置debug等级(500) >= 调用方法trace等级(600),条件不成立关闭日志打印。❌
系统配置ALL等级(2147483647) >= 调用方法debug等级(500),条件成立启用日志打印。✔️
系统配置OFF等级(0) >= 调用方法trace等级(600),条件不成立关闭日志打印。❌
2.代码分析
我们看下log4j2里面的代码是什么样的。
位置:log4j-api-2.10.0.jar!\org\
apache\logging\log4j\spi\AbstractLogger.class#logIfEnabled方法
1442行:就是判断条件是否成立的地方,只有条件成立启用日志才会走1443行,只有能走进这1443行后才能进行后续的jndi注入(至于后续怎么解析${}并注入jndi这里就不说了,感兴趣的可以自己去跟踪代码看一下)。
跟进isEnabled方法,这里面是通过当前配置进行过滤。
跟进this.privateConfig.filter方法,可看到295就是我们上面说到日志等级条件是否成立的问题。
this.intLevel就是当前系统的等级,level.intLevel()就是我们当前调用方法的等级。
这个this.intLevel(系统配置等级)在实际项目中的时候可能是会变的。
log4j2默认情况下系统配置等级是error,所以以前我在测试时候就只有error、fatal会成功。
在实际项目中一般开发人员都会配置log4j2的配置文件,一般都会命名为:log4j2.xml
里面的内容大概长这样,别看内容好像很多其实我们只需要留意关键字就行。
关键字:level="
<?xml version="1.0" encoding="UTF-8" ?> <RollingFile name="RollingFileInfo" fileName="logs/info.log" <RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log"
<configuration status="error">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<ThresholdFilter level="debug" />
<PatternLayout pattern="%d %-5level %class{36} %L %M - %msg%xEx%n"/>
</Console>
filePattern="logs/info-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
filePattern="logs/error-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
</appenders>
<loggers>
<root level="all">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>
注意:
存在有appenders子元素时候以appenders的level为准。
因为代码里面还会有第二次过滤,是通过遍历appenders子元素里面的每一个级别来进行过滤的。
this.intLevel是当前调用方法的级别,level.intLevel是appenders子元素里面配置给系统的级别。
这里一样得系统级别 >= 调用方法级别才可继续往下走。
没有appenders子元素时候以root子元素为准,root元素就是我们代码第一次过滤时候。
因为appenders优先级要高于root,会覆盖掉root的日志级别。
那么这里以appenders子元素里面的level为例。
等级从小到大分别是:warn(300) < info(400) < debug(500)
只要我们系统配置级别 >= 调用方法的级别,我们条件就可以成立。
这里可以调用的方法有:fatal(100)、error(200)、warn(300)、info(400)、debug(500),trace(600)则不成立。
这里就演示一个debug方法,成功执行。
trace方法不成立,直接没打印日志,程序也不会继续往下走jndi注入计算器也没有弹框。
需要注意的是实际项目中要是使用的springboot架构的项目,还要留意下application.properties文件,里面也会有等级的配置。
经测试application.properties文件配置的等级优先级比log4j2.xml里面的appenders子目录等级还要高。
1.审计log4j2漏洞入口点时候,先看是否存在漏洞版本范围。
2.查看application.properties文件、log4j2.xml文件里面的系统配置等级筛选出能利用的日志等级方法。
3.全局代码搜索漏洞入口方法跟踪参数查看是否由请求参数传进来。
实战项目:任意文件上传+目录穿越代码审计
项目架构:SpringBoot + Mybaits
1)针对文件操作类型的漏洞寻找入口点,全局搜索upload关键字找到一个跟文件上传相关的controller。
@ApiOperation(value = "上传")
@PostMapping("/uploadFile")
// 请求参数file:MultipartFile类型、参数title:String、参数detail:string
public Response<Boolean> uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("title")String title, @RequestParam("detail")String detail) throws IOException {
// 这里重点关注NoticesService.uploadFileF方法
Wj file = NoticesService.uploadFileF(file);
String contentType = file.getContentType();
String fileName = file.getOriginalFilename();
ZhiNa zhinan = new ZhiNa();
zhinan.setDetail(detail);
zhinan.setTitle(title);
zhinan.setFileName(fileName);
zhinan.setFileId(file.getFileId());
boolean ret = payzhinanService.save(zhinan);
return Response.successData(ret);
}
2)在file参数存在未过滤的情况,跟进NoticesService.uploadFileF(file)进一步查看是否有过滤。
@Override
public Wj uploadFileF(MultipartFile wjFile) {
// 把文件上传到指定服务器,这两个变量是配置文件里面固定写死的。
// ServerUrl:http://192.2.16.22:42098
// UploadUrl:inner/Service/upload
String uploadUrl = ServerUrl + "/" + UploadUrl;
Map<String, File> param = new HashMap<>();
// 重点关注transferToFile方法
File file = FileUtils.transferToFile(wjFile);
param.put("file", file);
logger.info("文件中心上传路径:{}", uploadUrl);
String response = StorageClient.uploadServerFile(uploadUrl, appid, DirectoryId, param);
JSON parse = JSONUtil.parse(response);
JSONArray jsonArray = (JSONArray) parse.getByPath("body");
JSONObject jsonObject = (JSONObject) jsonArray.get(0);
Wj Wj = jsonObject.toBean(Wj.class);
return Wj;
}
3)继续跟进FileUtils.transferToFile(wjFile)
第9行:在当前文件路径下创建一个文件,文件名是以请求参数传递过来的文件名。
注意:这里的getOriginalFilename未进行任何过滤直接拼接到文件路径上。
3.1 未进行黑白名过滤后缀,可上传任意格式后缀的文件。
3.2 未对 “../”进行过滤,可造成目录穿越,上传文件到任意目录。
10~14行:判断文件是否存在,不存在则创建文件,并把内容写进去目标文件。
/**
* 将MultipartFile传成File
* @param multipartFile
* @return
*/
public static File transferToFile(MultipartFile multipartFile){
File file = null;
try {
String filePath = new File("").getCanonicalPath() + File.separator + multipartFile.getOriginalFilename();
file = new File(filePath);
if(!file.exists()) {
file.createNewFile();
}
multipartFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
return file;
}
1)任意文件上传复现
由于在前端没找到上传页面,这里手动构造请求包。
三个必须要传的参数, file、title、detail,响应200上传成功。
filename处写上“.jsp”后缀名。
再用另一个queryFile接口查看是否上传成功,可看到这里已经上传成功,并且后缀是jsp。
2)目录穿越复现
为了证明是可以目录穿越的,尝试上传到不存在的目录,上传到不存在的目录会上传失败,且会返回报错信息。
上传到存在的目录,响应会正常返回。
后续getshell的敏感操作请恕不能展示了,真实项目点到为止。
1.通过搜索关键字:upload,write, fileName, filePath等快速寻找跟文件操作相关的漏洞点。
2.查看文件操作相关的变量是否由前端传值进来,并且可控。
3.跟踪请求传进来的变量查看是否有进行过滤处理,如:后缀名黑白名单、“../”、双写后缀名、大小写变换等。
4.根据代码所需要的请求参数构造请求包,并复现漏洞。
最后再给点学习代码审计的建议:
1.多实践、多写代码、多理解开发的想法和思维;
2.多给自己一点耐心要勇于客服困难,多点耐心看代码,不能心浮气躁;
3.多阅读别人的项目源代码了解别人的开发思想;
4.多动手去打断点调试代码、跟踪代码走向,多做笔记与总结。
5.自学会走好多弯路且难以坚持,建议报班,高效率高收益最重要;
以下是本人对课程总结的特点:
1.完全从Java零基础入门讲起,详细的讲解了常用的开发思想和java技术;
2.课程实践性强、不枯燥、重点内容突出,课程内容和过渡合理;
3.课程案例丰富、笔记完整且详细,特别是总结了常见的错误和问题;
4.作业布置合理,能够练习大量的审计案例;
5.售后服务有保障,提供及时的答疑和技术支持服务。
公众号
MS08067安全实验室
— 实验室旗下直播培训课程 —