PHP SECURITY CALENDAR 2017 (Day 13 - 16)
Day 13 - Turkey Baster(特定场合下addslashes函数的绕过)
源码是这样的
1 class LoginManager { 2 private $em; 3 private $user; 4 private $password; 5 6 public function __construct($user, $password) { 7 $this->em = DoctrineManager::getEntityManager(); 8 $this->user = $user; 9 $this->password = $password; 10 } 11 12 public function isValid() { 13 $user = $this->sanitizeInput($this->user); 14 $pass = $this->sanitizeInput($this->password); 15 16 $queryBuilder = $this->em->createQueryBuilder() 17 ->select("COUNT(p)") 18 ->from("User", "u") 19 ->where("user = '$user' AND password = '$pass'"); 20 $query = $queryBuilder->getQuery(); 21 return boolval($query->getSingleScalarResult()); 22 } 23 24 public function sanitizeInput($input, $length = 20) { 25 $input = addslashes($input); 26 if (strlen($input) > $length) { 27 $input = substr($input, 0, $length); 28 } 29 return $input; 30 } 31 } 32 33 $auth = new LoginManager($_POST['user'], $_POST['passwd']); 34 if (!$auth->isValid()) { 35 exit; 36 }
这是典型的用户登录程序,从代码来看,考察的是通过SQL注入绕过登陆验证。代码第33行 ,通过 POST 方式传入 user 和 passwd 两个参数,通过 isValid() 来判断登陆是否合法。
跟进一下 isValid() 这个函数,该函数主要功能代码在第12行-第22行,13行和14行调用 sanitizeInput() 针对 user 和 password 进行相关处理。
跟进一下 sanitizeInput() ,主要功能代码在第24行-第29行,这里针对输入的数据调用 addslashes 函数进行处理,然后再针对处理后的内容进行长度的判断,如果长度大于20,就只截取前20个字符。
addslashes 在单引号(')、双引号(")、反斜线(\)与 NUL( NULL 字符)字符之前加上反斜线。
这题已经过滤了单引号,正常情况下是没有注入了,原因出在了 substr 函数
所以这道题思路是因为反斜杠可以取消特殊字符的用法,而注入需要通过单引号闭合,在这道题里一定会引入反斜杠。所以可以在反斜杠与单引号之间截断掉,只留一个反斜杠
实验一下
input=1234567890123456789' 拼接出来的 sql 语句为
select count(p) from user u where user = '1234567890123456789\' AND password = '$pass'
这里的sql语句由于反斜杠的原因,user = '1234567890123456789\' 最后这个单引号便失去了它的作用。这里再让 pass=or 1=1#,那么最后的sql语句为
select count(p) from user where user = '1234567890123456789\' AND password = 'or 1=1#'
user 值为 1234567890123456789\' AND password = ,因此可以保证带入数据库执行的结果为 True ,然后就能够顺利地通过验证
所以这题最后的 payload 为:
user=1234567890123456789'&passwd=or 1=1#
(*)修复
防御手段太多也会出现漏洞,这道题删掉截取前20个字符的代码就可以修复了
参考:
Day 14 - Snowman(从变量覆盖到getshell)
源码是这样的
1 class Carrot { 2 const EXTERNAL_DIRECTORY = '/tmp/'; 3 private $id; 4 private $lost = 0; 5 private $bought = 0; 6 7 public function __construct($input) { 8 $this->id = rand(1, 1000); 9 10 foreach ($input as $field => $count) { 11 $this->$field = $count++; 12 } 13 } 14 15 public function __destruct() { 16 file_put_contents( 17 self::EXTERNAL_DIRECTORY . $this->id, 18 var_export(get_object_vars($this), true) 19 ); 20 } 21 } 22 23 $carrot = new Carrot($_GET);
这道题目讲的是变量覆盖与路径穿越问题。在第10-11行处, Carrot 类的构造方法将超全局数组 $_GET 进行变量注册,这样即可覆盖第8行已定义的 $this-> 变量。而在第16行处的析构函数中,file_put_contents 函数的第一个参数又是由 $this-> 变量拼接的,这就导致可以控制写入文件的位置,最终造成任意文件写入问题。
payload :id=../var/www/html/shell.php&shell=',)%0a<?php phpinfo();?>//
shell.php
array( 'id' => '../var/www/html/shell.php', 'lost' => 0, 'bought' => 0, 'shell' => '\',) <?php phpinfo();?>//', )
基础知识:
__construct() 在一个类中只能声明一个构造方法,而是只有在每次创建对象的时候都会去调用一次构造方法,不能主动的调用这个方法,所以通常用它执行一些有用的初始化任务。该方法无返回值。
通过构造方法对对象属性进行初始化赋值例子:
<?php class Person { var $name; var $age; //定义一个构造方法初始化赋值 function __construct($name, $age) { $this->name=$name; $this->age=$age; } function say() { echo "我的名字叫:".$this->name."<br />"; echo "我的年龄是:".$this->age; } } $p1=new Person("张三", 20); $p1->say(); ?>
运行该例子,输出:
我的名字叫:张三
我的年龄是:20
__destruct() 与构造方法对应的就是析构方法,析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集等。析构函数不能带有任何参数,其名称必须是 __destruct() 。
在上面的例子中加入下面的析构方法:
function __destruct() { echo "再见".$this->name; }
再次运行该例子,输出:
我的名字叫:张三
我的年龄是:20
再见张三
(*)修复
检测变量名是否为PHP原有的超全局数组,如果是则直接退出并告知变量不允许
参考:
Day 15 - Sleigh Ride($_SERVER['PHP_SELF']导致的防御失效问题)
源码是这样的
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']); }
分析一下程序的运行
如果有 $_GET['redirect'] 参数,那么就 New 一个 Redirect 对象,同时调用 Redirect 类的 startRedirect 方法
startRedirect 函数接受一个 GET 类型的 params 参数,然后在 explode() 函数中,将 $_SERVER['PHP_SELF'] 得到的值,以 / 分割成一个 $parts 数组
$baseFile 的值为 $parts 数组的最后一个值
$url 的值为 $baseFile?http_build_query($params) ,其中的 http_build_query() 函数就是一个将参数进行URL编码的一个操作
然后调用 setHeaders 函数,首先解码 $url 参数,然后 header() 函数直接跳转 $url
$_SERVER['PHP'] 存在的问题:
初看这个程序没什么问题,但是PHP自带的 $_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的pathinfo模式,要比普通的格式如http://localhost/index.php?a=2&b=3优雅,格式如:http://localhost/index.php/good/a/2/b/3)
双编码问题:
URL本来是被浏览器编码过一次,服务器接收到来自浏览器URL请求的时候,会将URL解码一次,由于在程序中我们看到有 urldecode() 函数存在,它会再次解码一次URL,此时双编码URL就可以利用,用于绕过某些关键词检测。比如将 / 编码为: %252f
漏洞利用:
比如要跳转到其他网站,那么就可以构造 Payload :/index.php/http:%252f%252fwww.domain.com?redirect=1 ,访问即可重定向跳转到 http://www.domain.com 网址,此错误使攻击者可以将用户从原始站点重定向到攻击者将要重定向的站点。然后攻击者可以进行网络钓鱼
(*)修复
这道题是一个 $_SERVER['PHP_SELF'] 的问题,遇上伪静态规则配合下,就会导致各种由此形成的漏洞。推荐使用 $_SERVER['SCRIPT_NAME'] 修复漏洞。
区别是:
例如:http://www.5idev.com/php/index.php/test/foo?username=hbolive
$_SERVER['PHP_SELF'] 得到:/php/index.php/test/foo
$_SERVER['SCRIPT_NAME'] 得到:/php/index.php
$_SERVER['REQUEST_URI'] 得到:/php/index.php/test/foo?username=hbolive
从该例子可以看出:
$_SERVER['PHP_SELF'] 则反映的是 PHP 程序本身
$_SERVER['SCRIPT_NAME'] 反映的是程序文件本身(这在页面需要指向自己时非常有用)
$_SERVER['REQUEST_URI'] 则反映了完整 URL 地址(不包括主机名)
参考:
https://blog.csdn.net/resilient/article/details/55505831
Day 16 - Poem(深入理解$_REQUESTS数组)
源码是这样的
1 class FTP { 2 public $sock; 3 4 public function __construct($host, $port, $user, $pass) { 5 $this->sock = fsockopen($host, $port); 6 7 $this->login($user, $pass); 8 $this->cleanInput(); 9 $this->mode($_REQUEST['mode']); 10 $this->send($_FILES['file']); 11 } 12 13 private function cleanInput() { 14 $_GET = array_map('intval', $_GET); 15 $_POST = array_map('intval', $_POST); 16 $_COOKIE = array_map('intval', $_COOKIE); 17 } 18 19 public function login($username, $password) { 20 fwrite($this->sock, "USER " . $username . "\n"); 21 fwrite($this->sock, "PASS " . $password . "\n"); 22 } 23 24 public function mode($mode) { 25 if ($mode == 1 || $mode == 2 || $mode == 3) { 26 fputs($this->sock, "MODE $mode\n"); 27 } 28 } 29 30 public function send($data) { 31 fputs($this->sock, $data); 32 } 33 } 34 35 new FTP('localhost', 21, 'user', 'password');
第7行代码,可以发现程序使用 cleanInput 方法过滤 GET 、POST 、COOKIE 数据,将他们强制转成整型数据。然而在第8行处,却传入了一个从 REQUEST 方式获取的 mode 变量。超全局数组 $_REQUEST 中的数据,是 $_GET 、$_POST 、$_COOKIE 的合集,而且数据是复制过去的,并不是引用。REQUEST 数据丝毫不受过滤函数的影响。
代码第21行,这里用了 == 弱比较(== 运算符只会判断两边数据的值是否相等,并不会判断数据的类型。而语言定义,除了 0、false、null 以外均为 true ,所以使用 true 和数字进行比较,返回的值肯定是 true)
payload:?mode=1%0a%0dDELETE%20test.file ,可以达到删除FTP服务器文件的效果(%0a换行%0d回车)
(*)修复
不以 REQUEST 方式获取变量,变量经过滤;== 换成 ===
参考: