# L3HCTF

来自 L3HCTF Writeup - 星盟安全团队

# gateway_advance

这道题用 lua (一个脚本语言) 配置了 ng

worker_processes 1;
events {
    use epoll;
    worker_connections 10240;
}
http {
    include mime.types;
    default_type text/html;
    access_log off;
    error_log /dev/null;
    sendfile on;
    init_by_lua_block {
        f = io.open("/flag", "r")
        f2 = io.open("/password", "r")
        flag = f:read("*all")
        password = f2:read("*all")
        f:close()
#此处只有 f:close (),但是没有 f2:close (),这意味着 password 可以通过 /proc/self/fd/<num > 读取(用于当前进程正在打开文件资源,通过符号链接)
        password = string.gsub(password, "[\n\r]", "")
        os.remove("/flag")
        os.remove("/password")
    }
 
    server {
        listen 80 default_server;
        location / {
            content_by_lua_block {
                ngx.say("hello, world!")
            }
        }
        location /static {
            alias /www/;
            access_by_lua_block {
                if ngx.var.remote_addr ~= "127.0.0.1" then
                    ngx.exit(403)
                end
            }
            add_header Accept-Ranges bytes;
        }
        location /download {
            access_by_lua_block {
                local blacklist = {"%.", "/", ";", "flag", "proc"}
                local args = ngx.req.get_uri_args()
#获取所有 url 参数,过滤./ 防止路径遍历,; 防止读取 URL 参数注入。proc 是 linux 目录,包含系统进程,内核信息
                for k, v in pairs(args) do
                    for _, b in ipairs(blacklist) do
                        if string.find(v, b) then
                            ngx.exit(403)
                        end
                    end
                end
            }
#上面这一块用于前置的检查
            add_header Content-Disposition "attachment; filename=download.txt";
#强制规范下载的文件名,后端的具体文件名不会暴露
            proxy_pass http://127.0.0.1/static$arg_filename;
#代理到后端目录,这里有个很明显的错误,static 后面没加 /, 导致直接拼接进去
            body_filter_by_lua_block {
                local blacklist = {"flag", "l3hsec", "l3hctf", "password", "secret", "confidential"}
                for _, b in ipairs(blacklist) do
                    if string.find(ngx.arg[1], b) then
                        ngx.arg[1] = string.rep("*", string.len(ngx.arg[1]))
                    end
                end
            }
#这里是过滤响应的内容,将响应中敏感的信息过滤,ngx.arg [1] 表示当前响应体,因为文件内容是分块传输的,需要处理每一块。rep 用于类似于 str_replace
        }
        location /read_anywhere {
            access_by_lua_block {
                if ngx.var.http_x_gateway_password ~= password then
                    ngx.say("go find the password first!")
                    ngx.exit(403)
                end
            }
            content_by_lua_block {
                local f = io.open(ngx.var.http_x_gateway_filename, "r")
                if not f then
                    ngx.exit(404)
                end
                local start = tonumber(ngx.var.http_x_gateway_start) or 0
                local length = tonumber(ngx.var.http_x_gateway_length) or 1024
                if length > 1024 * 1024 then
                    length = 1024 * 1024
                end
                f:seek("set", start)
                local content = f:read(length)
                f:close()
                ngx.say(content)
                ngx.header["Content-Type"] = "application/octet-stream"
            }
        }
    }
}

绕过第一个 waf:

漏洞点是 lua 自身的 ngx.req.get_uri_args (),nginx 本身对于传递的参数个数是没有限制的,但是

这个 ngx.req.get_uri_args 的参数上限默认是 100, 这一点通过官方文档就能发现

由于 static 设置的问题,我们可以直接构造 filename=../../../etc/passwd,实现任意文件读取,用种方法直接读取 fd:filename=../proc/self/fd/3~20 (一般而言一个进程开不了那么多文件,0 是 stdin,1 是 stdout,2 是 stderr)

绕过第二个 waf:

直接读取会被后面的 waf 过滤,可以通过单字符读取来绕过
在请求头中加入: Range: bytes=0~4

现在的问题在于,flag 文件已经被删除了,并且 f:close (),只能尝试扫描内存(/proc/self/maps)

proc/self/maps,虚拟内存映射表文件,用于实时展示虚拟内存空间的分区以及对于的物理资源:
1760251739514

# best_profile

先审计到一个可以注入的地方

1760252892340

找到对应接口

1760252775440

而且