记对某Spring项目代码审计
The following article is from 顶级玩家安全团队 Author 顶级玩家团队
公众号现在只对常读和星标的公众号才展示大图推送,
建议大家把听风安全设为星标,否则可能就看不到啦!
----------------------------------------------------------------------
一. 前言
之前实战遇到的一个系统,尽管开发者使用了一些安全校验过滤的措施,然而系统本身仍然存在不少漏洞,遂找到源码审计了一番,特此记录下。文章中所有漏洞均已提交CNVD。
二. 审计前准备
首先确认技术栈:Spring+servlet+mybatis+redis
目录结构:
检查有哪些过滤器,后面验证漏洞的时候需要注意是否能bypass:
1)权限过滤器
在CommandFilter类中实现,看下逻辑,依次实现三个功能:
1.获取实例化bean,读取配置
定义了一个接口白名单,凡是白名单中的接口请求,都会直接放行,不再进行后续验证
2.判断校验csrf token是否存在且不为空,通过代码可以看出这个token是base64编码的,而且过滤器只对其进行了简单的解码后格式验证就直接放行请求,再没有做更多的验证。
3.最后根据Action参数使用if嵌套判断用户是否拥有执行对应操作的权限,如果该操作不需要鉴权或者用户拥有权限就放行请求,反之则返回203错误。
2)防sql注入过滤器
进入实现类SqlInjectionFilter,定义了两个字符串数组injectionWords和punctuations
使用isSqlInjection()方法判断请求是否为sql注入,我们跟进这个方法:
程序首先获取所有请求参数中的值,存入到名为params的collection集合中,使用迭代器循环读取它们,然后转储到var5这个字符串数组中,在for循环中使用hasInjectionWords()方法逐个逐个判断请求参数的值是否包含sql注入关键字,我们继续跟进:
首先将待检查的字符转成小写,用tmpParam变量接收,然后将其传入replacePunctuation()方法判断是否包含上述提到的特殊符号,如等号=、大于小于符号><等,如果存在,则将特殊符号替换为空格后返回,返回的值用tmp变量接收
随后,使用split()方法,以空格作为切割符,分割出一个字符串数组,最后判断这个数组中的字符串有没有非法关键字,若有则返回true,确认是sql注入。
简单总结一下这个过滤器的工作机制:读取所有请求参数的值,挨个判断是否包含特殊符号,包含的话先用空格过滤掉,再判断过滤后的值是否包含非法关键字。
3)文件过滤器
在FileFilter类中实现,看关键代码,获取实例化的bean,拼接好正则表达式,每次访问/SystemFile/路径下的文件都会尝试匹配,如果匹配到是恶意后缀直接返回403错误。
二. 审计
组件漏洞
进入pom.xml,简单看一下是否使用了存在已知漏洞的组件:
并不满足log4j2的利用版本,忽略。
FasterXML Jackson-databind的反序列化,在commonlib类下多次使用了这个包【等待后续分析】
手工审计
进入Controller层,我们还是着重挖掘文件上传、SQL注入这类危害等级高一点的漏洞。
1)逻辑漏洞挖掘
·第一次尝试
首先从登录接口入手,定位到RegisterLogin.do的代码,看一下系统是如何处理登录逻辑的获取用户的输入
对比用户输入的验证码和session中的验证码字符串是否符合
随后判断用户名密码,代码第112行,发现系统存在硬编码的用户名和密码,如果用户输入了对应账号密码,系统会将userId变量赋值为1,否则才会去数据库中检查
若账号密码正确则返回相应用户的userId,而如果userId大于0的话允许其登录
我们尝试利用这个账号登录
可以登录,且由于admin账户的userId为1,所以直接获取到admin账号权限
成功获得后门账号一枚
·第二次尝试
继续翻看代码,在Home.do接口,发现一个获取用户信息的方法
代码第64行读取cookie,跟进分析getCookieInfo()方法
读取某个cookie字段,将其值进行base64解码
随后使用for循环加if判断读取需要的参数,此例中是UID参数
我们将登陆后的cookie进行手动base64解码,可以得到这样的字符串
回到接口代码,UID参数对应账号的userId字段,程序直接根据cookie中的UID参数就向数据库请求用户数据而没有再做过多的验证
Sql语句直接使用了”select *” 且没有对返回结果中的password字段进行屏蔽或过滤,导致密码hash泄露
猜测此接口可能存在未授权访问,到权限过滤器搜索,果不其然,并没有定义访问该接口所需的权限,导致未授权攻击者可获取任意账号的密码hash
手动构造payload就可以获得系统内任意用户的信息
编写exp即可批量获取账号信息
密码采用md5进行hash,彩虹表可破解
2)任意文件下载/读取漏洞挖掘
通过全局搜索download、fileutil等字样定位到文件操作有关的方法
代码第108行,直接将用户输入的路径与“/SystemFile”进行拼接,未作任何校验和过滤,导致我们可以使用“../”穿越到上层目录,实现任意文件的读取。Poc:
直接读取web.xml:
直接读取系统配置:
同时我们发现该接口在白名单中,未授权用户也可以直接读取服务器上的任意文件,危害更大。
这里我们删掉Token和Cookie后发包测试,发现依然能成功读取到,验证成功:
同样的还有DeviceFileUpload.do接口的Download操作
该接口在请求白名单中,未授权用户也可以直接利用。
以及Service.do接口的Download操作,都存在相同的漏洞。
3)Sql注入漏洞挖掘
系统使用了mybatis框架,随着mybatis这类ORM框架的使用,sql注入的可能性也在慢慢降低,然而在mybatis使用不当的情况下,仍然可能出现漏洞。
使用小工具快速扫一下
工具由于是基于正则匹配的,容易出现误报,还是需要我们手工验证一下
这里工具扫到第80行有漏洞,但是查看后发现该语句被注释掉,这个漏洞点也被修复了
功夫不负有心人,找到一处在in后使用${}查询的语句
根据id找到对应
4)文件上传挖掘
·第一次尝试
通过全局搜索upload、MultipartFile等字样,定位文件上传点,
找到一个,但是对后缀名进行了限制,仅允许上传Excel表格文件
·第二次尝试
换一个,代码第341行,只要后缀名不为jsp就可以成功上传,一般看到这里可能首先想到的就是Windows不区分大小写的特性,使用大小写混写绕过。
然而与php不同的是,如果jsp后缀这三个字母不均为小写的话,tomcat不会对其解析。
如图,将shell.jsp文件的字母p改为大写后无法被tomcat解析:
除了大小写混写,我们还可以使用空格绕过或者点号绕过,代码要求后缀名不为jsp即可,那我们构造文件名”shell.jsp.”进行上传
响应包显示Code是200,上传结果却失败了,回到代码
如果后缀名错误或者上传过程捕获到错误,响应Code都应该是202才对
再看一下上传目录,我们上传的文件全被删除了
代码中只有第344行出现了文件删除的操作,判断条件是result结果为false或者code等于202,结合响应包中Code为200我们可以知道是第336行的代码执行返回了false,我们跟进这个setMaintainPath()方法
分析代码发现系统会起一个web service client服务,当上传文件后,系统会作为client端调用远程web service服务端,将上传文件保存路径发送到远程服务器,根据配置文件中的server_address字段,wsdl文档部署在本地的8210端口
尝试访问这个文档地址时却发现404报错了,说明创建失败了
远程web service也无法正常连接
因此无论上传什么文件最终都会都会失败,这个接口也无法利用。
·第三次尝试
前两次出师不利,这次我们换个思路,白盒加黑盒审计,根据请求的接口去审计对应接口的代码:
在后台找到一个文件管理的模块,进行一次正常的文件上传试试
通过burp抓包发现一次完整的上传发送了多个请求包
前三个GET请求都是在判断是否存在同名文件以及文件路径是否过长,返回都是正常的,我们可以直接忽略掉,只关注Upload和UploadFile这两个方法。
选择上传文件后请求了Upload方法
代码第578行,生成了一段随机字符串作为暂时文件名tempName,随后使用checkFileType函数对真实文件名realName进行检查,跟进这个方法
定义了一个非法后缀名List,能禁的后缀名差不多禁完了。
检查通过后,在代码第598行生成暂时保存的完整路径,以我们上传的111.txt为例
这个文件会保存到/Plugin/FileManage/Temp/目录下的165607238767794txt文件中
点击“确认”按钮后调用UploadFile方法
可以看到,程序仍然使用了checkFileType()方法判断请求中FileName参数,然后使用addFile()方法,根据上一步文件上传过程中生成的tempName参数找到对应文件,将这个文件移出暂存路径,重命名为FileName参数值,随后移动到SystemPath路径下,最后将文件属性写入数据库方便查询。
看似滴水不漏,然而由于UploadFile操作中对FileName的过滤不严导致我们仍然可以成功上传webshell
绕过思路如下:
选择上传文件时,上传一个文件内容为webshell,文件名为合法后缀名的文件,目的是绕过checkFileType()方法的后缀检查
通过响应包看到webshell上传成功了,随后点击“确定”,系统调用UploadFile接口
此时抓包修改FileName参数,在其后面添加一个点,checkFileType()方法会判断目标文件的后缀为空,继续执行后面的操作,将我们之前上传的文件移动并重命名为”shell.jsp.”,又由于Windows特性,会自动抹去不符合规则符号,于是最终这个文件名为”shell.jsp”,是一个正常的webshell了(加::$DATA等绕过技巧同样适用,但是由于程序使用了trim()方法去除后置空格,所以加空格绕过会失败)。
从管理面板上看到入库的数据仍然是”shell.jsp.”
而实际物理文件却是”shell.jsp”
尝试访问,结果忘了还有文件过滤器这玩意儿了,寄!
不甘心,尝试把这个webshell移动到某个不受文件过滤器检查的目录去,想到Java可以使用File类的renameTo()方法,以及commons.io.FileUtils#moveFile函数移动文件,于是全局检索对应关键字来寻找可用接口。
通过全局搜索moveFile关键字,找到一处移动文件的接口,
是updateFile()方法的实现类,再搜索这个关键字,定位到controller层某个更新文件的接口
初略看一下代码,根据已上传文件的id找到对应物理文件,将其移动到SYSTEMPATH目录下去
然而这个SYSTEMPATH仍然在/SystemFile/路径下,也就是说移动后访问仍然会被过滤器拦截,这条路至此也走不通了。