░░░                              ░░░             ▀▄
                       ▄ ▀▀▄              ░░░          ░░░
                     ▄      ▒        ▄▄                ▒    ▄▀       ▒           ▀▄
                     ▄             ▄                ▄█    █       ▄▀           ▀▀     ▀▀ ▄
                      ▀█▄▄▄▄▄██▀███▄▄      ▄▄█▄▄ ▄▀▄▄▀  ▄▄ ▄▄▄▀
                      ▀▀▄▄  █▄▀     ▄▄ ▀▀▀▀  ▀▄▄██▀▀▌  ▐          ▄█▀▀▀▄ ▄▄    ░░      ■▀ ▀▄       ▄▀▀ ██▀▄
                          ██       ▓▓     ██    ██         ▐▌██  ▄█    ██            
                          ▀█       ██░░    ▓▓  █▄     ██  ▓▓                  
                         ▓█      ▀█▄▄▄▀   ▒▒     ░░▒▒    ▒▒  ░░  ▐▌ ██      ▄▄▄          
                        █▓      ▀▀▄▄▄▄▄▓▓▀▄█   █▓  ▓  ▌▓   ▀██  ▄▄
           ▄╬▄             ▓▒       █▓     ▀▀   █▓  ▓█    █▓  ▓█ ▌  ▓▓  ▓▓█ ▄▀█▓     ▄█         ▄╬▄
      ,    █▌    ,              ▓█    ░░▄▄     ░▓  █▓ █▐        █▓    ,    ▐█    ,
        █▌    ░░▀▀▀▀ ██       ▀▄ ░░░▄▄ ░  ▓░    ▀█▐ ▐▌   ▌█  ░░         ▐█   
       ▀█▓█▓█▀    ▀▄ ░░░░     ▀▄▀▒       █▓▄▀▀▄▀░░    █▀▀▓█       ▓█ ▄▓ ▄▄   ▀█▓█▓█▀  
           ▀▓%           ▀▄     ░░     ▄▀  ▄▀     █▀  ▀█    ▀▄▄   ▄▄▀   ▒▀ █▄  ▀█    ▄▀       %▓▀      
                      ▀▄        ▄▄▀      ▄▀          ▀▄     ▄▀    ▄▄      ▄▀  ■▄                          
