Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Directory traversal vulnerabilities #80

Closed
gsuberland opened this issue Sep 28, 2022 · 2 comments
Closed

Directory traversal vulnerabilities #80

gsuberland opened this issue Sep 28, 2022 · 2 comments

Comments

@gsuberland
Copy link

I have discovered several directory traversal vulnerabilities. The capabilities are as follows:

  • Any .md file on the system can be read by an unauthenticated attacker
  • The contents of sibling directories to the wiki directory can be listed by unauthenticated attacker in certain circumstances.
  • Any .md file on the system can be overwritten by an unauthenticated attacker by default (authentication required if edit password protection is enabled)
  • Any .md file on the system can be deleted by an unauthenticated attacker by default (authentication required if edit password protection is enabled)
  • New directories and .md files can be created on the system with arbitrary contents and paths by an unauthenticated attacker by default (authentication required if edit password protection is enabled)

Details as follows:

Directory traversal in /list/

The /list/ request can be used to get a list of files in the wiki, or in a subdirectory of the wiki.

In wiki.py the list_wiki function that handles /list/ requests checks whether the canonical form of the requested path shares a common prefix with the configured "safe" wiki directory:

wikmd/wiki.py

Lines 102 to 107 in 04482d3

@app.route('/list/<path:folderpath>/', methods=['GET'])
def list_wiki(folderpath):
folder_list = []
safe_folder = os.path.realpath(cfg.wiki_directory)
requested_path = os.path.join(cfg.wiki_directory,folderpath)
if os.path.commonprefix((os.path.realpath(requested_path),safe_folder)) != safe_folder:

This is intended to prevent directory traversal. However, this does not prevent traversal to sibling directories whose names start with the same name as the configured wiki directory.

For example, consider the following directory structure:

home
└── user
    └── wikmd
        ├── wiki
        │   ├── homepage.md
        │   └── foo.md
        └── wiki_secret
            └── secret.md

If an attacker requests /list/../wiki_secret/, the following happens:

  1. folderpath is set to ../wiki_secret/
  2. cfg.wiki_directory is configured to serve out of the wiki directory, so safe_folder becomes /home/user/wikmd/wiki
  3. requested_path is created by joining cfg.wiki_directory and folderpath, which results in /home/user/wikmd/wiki/../wiki_secret/
  4. requested_path is canonicalised with os.path.realpath(), which returns /home/user/wikmd/wiki_secret.
  5. os.path.commonprefix() is called on the canonicalised request path and safe_folder, which searches character-by-character and returns the prefix substring up until the first difference. Since /home/user/wikmd/wiki and /home/user/wikmd/wiki_secret are the arguments, the result is /home/user/wikmd/wiki
  6. This is compared to safe_folder, which is /home/user/wikmd/wiki, and therefore the check passes.
  7. The contents of /home/user/wikmd/wiki_secret are listed.

This can be demonstrated simply by sending GET /list/../wiki_secret/ and two enter key presses to the server using netcat.

Directory traversal in page display

There is no directory traversal protection inside the file_page function. As such, any .md file on the system can be displayed.

This can be trivially demonstrated with any default install by sending GET /../README and two enter presses to the server using netcat. This will return the HTML rendered copy of the README.md file in the wikmd directory.

Directory traversal in page edit

There is no directory traversal protection inside the edit function. This can be used both for reading and writing any .md file on the system. This function is unauthenticated by default, but may be configured to require a password.

When a GET request is sent to an /edit/ route, this function displays the existing contents of the file. This can be demonstrated by sending GET /edit/../README and two enter presses to the server using netcat. This will display the HTML of the edit template with the contents of the README.md file in the wikmd directory included.

When a POST request is sent to an /edit/ route, the following code path is taken:

wikmd/wiki.py

Lines 276 to 282 in 04482d3

filename = os.path.join(cfg.wiki_directory, page + '.md')
if request.method == 'POST':
page_name = fetch_page_name()
if page_name != page:
os.remove(filename)
save(page_name)

The fetch_page_name() function acquires the page name from the PN form variable.

The page variable contains the portion of the URL after /edit/. No traversal check is performed. If the value provided in the PN form variable does not match the path in the URL, the file specified by the URL is deleted. Since the path is vulnerable to traversal, any .md file on the system can be deleted using this method.

The save() function saves the page. Since the page name is taken from the PN form variable without validation, this allows any .md file on the system to be written to. The save() function creates the full directory path to the target file name, so this enables the creation of directories anywhere on the system.

Directory traversal in new page creation

The add_new() function handles /add_new. This function is unauthenticated by default, but may be configured to require a password.

The PN form variable is used to specify the name/path of the page to create, but no traversal protection is performed. This function calls into save() in the same way that the page edit functionality does, with the same impact.

Directory traversal in remove

The remove() function handles /remove/ route requests. This function is unauthenticated by default, but may be configured to require a password.

The path is taken from the URL and is not validated or protected against traversal. This allows for any .md file on the system to be deleted.


Remediation

The existing commonprefix() traversal check in list_wiki() is broken and should be removed.

The safe_join() function provided by Flask should be utilised instead of os.path.join() in all cases where one or more of the path arguments is untrusted. This function will raise a NotFound exception if traversal out of the base directory is attempted.

@Oliver-Hanikel
Copy link
Contributor

Thanks for the issue!
Can you check if my branch fixes these vulnerabilities or have I missed something?

@Linbreux
Copy link
Owner

Linbreux commented Oct 2, 2022

Thanks for the detailed description @gsuberland! Really appreciate this. @Oliver-Hanikel You can already create a PR for this if you want.

Linbreux added a commit that referenced this issue Oct 3, 2022
Fix the directory traversal vulnerabilities as described in #80
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants