<think>首先,用户询问的是Bugku CTF web题目“备份是个好习惯”的解题方法。参考引用[1]列出了该题目在Web部分,引用[2]提供了题目的PHP代码片段。
从引用[2]中,我得到了题目的关键PHP代码:
```php
include_once "flag.php";
ini_set("display_errors", 0);
$str = strstr($_SERVER['REQUEST_URI'], '?');
$str = substr($str,1);
$str = str_replace('key','',$str);
parse_str($str);
echo md5($key1);
echo md5($key2);
if(md5($key1) == md5($key2) && $key1 !== $key2){
echo $flag."取得flag";
}
```
解题的关键在于满足条件:`md5($key1) == md5($key2)` 且 `$key1 !== $key2`。这涉及到MD5哈希碰撞或弱点利用。
在PHP中,`==` 操作符用于松散比较,它可能会在类型转换时导致漏洞。例如,如果两个不同的字符串的MD5哈希都以"0e"开头,且后面全是数字,PHP可能会将它们解释为科学计数法的0,从而导致比较为真。
因此,解题思路是找到两个不同的字符串 `key1` 和 `key2`,使得 `md5($key1)` 和 `md5($key2)` 都以 "0e" 开头,后面全是数字。
常见的例子包括:
- `key1 = "240610708"`,MD5: "0e462097431906509019562988736854"
- `key2 = "QNKCDZO"`,MD5: "0e830400451993494058024219903391"
这些字符串的MD5哈希都以 "0e" 开头,后面是数字,所以在松散比较时等于0。
在代码中,参数是通过URL传递的。代码处理URL查询字符串:提取问号后的部分,移除"key"字符串,然后解析查询字符串。
具体步骤:
1. `$str = strstr($_SERVER['REQUEST_URI'], '?');` – 获取问号后的部分。
2. `$str = substr($str,1);` – 移除问号,所以剩余的是查询字符串如 "key1=value1&key2=value2"。
3. `$str = str_replace('key','',$str);` – 移除所有 "key" 子串。这可能会改变参数名。
例如,如果原始查询是 "key1=a&key2=b",移除 "key" 后变成 "1=a&2=b"。
然后,`parse_str($str);` 会解析这个字符串并创建变量。移除 "key" 后,原来的 "key1" 变成 "1","key2" 变成 "2",所以变量 `$key1` 和 `$key2` 可能不会被正确设置。
用户需要设置变量 `$key1` 和 `$key2`,但由于移除了 "key",应该使用参数名如 "1" 和 "2" 来设置值。
例如,如果查询字符串是 "1=value1&2=value2",那么 `parse_str` 会创建 `$1` 和 `$2`,但代码中用的是 `$key1` 和 `$key2`。
代码中直接使用 `$key1` 和 `$key2`,但参数名被修改了。所以,可能需要使用不同的参数名。
在移除 "key" 后,原本的 "key1" 变成 "1",所以变量名是 `$1`,但代码中检查 `$key1`,这会导致未定义的变量。
PHP中,如果变量未定义,使用它会引发警告,但由于 `display_errors=0`,不会显示错误,但 `$key1` 和 `$key2` 可能为 null。
但条件 `$key1 !== $key2` 可能为真如果它们都未定义,但 `md5(null)` 是固定的,`md5(null)` 是 "d41d8cd98f00b204e9800998ecf8427e",不等于另一个 null 的 MD5,但它们相同,所以 `md5($key1) == md5($key2)` 可能为真,但 `$key1 !== $key2` 对于 null 可能为假,因为 `null !== null` 是假的?等号比较:`null === null` 为真,`null == null` 为真,但 `!==` 严格不相等。
`$key1` 和 `$key2` 如果未定义,都是 null,所以 `$key1 !== $key2` 是 false,因为 `null !== null` 是 false。
所以,需要正确设置变量。
在移除 "key" 后,参数名改变了。例如,如果原始查询有 "key1" 和 "key2",移除 "key" 后变成 "1" 和 "2",然后 `parse_str` 会设置变量 `$1` 和 `$2`,但不是 `$key1` 和 `$key2`。
所以,代码中 `$key1` 和 `$key2` 可能未定义。
但代码中直接使用 `$key1` 和 `$key2`,它们没有被设置。
或许用户需要使用参数名 "ey1" 或类似来绕过移除。
`str_replace('key','',$str)` 移除所有出现的 "key" 子串。
例如:
- "key1" 移除 "key" 后变成 "1"
- "akey1" 移除 "key" 后变成 "a1"
- "kkeyey1" 变成 "1" 因为移除所有 "key"
要设置 `$key1`,但 "key" 被移除,所以变量名无效。
但代码中,`parse_str` 后,使用的变量是 `$key1` 和 `$key2`,但参数名被修改,所以 `$key1` 可能不存在。
除非参数名中包含 "key",但移除后剩下部分被用作变量名。
例如,如果参数名是 "kkeyey1",移除 "key" 后:先移除所有 "key",所以 "kkeyey1" -> 移除 "key" 两次? "kkeyey1" 有 "kkey" 其中 "key" 被移除,剩下 "k",然后 "ey1" 没有 "key",所以变成 "key"? 移除所有出现的子串 "key"。
在字符串中,移除所有连续或非连续的 "key" 子串? `str_replace` 移除所有出现的子串。
例如 "keykey1" 移除所有 "key" 后变成 "1"。
"akeybkeyc" 移除后变成 "abc"。
对于参数名,假设原始查询是 "key1=value1&key2=value2",移除 "key" 后变成 "1=value1&2=value2",然后 `parse_str($str)` 会设置 `$1 = value1` 和 `$2 = value2`,但代码中用的是 `$key1` 和 `$key2`,这些变量未被设置,所以可能为 null。
然后 `md5($key1)` 和 `md5($key2)` 都是 `md5(null)`,即 "d41d8cd98f00b204e9800998ecf8427e",所以 `md5($key1) == md5($key2)` 为 true(因为哈希相同),但 `$key1 !== $key2`:由于两者都为 null,`$key1 !== $key2` 是 false,因为 null 与 null 在严格比较下是相等的? 等等,不对。
PHP 中的比较:
- `$key1 == $key2`:如果都是 null,则为 true。
- `$key1 === $key2`:如果都是 null,则为 true,因为类型和值都相同。
- `$key1 !== $key2` 是 `!($key1 === $key2)`,所以如果都是 null,就是 false。
但条件要求 `$key1 !== $key2`,如果都是 null,这就为 false。
所以条件不成立。
需要 `$key1` 和 `$key2` 被设置且不同。
所以,参数命名的方式必须保证移除 "key" 后,变量名被设置为 `$key1` 和 `$key2`,但原始名称中有 "key",移除后,名称发生了变化。
例如,如果参数名为 "xkey1",移除 "key" 后变成 "x1",所以变量是 `$x1`,而不是 `$key1`。
无法直接设置 `$key1`,因为名称中包含 "key" 会被移除。
但变量名 `$key1` 是硬编码的,所以可能无法设置。
除非使用其他方法。
或许 `parse_str` 会设置变量,但名称是修改后的字符串。
在代码中,echo md5($key1) 和 echo md5($key2) 之后,如果变量未定义,md5 的参数是 null,但代码可能出错,不过 display_errors=0,所以没有输出,但逻辑继续。
但在 if 条件中,如果 $key1 和 $key2 未定义,$key1 !== $key2 是 false。
但我们需要它们被定义且不同。
解决方案:使用在移除 "key" 后的参数名来设置值。
例如,在移除 "key" 后的字符串中,如果有 "1" 和 "2",那么变量 $1 和 $2 将被设置,但代码使用的是 $key1 和 $key2,而不是这些变量。
所以,代码中可能有一个错误,或者需要特定的方式。
回顾代码:
$str = strstr($_SERVER['REQUEST_URI'], '?'); // 例如,对于 /path/script.php?key1=val&key2=val,$str 是 "?key1=val&key2=val"
$str = substr($str,1); // 去掉 '?',所以是 "key1=val&key2=val"
$str = str_replace('key','',$str); // 移除所有 'key',所以如果输入是 "key1=val&key2=val",输出是 "1=val&2=val"
然后 parse_str($str); // 解析查询字符串 "1=val&2=val",所以它设置了变量 $1 和 $2,值为 val。
但在PHP中,变量名不能以数字开头,所以 $1 是无效变量名。parse_str 可能会忽略它或产生警告,但 display_errors=0,所以没有错误,但 $1 和 $2 不会被定义。
在 PHP 中,变量名不能以数字开头,所以 parse_str 对于像 "1=val" 这样的键无法创建变量,因此这些值不会被设置。
因此,$key1 和 $key2 没有被定义。
但是,在代码中,有 echo md5($key1); 和 echo md5($key2);,所以如果未定义,md5(null) 会被计算,但if条件不起作用。
我们需要一种方法来设置 $key1 和 $key2。
关键是原始参数名中包含 'key',所以移除后,名称发生变化,但我们需要参数名在移除 'key' 后,能够被解析为变量。
例如,如果参数名中没有 'key',但名字是 $key1,而 'key' 被移除,所以如果我们使用参数名 'key1',就变成了 '1',无效。
所以,解决方案是使用在移除 'key' 后有效且能设置所需变量的参数名。
但所需变量是 $key1 和 $key2,它们不是由 parse_str 设置的,因为键被修改了。
除非代码使用不同的变量名,但代码中写的是 $key1 和 $key2。
也许在移除之后,字符串可以被解析,使得变量名是 'key1',但原始字符串中有 'key',所以被移除了。
另一个想法:使用像 'kkeyey1' 这样的参数名,在移除 'key' 之后,会变成 'key1' 或类似的东西。
例如,'kkeyey1':'kkeyey1' 包含 'key' 吗?'kkeyey1' - 移除 'key':位置,'kkey' 有 'key' 子串,移除后剩下 'k',但 'kkey' 是 'k' + 'key',移除 'key' 后剩下 'k',然后 'ey1',没有 'key',所以是 'key'?我们来正确使用 str_replace。
str_replace('key', '', $str) 会替换所有出现的 'key'。
对于 'kkeyey1':它包含两个 'key' 吗?'kkey' 可以视为 'k-key',但 'key' 是一个子串,所以 'kk ey' 不行,'kkeyey1' 这个字符串,子串:从索引 0 开始,'kke' 不是 'key',索引 1 'key' 是 'key'? 字符:索引 0:'k', 1:'k', 2:'e', 3:'y', 4:'1',所以子串从索引 1 开始是 'ke y' 不行,在 'kkeyey1' 中,如果它是 'kk' + 'ey',就没有 'key' 子串,除非是 'k-key',但它是连续的。
'key' 子串必须是连续的字符。
所以对于 'kkeyey1',它没有 'key' 子串,因为 'k' 然后 'k' 然后 'e' 然后 'y',所以没有 "k" 后接 "e" 后接 "y" 的连续序列。
例如,'key1' 有 'key'。
'akey1' 有 'key'。
'kkey' 有 'key'(位置1-3:k, k, e, y,索引1-3是 k,e,y?索引1是第二个k,索引2是e,索引3是y,所以索引1-3是 "key" 如果我们考虑索引1,2,3:但索引0是k, 索引1是k, 索引2是e, 索引3是y,所以索引1-3是 "key" 如果我们取索引1,2,3:字符索引1是k, 索引2是e, 索引3是y,所以 "key" 子串存在。
在 "kkey" 中,从索引1开始的子串是 "key",所以 str_replace 会移除它。移除后,剩下索引0的 'k',然后索引4及之后的内容。
对于 "kkeyey1",我们分解一下:假设字符串是 "kkeyey1"。
字符:索引0:'k', 1:'k', 2:'e', 3:'y', 4:'e', 5:'y', 6:'1'
子串 "key" 出现在索引1-3:字符1:'k',2:'e',3:'y' -> "key",移除后移除字符1,2,3。
移除后,剩下索引0: 'k', 然后索引4:'e', 5:'y', 6:'1',所以是 "key1"?索引4是 'e', 5是 'y', 6是 '1',所以 "ey1"。
所以移除后,字符串是 "k" + "ey1" = "key1"。
"k" 和 "ey1" 所以是 "key1"。
类似地,对于 "kkeyey2",移除后变成 "key2"。
然后,在查询字符串中使用这些参数名。
例如,在URL中使用参数名 kkeyey1 和 kkeyey2。
所以,对于原始查询:?kkeyey1=value1&kkeyey2=value2
然后 $str = strstr(..., '?') 是 "?kkeyey1=value1&kkeyey2=value2"
substr($str,1) 后是 "kkeyey1=value1&kkeyey2=value2"
str_replace('key','',$str) 后:对于 "kkeyey1",如上所述,移除 "key" 子串,变成 "key1"。
类似地,"kkeyey2" 变成 "key2"。
所以字符串变为 "key1=value1&key2=value2"
然后 parse_str($str) 设置 $key1 = value1 和 $key2 = value2。
完美。
然后,我们需要 $key1 和 $key2 的值不同,但 md5 值在弱比较中相等。
所以,使用像 "240610708" 和 "QNKCDZO" 这样的值。
所以,解题方法是构造一个类似如下的URL:
http://xxx/?kkeyey1=240610708&kkeyey2=QNKCDZO
然后,代码会设置 $key1 = "240610708", $key2 = "QNKCDZO"
md5($key1) = md5("240610708") = "0e462097431906509019562988736854"
md5($key2) = md5("QNKCDZO") = "0e830400451993494058024219903391"
在松散比较中,md5($key1) == md5($key2) 为 true,因为两者都以 "0e" 开头,后面是数字,表示在科学记数法中为 0。
而 $key1 !== $key2 为 true,因为字符串不同。
因此,条件满足,将回显标志。
在代码中,在判断之前有 echo md5($key1); 和 echo md5($key2);,因此输出哈希值,但判断后会输出 $flag。
对于 CTF,用户需要访问带有这些参数的 URL。
现在,在响应中,应该包含标志。
根据引用 [2],标题是“备份是个好习惯”,我假设这是一个 Web 题目,用户需要利用这个漏洞。
所以,解题方法是:
1. 访问带有特定参数的 URL。
2. 使用参数名称,在移除 'key' 后变为 'key1' 和 'key2',例如 'kkeyey1' 和 'kkeyey2'。
3. 设置值以实现 MD5 冲突,例如 '240610708' 和 'QNKCDZO'。
4. 提交请求,响应中将包含标志。
在原始代码中,在移除之后,它将设置 $key1 和 $key2,然后进行检查。
在响应中,如果条件满足,将输出 $flag。
所以,对于答案,我应该用简单的语言解释步骤。
另外,根据系统指令,使用中文回答,在 LaTeX 中使用行内数学格式,但对于这个答案,可能没有数学内容,但如果有变量,要使用 $...$。
在这个上下文中,主要是讨论代码和字符串,所以可能不需要 LaTeX。
但例如,在解释时,如果有类似变量的内容,可以使用 $...$。
另外,生成相关问题。
在回答末尾添加引用标识。
引用来自用户的消息,所以我可以在末尾添加 [^1] 和 [^2],但用户说“上述引用仅供参考”,所以我应该基于引用内容进行回答。
最后,将答案结构清晰地呈现。
</think>### Bugku CTF "备份是个好习惯" 题目解题方法
在 Bugku CTF 的 Web 题目 "备份是个好习惯" 中,解题的关键在于分析提供的 PHP 代码,并利用其逻辑漏洞来触发条件获取 flag。以下是基于引用 [2] 中代码的详细解题步骤。代码的核心逻辑是:通过 URL 参数传入两个值 `$key1` 和 `$key2`,满足条件 `md5($key1) == md5($key2)` 且 `$key1 !== $key2` 时,输出 flag[^2]。
#### 解题步骤
1. **理解代码漏洞**:
- 代码从 URL 查询字符串中提取参数,并移除所有 "key" 子串:`$str = str_replace('key','',$str);`。
- 移除 "key" 后,参数名会被修改,然后将字符串解析为变量:`parse_str($str);`。
- 目标是通过巧妙构造参数名,使解析后变量 `$key1` 和 `$key2` 被正确设置,且满足:
- `md5($key1) == md5($key2)`:PHP 的 `==` 是松散比较,可利用 MD5 哈希的 "0e" 开头漏洞(即哈希值以 "0e" 开头后跟数字,比较时被当作科学计数法的 0)。
- `$key1 !== $key2`:严格比较,确保两个值不同。
- 推荐的漏洞利用值:
- `$key1 = "240610708"`:MD5 哈希为 `0e462097431906509019562988736854`。
- `$key2 = "QNKCDZO"`:MD5 哈希为 `0e830400451993494058024219903391`。
- 松散比较时,两个哈希都等于 0,因此 `md5($key1) == md5($key2)` 为真,但字符串值不同,满足 `$key1 !== $key2`。
2. **绕过参数名修改**:
- 代码移除所有 "key" 子串后解析参数。直接使用 `key1` 和 `key2` 会被修改(例如 `key1` 变成 `1`),导致变量无法正确设置。
- 解决方案:使用参数名 `kkeyey1` 和 `kkeyey2`。移除 "key" 子串后:
- `kkeyey1` → 移除 "key"(在索引位置)后变为 `key1`。
- `kkeyey2` → 移除 "key" 后变为 `key2`。
- 这样,`parse_str` 会正确设置变量 `$key1` 和 `$key2`。
3. **构造攻击 URL**:
- 访问题目 URL,并附加查询参数:
- 完整 URL 格式:`http://题目域名/路径?kkeyey1=240610708&kkeyey2=QNKCDZO`
- 示例(实际域名需替换):`http://example.com/challenge?kkeyey1=240610708&kkeyey2=QNKCDZO`
- 参数解释:
- `kkeyey1=240610708`:设置 `$key1` 的值。
- `kkeyey2=QNKCDZO`:设置 `$key2` 的值。
4. **触发 flag 输出**:
- 发送 HTTP 请求(使用浏览器或工具如 curl)后,代码会:
- 移除 "key" 子串,解析出 `$key1` 和 `$key2`。
- 计算并输出 MD5 哈希(调试信息,可忽略)。
- 检查条件:满足时输出 `$flag` 和 "取得flag"。
- 如果条件正确,响应中将包含 flag,格式类似:`flag{...}取得flag`。
#### 注意事项
- **测试工具**:可以使用 curl 命令测试:
```bash
curl "http://example.com/challenge?kkeyey1=240610708&kkeyey2=QNKCDZO"
```
- **环境依赖**:题目可能部署在 Bugku CTF 平台,确保访问正确的 URL(参考引用 [1] 中的题目列表)[^1]。
- **原理总结**:本题利用 PHP 松散比较的漏洞和字符串处理缺陷,通过巧妙参数名和 MD5 碰撞值绕过检查。
#### 相关问题
1. 如何防御 PHP 中的 MD5 松散比较漏洞?
2. CTF Web 题目中还有哪些常见的哈希碰撞利用方法?
3. PHP 的 `parse_str` 函数可能导致哪些安全问题?
[^1]: BugkuCTF练习题解——Web一。
[^2]: 备份是个好习惯题目源码。