LFI

Path Traversal Via os.path.join

NullCon CTF 2022 - Sourcer

Chal URL: http://52.59.124.14:10000/

Website :

Listing files in /usr/src/app/files/:


/app.py will give the Source Code of this Challenge.

/flag.txt:

Flag is not here, it is in ../

We Need to Perform a Path Traversal to Get One Directory Back to Read the Flag.

Source Code :


#!/usr/bin/python
import http.server
import threading
import socketserver
import re
import os
from io import StringIO

BASEPATH="/usr/src/app"

class FileServerHandler(http.server.SimpleHTTPRequestHandler):
    server_version = "Fil3serv3r"

    def do_GET(self):
        self.send_response(-1337)
        self.send_header('Content-Length', -1337)

        s = StringIO()

        path = self.path.lstrip("/")
        counter = 0
        while ".." in path:
            path = path.replace("../", "")
            counter += 1
            if counter > 10:
                s.write(f"No")
                self.end_headers()
                self.wfile.write(s.getvalue().encode())
                return

        fpath = os.path.join(BASEPATH, "files", path)

        s.write(f"Welcome to @gehaxelt's file server.\n\n")
        if len(fpath) <= len(BASEPATH):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Hm, this path is not within {BASEPATH}")
        elif os.path.exists(fpath) and os.path.isfile(fpath):
            self.send_header('Content-Type', 'application/octet-stream')
            with open(fpath, 'r') as f:
                s.write(f.read())
        elif os.path.exists(fpath) and os.path.isdir(fpath):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Listing files in {fpath}:\n")
            for f in os.listdir(fpath):
                s.write(f"- {f}\n")            
        else:
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Oops, not found.")

        self.end_headers()

        self.wfile.write(s.getvalue().encode())


if __name__ == "__main__":
    PORT = 8000
    HANDLER = FileServerHandler
    with socketserver.ThreadingTCPServer(("0.0.0.0", PORT), HANDLER) as httpd:
        print("serving at port", PORT)
        httpd.serve_forever()

GET Route :

As Mentioned Below, ‘/’ will Return file in /usr/src/app/files/

Flag is in /usr/src/app/flag.txt

Base Path :

BASEPATH="/usr/src/app"

FileServerHandler :

class FileServerHandler(http.server.SimpleHTTPRequestHandler):
    server_version = "Fil3serv3r"

    def do_GET(self):
        self.send_response(-1337)
        self.send_header('Content-Length', -1337)

        s = StringIO()

        path = self.path.lstrip("/")
        counter = 0
        while ".." in path:
            path = path.replace("../", "")
            counter += 1
            if counter > 10:
                s.write(f"No")
                self.end_headers()
                self.wfile.write(s.getvalue().encode())
                return

        fpath = os.path.join(BASEPATH, "files", path)

        s.write(f"Welcome to @gehaxelt's file server.\n\n")
        if len(fpath) <= len(BASEPATH):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Hm, this path is not within {BASEPATH}")
        elif os.path.exists(fpath) and os.path.isfile(fpath):
            self.send_header('Content-Type', 'application/octet-stream')
            with open(fpath, 'r') as f:
                s.write(f.read())
        elif os.path.exists(fpath) and os.path.isdir(fpath):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Listing files in {fpath}:\n")
            for f in os.listdir(fpath):
                s.write(f"- {f}\n")            
        else:
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Oops, not found.")

        self.end_headers()

        self.wfile.write(s.getvalue().encode())

path.lstrip :

 path = self.path.lstrip("/")

lstrip will Just Remove the / comes Left side of the String

While Loop :

while ".." in path:
            path = path.replace("../", "")
            counter += 1
            if counter > 10:
                s.write(f"No")
                self.end_headers()
                self.wfile.write(s.getvalue().encode())
                return

The Above While Loop will Run If the Path Variable Have 2 Dots (..)

path.replace :

path = path.replace("../", "")

path.replace("../","") will Search for ../ in Path Variable and Replace that with "".

path.join

fpath = os.path.join(BASEPATH, "files", path)

We Have Control of the Path Variable!

Exploit Idea :

After Some Trial and Error, I came Across this Interesting thing…

When the Path Variable Starts with /, The Overall Path is Overwritten. Now We have Control of the Path. Let’s Exploit

If Else Condition Statements :

        s.write(f"Welcome to @gehaxelt's file server.\n\n")
        if len(fpath) <= len(BASEPATH):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Hm, this path is not within {BASEPATH}")
        elif os.path.exists(fpath) and os.path.isfile(fpath):
            self.send_header('Content-Type', 'application/octet-stream')
            with open(fpath, 'r') as f:
                s.write(f.read())
        elif os.path.exists(fpath) and os.path.isdir(fpath):
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Listing files in {fpath}:\n")
            for f in os.listdir(fpath):
                s.write(f"- {f}\n")            
        else:
            self.send_header('Content-Type', 'text/plain')
            s.write(f"Oops, not found.")

        self.end_headers()

        self.wfile.write(s.getvalue().encode())

These If Else Statements are Responsible for Sending Response.

  • If the Length of fpath is equal or lesser that BASEPATH, the Server will return, Hm, this path is not within {BASEPATH}

Exploit :

  • Note: We Can Control Path Variable.
  • Our Input is Filtered Before Passed into
    fpath = os.path.join(BASEPATH, "files", path)

  • To Exploit, Our Payload Want to Looks Like this, fpath = os.path.join(BASEPATH, "files", "/usr/src/app/flag.txt") to Read the Flag.

  • Final Payload : ..//usr/src/app/flag.txt

Let me Explain Why this Works:

  • After path.lstrip:

  • In While Loop, Replace Function is Called,
path = path.replace("../", "")

After This Replace Function,

  • Then, Our Path Variable is Passed to .join

And that Works :)

FLAG: ENO{PYTH0Ns_0sp4thj01n_fun_l0l}