0%

[XNUCA 2019] ezphp

去年XNUCA的一个题,因为一个点死扣了两天,docker配置Apache配置全都重新研究了一般,开端口防火墙全都整了一遍呜呜呜

源码

<?php
$files = scandir('./');
foreach($files as $file) {
    if(is_file($file)){
        if ($file !== "index.php") {
            unlink($file);
        }
    }
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
    highlight_file(__FILE__);
    die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
    echo "Hacker";
    die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
    echo "Hacker";
    die();
}
$files = scandir('./');
foreach($files as $file) {
    if(is_file($file)){
        if ($file !== "index.php") {
            unlink($file);
        }
    }
}
file_put_contents($filename, $content . "\nJust one chance");
?>

大致意思就是访问开始和结束都把index.php以外的文件都删了,然后让你写一个文件,filename只能是字母和.,content过滤了一些奇奇怪怪的内容
而由于filename限制在了.a-z中,所以用伪协议编码绕过内容过滤是不可能了
但是这些过滤和我上传一个马有什么关系呢?,所以直接传一个shell上去,不能解析,gg
说实话第一次遇到不能解析除了index.php以外的php文件的情况,人都傻了,后来去看了他们的docker,发现在Apache配置里写了只解析index.php
后来才知道应该是传.htaccess,这就是为什么上面有那一堆奇奇怪怪的过滤,然后最后又给你加了一句无意义字符。.htaccess中如果有不符合语法的内容会直接导致当前目录爆炸500
因为服务器刚好是Apache,只有在Apache中才能用.htaccess重写,当然也要在配置里面设置允许重写

阅读全文 »

[DDCTF 2019]homebrew event loop

滴滴ctf的一个题,python的代码审计,没审出来

给了源码,很长一截,首先理清楚头绪就花了不久

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************'  # censored
url_prefix = '/d5afe1f66147e857'


def FLAG():
    return '*********************'  # censored


def trigger_event(event):
    session['log'].append(event)
    if len(session['log']) > 5:
        session['log'] = session['log'][-5:]
    if type(event) == type([]):
        request.event_queue += event
    else:
        request.event_queue.append(event)


def get_mid_str(haystack, prefix, postfix=None):
    haystack = haystack[haystack.find(prefix)+len(prefix):]
    if postfix is not None:
        haystack = haystack[:haystack.find(postfix)]
    return haystack


class RollBackException:
    pass


def execute_event_loop():
    valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
    resp = None
    while len(request.event_queue) > 0:
        # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
        event = request.event_queue[0]
        request.event_queue = request.event_queue[1:]
        if not event.startswith(('action:', 'func:')):
            continue
        for c in event:
            if c not in valid_event_chars:
                break
        else:
            is_action = event[0] == 'a'
            action = get_mid_str(event, ':', ';')
            args = get_mid_str(event, action+';').split('#')
            try:
                event_handler = eval(action + ('_handler' if is_action else '_function'))   # 仅仅是拼接字符串罢了
                ret_val = event_handler(args)   # 函数在这里执行
            except RollBackException:
                if resp is None:
                    resp = ''
                resp += 'ERROR! All transactions have been cancelled. <br />'
                resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
                session['num_items'] = request.prev_session['num_items']
                session['points'] = request.prev_session['points']
                break
            except Exception as e:
                if resp is None:
                    resp = ''
                # resp += str(e) # only for debugging
                continue
            if ret_val is not None:
                if resp is None:
                    resp = ret_val
                else:
                    resp += ret_val
    if resp is None or resp == '':
        resp = ('404 NOT FOUND', 404)
    session.modified = True
    return resp


@app.route(url_prefix+'/')
def entry_point():
    querystring = urllib.unquote(request.query_string)
    request.event_queue = []
    if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
        querystring = 'action:index;False#False'
    if 'num_items' not in session:
        session['num_items'] = 0
        session['points'] = 3
        session['log'] = []
    request.prev_session = dict(session)
    trigger_event(querystring)
    return execute_event_loop()

# handlers/functions below --------------------------------------


def view_handler(args):
    page = args[0]
    html = ''
    html += '[INFO] you have {} diamonds, {} points now.<br />'.format(
        session['num_items'], session['points'])
    if page == 'index':
        html += '<a href="./?action:index;True%23False">View source code</a><br />'
        html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
        html += '<a href="./?action:view;reset">Reset</a><br />'
    elif page == 'shop':
        html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
    elif page == 'reset':
        del session['num_items']
        html += 'Session reset.<br />'
    html += '<a href="./?action:view;index">Go back to index.html</a><br />'
    return html


