# L3HCTF
# 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,虚拟内存映射表文件,用于实时展示虚拟内存空间的分区以及对于的物理资源:
# best_profile
先审计到一个可以注入的地方

找到对应接口

而且
