# 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="/files/DEaGoM9RDm59Rj4X3A07" 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ctf.zeyu2001.com/2022/stack-the-flags-2022/guthib-actions.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