def index_handler(args):
    bool_show_source = str(args[0])
    bool_download_source = str(args[1])
    if bool_show_source == 'True':

        source = open('eventLoop.py', 'r')
        html = ''
        if bool_download_source != 'True':
            html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
            html += '<a href="./?action:view;index">Go back to index.html</a><br />'

        for line in source:
            if bool_download_source != 'True':
                html += line.replace('&', '&amp;').replace('\t', '&nbsp;'*4).replace(
                    ' ', '&nbsp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br />')
            else:
                html += line
        source.close()

        if bool_download_source == 'True':
            headers = {}
            headers['Content-Type'] = 'text/plain'
            headers['Content-Disposition'] = 'attachment; filename=serve.py'
            return Response(html, headers=headers)
        else:
            return html
    else:
        trigger_event('action:view;index')


def buy_handler(args):
    num_items = int(args[0])
    if num_items <= 0:
        return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
    session['num_items'] += num_items
    trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])


def consume_point_function(args):
    point_to_consume = int(args[0])
    if session['points'] < point_to_consume:
        raise RollBackException
    session['points'] -= point_to_consume


def show_flag_function(args):
    flag = args[0]
    # return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
    return 'You naughty boy! ;) <br />'


def get_flag_handler(args):
    if session['num_items'] >= 5:
        # show_flag_function has been disabled, no worries
        trigger_event('func:show_flag;' + FLAG())
    trigger_event('action:view;index')


if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0')

看一遍下来大概能看懂怎么回事,定义了trigger_event用来向列表中添加事件,而命令的执行则集中在execute_event_loop中完成,定义了一系列handler和function,实现用户功能,并且用户只能传action的参数执行handler,所有的function均由handler函数进一步调用

阅读全文 »

PHP文件操作漏洞

今天师傅们在群里聊到一个问题,有关于PHP文件操作对目录和文件判定相关的一个点,看了师傅们找到的一篇文章,感觉很有意思

PHP底层实现存在的一些问题,导致PHP写入、读取时对畸形文件名存在一些错误处理,导致漏洞产生

例一

file_put_contents和fopen等函数,使用了PHP的底层封装流,面对/var/www/html/index.php/. 这种畸形路径时会将其解析为正常的/var/www/html/index.php,导致对后缀名的绕过,而对于rename,unlink等函数,则会直接将此路径在Linux下进行系统调用,得到not a directory的错误,无法删除
此问题在Linux与Windows系统下均存在

阅读全文 »

[watevrCTF-2019]Pickle Store

之前做了他们的一个cookie store的题目,cookie伪造一下就能过,是个水题,就顺着做了他们的pickle store,做起来就不太对劲了,难度upup

同样在cookie处能取得一串base64,解码之后用pickle.loads加载,可以看到一个数组,记载了钱数和货物,但是这回多了一个hmac的签名,加密用的密钥是没办法搞出来了,那就只能另寻他路

pickle反序列化漏洞

python的反序列化漏洞危害比PHP要大,因为能做到任意代码执行,直接写一个类,创建一个__reduce__方法,该方法在该类被序列化时使用,返回字符串或元组。
我们可以在返回值处调用我们想要使用的函数,不过这种方法只能调用一个函数,例如return (os.system,('ls',)),并且这里是一定要加逗号的,圆括号中添加逗号会将其声明为元组,而规定__reduce__方法的返回值必须是字符串或元组,使用不这么用就会报错

阅读全文 »

[嘶吼CTF] Online Proxy

嘶吼的一个题,好像上次打了嘶吼,然后一个题磨了一天没摸出来

一开始以为是ssrf,试了半天发现写死了必须要http:// 开头,感觉就没机会了,查看源码,发现一个注释显示显示了当前IP,修改XFF头发现可控,并且会保存上一次访问的IP,这里就会用到数据库,所以可能存在一个注入

测试,打一个0’ or sleep(5) or ‘0过去,无延迟,显示payload,然后再重新随便打一个xff过去,出现延迟,在输入相同的xff,返回了0,出现延迟证明存在注入,只是没懂后端的查询代码是怎么样的,给人的感觉就像是直接是一句select xff一样。union select打了一下没有反应,报错也不存在,返回值有0和1,那就只能盲注了

然后自己动手做盲注,踩了一个小坑,即or和||是一致的,而|代表按位或,一开始用0’ or length(payload) or ‘0这么打,连length的返回值都是1。因为这种用or and这些来进行判断的结果只能是true 1和false 0嘛,愚蠢了。
而与零按位或数字则保持不变,就能获取结果,但是0和字符串或就会把字符串强制类型转换成数字,所以还是只能盲注

阅读全文 »