▀▀▄▄▄█▄▒                                             ▀█                ▄▀      ▀▄                ▒▄█▄▄▄▀▀
    ▄▄▀                                        
 ▒▄[ Arte por L.Ayres ]▄▒ 
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
                                 o   
 ▒▒                                ___      o                                                                      ▒▒
                      ( - -\__»»     
          ___________.'/'--'         
      ___( ___________|_____         
 ▒▒            __|___|___________ )_____|                                                                          ▒▒
  <   |  |___________ )    ||        
   \  |  |___________ )    ||        
    \ |__|___________ )____|`------. 
 ▒▒              \|__|___________ )____| _____ |                                                                   ▒▒
      |  |___________ )     |     || 
      |  |___________ )     |     || 
  ¡             |  |___________ )     |     ||                                                                   ▒▒
  ▒,            |  ( ___________|     |     || 
  █▀▀°            |  |___________ )     |    //' 
 ▒▒               |   ___________ |     |  //'                                                                     ▒▒
      (  ( ___________)     )//'     
      ;  +-----------.`\    ;'       
       \     <   ^   >`\Oo /         
 ▒▒                 `\    `\ | /'    /'                                                                            ▒▒
          `\____|||____/'            
           |    ___    |             
          /    (___)    \            
 ▒▒                  |_______________|                                                                             ▒▒
                                     
  █                                                                                                                █ 
  ▒                                                                                                                ▒ 
 ▒▒          Imagens sao dificeis - Thumbor 0days                                                                ¡ ▒▒
 por caioluders                                                                                     ,
 °▀▀
 ▒▒          1 - Introducao: Por que Thumbor?                                                                      ▒▒
  ▒                                                                                                                ▒ 
 Eu queria estudar pwning, então procurei por algum projeto open-source "grande",
 brasileiro e com essa superfície de ataque. Pedi pra IA procurar e me dar umas
 ▒▒          opções e o thumbor pareceu ser legal pra fazer pesquisa, sei lá.                                      ▒▒
 Essa pesquisa é mais didática que ofensiva e os exploits apresentados são bem 
 difíceis de explorar no mundo real. De toda forma, os bugs são interessantes e
 me fizeram aprender bastante, espero que a vocês também.
 ▒▒                                                                                                                ▒▒
 O Thumbor é um serviço open-source de processamento de imagens criado
 pela Globo.com [10] [11]. É amplamente usado em produção para resize, crop e
 aplicacao de filtros on-the-fly. 10k+ estrelas.
  ¡                                                                                                              ▒▒
  ▒,       Basicamente a API exposta pra internet consiste da url da imagem que será
  █▀▀°       processada, os filtros que serão aplicados , seus parâmetros e do tamanho final
 ▒▒          da imagem (também existe uma assinatura por segurança que explicarei ja ja).                          ▒▒
  ▒                                                                                                                ▒ 
 +-------------------------------------------------------------------+
 |                         REQUISIÇÃO HTTP                           |
 ▒▒          |                                                                   |                                 ▒▒
 |   /unsafe/{largura}x{altura}/filters:{filtro}({params})/url       |
 +-------------------------------------------------------------------+
                                |
 ▒▒                                         v                                                                      ▒▒
 +-------------------------------------------------------------------+
 |                         SERVIDOR THUMBOR                          |
 |                                                                   |
 ▒▒          |   +-----------+      +-----------+      +----------------------+  |                               ¡ ▒▒
 |   |  Loader   |----->|    PIL    |----->|  Filtros em C        |  |                              ,
 |   | (http/fs) |      | (decode)  |      | (_composite,         |  |                              °▀▀
 ▒▒          |   +-----------+      +-----------+      |  _sharpen,           |  |                                 ▒▒
 |        |                   |            |  _nine_patch, ...)   |  |
 |        |                   |            +----------------------+  |
 |   entrada do usuário    parsing             código nativo         |
 ▒▒          +-------------------------------------------------------------------+                                 ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
 Alguns filtros foram implementados em C como extensões para o Python, aqui que
 ▒▒          fica legal.                                                                                           ▒▒
  ▒                                                                                                                ▒ 
 2 - HMAC vs .replace()
  ▒                                                                                                                ▒ 
  ¡            # URL segura vs insegura                                                                          ▒▒
  ▒,           /unsafe/300x200/https://exemplo/image.jpg               # Sem verificacao
  █▀▀°           /8Lz2x5...HMAC/300x200/https://exemplo/image.jpg        # HMAC signature
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 O Thumbor usa HMAC-SHA1 para assinar URLs e prevenir abuso, como carregar
 imagens arbitrárias de qualquer URL ou chamar qualquer filtro, sem isso daria
 ▒▒          pra colocar qualquer imagem no subdomínio do g1 por exemplo. Todo thumbor em                          ▒▒
 prod deveria estar configurado assim.
  █                                                                                                                █ 
 Então para um ataque completamente externo dar certo você precisa bypassar o
 ▒▒          HMAC-SHA1, apesar do SHA1 ter fraquezas conhecidas ha quase 20 anos e com                             ▒▒
 varios ataques de colisao comprovados [3]. Infelizmente nenhum deles ajuda aqui:
 a seguranca do HMAC depende da funcao de compressao ser uma PRF, nao de
 resistencia a colisoes [1]. O Shambles necessita que voce saiba o prefixo
 ▒▒          que gerou a hash, que seria justamente o segredo do HMAC [2], entao podemos                         ¡ ▒▒
 descartar colisoes (mas nao usem SHA1 de todo jeito, nao custa nada usar                           ,
 SHA256).                                                                                           °▀▀
 ▒▒                                                                                                                ▒▒
 Estado atual de colisões SHA1:
  █                                                                                                                █ 
 Custo aproximado (USD)
 ▒▒          ^                                                                                                     ▒▒
 |  ∞   |        █  2005 — Ataque Teórico (primeiras colisões)
 |      |        █  https://link.springer.com/chapter/10.1007/11535218_2
 |      |
 ▒▒          |120k  |                    █  2017 — SHAttered                                                       ▒▒
 |      |                    █  https://eprint.iacr.org/2017/190
 |      |
 | 60k  |
  ¡        |      |                                                                                              ▒▒
  ▒,       | 20k  |                           █  2020 — SHA-1 is a Shambles  
  █▀▀°       |      |                           █  https://eprint.iacr.org/2020/014
 ▒▒          |      |                                                                                              ▒▒
 |  0k  |
 +-------------------------------------------------------------------------> Ano
                    2005        2017   2020
 ▒▒                                                                                                                ▒▒
 Ja que precisamos conseguir carregar imagens arbitrarias no servidor, e para
 isso precisamos bypassar a checagem do HMAC vamos procurar por bugs logicos.
  ▒                                                                                                                ▒ 
 ▒▒          thumbor/thumbor/handlers/imaging.py                                                                   ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
         url_signature = self.context.request.hash
 ▒▒                  if url_signature:                                                                           ¡ ▒▒
             signer = self.context.modules.url_signer(                                              ,
                 self.context.server.security_key                                                   °▀▀
 ▒▒                      )                                                                                         ▒▒
  ▒                                                                                                                ▒ 
             try:
                 quoted_hash = quote(self.context.request.hash)
 ▒▒                      except KeyError:                                                                          ▒▒
                 self._error(400, f"Invalid hash: {self.context.request.hash}")
                 return
  ▒                                                                                                                ▒ 
 ▒▒                      url_to_validate = url.replace(                                                            ▒▒
                 f"/{self.context.request.hash}/", ""
             ).replace(f"/{quoted_hash}/", "") # <-------- REPLACE
  ▒                                                                                                                ▒ 
  ¡                    valid = signer.validate(                                                                  ▒▒
  ▒,                       unquote(url_signature).encode(), url_to_validate
  █▀▀°                   )
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 Como a hash esta contida na propia URL que ela precisa calcular, o Thumbor faz
 um .replace() para remover a hash da URL, assim como a hash com URL-encoding
 ▒▒          (pois o SHA1 resulta em = %3d no final). O problema eh que o .replace()                               ▒▒
 remove tudo na URL que corresponder, nao apenas o primeiro que encontrar. Isso
 quer dizer que a gente consegue inserir a hash varias vezes em qualquer lugar
 da URL e o Thumbor vai validar (: Ah mas e dai?
 ▒▒                                                                                                                ▒▒
 http://localhost:8888/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/v1/AUTH_bc8228b6673
 f488aa253bbcb03c80ec5/audiopub-episodes/bs/2025/j/7/6At7gYSBADbccmAiYtAg/agif25082417424226.
 jpg
 ▒▒                                                                                                              ¡ ▒▒
 ,
 Aqui temos uma linda imagem com hash ddoyYVYUbDf6Po_dzOrBhCDrXLc= e que                            °▀▀
 ▒▒          carrega diretamente de s3.glbimg.com.                                                                 ▒▒
  ▒                                                                                                                ▒ 
 http://localhost:8888/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.co/ddoyYVYUbDf6Po_dzOrB
 hCDrXLc=/m/v1/AUTH_bc8228b6673f488aa253bbcb03c80ec5/audiopub-episodes/bs/2025/j/7/6At7gYSBAD
 ▒▒          bccmAiYtAg/agif25082417424226.jpg                                                                     ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
 Agora a imagem carregada vem de s3.glbimg.co ! Note que inserimos a hash
 ▒▒          depois do .co , resultando em outro dominio.                                                          ▒▒
 Mais bypass? O Thumbor faz o replace duas vezes, permitindo que manipulemos 
 com mais finesse. Se colocarmos um quoted_hash no meio de uma hash normal
 dividida no meio, da pra criar paths sem o = , que facilita a exploracao.
  ¡                                                                                                              ▒▒
  ▒,       http://localhost:8888/ddoyYVYUbDf6Po_dzOrBhCDrXLc=/300x200/s3.glbimg.com/ddoyYVYUbDf6Po/ddoy
  █▀▀°       YVYUbDf6Po_dzOrBhCDrXLc%3D/ddoyYVYUbDf6Po/v1/AUTH_bc8228b6673f488aa253bbcb03c80ec5/audiopub-
 ▒▒          episodes/bs/2025/j/7/6At7gYSBADbccmAiYtAg/agif25082417424226.jpg                                      ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
 MAIS BYPASS? 
 ▒▒          O Thumbor usa uma configuracao ALLOWED_SOURCES para garantir que as imagens                           ▒▒
 so serao carregadas de um certo dominio, talvez para nao necessitar do HMAC.
 O problema ? Eh uma lista de regex e maioria das documentacoes/exemplos nao
 escapa os pontos, fazendo o . funcionar como wildcard :>
 ▒▒                                                                                                                ▒▒
 # ALLOWED_SOURCES = ["mydomain.com"]     # "." casa com qualquer char
 ALLOWED_SOURCES=['http://s.glbimg.com']  # aceita sXglbimg.com
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                              ¡ ▒▒
 Permitindo http://sAglbimg.com, ou qualquer outro caractere no lugar do ponto.                     ,
 °▀▀
 ▒▒          3 - Quase-Vulnerabilidades                                                                            ▒▒
  ▒                                                                                                                ▒ 
 Ja que superamos o problema do HMAC, vamos procurar bugs de pwn de verdade.
  ▒                                                                                                                ▒ 
 ▒▒          No principio, tudo era grep. Sai greppando tudo que é função possivelmente                            ▒▒
 problemática em C. "Clawd please find every unsafe function in this repository".
 Uma metodologia claramente ineficiente. Aqui vai uma lista das vulnerabilidades 
 inúteis, que não da pra alcançar pela API web ou tem alguma pré-condição.
 ▒▒                                                                                                                ▒▒
 +--------------+----------------------+------------------------------+
 | Filtro       | Bug                  | Código                       |
 +--------------+----------------------+------------------------------+
  ¡        | _alpha.c     | Loop infinito        | mode vem do PIL              |                                ▒▒
  ▒,       |              | se mode vazio        |------------------------------|
  █▀▀°       |              |                      | _alpha.c:18                  |
 ▒▒          |              |                      | int num_bytes =              |                                ▒▒
 |              |                      |   bytes_per_pixel(...);      |
 |              |                      | _alpha.c:25                  |
 |              |                      | for(; i <= size;             |
 ▒▒          |              |                      |     i += num_bytes) {        |                                ▒▒
 +--------------+----------------------+------------------------------+
 | _fill.c      | Divisão por zero     | Buffer do HTTP nunca vazio   |
 |              | se buffer vazio      |------------------------------|
 ▒▒          |              |                      | _fill.c:21                   |                                ▒▒
 |              |                      | int image_area =             |
 |              |                      |   (size / num_bytes);        |
 |              |                      | _fill.c:31                   |
 ▒▒          |              |                      | r /= image_area;             |                              ¡ ▒▒
 +--------------+----------------------+------------------------------+                             ,
 | _convolution | Buffer mismatch OOB  | PIL normaliza as dimensões   |                             °▀▀
 ▒▒          | .c           |                      |------------------------------|                                ▒▒
 |              |                      | _convolution.c:74            |
 |              |                      | int width_bytes_count =      |
 |              |                      |   width * num_bytes;         |
 ▒▒          |              |                      | _convolution.c:87            |                                ▒▒
 |              |                      | int tmp_idx = ...            |
 +--------------+----------------------+------------------------------+
 | _bounding_bo | Integer overflow     | Não alcançável via HTTP      |
 ▒▒          | x.c          |                      |------------------------------|                                ▒▒
 |              |                      | _bounding_box.c:37           |
 |              |                      | reference_pixel = image +    |
 |              |                      |   (width * stride * ...);    |
  ¡        +--------------+----------------------+------------------------------+                                ▒▒
  ▒,       | _curve.c     | Truncation uchar     | Heap corruption "cega"       |
  █▀▀°       |              |                      |------------------------------|
 ▒▒          |              |                      | _curve.c:9                   |                                ▒▒
 |              |                      | items[i*2] =                 |
 |              |                      |   (unsigned char)PyLong...   |
 +--------------+----------------------+------------------------------+
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 4 - Meu deus quanto Integer Overflow
  ▒                                                                                                                ▒ 
 ▒▒          Como a gente viu antes, a imagem passa primeiro pelo Pillow, ela é lida, relida                       ▒▒
 e parseada pela maior biblioteca de imagem do Python [12], então praticamente todos
 os inputs dos filtros em C vem do PIL, que da pra "confiar". Certo? CEEERTOO??
 Sim, certo, é a porra do PIL isso aqui não é um 0day que afeta toda a internet,
 ▒▒          vai com calma. Então a imagem precisa ser válida, nada de tamanho errado,                           ¡ ▒▒
 formato válido, tudo válido.                                                                       ,
 °▀▀
 ▒▒          O problema é que quase todos os filtros usam int32 para lidar com as dimensões                        ▒▒
 da imagem [9]. int32, que vai de -2.147.483.648 até 2.147.483.647, 
 altura = -2 bilhões, comprimento = -100 milhões, é a imagem mais estranha do 
 mundo.
 ▒▒                                                                                                                ▒▒
 Sabe o que acontece se você mandar uma imagem de largura 2.147.483.64OITO ?
  █                                                                                                                █ 
 int32
 ▒▒                                                                                                                ▒▒
  00000000 00000000 00000000 00000000   = 0
  ...
  01111111 11111111 11111111 11111111   = 2147483647 (MAX)
  ¡                             + 1                                                                              ▒▒
  ▒,                            
  █▀▀°                            v
 ▒▒           10000000 00000000 00000000 00000000   = -2147483648 (OVERFLOW)                                       ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
 É a maior imagem em linha reta do mundo.
 ▒▒                                                                                                                ▒▒
 E sabe o que quase todos os filtros em C fazem com width/height da imagem?
 Cálculo de offset de ponteiro, que maravilha. Esse padrão se repete por quase
 todos os filtros, uns mais exploráveis que outros.
 ▒▒                                                                                                                ▒▒
     // thumbor/ext/filters/_composite.c:45
     int line_offset1 = ((y_pos + y - start_y) * width1 * num_bytes);
     //                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 ▒▒              //                   int32 * int32 * int32 = OVERFLOW!                                          ¡ ▒▒
     ...                                                                                            ,
     aux1 = ptr1 + line_offset1 + (x_pos * num_bytes);                                              °▀▀
 ▒▒              ...                                                                                               ▒▒
                 r1 = aux1[r_idx];
  █                                                                                                                █ 
  ▒                                                                                                                ▒ 
 ▒▒          Se você der um número negativo para um índice, ou subtrair de um ponteiro, dá                         ▒▒
 pra escrever antes do buffer da nossa imagem na heap (: Bora calcular:
  █                                                                                                                █ 
     width1 = 65500
 ▒▒              y_pos = 16393                                                                                     ▒▒
     num_bytes = 4
  █                                                                                                                █ 
     Calculo: 16393 * 65500 * 4 = 4294966000
  ¡                                                                                                              ▒▒
  ▒,           Como uint32: 4,294,966,000 (valido)
  █▀▀°           Como int32:  -1296 (OVERFLOW!)
 ▒▒                                                                                                                ▒▒
     buffer[-1296] !
  █                                                                                                                █ 
  ▒                                                                                                                ▒ 
 ▒▒          4.1 - Diagrama de Memória                                                                             ▒▒
  ▒                                                                                                                ▒ 
 Ok , a gente consegue escrever antes do nosso buffer, mas o que tem antes do
 buffer? Vamos pensar no layout da heap. Ainda estamos falando de modo geral, 
 ▒▒          tanto o controle do ponteiro, quanto o controle do que vai ser escrito tem suas                       ▒▒
 grandes dificuldades, que serão explicadas mais na frente. O buffer da nossa
 imagem é grande devido a necessidade da largura ser grande o suficiente para
 causar o integer overflow, na prática isso quer dizer que o malloc (não o
 ▒▒          pymalloc, que so lida com objetos <= 512 bytes [13]) vai alocar nosso buffer                        ¡ ▒▒
 via mmap [7] numa página de memória só para ele, ficando longe de qualquer                         ,
 outro objeto minimamente útil. Se tentarmos escrever muito longe teremos dois                      °▀▀
 ▒▒          problemas, a imagem vai ficar muito grande (>4gb) e o layout vai ser muito                            ▒▒
 imprevisível devido ao ASLR. Então efetivamente só conseguimos sobrescrever o
 header do nosso própio buffer (pelo menos eu só consegui isso, aceito ideias).  
  ▒                                                                                                                ▒ 
 ▒▒          Nossa imagem é um PyBytesObject [4], que tem os seguintes elementos na sua struct                     ▒▒
 (struct meramente ilustrativa):
  █                                                                                                                █ 
 typedef struct {
 ▒▒              /* ---- PyObject ---- */                                                                          ▒▒
     Py_ssize_t ob_refcnt;        /* Contador de referencias */
     struct _typeobject *ob_type; /* &PyBytes_Type */
  ▒                                                                                                                ▒ 
  ¡            /* ---- PyVarObject ---- */                                                                       ▒▒
  ▒,           Py_ssize_t ob_size;          /* tamanho do objeto */
  █▀▀°
 ▒▒              /* ---- PyBytesObject ---- */                                                                     ▒▒
     Py_hash_t ob_shash;          /* cached hash (-1 se não computado) */
  █                                                                                                                █ 
     /* flexible array member */
 ▒▒              char ob_sval[1];             /* começo do buffer (+1 NULL) */                                     ▒▒
  ▒                                                                                                                ▒ 
 } PyBytesObject;
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                                ▒▒
 Desses, o mais interessante é o *ob_type , ele é um ponteiro para um
 PyTypeObject que determina o tipo desse objeto. No nosso caso, o tipo Bytes.
 Teoricamente, se construirmos um fake PyTypeObject do jeito certo, conseguimos
 ▒▒          apontar o tp_dealloc para qualquer função quando ele for dealocado [6], quando                      ¡ ▒▒
 ob_refcnt chegar a zero, tipo um system(). Existe tambem a possibilidade de                        ,
 usar o tp_repr ou tp_getattro com mais consistencia [14] [15].                                     °▀▀
 ▒▒                                                                                                                ▒▒
 Layout da nossa imagem PyBytesObject: 
  █                                                                                                                █ 
                         <-- OVERFLOW WRITE (-1296) do cálculo anterior
 ▒▒                                  |   iria pra algum lugar incerto                                              ▒▒
     +-------------------+------------------------------------+
     |   PyBytesObject   |         Image Buffer               |
     +-------------------+------------------------------------+
 ▒▒              | -32: ob_refcnt    |                                    |                                        ▒▒
     | -24: ob_type  <---+-- ALVO!                            |
     | -16: ob_size      |                                    |
     |  -8: ob_hash      |                                    |
  ¡            |   0: ob_sval -----+--> [os pixels/pixeus/pixeis?]      |                                        ▒▒
  ▒,           +-------------------+------------------------------------+
  █▀▀°
 ▒▒              Novo cálculo:                                                                                     ▒▒
              offset = -1296 + (x_pos * 4)
              x=318 -> offset = -24 (ob_type!)
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                                ▒▒
 Então, se conseguirmos que o offset seja -24 dá pra sobrescrever o ponteiro
 ob_type e fazer ele apontar para um objeto fake nosso.
  ▒                                                                                                                ▒ 
 ▒▒          Agora, para os (っ◔◡◔)っ ♥ P R O B L E M A S ♥                                                        ▒▒
                                 tem muito problema jesus.
  █                                                                                                                █ 
 Primeiro: A gente não tem nenhum infoleak para descobrir os valores dos 
 ▒▒          ponteiros, do ponteiro do system, do ponteiro do nosso PyBytesObject, etc.                          ¡ ▒▒
 Segundo: Vou explicar com mais detalhes, mas não controlamos 100% do que é                         ,
 escrito. Efetivamente os valores escritos são pixeis com seus R G B A (4bytes)                     °▀▀
 ▒▒          Terceiro: Problema menor, mas nossa imagem precisa ter alguns gigas de tamanho.                       ▒▒
 SE O SEU SERVER TEM MENOS DE 4gb DE MEMORIA O PROBLEMA EH SEU.
 Quarto: sei la porra não roda no mercúrio retrógrado.
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                                ▒▒
 4.2 - 1o Problema: Blend Math
  █                                                                                                                █ 
     # Cada pixel do watermark escreve 4 bytes (RGBA)
 ▒▒              # x_pos controla ONDE escrevemos                                                                  ▒▒
     # pixel values controlam O QUE escrevemos
  █                                                                                                                █ 
     watermark_pixel = (R, G, B, A)
  ¡                               |  |  |  |                                                                     ▒▒
  ▒,                              v  v  v  v
  █▀▀°                            [b0,b1,b2,b3]  # Little-endian
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 O filtro que resolvi explorar foi o _composite.c porque depois de tentar
 varios, ele foi o mais "simples", que permite mais "controle". e bote aspas.
 ▒▒          Mas todos tem vulnerabilidades.                                                                       ▒▒
  ▒                                                                                                                ▒ 
 #define ALPHA_COMPOSITE_COLOR_CHANNEL(color1, alpha1, color2, alpha2) \
     ( ((1.0 - (alpha1 / MAX_RGB_DOUBLE)) * (double) color1) + \
 ▒▒                ((1.0 - (alpha2 / MAX_RGB_DOUBLE)) * (double) color2 * (alpha1 / MAX_RGB_DOUBLE)) )             ▒▒
 #define ADJUST_COLOR_DOUBLE(c) ((int)((c > MAX_RGB_DOUBLE) ? MAX_RGB : ((c < 0.0) ? 0 : 
 c)))  
     
 ▒▒              // _composite.c                                                                                 ¡ ▒▒
      int width1, width2,                                                                           ,
         height1, height2,                                                                          °▀▀
 ▒▒                  x_pos, y_pos,                                                                                 ▒▒
         merge = 1;
     ...
         int line_offset1 = ((y_pos + y - start_y) * width1 * num_bytes),
 ▒▒                      line_offset2 = (y * width2 * num_bytes);                                                  ▒▒
     ...
     if (merge) {
                 delta = (a2 / MAX_RGB_DOUBLE) * (a1 / MAX_RGB_DOUBLE);
 ▒▒                                                                                                                ▒▒
                 a = MAX_RGB_DOUBLE * delta;
  █                                                                                                                █ 
                 delta = 1.0 - delta;
  ¡                        delta = (delta <= SMALL_DOUBLE) ? 1.0 : (1.0 / delta);                                ▒▒
  ▒,
  █▀▀°                       r = delta * ALPHA_COMPOSITE_COLOR_CHANNEL(r2, a2, r1, a1);
 ▒▒                          g = delta * ALPHA_COMPOSITE_COLOR_CHANNEL(g2, a2, g1, a1);                            ▒▒
                 b = delta * ALPHA_COMPOSITE_COLOR_CHANNEL(b2, a2, b1, a1);
             }
     ...
 ▒▒                      r1 = aux1[r_idx];                                                                         ▒▒
             g1 = aux1[g_idx];
             b1 = aux1[b_idx];
             a1 = aux1[a_idx];
 ▒▒                                                                                                                ▒▒
             r2 = aux2[r_idx];
             g2 = aux2[g_idx];
             b2 = aux2[b_idx];
 ▒▒                      a2 = aux2[a_idx];                                                                       ¡ ▒▒
 ,
     ...                                                                                            °▀▀
 ▒▒                      aux1[r_idx] = ADJUST_COLOR_DOUBLE(r);                                                     ▒▒
             aux1[g_idx] = ADJUST_COLOR_DOUBLE(g);
             aux1[b_idx] = ADJUST_COLOR_DOUBLE(b);
             aux1[a_idx] = ADJUST_COLOR_DOUBLE(a);        
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 o aux1 eh onde conseguimos o overflow e apontar para o propio PyBytesObject, o
 problema eh o ALPHA_COMPOSITE_COLOR_CHANNEL que mistura as cores das duas
 ▒▒          imagens de input (o filtro _composite junta duas imagens), entao so conseguimos                       ▒▒
 total controle do que sera escrito se o valor do alpha (a opacidade)  
 for 0xff ou 0x00, porque o pixel vai substituir o anterior ou sera ignorado.
  ▒                                                                                                                ▒ 
  ¡        Isso tudo pra falar que a gente precisa que o byte[3] que queremos escrever                           ▒▒
  ▒,       TEM que ser 0xff ou 0x00, senao o valor escrito sera diferente. O problema?
  █▀▀°       A gente ta tentando escrever um valor de ponteiro e precisamos que um dos
 ▒▒          valores seja 0xff, ~0.8% dos layouts funcionam.                                                       ▒▒
  ▒                                                                                                                ▒ 
     Restrição do ASLR                
                                                   
 ▒▒                PyByteArray_Type = 0x00 00 7f XX ?? ?? ?? ??                                                    ▒▒
                                     ^                   
                                     |                   
        Escrevemos 4 bytes (RGBA) por vez.
 ▒▒                 O byte que cai na posicao do alpha (byte[3]                                                    ▒▒
        do grupo de 4) precisa ser 0x00 ou 0xFF
        para o blend nao corromper o valor.
                                                   
 ▒▒              Probabilidade: 2/256 ~ 0.8%                                                                     ¡ ▒▒
 ,
 °▀▀
 ▒▒          Como vamos resolver? Tentando e tentando ate dar certo ( ╥ω╥ ) Umas 300 vezes                         ▒▒
 deve dar.
  █                                                                                                                █ 
 5 - Ideias para RCE
 ▒▒                                                                                                                ▒▒
     // Include/cpython/bytesobject.h
     typedef struct {
         PyObject_VAR_HEAD      // ob_refcnt, ob_type, ob_size
 ▒▒                  Py_hash_t ob_shash;    //  hash                                                               ▒▒
         char ob_sval[1];       // Buffer 
     } PyBytesObject;
  ▒                                                                                                                ▒ 
  ¡            Offset  Campo         Tamanho                                                                     ▒▒
  ▒,           ------  -----         -------
  █▀▀°           +0      ob_refcnt     8 bytes
 ▒▒              +8      ob_type       8 bytes  <-- ALVO PRINCIPAL                                                 ▒▒
     +16     ob_size       8 bytes
     +24     ob_shash      8 bytes
     +32     ob_sval[0]    ... (buffer de pixels)
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 Existem algumas maneiras ja documentadas de conseguir RCE nesse contexto de
 sobrescrita de um Python Object [5], aqui nao estamos inventando nada, so tentando
 ▒▒          aplicar a tecnica dado as nossas restricoes. Mas para qualquer tecnica dar                            ▒▒
 certo precisamos primeiro uma leitura "arbitraria" para descobrir os valores dos
 ponteiros. [14] [15]
  ▒                                                                                                                ▒ 
 ▒▒          5.1 - Type Confusion: PyBytes -> PyByteArray                                                        ¡ ▒▒
 ,
     PyBytesObject (antes):          PyByteArrayObject (depois):                                    °▀▀
 ▒▒              +--------------------+          +--------------------+                                            ▒▒
     | ob_refcnt          |          | ob_refcnt          |
     | ob_type -----------+--------->| PyByteArray_Type   |
     | ob_size            |          | ob_size            |
 ▒▒              | ob_shash <---------+--------->| ob_alloc           |                                            ▒▒
     | ob_sval[0...] <----+--------->| ob_bytes (ptr!)    |
     +--------------------+          +--------------------+
                                             |
 ▒▒                                                      v                                                         ▒▒
                                       Arbitrary Read!
  █                                                                                                                █ 
     PyByteArrayObject tambem tem ob_exports e ob_start
  ¡            que nao existem em PyBytesObject. Dependendo da versao do                                         ▒▒
  ▒,           CPython o overlap pode nao ser tao limpo assim. Testado
  █▀▀°           no CPython 3.11 que o Thumbor usa.
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 Uma das ideias eh primeiro apontar o ob_type para uma PyByteArrayObject
 porque daria para ler os bytes subsequentes, poderiamos fazer o ob_size ser
 ▒▒          gigante para tentar ler os ponteiros dos proximos objetos na heap, ou apontar o                       ▒▒
 ob_bytes para um outro objeto. Infelizmente, depois de horas debuggando, me
 parece que pelo tamanho do nosso buffer (4GB) ele esta sendo alocado pelo malloc
 e nao pelo pymalloc, que so eh usado se o objeto for menor que 512 bytes [13].
 ▒▒          Dessa forma o nosso objeto fica numa pagina so pra ele e nao encontrei nenhum                         ▒▒
 objeto depois que a gente pudesse ler :`(
  █                                                                                                                █ 
 6 - Tentando Leak: Sharpen e ByteArray
 ▒▒                                                                                                              ¡ ▒▒
 O type confusion funciona, mas sem um infoleak nao sabemos os enderecos que                        ,
 precisamos escrever. Tentei procurar nos outros filtros se a gente conseguiria                     °▀▀
 ▒▒          pelo menos um infoleak remoto. O wavelet_sharpen pareceu interessante.                                ▒▒
  ▒                                                                                                                ▒ 
 6.1 - Sharpen Heap Overflow
  ▒                                                                                                                ▒ 
 ▒▒              // wavelet_sharpen.c - hat_transform()                                                            ▒▒
     void hat_transform(FLOAT *temp, FLOAT *base, int st, int size, int sc) {
         for (i = 0; i < size; i++) {
             temp[i] = 2*base[st*i] + base[st*(sc-i)] + base[st*(i+sc)];
 ▒▒                      //                       ^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^                                ▒▒
             //                       OOB quando sc > size!
         }
     }
  ¡                                                                                                              ▒▒
  ▒,
  █▀▀°       Com width=3000, height=1:
 ▒▒                                                                                                                ▒▒
     Buffer: 3000 floats (12KB)
     Level 4 (sc=16): acessa base[width*31] = base[93000]
                      -> 360KB alem do buffer!
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 Infelizmente os dados que sao lidos sao tratados como pixels e floats [8], fazendo
 a gente perder informacao.
 ▒▒                                                                                                                ▒▒
                Float Interpretation Problem                   
                                                               
       Heap contem: 0x7f12 3456 789a bcde (libc pointer)       
 ▒▒                                                                                                              ¡ ▒▒
       Interpretado como float (IEEE 754):                                                          ,
       bytes [0:4] = 0x789abcde -> float ~ 2.3e+34                                                  °▀▀
 ▒▒                bytes [4:8] = 0x7f123456 -> float ~ 1.9e+38                                                     ▒▒
                                                               
       Apos 5 niveis wavelet (x0.25^5):                        
       -> Valor final ~ 10^30 -> SATURA para pixel 255         
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 Tive ate uma ideia de tentar usar isso como oraculo de ponteiro e diminuir o
 brute force necessario, mas nao adiantou pra muita coisa.
 ▒▒                                                                                                                ▒▒
     Ponteiro na heap produz assinatura caracteristica:
  █                                                                                                                █ 
              bytes:  [low] [low] [low] [low] [0x7f] [XX] [00] [00]
  ¡                               |     |     |     |      |     |    |    |                                     ▒▒
  ▒,               float        : +-----+-----+-----+      |     |    |    |
  █▀▀°                              |     +-----+-----+------+     |    |    |
 ▒▒                                 |     |     +-----+------+-----+    |    |                                     ▒▒
                        |     |     |     +------+-----+----+    |
                        |     |     |     |      +-----+----+----+
                        v     v     v     v      v     v    v    v
 ▒▒                  pixels:       [~0]  [~0]  [~0]  [~0]  [255] [255] [~0] [~0]                                   ▒▒
  ▒                                                                                                                ▒ 
         Padrao detectavel: [..., 0, 0, 255, 0, 0, ...]
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                                ▒▒
 6.2 - qq a gente faz
  █                                                                                                                █ 
 Outra ideia que tive foi usar o fato que controlamos o tamanho da imagem para
 ▒▒          forcar o alocador a alocar outra imagem no mesmo lugar da primeira, ja que elas                     ¡ ▒▒
 tem o mesmo tamanho, dai daria para:                                                               ,
 °▀▀
 ▒▒          1. fazer um type confusion para ByteArray                                                             ▒▒
 2. apontar para si mesmo para descobrir o ponteiro do nosso propio buffer
 3. mandar outra imagem com o fake PyTypeObject dentro dela e fazer overwrite
 para o ob_type apontar para dentro do propio buffer, ja que agora sabemos o 
 ▒▒          endereco reutilizado, daria ate pra fazer heap spray com varios fake obj dentro                       ▒▒
 da imagem.
  █                                                                                                                █ 
 --[ 7 - A teoria do RCE
 ▒▒                                                                                                                ▒▒
 Isso aqui eh praticamente teorico.
 O que eu tenho eh cada stage individual funcionando no GDB com
 breakpoints, enderecos hardcoded e muita paciencia. Stage 0 precisa de varios
  ¡        crashes ate funcionar. E mesmo com acesso local, cada tentativa reinicia o                            ▒▒
  ▒,       processo inteiro.Nao eh um one-shot, eh mais um
  █▀▀°       "se todas as estrelas se alinharem coisas interessantes acontecem com a heap do 
 ▒▒          CPython via HTTP".                                                                                    ▒▒
  ▒                                                                                                                ▒ 
     +-------------------------------------------------------------+
     |                    EXPLOIT CHAIN                            |
 ▒▒              +-------------------------------------------------------------+                                   ▒▒
     |                                                             |
     |  STAGE 0: Encontrar ASLR Favoravel                          |
     |  +--------------------------------------------------------+ |
 ▒▒              |  | Loop:                                                  | |                                   ▒▒
     |  |   1. Crashar/reiniciar thumbor                         | |
     |  |   2. GDB: checar PyByteArray_Type byte[3]              | |
     |  |   3. Se 0x00 ou 0xFF -> EXPLOITAVEL                    | |
 ▒▒              |  |   4. Senao -> restart (sim, de novo)                   | |                                 ¡ ▒▒
     |  |   5. Salvar enderecos: PyByteArray_Type, system()      | |                                ,
     |  | Media: ~128 tentativas (na pratica fiz ~200)           | |                                °▀▀
 ▒▒              |  |                                                        | |                                   ▒▒
     |  +--------------------------------------------------------+ |
     |                           |                                 |
     |                           v                                 |
 ▒▒              |  STAGE 1: Type Confusion                                    |                                   ▒▒
     |  +--------------------------------------------------------+ |
     |  | HTTP Request:                                          | |
     |  | /65500x16394/filters:watermark(wm.png,318,16393,0)/    | |
 ▒▒              |  |                                                        | |                                   ▒▒
     |  | Integer overflow em _composite.c:                      | |
     |  |   16393 * 65500 * 4 = 4294966000                       | |
     |  |   (int32) -> -1296                                     | |
  ¡            |  |                                                        | |                                   ▒▒
  ▒,           |  | Watermark pixels sobrescrevem o header:                | |
  █▀▀°           |  |   x=318 -> offset -24 (ob_type)                        | |
 ▒▒              |  |   Pixel RGBA = bytes low de PyByteArray_Type           | |                                   ▒▒
     |  |   byte[3] precisa ser 0x00 ou 0xFF (filtrado no S0)    | |
     |  |                                                        | |
     |  | Resultado: ob_type agora aponta para PyByteArray_Type  | |
 ▒▒              |  | O objeto PyBytes agora eh um PyByteArray               | |                                   ▒▒
     |  +--------------------------------------------------------+ |
     |                           |                                 |
     |                           v                                 |
 ▒▒              |  STAGE 2: Self-leak do endereco do buffer                   |                                   ▒▒
     |  +--------------------------------------------------------+ |
     |  | Apos type confusion, o campo ob_sval (offset +32) eh   | |
     |  | reinterpretado como ob_bytes (ponteiro para dados).    | |
 ▒▒              |  |                                                        | |                                 ¡ ▒▒
     |  | Se ob_bytes aponta para o proprio buffer, lendo o      | |                                ,
     |  | "bytearray" de volta via resposta HTTP podemos         | |                                °▀▀
 ▒▒              |  | reconstruir o endereco do nosso buffer na heap.        | |                                   ▒▒
     |  |                                                        | |
     |  | PROBLEMA: na pratica eu nao consegui confirmar que o   | |
     |  | Thumbor retorna os bytes crus do bytearray "confused"  | |
 ▒▒              |  | na resposta HTTP. O PIL pode rejeitar/corromper os     | |                                   ▒▒
     |  | dados antes.                                           | |
     |  +--------------------------------------------------------+ |
     |                           |                                 |
 ▒▒              |                           v                                 |                                   ▒▒
     |  STAGE 3: Fake PyTypeObject                                 |
     |  +--------------------------------------------------------+ |
     |  | Nova imagem com MESMO tamanho -> alocador reutiliza    | |
  ¡            |  | o mesmo endereco (agora conhecido do Stage 2).         | |                                   ▒▒
  ▒,           |  |                                                        | |
  █▀▀°           |  | Pixels da imagem contem:                               | |
 ▒▒              |  |   1. Fake PyTypeObject no offset conhecido:            | |                                   ▒▒
     |  |      - tp_dealloc (+48) = endereco de system() [15]    | |
     |  |      - restante dos slots = NULL                       | |
     |  |   2. Comando shell nos primeiros bytes do objeto:      | |
 ▒▒              |  |      ob_refcnt = "touch /t" (8 bytes)(PIL bypass?)     | |                                   ▒▒
     |  |      ob_type   = ponteiro para fake type no buffer     | |
     |  |                                                        | |
     |  | Alternativas para o slot de execucao:                  | |
 ▒▒              |  |   tp_dealloc (+48): trigger = refcnt chega a zero      | |                                   ▒▒
     |  |     - risco: deallocs de outros objs podem crashar     | |
     |  |   tp_repr    (+88): trigger = repr(obj) ou print(obj)  | |
     |  |   tp_getattro(+144): trigger = obj.qualquer_attr [14]  | |
 ▒▒              |  |     - mais confiavel, nao depende de refcnt            | |                                 ¡ ▒▒
     |  |     - RDI = obj_ptr, entao obj comeca com "/bin/sh\0"  | |                                ,
     |  +--------------------------------------------------------+ |                                °▀▀
 ▒▒              |                           |                                 |                                   ▒▒
     |                           v                                 |
     |  STAGE 4: Trigger                                           |
     |  +--------------------------------------------------------+ |
 ▒▒              |  | O integer overflow novamente sobrescreve ob_type para  | |                                   ▒▒
     |  | apontar para o fake PyTypeObject dentro do buffer.     | |
     |  |                                                        | |
     |  | Quando o Thumbor termina de processar a imagem:        | |
 ▒▒              |  |   - tp_dealloc: chamado ao liberar o objeto            | |                                   ▒▒
     |  |     system("touch /t...") (comando nos bytes iniciais) | |
     |  |   - tp_getattro: chamado se qualquer atributo for      | |
     |  |     acessado antes da liberacao                        | |
  ¡            |  |     system("/bin/sh\0...") (ob_refcnt = comando)       | |                                   ▒▒
  ▒,           |  +--------------------------------------------------------+ |
  █▀▀°           |                           |                                 |
 ▒▒              |                           v                                 |                                   ▒▒
     |                     +---------+                             |
     |                     |   RCE   |                             |
     |                     +---------+                             |
 ▒▒              +-------------------------------------------------------------+                                   ▒▒
  ▒                                                                                                                ▒ 
  █                                                                                                                █ 
 8 - Conclusao
 ▒▒                                                                                                                ▒▒
 Sem PoC funcional pro 0day (nao resolvi o infoleak kkkkk).
 Daria um chall legal de CTF.
  ▒                                                                                                                ▒ 
 ▒▒          9 - Referencias                                                                                     ¡ ▒▒
 ,
     [1] HMAC - Keyed-Hashing for Message Authentication                                            °▀▀
 ▒▒                  https://pt.wikipedia.org/wiki/HMAC                                                            ▒▒
  ▒                                                                                                                ▒ 
     [2] Leurent, G., & Peyrin, T. (2020). SHA-1 is a Shambles.
         USENIX Security 2020.
 ▒▒                  https://sha-mbles.github.io/                                                                  ▒▒
  ▒                                                                                                                ▒ 
     [3] Stevens, M., et al. (2017). The first collision for full SHA-1.
         https://shattered.io/
 ▒▒                                                                                                                ▒▒
     [4] CPython Source Code - bytesobject.c
         https://github.com/python/cpython/blob/main/Objects/bytesobject.c
  ▒                                                                                                                ▒ 
  ¡            [5] CPython Internals Book                                                                        ▒▒
  ▒,               https://realpython.com/cpython-source-code-guide/
  █▀▀°
 ▒▒              [6] Phrack - "Once upon a free()"                                                                 ▒▒
         http://phrack.org/issues/57/9.html
  █                                                                                                                █ 
     [7] glibc malloc internals
 ▒▒                  https://sourceware.org/glibc/wiki/MallocInternals                                             ▒▒
  ▒                                                                                                                ▒ 
     [8] IEEE 754 Floating Point
         https://en.wikipedia.org/wiki/IEEE_754
 ▒▒                                                                                                                ▒▒
     [9] CWE-190: Integer Overflow
         https://cwe.mitre.org/data/definitions/190.html
  ▒                                                                                                                ▒ 
 ▒▒              [10] Thumbor Documentation                                                                      ¡ ▒▒
          https://thumbor.readthedocs.io/                                                           ,
 °▀▀
 ▒▒              [11] Thumbor Source Code                                                                          ▒▒
          https://github.com/thumbor/thumbor
  █                                                                                                                █ 
     [12] Pillow (Python Imaging Library Fork)
 ▒▒                   https://github.com/python-pillow/Pillow                                                      ▒▒
  ▒                                                                                                                ▒ 
     [13] The pymalloc allocator - CPython docs
          https://docs.python.org/3/c-api/memory.html#the-pymalloc-allocator
 ▒▒                                                                                                                ▒▒
     [14] kn32 - Exploiting a Use-After-Free for code execution in
          every version of Python 3
          https://pwn.win/2022/05/11/python-buffered-reader.html
  ¡                                                                                                              ▒▒
  ▒,           [15] bebop404 - CPython UAF: Exploit Chain Dev from UAF Trigger to RCE
  █▀▀°                https://www.bebop404.com/blog/cpython-uaf-exploit-chain     
 ▒▒                                                                                                                ▒▒
  ▒                                                                                                                ▒ 
 |=[ EOF ]=---------------------------------------------------------------=|
  ▒                                                                                                                ▒ 
 ▒▒                                                                                                                ▒▒
 ▒▒▒▒▒ ░░                                                                                                    ▒▒▒▒▒▒▒▒
                                                                                                                  ▓▄█
                                      T R A M O I A   ·   Z I N E   ·  2 0 2 6                                 gld ██
  ▀▄▄▄▄                                                                                                          ▒▒▄▄▀