案例来自某次攻防演习的真实案例中....
当大佬已经在内网漫游时,而身外菜鸡的我还在外边徘徊为拿不到shell而苦恼中....
决定对还未攻下的目标单位进行资产复盘看看能不能找到一些软柿子捏一捏
通过在小程序中搜索目标单位名称,发现存在一个订餐系统
Burp抓取小程序所发送的数据包,先康康请求的目标域名先。
拿到域名后扫一波端口,发现有开放 22 443 3306 5353 6379 8009 8082 端口,尝试一波弱口令后无果。
访问8082端口时返回一个tomcat的404页面
前边微信小程序在发送请求时以 /dining/api 作为API接口前缀,所以这里我尝试访问了一下 /dining,然后得到了如下界面。
尝试爆了一波弱口令无果...缺少点欧气,于是翻了一下页面的js,发现存在几个接口的路径。
尝试看看是否可以未授权访问,尝试不带 /dining/ 和带 /dining/ 两种前缀路径进行访问,放回两种不同的结果。
返回了一个 应用不匹配,刚开始以为是没法进行未授权访问。
后来我回去看了一下小程序端发送的数据包,发现小程序的数据包每个请求中都带有AppId: dining。
于是我将请求包中加上了该头,这时页面返回的结果不一样,返回了401 - 未经授权的,证明了加上AppId的头是有作用的。
但是这里提示未经授权,意味着我们还是需要登录之后才能访问到对应的接口,回看之前的小程序数据包会发现,数据包中使用的是JWT方式对用户进行认证。
于是我将小程序数据包中自己的JWT认证所用的token值拷到了测试的数据包中进行重放。
o.O???? 既然就返回了很多用户的数据,越权访问啊!有点香!!
但是返回的数据包里并没有用户密码之类的,只能看到用户名的格式 aabbbbb ,于是跑了一波弱口令 123456,成功进入到了后台(其实这里后来想一想应该可以直接用小程序端中的JWT认真头来直接访问后台页面...)
但是现在登录的仅仅是个普通用户,想要登录到管理员的账户。
因为目标系统使用的是Angular.Js写的前端,所以有些数据是需要存储在浏览器,所以看了下SessionStorage。
发现存在一个roleId,当时的思路是想替换为管理员的roleId,然后看看请求时是否有管理员的效果。(但是后来想想这可能不是一个正确的思路,因为浏览器在发送请求时并不会自动携带SessionStorage中的数据回传给服务器。)
但是roleId是一串无法预测的随机数值,所以没法进行遍历拿到id值。
前边已知该系统可以对接口进行越权访问,尝试构造一些和角色相关的路径进行访问,如: /roles
/user-role
/user-roles
等组合的路径,看看是否能看到roleId值之类的。
当直接访问 /role
时直接就拿到了数据,可以看到回显的数据包中有一项为 organizationName
的属性值,当时并没有在回显的数据包中看到有管理员
相关的字样时...
于是当时想了一下,要不试一试/admin看看有没什么东西。(这里真的靠猜,感觉来了...)
结果就直接返回了管理员账号和密码.... o.O!!!,cmd5解密一波。
有管理员账号后,摸索一番,尝试了以下思路。
之后我尝试了SQL注入,常规语句判断,无果! 但是我发现响应包中回显了一个 orderBy
参数,Js发送的请求包中并没有显示有该参数的存在。
这是一个大多数后台系统都会有的一个参数,而且也是比较容易出现注入的地方。于是我手动的将数据包中加上了orderBy=asc
的参数,然后对数据包进行了重放。
发现报错,得到两个信息。
xxxMapper.xml
的字样,那用的mybatis框架无疑。Unknown column 'material.asc'
思考,orderBy=XXX
,XXX
部分是否为SQL可控的部分?于是我又写了一些别的值,然后看了一下数据包报错的回显。
证实 orderBy=XXX 部分被并接到了material.XXX 的位置上
将SQL语句拷贝出来,然后进行构造。(先知这里不知道为什么缩进体现不出来,所以我用notepad截了个图)
可以看到实际上这里是用了预编译的,但是orderby位置没有处理好。(同时like处没有使用concat的安全写法,个人感觉应该可能也有注入的可能。大佬勿喷,这里纯属个人瞎哔哔)
select count(0) from (select material.id, material.name, material.code, material.description, material.status, material.type, material.unit, material.create_time from dining_material material WHERE (material.name like ? ) order by material.asc) tmp_count
这里只要构造并闭合一下即可,加上页面会回显错误信息,可以进行报错注入。
id) tmp_count union select updatexml(1,substr(current_user(),1,10),1)#
这里因为报错导致泄露了物理路径,所以尝试进行写shell,无果,响应包中的报错信息提示目录只读。(忘截图了这里)
之前发现该服务器有对外开放3306 6379 22等端口,所以这里想的是读取配置文件中的数据,然后连接数据库。
前边报错信息得到绝对路径前半段/WEB-INF/classes/db.properties
前边报错信息得到绝对路径前半段/WEB-INF/classes/redis.properties
下边只要构造语句进行读取即可(路径和文件名靠猜,因为大多数java的项目都喜欢这样命名)。
但是在读取时发现读取不到,发现WEB-INF路径变成了很奇怪的路径。
_w_e_b_-_i_
于是进行了一段时间的尝试(痛苦面具),一开始以为是有过滤啥的。
试了一段时间后发现将路径小写时就不会导致被转成奇奇怪怪的路径。
但是Linux中是对大小写敏感的,所以这里需要修改一下注入的payload。
id) tmp_count union select load_file(concat('前边报错信息得到绝对路径前半段',upper('web-inf'),'/classes/db.properties'))#
成功读取到文件内容,3306对外开放,直接进行连接,是root权限的账号。
但是这里mysql无法写入文件(报错信息提示目录为只读权限),所以尝试进行redis的提权。
先使用mysql,读取redis.properties配置文件信息,然后连接6379。(这里在对redis提权时最好先吧原先的配置记录下来,避免改回去时忘记值是多少。)
尝试redis提权姿势
最后尝试的是模块加载执行命令,但是就有一个核心问题,怎么样吧模块文件传上去?
可以尝试之前发现的文件上传,但是可能会出现文件并没有落地成功的问题,所以这里我用的是 mysql 的 dumpfile 功能,将so文件写入到了/tmp/目录下。
在gayhub下载了相应的exp.so后,使用010editor将so库文件的十六进制值拷贝出来。
set @test = 0x十六进制值; select @test into dumpfile "/tmp/1.so";
连接上redis,使用以下命令加载模块,然后进行命令执行。
module load /tmp/1.so
system.exec "whoami;id"
当我正高兴可以命令执行时,意外发生了,redis突然卡住,然后连接不上了,但是端口一直是通的。
后来通过本地模拟加载exp.so
进行命令执行的过程后发现了问题所在,在gayhub上所找到的exp.so
存在问题,当我换了另外一个exp.so
文件之后就不会出现该问题了(本地模拟)。
我大E了,再提权之前没有好好测试之前在gayhub上找到的exp.so
文件,导致这种问题出现。
于是当天想了其他办法也没能拿到shell,等着看看第二天是否服务回恢复正常。
没错,很狗血的第二天真的恢复了(redis上原本的一些测试数据都不见了,可能管理员重启了,才让服务恢复),重复之前的步骤,用测试过没问题的exp.so文件再次的进行了提权。
最终创建用户,通过22端口登录到目标服务器。
渗透测试过程中,要经常留意一些小细节上的东西,尽可能的将所收集到的所有信息都利用起来,同时对一些较为危险的操作能在本地复现环境的建议复现一次,不要大E了。