PHP SECURITY CALENDAR 2017 学习总结-更新中

     这篇文章主要以审计代码为主来分析每道题目中所存在的漏洞点,记录一下自己的学习:

1.Day 1 - Wish List

class Challenge {
    const UPLOAD_DIRECTORY = './solutions/';
    private $file;
    private $whitelist;

    public function __construct($file) {
        $this->file = $file;
        $this->whitelist = range(1, 24);
    }

    public function __destruct() {
        if (in_array($this->file['name'], $this->whitelist)) {
            move_uploaded_file(
                $this->file['tmp'],
                self::UPLOAD_DIRECTORY . $this->file['name']
            );
        }
    }
}

$challenge = new Challenge($_FILES['solution']);

这道题主要是php的弱类型绕过导致的文件上传漏洞,inarray在比较的时候,比如“5aaaa”==“5”的,因为php会自动将后面的截掉

这部分有道练习题,漏洞点在:

select * from user where id=$id;

即在$id处可以进行报错注入,updatexml()型注入,或者使用extractvalue()型注入。

updatexml函数是从特殊字符、字母后面开始截取的,我们就需要在我们想要的数据前面拼接上特殊字符。

extractvalue() :对XML文档进行查询的函数
语法:extractvalue(目标xml文档,xml路径)

updatexml()函数与extractvalue()类似,是更新xml文档的函数。
语法updatexml(目标xml文档,xml路径,更新的内容)

我们要使它报错的点在xml路径处,默认/xxx/xx是正确的路径,常见payload如下:

select username from security.user where id=1 and (extractvalue(‘anything’,concat(‘~’,(select database()))))
select username from security.user where id=1 and (updatexml(‘anything’,concat(‘~’,(select database())),’anything’))

其中concat为连接字符串的函数,页面会为我们返回错误,其中错误的信息中就包含了我们想要查询的敏感数据。

如果concat用不了,可以考虑其他的字符串连接函数,比如make_set()函数,常见payload如下:

select updatexml(1,make_set(3,'~',(select user())),1);  //3对应011,那么就会将第一位与第二位拼接

我们还可以找到类似的函数:lpad()、reverse()、repeat()、export_set()(lpad()、reverse()、repeat()这三个函数使用的前提是所查询的值中,必须至少含有一个特殊字符,否则会漏掉一些数据)。

有一点需要注意,extractvalue()能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用substring()函数截取,一次查看32位

这里查询前5位示意:

