Rating:

# Photoshoot Writeup

## Service Description

Simple Flask web service with following routes:
```
GET /
POST /load
POST /store
POST /upload
```

`load` and `store` are used to store and retrieve a flag from database. `upload`
accepts parameters `file`, `operations` and, optionally, `year`, which are
passed to `imager` binary.

JS code in `notsuspicious.js` was obfuscated but it was not necessary to
understand it.

## Imager

```
$ file imager
imager: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), <SKIPPED>, not stripped
```

### Description

Imager is written in C++, which can be seen from function names. Its purpose it
to load image file, apply selected filters on it and save result to disk. After
a bit of research we can notice that image should be in Netpbm format, supported
modes (magics) are P1, P5 and P6.

Following filters are supported, with their IDs:

```
0: NOP
1: Blur
2: Downsample
3: Lineblur
4: Watermark
5: Emboss
6: TV
```

### Research

Command line looks like `./imager <--config 888> 3 0 1 2 in out`, where config
is optional and provides the "year" parameter used in Blur filter. `3` is length
of filter list, which follows right after. `in` and `out` are input and output
files.

After parsing command line, `Task` structure is filled. It is not saved in debug
info, so you need to reverse engineer its layout:

``` cpp
struct Task
{
uint64_t filter_ids[3]; // filter IDs array
uint64_t filter_num; // number of filters to apply
int64_t (*filter_handlers[3])(uint8_t*); // array of pointers to filter functions
uint8_t *data; // image data
};
```

First bug can be found here: `Task` structure clearly can only hold up to 3
filters, but the code checks that passed filter list length is no more than 4.
It introduces off-by-one error.

After successful command line parsing `handleHeader` function is called. It
parses `in` image and fills the `Task::data` member. Image size should be
exactly 200x200. Interesting moment to note here is that `data` is mmapped as
RWX. Other than that, nothing wrong occurs here, and AFL did not find anything
interesting.

Next goes the filter applying stage, `doIt` function. It loops through filter
handlers and applies them to `data`, and it does it in rather peculiar way. The
code is best explained by itself so here it is:

``` cpp
if ( task->filter_num <= 4uLL )
{
filter_ptr = task->filter_handlers;
successes = 0;
max_iterations = 8;
while ( (unsigned __int64)successes < task->filter_num && max_iterations > 0 )
{
--max_iterations;
res = 1;
if ( *filter_ptr )
res = (*filter_ptr)(task->data);
if ( res )
++successes;
++filter_ptr;
}
}
```

First of all, we see `filter_num <= 4` comparison again, when it should be `<4`.
Next, note the `successes < task->filter_num` comparison. If filter returns 0,
`successes` won't be incremented and eventually `filter_ptr` will go past the
end of `filter_handlers` array. What goes right after `filter_handlers`? `data`
pointer, which is mmapped as RWX!

From this point exploitation path is clear: all we need to do is to get
`filter_ptr` to point to `data` field. It can be achieved in two ways: playing
with `filter_num` field on command line parsing stage, or returning 0 from a
filter somehow. We will proceed with the latter approach.

### Filters

Only two filters are interesting in some ways, out of seven provided: Blur and
Watermark. "Interesting" means that they have sufficiently complex logic in
them. For our purposes, returning 0 from a filter, only Blur is interesting,
since Watermark explicitly sets result to 1 before returning.

#### Blur

In Blur we have a structure on the stack. Its name is demangled as
`blur(unsigned char *)::{unnamed type#1}::blur` by IDA, let's call it `blur`.
Its layout, obtained after reverse engineering the whole Blur function:

``` cpp
struct Blur
{
int i;
int near_it;
int off;
int X;
int Y;
int offX;
int offY;
int off_px;
int mean;
uint32_t size;
uint8_t *buf; // from malloc, data is copied to buf
uint8_t near_px[20];
char result;
};
```

Blur uses the `--config` option value, which defaults to 20. It is used by
`getBlurSize` function in following SQL query: `SELECT value FROM config%d WHERE key = 'blursize'`.
There are only two such tables, `config20` and `config21`, and the values in
them are `2` and `3` respectively, which can be seen in `db.sql` file.

The size means how much adjacent pixel layers Blur filter should use for an
individual pixel. In other words, 2 means matrix 3x3, and 3 means matrix 5x5.

Blur code was definitely implemented first for blur size 2 only. After that, a
weak effort was made to support arbitrary blur sizes, but it was not enough.
First of all, it seems, that you can access `buf` at negative offsets, although
it is not profitable for exploitation. What is really useful for us is the
`near_px` array. All adjacent pixel values are copied there, so 9 in case of
size 2, and 25 in case of size 3. Since array size is only 20, running with blur
size 3 will overwrite the `result` field, which is set in the beginning of
function and is not modified afterwards. We need to set it to 1 if we want to
return 0 from Blur. It can be achieved by getting blur size 3 and setting last
few picture rows to 0x01.

## Exploitation

The plan is:
* Pass 21 to `--config`
* Pass `3 0 0 1` or something similar to filter list
* Return 0 from Blur
* that's all

### Passing 21

We can note that value `21` is explicitly prohibited in `app.py`. We can also
note that this parameter is parsed by Imager using `strtol(..., 0);` which gives
us a lot of ways to bypass the check and get 21 in Imager as result: `025`,
`0x21`, `4294967317`, ...

### Return 0 from Blur

As said before, just set last few pixel rows to 0x01.

### RCE!

Well, we jump to `data` now, but it is a bit "corrupted" after Blur, and by
"a bit" I mean "a lot". What we need to create is such picture that will produce
the desired shellcode at offset 0 after Blur.

First detail to note is that Blur does not process the outermost layer of
pixels, so they have values 0 after Blur. It means that right after calling
`data` we jump to a row of 200 zeroes, which thankfully form 100 harmless
instructions `add BYTE PTR [rax], al`

Another detail is that blur result is as if it was performed in 3x3 mode, even
if we pass `--config` 21.

### "Blur-resistant" shellcode

After a lot of trial and error we came to two-stage shellcode architecture.

First stage has following properties:
* It is easy to find an image that will produce this shellcode after Blur.
* All this stage does is writing second stage somewhere and jumping to it.

This shellcode can be split in 3 steps:
* Initializing
* Writing second stage
* Jumping to it

To get images from shellcode (perform reverse-Blur process) we used z3.

To explain a bit more why this task is not trivial: let's look what happens
during Blur.

```
a b c d A B C D
l m n o -> L M N O
w x y z W X Y Z

M = ((a+l+w) + (b+m+x) + (c+n+y) ) / 9
N = ( (b+m+x) + (c+n+y) + (d+o+z)) / 9
```

As you can see, N "shares" most of its value with M. N "depends on" M, and some
combinations of N and M are not possible because no initial a,b,c,... values
exist for them. Another thing to conclude here is that N value should be "near"
M, i.e., M=0 N=0x80 situation is not possible, as well as M=0 N=0xFF (no
wrapping).

To ease shellcode generation, we jump 3 rows forward in the end of each row. In
our example, it means that a,b,c,d,w,x,y,z will be used only in L,M,N,O
shellcode bytes calculation, and not any other ones that are on other rows.
First row with actual shellcode is 1, from it we jump to 4, 7, etc.

Another approach to ease shellcode generation is to find ways to break the chain
of dependencies in byte values. It can be done by inserting harmless
instructions which have bytes which we can set to arbitrary values. For example,
since we do not use conditional instructions, `cmp eax, 1234` is a great
candidate, because it is encoded with one byte 0x3D after which go 4 bytes of
immediate operand. We can set immediate to any value.

Image generating code from second stage shellcode: "gen_image.py"

### Payload

After getting arbitrary code execution we need to exfiltrate opponents' flags.
The vulnerable binary is executed by Python web app, so we don't get any
interactivity and the way to go is to drop some flags into the output file that
the web app will later serve to us.

For this purpose, we coded a shellcode that does requests to Postgres DB using
the functions already present in the binary itself. The flow of the shellcode is
as follows:

1. Use return address into `doIt` on the stack to determine binary ASLR base
2. Extract output filename from arguments in `main` stack frame
3. Call `queryString` function inside the binary with an argument of
`select value from comments where value like 'FAUST_%' order by date desc offset 0 limit 1;`
4. Write the result into the output file
5. Patch the SQL query to now say `offset 1 limit 1`, execute and write again,
repeat 5 times.
6. `exit(0)` to prevent Python throwing an exception

Full shellcode: "sqlshc" files.

### Exploit

Can be found in "photoshoot_vientvos_sploit.php".

[https://bushwhackers.ru/files/faust2020_photoshoot_writeup.7z](https://bushwhackers.ru/files/faust2020_photoshoot_writeup.7z)