CVE-2023-23752
简介:
Joomla是一个开源免费的内容管理系统(CMS),基于PHP开发。在其4.0.0版本到4.2.7版本中,存在一处属性覆盖漏洞,导致攻击者可以通过恶意请求绕过权限检查,访问任意Rest API。
影响版本:
Joomla CMS 4.0.0 ~ 4.2.7
靶场复现:
1. 春秋云镜
https://yunjing.ichunqiu.com/cve/detail/1161?type=1&pay=2
2.phpstudy+phpstorm
POC:
攻击者可以通过在访问Rest API时传入参数public=true
来绕过权限校验。
/api/index.php/v1/users?public=true
代码审计:
1.源码只有三个路由路口即:
/index.php
/api/index.php
/administartor/index.php
在 /api/index.php发现了JSON,我们知道JSON 已经快速发展成为REST API选择的格式,所以我们从这个路由开始入手
2.我们在/api/index.php会来到/includes/app.php(源码在上边自行提取)
我们进入 /app/includes/app.php 发现execute()方法执行应用程序的主要逻辑,处理API请求并生成响应
3.跟进execute方法 存在于CMSApplication.php
public function execute()
{
try {
$this->sanityCheckSystemVariables();
$this->setupLogging();
$this->createExtensionNamespaceMap();
// Perform application routines.
$this->doExecute();
// If we have an application document object, render it.
if ($this->document instanceof \Joomla\CMS\Document\Document) {
// Render the application output.
$this->render();
}
// If gzip compression is enabled in configuration and the server is compliant, compress the output.
if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') {
$this->compress();
// Trigger the onAfterCompress event.
$this->triggerEvent('onAfterCompress');
}
} catch (\Throwable $throwable) {
/** @var ErrorEvent $event */
$event = AbstractEvent::create(
'onError',
[
'subject' => $throwable,
'eventClass' => ErrorEvent::class,
'application' => $this,
]
);
// Trigger the onError event.
$this->triggerEvent('onError', $event);
ExceptionHandler::handleException($event->getError());
}
// Trigger the onBeforeRespond event.
$this->getDispatcher()->dispatch('onBeforeRespond');
// Send the application response.
$this->respond();
// Trigger the onAfterRespond event.
$this->getDispatcher()->dispatch('onAfterRespond');
}
这里主要关注doExecute()方法 因为该方法执行应用程序例行程序 我们跟进该方法
路径:
protected function doExecute()
{
// Initialise the application
$this->initialiseApp();
// Mark afterInitialise in the profiler.
JDEBUG ? $this->profiler->mark('afterInitialise') : null;
// Route the application
$this->route();
// Mark afterApiRoute in the profiler.
JDEBUG ? $this->profiler->mark('afterApiRoute') : null;
// Dispatch the application
$this->dispatch();
// Mark afterDispatch in the profiler.
JDEBUG ? $this->profiler->mark('afterDispatch') : null;
}
逻辑很简单 先初始化应用程序 ,然后调用了一个名为 route()
的方法来处理应用程序的路由逻辑,确定要执行的控制器和方法,然后调用了一个名为 dispatch()
的方法来执行路由后的应用程序逻辑,跟进route和dispatch函数
4. route方法
protected function route()
{
$router = $this->getContainer()->get(ApiRouter::class);
// Trigger the onBeforeApiRoute event.
PluginHelper::importPlugin('webservices');
$this->triggerEvent('onBeforeApiRoute', [&$router, $this]);
$caught404 = false;
$method = $this->input->getMethod();
try {
$this->handlePreflight($method, $router);
$route = $router->parseApiRoute($method);
} catch (RouteNotFoundException $e) {
$caught404 = true;
}
/**
* Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't
* tell us otherwise it uses the plain JSON API
*/
$priorities = ['application/vnd.api+json'];
if (!$caught404 && \array_key_exists('format', $route['vars'])) {
$priorities = $route['vars']['format'];
}
$negotiator = new Negotiator();
try {
$mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities);
} catch (InvalidArgument $e) {
$mediaType = null;
}
// If we can't find a match bail with a 406 - Not Acceptable
if ($mediaType === null) {
throw new Exception\NotAcceptable('Could not match accept header', 406);
}
/** @var $mediaType Accept */
$format = $mediaType->getValue();
if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) {
$format = $this->formatMapper[$mediaType->getValue()];
}
$this->input->set('format', $format);
if ($caught404) {
throw $e;
}
$this->input->set('controller', $route['controller']);
$this->input->set('task', $route['task']);
foreach ($route['vars'] as $key => $value) {
// We inject the format directly above based on the negotiated format. We do not want the array of possible
// formats provided by the plugin!
if ($key === 'format') {
continue;
}
// We inject the component key into the option parameter in global input for b/c with the other applications
if ($key === 'component') {
$this->input->set('option', $route['vars'][$key]);
continue;
}
if ($this->input->getMethod() === 'POST') {
$this->input->post->set($key, $value);
} else {
$this->input->set($key, $value);
}
}
$this->triggerEvent('onAfterApiRoute', [$this]);
if (!isset($route['vars']['public']) || $route['vars']['public'] === false) {
if (!$this->login(['username' => ''], ['silent' => true, 'action' => 'core.login.api'])) {
throw new AuthenticationFailed();
}
}
}
看下面这一行,调用 parseApiRoute
对路由进行处理
$route = $router->parseApiRoute($method);
继续看下面的if 这行代码的作用是在路由中检查是否存在 public
变量和vars, 如果public为flase 则进行登录验证。如果登录验证失败,则抛出 AuthenticationFailed
异常
我们debug可以发现 public参数控制着API是否对外开放,默认情况下public是false的,不对外开放
我们先跟进 parseApiRoute方法 看看它怎么进行处理的
$routePath = $this->getRoutePath(); //获取路由信息
$query = Uri::getInstance()->getQuery(true);//获取用户输入的url参数
继续观察下面
所以我们可以通过路径传入的值在之后可以覆盖替换原本的值 令?public=ture,此时route.var中的变量会被请求的变量覆盖。由于public=true,所以接口不需要身份验证,直接到达路由分发,也就可以访问内部资源了 所以得到flag
其他利用 :
v1/banners
v1/banners/:id
v1/banners
v1/banners/:id
v1/banners/:id
v1/banners/clients
v1/banners/clients/:id
v1/banners/clients
v1/banners/clients/:id
v1/banners/clients/:id
v1/banners/categories
v1/banners/categories/:id
v1/banners/categories
v1/banners/categories/:id
v1/banners/categories/:id
v1/banners/:id/contenthistory
v1/banners/:id/contenthistory/keep
v1/banners/:id/contenthistory
v1/config/application
v1/config/application
v1/config/:component_name
v1/config/:component_name
v1/contacts/form/:id
v1/contacts
v1/contacts/:id
v1/contacts
v1/contacts/:id
v1/contacts/:id
v1/contacts/categories
v1/contacts/categories/:id
v1/contacts/categories
v1/contacts/categories/:id
v1/contacts/categories/:id
v1/fields/contacts/contact
v1/fields/contacts/contact/:id
v1/fields/contacts/contact
v1/fields/contacts/contact/:id
v1/fields/contacts/contact/:id
v1/fields/contacts/mail
v1/fields/contacts/mail/:id
v1/fields/contacts/mail
v1/fields/contacts/mail/:id
v1/fields/contacts/mail/:id
v1/fields/contacts/categories
v1/fields/contacts/categories/:id
v1/fields/contacts/categories
v1/fields/contacts/categories/:id
v1/fields/contacts/categories/:id
v1/fields/groups/contacts/contact
v1/fields/groups/contacts/contact/:id
v1/fields/groups/contacts/contact
v1/fields/groups/contacts/contact/:id
v1/fields/groups/contacts/contact/:id
v1/fields/groups/contacts/mail
v1/fields/groups/contacts/mail/:id
v1/fields/groups/contacts/mail
v1/fields/groups/contacts/mail/:id
v1/fields/groups/contacts/mail/:id
v1/fields/groups/contacts/categories
v1/fields/groups/contacts/categories/:id
v1/fields/groups/contacts/categories
v1/fields/groups/contacts/categories/:id
v1/fields/groups/contacts/categories/:id
v1/contacts/:id/contenthistory
v1/contacts/:id/contenthistory/keep
v1/contacts/:id/contenthistory
v1/content/articles
v1/content/articles/:id
v1/content/articles
v1/content/articles/:id
v1/content/articles/:id
v1/content/categories
v1/content/categories/:id
v1/content/categories
v1/content/categories/:id
v1/content/categories/:id
v1/fields/content/articles
v1/fields/content/articles/:id
v1/fields/content/articles
v1/fields/content/articles/:id
v1/fields/content/articles/:id
v1/fields/content/categories
v1/fields/content/categories/:id
v1/fields/content/categories
v1/fields/content/categories/:id
v1/fields/content/categories/:id
v1/fields/groups/content/articles
v1/fields/groups/content/articles/:id
v1/fields/groups/content/articles
v1/fields/groups/content/articles/:id
v1/fields/groups/content/articles/:id
v1/fields/groups/content/categories
v1/fields/groups/content/categories/:id
v1/fields/groups/content/categories
v1/fields/groups/content/categories/:id
v1/fields/groups/content/categories/:id
v1/content/articles/:id/contenthistory
v1/content/articles/:id/contenthistory/keep
v1/content/articles/:id/contenthistory
v1/extensions
v1/languages/content
v1/languages/content/:id
v1/languages/content
v1/languages/content/:id
v1/languages/content/:id
v1/languages/overrides/search
v1/languages/overrides/search/cache/refresh
v1/languages/overrides/site/zh-CN
v1/languages/overrides/site/zh-CN/:id
v1/languages/overrides/site/zh-CN
v1/languages/overrides/site/zh-CN/:id
v1/languages/overrides/site/zh-CN/:id
v1/languages/overrides/administrator/zh-CN
v1/languages/overrides/administrator/zh-CN/:id
v1/languages/overrides/administrator/zh-CN
v1/languages/overrides/administrator/zh-CN/:id
v1/languages/overrides/administrator/zh-CN/:id
v1/languages/overrides/site/en-GB
v1/languages/overrides/site/en-GB/:id
v1/languages/overrides/site/en-GB
v1/languages/overrides/site/en-GB/:id
v1/languages/overrides/site/en-GB/:id
v1/languages/overrides/administrator/en-GB
v1/languages/overrides/administrator/en-GB/:id
v1/languages/overrides/administrator/en-GB
v1/languages/overrides/administrator/en-GB/:id
v1/languages/overrides/administrator/en-GB/:id
v1/languages
v1/languages
v1/media/adapters
v1/media/adapters/:id
v1/media/files
v1/media/files/:path/
v1/media/files/:path
v1/media/files
v1/media/files/:path
v1/media/files/:path
v1/menus/site
v1/menus/site/:id
v1/menus/site
v1/menus/site/:id
v1/menus/site/:id
v1/menus/administrator
v1/menus/administrator/:id
v1/menus/administrator
v1/menus/administrator/:id
v1/menus/administrator/:id
v1/menus/site/items
v1/menus/site/items/:id
v1/menus/site/items
v1/menus/site/items/:id
v1/menus/site/items/:id
v1/menus/administrator/items
v1/menus/administrator/items/:id
v1/menus/administrator/items
v1/menus/administrator/items/:id
v1/menus/administrator/items/:id
v1/menus/site/items/types
v1/menus/administrator/items/types
v1/messages
v1/messages/:id
v1/messages
v1/messages/:id
v1/messages/:id
v1/modules/types/site
v1/modules/types/administrator
v1/modules/site
v1/modules/site/:id
v1/modules/site
v1/modules/site/:id
v1/modules/site/:id
v1/modules/administrator
v1/modules/administrator/:id
v1/modules/administrator
v1/modules/administrator/:id
v1/modules/administrator/:id
v1/newsfeeds/feeds
v1/newsfeeds/feeds/:id
v1/newsfeeds/feeds
v1/newsfeeds/feeds/:id
v1/newsfeeds/feeds/:id
v1/newsfeeds/categories
v1/newsfeeds/categories/:id
v1/newsfeeds/categories
v1/newsfeeds/categories/:id
v1/newsfeeds/categories/:id
v1/plugins
v1/plugins/:id
v1/plugins/:id
v1/privacy/requests
v1/privacy/requests/:id
v1/privacy/requests/export/:id
v1/privacy/requests
v1/privacy/consents
v1/privacy/consents/:id
v1/privacy/consents/:id
v1/redirects
v1/redirects/:id
v1/redirects
v1/redirects/:id
v1/redirects/:id
v1/tags
v1/tags/:id
v1/tags
v1/tags/:id
v1/tags/:id
v1/templates/styles/site
v1/templates/styles/site/:id
v1/templates/styles/site
v1/templates/styles/site/:id
v1/templates/styles/site/:id
v1/templates/styles/administrator
v1/templates/styles/administrator/:id
v1/templates/styles/administrator
v1/templates/styles/administrator/:id
v1/templates/styles/administrator/:id
v1/users
v1/users/:id
v1/users
v1/users/:id
v1/users/:id
v1/fields/users
v1/fields/users/:id
v1/fields/users
v1/fields/users/:id
v1/fields/users/:id
v1/fields/groups/users
v1/fields/groups/users/:id
v1/fields/groups/users
v1/fields/groups/users/:id
v1/fields/groups/users/:id
v1/users/groups
v1/users/groups/:id
v1/users/groups
v1/users/groups/:id
v1/users/groups/:id
v1/users/levels
v1/users/levels/:id
v1/users/levels
v1/users/levels/:id
v1/users/levels/:id
修复:
1.官方:Comparing 4.2.7...4.2.8 · joomla/joomla-cms · GitHub
2.升级 Joomla为高版本
总结:
该漏洞是由于route方法 中var的变量会被url请求的变量覆盖而覆盖,即当public为ture时 ,攻击者可以任意访问内部资源
参考:
https://xz.aliyun.com/t/12175?time__1311=mqmhD5DK7IejhDBdPx2DUEFNHyDWucHb%2BD