select username from security.user where id=1 and (extractvalue(‘anything’,concat(‘#’,substring(hex((select database())),1,5))))

https://www.jb51.net/article/87120.htm  这个连接是mysql的字符串函数参考

2.Day 2 - Twig

// composer require "twig/twig"
require 'vendor/autoload.php';

class Template {
    private $twig;

    public function __construct() {
        $indexTemplate = '<img ' .
            'src="https://loremflickr.com/320/240">' .
            '<a href="{{link|escape}}">Next slide »</a>';

        // Default twig setup, simulate loading
        // index.html file from disk
        $loader = new Twig\Loader\ArrayLoader([
            'index.html' => $indexTemplate
        ]);
        $this->twig = new Twig\Environment($loader);
    }

    public function getNexSlideUrl() {
        $nextSlide = $_GET['nextSlide'];
        return filter_var($nextSlide, FILTER_VALIDATE_URL);
    }

    public function render() {
        echo $this->twig->render(
            'index.html',
            ['link' => $this->getNexSlideUrl()]
        );
    }
}

(new Template())->render();

twig是php的模板语言,就像jinjia2是flask的模板语言,都是起到了渲染的作用,在这段代码中用户可以控制的是$nextSlide变量,然后经过filter_var函数进行一个过滤,验证其是不是正确的url,然后再将过滤后的url经过escape过滤后让{{}}来渲染,那么肯定要了解这两个过滤函数是不是能够绕过?看起来它的确只是验证是否是以://分割的字符串

接下来看twig的escape函数:

twig的中文官方文档上说:

Internally, ``escape`` uses the PHP native `htmlspecialchars`_ function for the HTML escaping strategy.

escape的过滤策略和htmlspecialchars的过滤规则一样,就是和htmlspecialchars($link, ENT_QUOTES, 'UTF-8')一样,设置了ENT_QUOTES以后,就会将单引号和双引号进行编码。

所以首先利用javascript://comment来绕过filter的过滤,然后因为//是javascript的注释符号,所以真正的payload可以放在下一行,也就是可以使用%0a或者%250a来构造一个\n换行符,

所以完整的payload可以为:

javascript://comment%0aalert(1),就会触发xss漏洞。

PHP7.0.25 绕过filter_var可以使用0://   

file_get_contents()结合data协议进行xss 

相关链接 https://www.anquanke.com/post/id/101058

 day3-snow flake

function __autoload($className) {
    include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
    $controller = new $controllerName($data);
    $controller->render();
} else {
    echo 'There is no page with this name';
}

class HomeController {
    private $data;

    public function __construct($data) {
        $this->data = $data;
    }

    public function render() {
        if ($this->data['new']) {
            echo 'controller rendering new response';
        } else {
            echo 'controller rendering old response';
        }
    }
}

漏洞点在class_exist()函数判断类存不存在的时候会自动调用__autoload函数,如果__autoload函数有读写文件或可以包含文件,那么就有可能产生危险,在php小于5.4中

都有效,php的目前版本是7.3.2。

这道题又让我想到了file_exist()函数检查文件存在时会进行反序列化,还是不能够完全信任用户的数据,需要对数据进行消毒。

Day 4 - False Beard

  

class Login {
    public function __construct($user, $pass) {
        $this->loginViaXml($user, $pass);
    }

    public function loginViaXml($user, $pass) {
        if (
            (!strpos($user, '<') || !strpos($user, '>')) &&
            (!strpos($pass, '<') || !strpos($pass, '>'))
        ) {
            $format = '<xml><user="%s"/><pass="%s"/></xml>';
            $xml = sprintf($format, $user, $pass);
            $xmlElement = new SimpleXMLElement($xml);
            // Perform the actual login.
            $this->login($xmlElement);
        }
    }
}

new Login($_POST['username'], $_POST['password']);

这道题主要是strpos的trick,0==false,那么出现在字符串第一位也会返回为0,那么就能绕过。

Day 5 - Postcard

  

class Mailer {
    private function sanitize($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return '';
        }

        return escapeshellarg($email);
    }

    public function send($data) {
        if (!isset($data['to'])) {
            $data['to'] = 'none@vsplate.com';
        } else {
            $data['to'] = $this->sanitize($data['to']);
        }

        if (!isset($data['from'])) {
            $data['from'] = 'none@vsplate.com';
        } else {
            $data['from'] = $this->sanitize($data['from']);
        }

        if (!isset($data['subject'])) {
            $data['subject'] = 'No Subject';
        }

        if (!isset($data['message'])) {
            $data['message'] = '';
        }

        mail($data['to'], $data['subject'], $data['message'],
             '', "-f" . $data['from']);
    }
}

$mailer = new Mailer();
$mailer->send($_POST)

在这个题目中主要学会了escapeshellcmd和escapeshellarg同时使用时会出发漏洞

escapeshellcmd
escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$\, \x0A 和 \xFF。 ' 和 " 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
escapeshellarg
escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 。

  从上面的两次过滤中可以看到可以成功执行curl,对应的题目漏洞的CVE是https://github.com/opsxcq/exploit-CVE-2016-10033,主要利用了mail函数没有对第五个参数进行严格检查,导致可以附加恶意参数,并且要求php没有安装PCRE,没有开始safe_mode,这样对mail的address就会进行松散的检查。

 

Day 6 - Frost Pattern

class TokenStorage {
    public function performAction($action, $data) {
        switch ($action) {
            case 'create':
                $this->createToken($data);
                break;
            case 'delete':
                $this->clearToken($data);
                break;
            default:
                throw new Exception('Unknown action');
        }
    }

    public function createToken($seed) {
        $token = md5($seed);
        file_put_contents('/tmp/tokens/' . $token, '...data');
    }

    public function clearToken($token) {
        $file = preg_replace("/[^a-z.-_]/", "", $token);
        unlink('/tmp/tokens/' . $file);
    }
}

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);

这道题主要考察对preg_replace函数的正则匹配规则熟悉不熟悉,这里存在unlink函数,其中$file变量是经过正则匹配替换以后的,即$token变量,这段正则匹配的是除a-z和.-_之间的所有字符,那么表达的是一个区间,而我们想防止目录遍历的话这里

需要过滤.(点号),即需要对-进行转义。

Day 7 - Bells

function getUser($id) {
    global $config, $db;
    if (!is_resource($db)) {
        $db = new MySQLi(
            $config['dbhost'],
            $config['dbuser'],
            $config['dbpass'],
            $config['dbname']
        );
    }
    $sql = "SELECT username FROM users WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->bind_param('i', $id);
    $stmt->bind_result($name);
    $stmt->execute();
    $stmt->fetch();
    return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';
parse_str

功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。

定义 :void parse_str( string $encoded_string [, array &$result ] )

如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

 因为parse_str不会检查要注册的变量是否已经存在,所以会直接对变量进行覆盖,这里可以覆盖config变量从而连接任何我们想要连接的数据库及数据表。

Day 8 - Candle

header("Content-Type: text/plain");

function complexStrtolower($regex, $value) {
    return preg_replace(
        '/(' . $regex . ')/ei',
        'strtolower("\\1")',
        $value
    );
}

foreach ($_GET as $regex => $value) {
    echo complexStrtolower($regex, $value) . "\n";
}

这段代码里面我们可以提供正则表达式,和需要进行匹配的字符串,其中需要知道正则中的自匹配,\1就是匹配到的结果,preg_replace中的/e模式会将匹配到的字符串当作命令执行函数的参数进行执行,所以${phpinfo()}可以由正则匹配到,从而进行执行,

\e是在替换完字符串以后再用eval函数检测替换后的字符串。

php中在用${}包裹变量时,其中会把花括号中的字符串首先当作变量名进行解析,在解析的过程中如果该括号包含的是函数,则会首先对函数进行执行,如果是变量则会首先取变量的值,然后结合外面的${}生成新的变量,以值作为键名,也就是php里面的

动态变量。

 

class LanguageManager
{
    public function loadLanguage()
    {
        $lang = $this->getBrowserLanguage();
        $sanitizedLang = $this->sanitizeLanguage($lang);
        require_once("/lang/$sanitizedLang");
    }

    private function getBrowserLanguage()
    {
        $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
        return $lang;
    }

    private function sanitizeLanguage($language)
    {
        return str_replace('../', '', $language);
    }
}

(new LanguageManager())->loadLanguage();

 这里为了防止目录遍历漏洞使用str_replace()函数,但是....//能够绕过,如果是str_replace(array('../','./'),"",$str),那么.....///就能绕过

 

posted @ 2019-02-24 14:41  tr1ple  阅读(902)  评论(0编辑  收藏  举报