Recently, I encountered a coding test question related to Ice Scorpion. The question was:
When uploading a webshell and using Ice Scorpion to connect, what are the possible causes of errors?
Since I havenât spent much time on vulnerability research recently, my experience with webshell management tools is still limited to tools like Cknife and China Chopper. So, I started by reading some articles about Ice Scorpion on Google. Although the author has published several articles about Ice Scorpion on the Xianzhi platform, most of the other online articles focus on traffic detection for Ice Scorpion. However, the author hasnât made the source code public on GitHub. Since itâs written in Java, I downloaded it and decompiled it using JD-GUI. Fortunately, the author didnât obfuscate the code. I took this opportunity to read through the source code and learn from the authorâs approach.
PS: As a junior in college, my skills are still developing. My analysis of Ice Scorpionâs source code may differ from the authorâs original intentions. This article is merely my learning process, and I welcome constructive criticism from experienced professionals.
After downloading and extracting Ice Scorpion, I imported it into JD-GUI to analyze the directory structure.
Ice Scorpion uses AES encryption for traffic, so it cannot utilize a basic one-liner webshell. Instead, it requires the corresponding Ice Scorpion webshell from the server files. The ânetâ directory contains the main tool code, âtestâ contains test code, and the rest are JAR dependencies used by the tool. Weâll focus on analyzing the ânetâ directory, specifically the âcoreâ and âutilsâ packages.
I spent nearly a week reading through Ice Scorpionâs source code. Below is a rough summary of the structure I compiled.
The âcoreâ package contains the main logic of Ice Scorpion. Letâs analyze it step by step, starting with the Crypt and Decrypt classes.
Ice Scorpion Crypt Class
I noticed something odd here: there are two classes, Crypt and Decrypt. Initially, I assumed they were for encryption and decryption, respectively. However, the Crypt class contains both encryption and decryption methods. Moreover, the ShellService class only calls methods from the Crypt class, leaving me puzzled about the purpose of the Decrypt class.
The Crypt class includes encryption and decryption methods for different server-side languages.
Letâs examine the encryption and decryption methods, using the C# implementation as an example. I found that many Java AES encryption implementations online are quite similar, making it seem like this code could be directly copied.
Next, letâs look at the PHP and ASP encryption methods. The encryption varies based on the type, and as the author mentioned, since the client integrates shell management for different languages, some code is reused. For example, the XOR operation in PHP directly calls the ASP implementation.
Ice Scorpion uses AES encryption during communication. Java and .NET natively support AES, but PHP requires the OpenSSL extension. After the v2.0 update, PHP encryption dynamically adapts based on server support, removing the dependency on OpenSSL and expanding Ice Scorpionâs capabilities. For unsupported AES cases, the ASP implementation is directly invoked. The decryption methods follow the same logic, so I wonât elaborate further.
Since the Decrypt classâs code is never called, weâll skip analyzing it.
Params Class
The author previously mentioned this in their article on implementing a new type of one-liner webshell using dynamic binary encryption in Java, and the concept is worth noting.
If an attacker sends a request not as plain text source code but as compiled bytecode (e.g., a Java class file), the bytecode is a binary data stream without parameters. The attacker encrypts the bytecode, and defenders only see encrypted binary data. Even if the same payload is executed multiple times, using different encryption keys ensures that the request data appears different each time, making it impossible for defenders to extract patterns through traffic analysis.
To dynamically parse byte streams into classes on the server side, Java doesnât provide a direct interface for parsing class byte arrays. However, the ClassLoaderâs protected defineClass method can convert byte[] into a Class.
The author cleverly defined a subclass of ClassLoader to override this method, bypassing the protected restriction and returning the parsed Class. Reflection could also achieve this. This approach allows dynamic parsing and execution of compiled class byte streams, but it requires mentioning Javaâs ASM framework.
ASM is a Java bytecode manipulation framework. It can dynamically generate classes or enhance existing ones. ASM can directly produce binary class files or modify class behavior dynamically before they are loaded into the JVM.
The commands to be executed are encoded in the class file since itâs precompiled. To avoid recompiling the payload for every execution, the payload must accept parameters. To bypass WAF interception, these parameters are also binary streams. The author uses the ASM framework to dynamically modify class properties. As shown in the code, passing the payload class name and parameter list to the getParamedClass method generates a parameterized payload class.
Letâs move on to the PHP implementation.
Similarly, the .NET and ASP implementations follow the same principle with minor differences. The .NET implementation uses precompiled DLL files, which can be referenced in the authorâs original article.
As a side note, since JD-GUI only displays Java code, the payload code isnât visible. Exporting the entire project source code allows you to view it in the file directory.
PluginUtils Class
Ice Scorpion doesnât currently support plugins, but the author left a plugin class here. I suspect future versions will include plugin support.
ShellEntity Class
The ShellEntity class retrieves specific shell parameter information, which doesnât require much analysis.
ShellManager Class
The constructor checks for the existence of the database file and database connection drivers. After reviewing all methods, I found they primarily handle database operations, as shown below. Thereâs not much to analyze here.
Initially, I thought the SQL assignment statements were overly verbose. After consulting a friend, I learned that without a framework, JDBC requires this approach, which is quite cumbersome.
ShellService Class
This class handles operations after connecting to a shell, corresponding to the following interface. It represents one of the core functional components.
Letâs first examine the constructor of this class, which, unsurprisingly, assigns values to certain properties. When the type is determined to be PHP, an additional header is appended, and two headers are merged.
Here, the `Utils.getKeyAndCookie` method is called. Letâs dive into this method. Since the code for this method is relatively lengthy but crucial, Iâll paste it directly below for reference:
java public static Map<String, String> getKeyAndCookie(String getUrl, String password, Map<String, String> requestHeaders) throws Exception { disableSslVerification(); Map<String, String> result = new HashMap<>(); StringBuffer sb = new StringBuffer(); InputStreamReader isr = null; BufferedReader br = null; URL url;
if (getUrl.indexOf(â?â) > 0) { url = new URL(getUrl + â&â + password + â=â + new Random().nextInt(1000)); } else { url = new URL(getUrl + â?â + password + â=â + new Random().nextInt(1000)); }
boolean error = false; String errorMsg = ââ; if (urlConnection.getResponseCode() == 500) { isr = new InputStreamReader(urlConnection.getErrorStream()); error = true; errorMsg = âFailed to retrieve the key, incorrect password?â; } else if (urlConnection.getResponseCode() == 404) { isr = new InputStreamReader(urlConnection.getErrorStream()); error = true; errorMsg = â404 error: Page not foundâ; } }
From the function name `getKeyAndCookie`, we can infer that it retrieves a key and cookie values. Initially, it disables automatic redirection because the `Location` header will be analyzed later. The code first checks if the URL contains a `?`. Since parameters follow the `?`, if itâs absent, the `password` and a pseudo-random number between 0 and 1000 are appended. It then determines whether the current protocol is HTTPS or HTTP, checks for proxy settings, and adds the corresponding headers before establishing the connection. If no proxy is set, it connects directly.
After sending the request, if the response status code is 301 or 302, the `Location` value from the header is extracted as `urlWithSession`. To better understand the code, we can analyze the process using packet capture tools. Here, I used Charles to analyze the traffic by setting up a proxy in Behinder.
Next, I opened Charles and connected to our shell, observing that four requests were sent simultaneously.
We can see two requests to `http://127.0.0.1/shell.php?pass=*`. The first request checks the serverâs response status code. If itâs 200, it further determines whether the key can be retrieved. The second request retrieves the AES encryption key used later. The first request is essentially a test.
Thus, before establishing a formal connection, Behinder sends a GET request. If the server responds correctly, it returns a 16-character string. For every connection request, Behinder sends a GET request to the server to fetch this 16-character key, which is then used to encrypt communication between the client and server, ensuring evasion of detection.
By analyzing the code step by step, we find that the `getKeyAndCookie` method actually calls the `getRawKey` method. Letâs examine the code for this method.
Comparing these two methods, we see that the code is largely identical. Both methods are quite lengthy, and the authorâs code appears overly redundant. Code with similar functionality could be refactored into a separate method for better reusability. This redundancy explains why `shell.php` is requested twice for keys: the first request is a test initiated by the `getKeyAndCookie` method, and if successful, the `getRawKey` method is called to fetch the actual key.
From the above two methods, we can answer the question posed at the beginning of this article using the authorâs source code:
1. **500**: Failed to retrieve the key, incorrect password. 2. **404**: Shell file not found. 3. The page exists, but the key cannot be retrieved. 4. Request returned an exception, which needs to be diagnosed based on the `Exception` message.
Letâs return to the `shellservice` code and compare its functionality with the toolâs features. Starting with basic information:
Initially, I planned to analyze the `Utils` class methods independently. However, since the `shellservice` class heavily relies on this class, Iâll directly trace and analyze the invoked methods without separate analysis. Letâs examine the `Utils.getData` method.
Next, we trace the `requestAndParse` method.
We can see that it sends a request by calling the `sendPostRequestBinary` method below, which also includes data parsing functionality. In this method, the `Content-Type` is set to `application/octet-stream`. However, during packet capture and analysis, we noticed that PHP uses `application/x-www-form-urlencoded`. This discrepancy arises because, as analyzed earlier in the `ShellService` constructor, the author replaces the `Content-Type` when the type is identified as PHP. Therefore, PHPâs `Content-Type` differs from the other three. Letâs move on to examining the code for command execution.
Similarly, letâs observe the file management code, which includes methods for displaying, uploading, downloading, and deleting files. These methods essentially follow the same logic. File uploads and downloads are encrypted during transmission by invoking the `Utils.requestAndParse` method (which handles encryption and encoding). Parameters are passed, and the results are decrypted and then base64-decoded to retrieve the output.
The virtual terminal functionality operates by passing a path, which can be either `cmd` or `powershell`, and then executing and returning the output. This process is fundamentally the same as the one described above. Next is code execution. As previously analyzed, Java achieves this by transmitting the payload as class bytecode, which the server then parses. Similarly, in this case, the `Utils.getClassFromSourceCode` method is used to convert the code into a class.
Additionally, there is the SOCKS proxy. The virtual terminal functionality already partially implements the capability for internal network penetration. All actions performed in the shell environment occur within the internal network. However, to facilitate the use of other tools, the client also provides a SOCKS proxy feature based on a âone-line web shell.â I think the authorâs explanation here is quite good. Since I couldnât get much use out of this feature locally, Iâll directly include the authorâs animation.
After comparing the code, I found that the implementation principles are essentially the same. Most of the functionality and code are quite similar, so this concludes the analysis.
Summary:
After analyzing the core code of Behinder, its principles and workflow become much clearer. Letâs revisit the authorâs original flowchart for reference.
Reading Behinderâs source code took me about a week. Of course, I still have many areas to improve in terms of my personal skills. Although Iâm not very proficient in Java, I forced myself to tackle this challenging task to learn from the authorâs thought process. By analyzing the toolâs source code and referencing some senior researchersâ analyses and detection methods for Behinderâs traffic, Iâve learned a lot. The authorâs approach of bypassing WAFs using encrypted traffic is truly impressive, as it avoids traditional code obfuscation techniques and achieves a more permanent solution. Additionally, the idea of transmitting class bytecode to the server for dynamic execution in JSP web shells is nothing short of ingenious!
From a developerâs perspective, I feel that some parts of the authorâs code are a bit redundant and could be further optimized. Overall, Behinder is an exceptionally well-designed security tool.
Finally, I hope that one day I can develop such outstanding tools myself and contribute to the cybersecurity community.