Tales of Python's Encoding

This article was also published in the third issue of the International Journal of PoC || GTFO. This is my submission after editorial "grooming" and "[dressing] in the best Sunday clothes of proper church English" :-).

Many beginners of Python have suffered at the hand of the almighty SyntaxError. One of the less frequently seen, yet still not uncommon instances is something like the following, which appears when Unicode or other non-ASCII characters are used in a Python script.

SyntaxError: Non-ASCII character ... in ..., but no encoding declared;
see http://www.python.org/peps/pep-0263.html for details

The common solution to this error is to place this magic comment as the first or second line of your Python script. This tells the interpreter that the script is written in UTF8, so that it can properly parse the file.

# encoding: utf-8

I have stumbled upon the following hack many times, but I have yet to see a complete write-up in our circles. It saddens me that I can’t correctly attribute this trick to a specific neighbor, as I have forgotten who originally introduced me to this hackery. But hackery it is.

The background

Each October, the neighborly FluxFingers team hosts hack.lu’s CTF competition in Luxembourg. Just last year, I created a tiny challenge for this CTF that consists of a single file called “packed” which was supposed to contain some juicy data. As with every decent CTF task, it has been written up on a few blogs. To my distress, none of those summaries contains the full solution. The challenge was in identifying the hidden content of the file, of which there were three. Using the liberal interpretation of the PDF format1, one could place a document at the end of a Python script, enclosed in multi-line string quotes2. The Python script itself was surrounded by weird unprintable characters that make rendering in command line tools like less or cat rather unenjoyable. What most people identified was an encoding hint.

00000a0: 0c0c 0c0c 0c0c 0c0c 2364 6973 6162 6c65  ........#disable
00000b0: 642d 656e 636f 6469 6e67 3a09 5f72 6f74  d-encoding:._rot
0000180: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f  ________________
0000190: 3133 037c 1716 0803 2010 1403 1e1b 1511  13.|.... .......

Despite the unprintables, the long range of underscores didn’t really fend off any serious adventurer. The following content therefore had to be rot13 decoded. The rest of the challenge made up a typical crackme. Hoping that the reader is entertained by a puzzle like this, the remaining parts of that crackme will be left as an exercise. The real trick was sadly never discovered by any participant of the CTF. The file itself was not a PDF that contained a Python script, but a python script that contained a PDF. The whole file is actually executable with your python interpreter! Due to this hideous encoding hint, which is better known as a magic comment,3 the python interpreter will fetch the codec’s name using a quite liberal regex to accept typical editor settings, such as “vim: set fileencoding=foo” or “-*- coding: foo”. With this codec name, the interpreter will now import a python file with the matching name4 and use it to modify the existing code on the fly.

The PoC

Recognizing that the cevag is the Rot13 encoding of Python’s print command, it’s easy to test this strange behavior.

% cat poc.py
#! /usr/bin/python
#encoding: rot13
cevag ’Hello World’
% ./poc.py
Hello World


Sadly, this only works in Python versions 2.X, starting with 2.5. My current test with Python 3.3 yields first an unknown encoding error (the “rot13” alias has sadly been removed, so that only “rot-13” and “rot_13” could work). But Python 3 also distinguishes strings from bytearrays, which leads to type errors when trying this PoC in general. Perhaps rot_13.py in the python distribution itself might be broken? There are numerous other formats to be found in the encodings directory, such as ZIP, BZip2 and Base64, but I’ve been unable to make them work. Most lead to padding and similar errors, but perhaps a clever reader can make them work. And with this, I close the chapter of Python encoding stories:


  1. As seems to be mentioned in every PoC||GTFO issue, the header doesn’t need to appear exactly at the file’s beginning, but within the first 1,024 bytes. 

  2. """This is a multiline Python string. It has three quotes.""" 

  3. See Python PEP 0263, Defining Python Source Code Encodings 

  4. See /usr/lib/python2.7/encoding/__init__.py near line 99 

If you find a mistake in this article, you can submit a pull request on GitHub.

Other posts

  1. The Mozilla Monument in San Francisco (Fri 05 July 2024)
  2. What is mixed content? (Sat 15 June 2024)
  3. How I got a new domain name (Sat 15 June 2024)
  4. How Firefox gives special permissions to some domains (Fri 02 February 2024)
  5. Examine Firefox Inter-Process Communication using JavaScript in 2023 (Mon 17 April 2023)
  6. Origins, Sites and other Terminologies (Sat 14 January 2023)
  7. Finding and Fixing DOM-based XSS with Static Analysis (Mon 02 January 2023)
  8. DOM Clobbering (Mon 12 December 2022)
  9. Neue Methoden für Cross-Origin Isolation: Resource, Opener & Embedding Policies mit COOP, COEP, CORP und CORB (Thu 10 November 2022)
  10. Reference Sheet for Principals in Mozilla Code (Mon 03 August 2020)
  11. Hardening Firefox against Injection Attacks – The Technical Details (Tue 07 July 2020)
  12. Understanding Web Security Checks in Firefox (Part 1) (Wed 10 June 2020)
  13. Help Test Firefox's built-in HTML Sanitizer to protect against UXSS bugs (Fri 06 December 2019)
  14. Remote Code Execution in Firefox beyond memory corruptions (Sun 29 September 2019)
  15. XSS in The Digital #ClimateStrike Widget (Mon 23 September 2019)
  16. Chrome switching the XSSAuditor to filter mode re-enables old attack (Fri 10 May 2019)
  17. Challenge Write-up: Subresource Integrity in Service Workers (Sat 25 March 2017)
  18. Finding the SqueezeBox Radio Default SSH Password (Fri 02 September 2016)
  19. New CSP directive to make Subresource Integrity mandatory (`require-sri-for`) (Thu 02 June 2016)
  20. Firefox OS apps and beyond (Tue 12 April 2016)
  21. Teacher's Pinboard Write-up (Wed 02 December 2015)
  22. A CDN that can not XSS you: Using Subresource Integrity (Sun 19 July 2015)
  23. The Twitter Gazebo (Sat 18 July 2015)
  24. German Firefox 1.0 ad (OCR) (Sun 09 November 2014)
  25. My thoughts on Tor appliances (Tue 14 October 2014)
  26. Subresource Integrity (Sun 05 October 2014)
  27. Revoke App Permissions on Firefox OS (Sun 24 August 2014)
  28. (Self) XSS at Mozilla's internal Phonebook (Fri 23 May 2014)
  29. Tales of Python's Encoding (Mon 17 March 2014)
  30. On the X-Frame-Options Security Header (Thu 12 December 2013)
  31. html2dom (Tue 24 September 2013)
  32. Security Review: HTML sanitizer in Thunderbird (Mon 22 July 2013)
  33. Week 29 2013 (Sun 21 July 2013)
  34. The First Post (Tue 16 July 2013)