Утром 28 июля 2015г. на главной странице биржи fl.ru был опубликован очень интересный проект. Проект опубликован с аккаунта главного администратора биржи!
Поскольку в описание проекта присутствует ненормативная лексика, она на скрине замазана.

Это значит, что случился очередной взлом fl.ru. Напомню, что предыдущая утечка была менее 6 месяцев назад, тогда в сеть попали данные пользователей.
На этот раз в созданном проекте оказался выложен исходный код сайта! 98,6Мб - в архиве, либо 136,6Мб в сыром виде. Любой фрилансер мог скачать исходники. Пользователь под ником veryEvilMan выложил исходный код fl.ru на github. Репозиторий форкнули уже более 250 раз. Репозиторий с исходным кодом я тоже форкнул, можно почитать на досуге. Наверняка, уже кто то изучает исходники в поиске новых дыр. Судя по коду, они там обязаны быть. index.php доставляет.
Администрация быстро смекнула и через 5-8 минут сайт вообще закрылся на ремонтные работы. Пол дня на сайте проводились технические работы:

Какой-либо информации о слитых базах данных не поступало. Возможно, что взломщики, решили не выкладывать столь ценную информацию в открытый доступ. А ограничились лишь исходным php-кодом. В коде, кстати, присутствует информация о конфигах к дазам данных. Так что, есть ненулевая вероятность, что также и слиты некоторые данные фрилансеров и работадателей.
<?php
$g_page_id = "0|1";
// у раздела сделаю свои вопросы в окне помощи
if (isset($_GET['kind']) && 8 == $_GET['kind']) {
$g_help_id = 202;
}
// Формируем JS внизу страницы
define('JS_BOTTOM', true);
// первым делом запоминаем была ли попытка переключиться на антиюзера или сменить антиюзера
// иначе при подключении /classes/stdf.php очистится $_POST
// подробнее тут: #19492
$switch = (isset($_POST['action']) && 'switch' === $_POST['action']);
$change_au = (isset($_POST['action']) && 'change_au' === $_POST['action']);
require_once($_SERVER['DOCUMENT_ROOT'] . "/classes/stdf.php");
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/yii/tinyyii.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/classes/tservices/tservices_catalog.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/classes/tservices/tservices_helper.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/models/TServiceModel.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/models/FreelancerModel.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/widgets/TServiceFilter.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/widgets/TServiceFreelancersCategories.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/widgets/TServiceNavigation.php');
$g_folders = array(0=>1, 1=>1, 2=>3, 3=>2, 4=>4);
$main_page = true;
session_start();
if($_GET['full_site_version'] == 1) {
$show_full_site_version = 1;
setcookie("full_site_version", "1", time()+60*60*24*30, "/");
}
@$action = strip_tags(trim($_GET['action']));
if (!$action) @$action = strip_tags(trim($_POST['action']));
// определяем, был ли сброс массива POST
if (!$action && ($switch || $change_au)) {
$action = "switch_error";
}
switch ($action)
{
case "change_au": // добавляем/изменяем антиюзера.
$response = array();
$location = ($_SESSION['ref_uri'])? HTTP_PFX.$_SERVER["HTTP_HOST"].urldecode($_SESSION['ref_uri']) : HTTP_PFX.$_SERVER["HTTP_HOST"]."/";
$_SESSION['pro_last'] = payed::ProLast($_SESSION['login']);
$_SESSION['pro_last'] = $_SESSION['pro_last']['is_freezed'] ? false : $_SESSION['pro_last']['cnt'];
$_SESSION['anti_pro_last'] = payed::ProLast($_SESSION['anti_login']);
$_SESSION['anti_pro_last'] = $_SESSION['anti_pro_last']['is_freezed'] ? false : $_SESSION['anti_pro_last']['cnt'];
if( !($uid=get_uid()) ) { header("Location: ".$location); exit; }
$post_pwd = stripslashes($_POST['passwd']);
$anti_login = __paramInit('string',NULL,'a_login');
// получаем класс антиюзера. Он всегда противоположен классу юзера.
$anti_class = is_emp() ? 'freelancer' : 'employer';
$anti = new $anti_class();
// запоминаем данные антиюзера.
$anti->GetUser($anti_login, true, true);
$anti_uid = $anti->uid;
$anti_uname = $anti->uname;
$anti_usurname = $anti->usurname;
if( !$anti_uid ) {
echo json_encode(array('success' => false));
exit;
} // т.е. нет юзера с логином $anti_login среди $anti_class.
// сначала изменяем антиюзера у антиюзера (т.е. устанавливаем ему uid текущего юзера в поле anti_uid).
$anti = new $anti_class();
$anti->anti_uid = $uid;
if( !$anti->Update($anti_uid, $res, "AND passwd = '".users::hashPasswd(iconv('UTF-8', 'windows-1251', $post_pwd))."'")
&& $res
&& pg_affected_rows($res) )
{
// устанавливаем антиюзера текущему пользователю.
$user_class = is_emp() ? 'employer' : 'freelancer';
$user = new $user_class();
$user->anti_uid = $anti_uid;
if(!$user->Update($uid, $res) && $res && pg_affected_rows($res)) {
$_SESSION['anti_uid'] = $anti_uid;
$_SESSION['anti_login'] = $anti_login;
$_SESSION['anti_name'] = $anti_uname;
$_SESSION['anti_surname'] = $anti_usurname;
if($user->is_verify=='t') {
$anti->is_verify = 't';
$anti->Update($anti_uid, $res);
}
}
$action = "switch";
$response['success'] = true;
} else {
echo json_encode(array('success' => false));
exit;
}
unset($anti, $user, $post_pwd);
case "switch": // переключаемся на антилогин.
$adCatalog = $_SESSION['toppayed_catalog'];
$adMain = $_SESSION['toppayed_main'];
$adHead = $_SESSION['toppayed_head'];
$adText = $_SESSION['toppayed_text'];
$uid = get_uid(0);
$anti_uid = $_SESSION['anti_uid'];
// переключаться может только зарегистрированый пользователь и нельзя переключаться на самого себя
if (!$uid || !$anti_uid || $uid == $anti_uid) {
$response['success'] = true;
exit(json_encode($response));
}
case "login": // логинимся.
$_redirect = __paramInit('link', NULL, 'redirect');
$guest_query = __paramInit('string', null, 'guest_query');
if($_redirect) $_SESSION['ref_uri'] = trim($_redirect);
$ref_uri = urldecode($_SESSION['ref_uri']);
if(isset($_COOKIE['global_anchor']) && $_COOKIE['pathname_anchor'] == $ref_uri) {
$anchor = $_COOKIE['global_anchor'];
}
$autologin = __paramInit('bool', NULL, 'autologin');
$is_ajax = ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
if($action=='switch') {
if( !($uid=get_uid()) || !$_SESSION['anti_uid'] ) break;
$s_login = $_SESSION['anti_login'];
$location = str_replace("/users/{$_SESSION['login']}/setup/", "/users/{$s_login}/setup/", $location);
$user_class = is_emp() ? 'freelancer' : 'employer';
$user = new $user_class();
$pwd = $user->GetField($_SESSION['anti_uid'], $error, "passwd");
logout();
}
else {
$s_login = strip_tags(trim($_POST['login']));
$pwd = users::hashPasswd(trim(stripslashes($_POST['passwd'])));
}
//Если пусто то даже непробуем авторизоваться
$is_log = 0;
if (!empty($s_login) && !empty($pwd)) {
$is_log = login($s_login, $pwd, $autologin);
}
unset($pwd);
if($_redirect) $_SESSION['ref_uri'] = trim($_redirect);
$default_location = is_emp() ? '/' : '/projects/';
if (!$ref_uri || $ref_uri == '/') {
$ref_uri = $default_location;
}
$location = HTTP_PFX . $_SERVER['HTTP_HOST'] . $ref_uri . $anchor;
// #0012501
$location = preg_replace("/\/router\.php\?pg=/", "", $location);
// #0011589
if(strpos($location, '/remind/')
|| strpos($location, 'inactive.php')
|| strpos($location, 'checkpass.php')
|| strpos($location, '/registration/')
|| strpos($location, 'fbd.php'))
$location = $default_location;
if(!$is_ajax) {
if ($is_log > 0){
session_write_close();
} elseif ($is_log == -1) {
$_SESSION['rand'] = csrf_token();
$location = "/banned.php?login={$s_login}&rnd={$_SESSION['rand']}";
} elseif ($is_log == -2) {
$location = '/inactive.php';
} elseif ($is_log == -3) {
$location = '/denyip.php?login='.$_POST['login'];
} elseif ($is_log == users::AUTH_STATUS_2FA) {
//Редирект на 2ой атап авторизации
$location = '/auth/second/';
} else {
$location = '/remind/?incorrect_login=1';
}
// ##0025730 - Автоматический редирект на создание проекта, если незарег. пользователь нажал кнопку "Опубликовать проект"
$_user_action = (isset($_REQUEST['user_action']) && $_REQUEST['user_action'])?substr(htmlspecialchars($_REQUEST['user_action']), 0, 25):'';
switch($_user_action) {
case 'tu':
//@todo: возможно код не используется $redirect_to - неиспользуется
$_redirect = trim($_redirect);
if($_redirect) {
$redirect_to = HTTP_PFX.$_SERVER["HTTP_HOST"].urldecode($_redirect);
$_SESSION['ref_uri2'] = NULL;
}
break;
case 'new_tu':
if($is_log > 0) {
$location = '/users/'.$s_login.'/tu/new/';
}
break;
case 'promo_verification':
$location = '/promo/verification';
break;
case 'buypro':
if($is_log > 0) {
if(is_emp()) {
$location = '/payed-emp/';
} else {
$location = '/payed/';
}
}
break;
case 'masssending':
if($is_log>0) { $location = '/masssending/'; }
break;
}
if ((is_emp() || $is_log == users::AUTH_STATUS_2FA) && $guest_query) {
require_once($_SERVER['DOCUMENT_ROOT'] . '/guest/models/GuestHelper.php');
require_once($_SERVER['DOCUMENT_ROOT'] . '/guest/models/GuestMemoryModel.php');
$dataForm = GuestHelper::overrideDataFromString($guest_query);
if (isset($dataForm['kind']) && is_numeric($dataForm['kind'])) {
$guestMemoryModel = new GuestMemoryModel();
$hash = $guestMemoryModel->saveData($dataForm);
$_location = '/public/?step=1&kind=' . $dataForm['kind'] . '&hash=' . $hash;
if ($is_log == users::AUTH_STATUS_2FA) {
$_SESSION['ref_uri'] = $_location;
} else {
$location = $_location;
}
}
}
header("Location: {$location}");
exit;
} else {
$_SESSION['toppayed_catalog'] = $adCatalog;
$_SESSION['toppayed_main'] = $adMain;
$_SESSION['toppayed_head'] = $adHead;
$_SESSION['toppayed_text'] = $adText;
if ($is_log > 0){
session_write_close();
$response['redir'] = $location;
} elseif ($is_log == -1) {
$response['success'] = false;
$_SESSION['rand'] = csrf_token();
$response['redir'] = "/banned.php?login={$s_login}&rnd={$_SESSION['rand']}";
} elseif ($is_log == -2) {
$response['success'] = false;
$response['redir'] = HTTP_PFX . $_SERVER["HTTP_HOST"] . '/inactive.php';
} elseif ($is_log == -3) {
$response['success'] = false;
$response['redir'] = HTTP_PFX . $_SERVER["HTTP_HOST"] . '/denyip.php?login='.$s_login;
} elseif ($is_log == users::AUTH_STATUS_2FA) {
//Редирект на 2ой атап авторизации
$response['success'] = false;
$response['redir'] = HTTP_PFX . $_SERVER['HTTP_HOST'] . '/auth/second/';
} else {
$response['success'] = false;
$response['redir'] = HTTP_PFX . $_SERVER["HTTP_HOST"] . '/remind/?incorrect_login=1';
}
exit(json_encode($response));
//exit;
}
break;
case "switch_error":
$response['success'] = true;
exit(json_encode($response));
break;
case "postproject":
include ("user/employer/setup/newproj.php");
break;
case "prj_close":
if ($_GET["prid"]) {
require_once($_SERVER['DOCUMENT_ROOT'] . "/classes/projects.php");
$portf = new projects();
if (intval($_GET["prid"])) {
if (!$portf->CheckBlocked(intval($_GET['prid'])) || hasPermissions('projects')) {
$error .= $portf->SwitchStatusPrj(get_uid(), intval($_GET["prid"]));
header("Location: /");
exit;
}
}
}
break;
case "warn":
if (hasPermissions('projects')) {
require_once(ABS_PATH . "/classes/messages.php");
require_once(ABS_PATH . "/classes/users.php");
require_once(ABS_PATH . "/classes/projects.php");
$usr=new users();
$usr->Warn($_GET["ulogin"]);
$threadid = intval(trim($_GET['threadid']));
$uid = get_uid();
//messages::SendWarn($_GET["ulogin"],$_GET['blogid'],$_GET['threadid']); - это тут не работает!
$tprj=new projects();
$tprj->DeletePublicProject(intval($_GET["prid"]) , get_uid() , hasPermissions('projects'));
}
break;
case "post_offers_filter":
$offers_filter = new offers_filter();
$f_category = $_POST['pf_categofy'];
if((int)$_POST['comboe_column_id'] === 1 && $_POST['comboe_db_id'] > 0 ) {
$f_category[1][$_POST['comboe_db_id']] = 1;
}
if((int)$_POST['comboe_column_id'] === 0 && $_POST['comboe_db_id'] > 0 ) {
$f_category[0][$_POST['comboe_db_id']] = 0;
}
if($_POST['pf_category'] && !$_POST['pf_subcategory']) {
$f_category[0][$_POST['pf_category']] = 0;
}
if($_POST['pf_subcategory']) {
$f_category[1][$_POST['pf_subcategory']] = 1;
}
$f_only_my_offs = $_POST['pf_only_my_offs'] ? true : false;
$offers_filter->Save(get_uid(), $f_category, $f_only_my_offs);
break;
case "delete_offers_filter":
$offers_filter = new offers_filter();
$offers_filter->DeleteFilter(get_uid());
break;
case "activate_offers_filter":
$offers_filter = new offers_filter();
$offers_filter->ActivateFilter(get_uid());
break;
case "delete_offers":
if(!hasPermissions('projects')) break;
$fid = intval($_GET['fid']);
$frl_offers->Delete($fid);
$page_uri = $_GET['page']>1?"&page=".$_GET['page']:"";
header("Location: /projects/?kind=8{$page_uri}");
break;
case "unblock_offers":
if(!hasPermissions('projects')) break;
$update = array("is_blocked" => 'f');
case "block_offers":
if(!hasPermissions('projects')) break;
$fid = intval($_GET['fid']);
if(!$update) $update = array("is_blocked" => 't');
$frl_offers->Update($fid, $update);
$page_uri = $_GET['page']>1?"&page=".$_GET['page']:"";
header("Location: /projects/?kind=8{$page_uri}#offers".$fid);
break;
}
// Для авторизованных пользователей не показываем лендинг, делаем редирект на нужный раздел
/*if (is_emp()) {
header('Location: /tu/');
exit();
}
else if (get_uid(false)) {
header('Location: /projects/');
exit();
}*/
$rpath="../";
// Дополнительные стили
//$css_file[] = "nav.css";
$js_file[] = "tservices/tservices_catalog.js";
//$js_file[] = "landings/livetex.js";
$landing_page = true;
// Дополнительный стиль для отображения фона страницы
$body_additional_class = 'landing-fon';
// Прячем карусель вверху страницы
$hide_carouser = true;
// Прячем блок с сообщениями
$hide_notification_bar = true;
$header = "../header.php";
$footer = "../footer.html";
/**
* Типовые услуги
**/
require_once($_SERVER['DOCUMENT_ROOT'] . '/classes/tservices/tservices_binds.php');
$page = 1;
// Количество типовых услуг на главной странице
$limit = 12;
$tserviceModel = TServiceModel::model();
$freelancerModel = FreelancerModel::model();
$tservicesCatalogModel = new tservices_catalog();
$tservicesCatalogModel->setPage($limit, $page);
//Сначала берем закрепленные
$tservices_binded = $tservicesCatalogModel->getBindedList(tservices_binds::KIND_LANDING);
$binded_ids = array();
if (count($tservices_binded)) {
foreach ($tservices_binded as $tservice) {
$binded_ids[] = $tservice['id'];
}
// расширение сведений о типовых услугах
$tserviceModel
->extend($tservices_binded, 'id')
->readVideos($tservices_binded, 'videos', 'videos'); // во всех строках "распаковать" массив видео-клипов
// расширение сведений о пользователях
$freelancerModel->extend($tservices_binded, 'user_id', 'user');
}
$popups = array();
$tservices_search = array();
if (count($tservices_binded) < $limit) { //Есть места для отображения незакрепленных услуг
// поиск записей
$tservicesCatalogModel->setPage($limit, $page);
$list = $tservicesCatalogModel->cache(300)->getList();
$tservices_search = $list['list'];
$total = $list['total'];
// расширение сведений о типовых услугах
$tserviceModel
->extend($tservices_search, 'id')
->readVideos($tservices_search, 'videos', 'videos'); // во всех строках "распаковать" массив видео-клипов
// расширение сведений о пользователях
$freelancerModel->extend($tservices_search, 'user_id', 'user');
}
$tservices = $tservices_binded;
foreach ($tservices_search as $tservice) {
if (!in_array($tservice['id'], $binded_ids) && count($tservices) < $limit) {
$tservices[] = $tservice;
}
}
$uid = get_uid(false);
if ($uid && !is_emp()) {
require_once($_SERVER['DOCUMENT_ROOT'] . "/xajax/quick_payment.common.php");
$use_ajax = true;
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/widgets/TServiceBindTeaser.php');
$tserviceBindTeaser = new TServiceBindTeaser();
$tserviceBindTeaser->init(array(
'kind' => tservices_binds::KIND_LANDING,
'uid' => $uid
));
require_once($_SERVER['DOCUMENT_ROOT'] . '/tu/widgets/TServiceBindTeaserShort.php');
$tServiceBindTeaserShort = new TServiceBindTeaserShort();
$isExistsBindUp = false;
//Добавляем попапы продления и поднятия к услугам текущего юзера
foreach ($tservices as $key=>$tservice) {
$is_owner = $tservice['user_id'] == $uid;
if ($is_owner) {
require_once($_SERVER['DOCUMENT_ROOT'] . '/classes/quick_payment/quickPaymentPopupTservicebind.php');
if (quickPaymentPopupTservicebind::getInstance()->inited == false) {
quickPaymentPopupTservicebind::getInstance()->init(array(
'uid' => $uid,
'kind' => tservices_binds::KIND_LANDING
));
}
$popup_id = quickPaymentPopupTservicebind::getInstance()->getPopupId($tservice['id']);
$popups[] = quickPaymentPopupTservicebind::getInstance()->render(array(
'is_prolong' => true,
'date_stop' => $tservice['date_stop'],
'popup_id' => $popup_id,
'tservices_cur' => $tservice['id'],
'tservices_cur_text' => $tservice['title']
));
if ($key > 0) {
$isExistsBindUp = true;
require_once($_SERVER['DOCUMENT_ROOT'] . '/classes/quick_payment/quickPaymentPopupTservicebindup.php');
if (quickPaymentPopupTservicebindup::getInstance()->inited == false) {
quickPaymentPopupTservicebindup::getInstance()->init(array(
'uid' => $uid,
'tservices_id' => $tservice['id'],
'tservices_title' => $tservice['title'],
'kind' => tservices_binds::KIND_LANDING
));
}
$popup_id = quickPaymentPopupTservicebindup::getInstance()->getPopupId($tservice['id']);
$popups[] = quickPaymentPopupTservicebindup::getInstance()->render(array(
'popup_id' => $popup_id,
'tservices_cur' => $tservice['id'],
'tservices_cur_text' => $tservice['title']
));
}
}
}
if ($isExistsBindUp) {
$tservicesBinds = new tservices_binds(tservices_binds::KIND_LANDING);
$bindUpPrice = $tservicesBinds->getPrice(true, $uid);
}
}
$suffix = $uid <= 0? '_anon' : (is_emp()? '_emp' : '_frl');
$content_landing_image = $_SERVER['DOCUMENT_ROOT']."/templates/landings/tpl.landing_image{$suffix}.php";
$content = $_SERVER['DOCUMENT_ROOT']."/templates/landings/tpl.landing_tservices.php";
// Список профессий
$prfs = new professions();
$profs = $prfs->GetAllProfessions("",0, 1);
// Сортировка категорий профессий по названию
//usort($profs, function($a, $b) { return strcmp($a['groupname'], $b['groupname']);});
$page_title = 'Фриланс сайт удаленной работы №1. Фрилансеры, работа на дому, freelance : FL.ru';
// отрисовка страницы
include ($_SERVER['DOCUMENT_ROOT']."/template3.php");
UPD:
В 15:30 28 июля 2015г. сайт fl.ru снова заработал. Был в даунтайме 4 часа.
Полезные ссылки:
UPD 2:
Выложены nginx-конфиги тестовых серверов
fl.ru правит уязвимости (fixed):
- memcache был доступен из-вне: один из серверов memcached, на котором хранились сессии пользователей, был доступен из-вне. В результате чего можно было получать чужие сесии и вообще проводить любые операции с сессиями. Вплоть до того, что наделить себя возможностями админа.
Наверное кому то очень не понравилась такая политика fl.ru.
Зацените значение константы WM_VERIFY_KEYPASS )
Вообще, если это их настоящий исходный код, то я разачарован.